TON CTF 2024 Writeup
Preface
The TON CTF 2024, sponsored by the TON Foundation and organized by Tonbit and TONX Studio, was an exciting competition that featured 8 challenges. The tasks were divided into three categories: 3 easy challenges worth 100 points, 3 medium challenges worth 200 points, and 2 difficult ones valued at 300 points.
What made this CTF unique was that all the challenges were written in Tact, a high-level language designed for the TON ecosystem. Tact compiles down to Func, another programming language, which then further compiles into Fift and finally down to bytecode for the VM. While I had previously dabbled with Func, this was my first time working with Tact, and I picked up quite a few new things along the way.
The Challenges of TON CTF vs. Other Web3 CTFs
Compared to other Web3 CTFs, the TON ecosystem presents unique challenges. First, the tooling within the TON ecosystem is still in active developing. Additionally, due to TON’s emphasis on speed, many operations, such as smart contract calls, are asynchronous. Finally, all the challenges in this CTF were deployed on a private chain, which introduced another layer of complexity when interacting with the contracts.
Tact contracts offer TypeScript interfaces for interaction, but since I’m not very familiar with TypeScript and wanted more granular control over the cells being sent, I decided to write my own interaction framework in Python.
Building a Python Framework for TON Interactions
The provided RPC interface was similar to Ton Center v2, which meant I could override the Client
class from the tonutils
library with a few modifications:
|
|
As I mentioned earlier, smart contract interactions in the TON blockchain aren’t atomic. To ensure that transactions from my wallet to the challenge contract were fully executed, I added a new method to the Client
class, get_last_transaction
. Although the method name might not seem entirely fitting, its purpose was to wait for a new transaction (usually the one I intended) to appear in the challenge contract.
Leveraging the Framework for the CTF
With this framework in place, interacting with the contracts became straightforward. After initializing a CTFWallet
object, I simply used the transfer
method to fill in the desired cell in the transaction body:
|
|
This setup provided me with the flexibility and control I needed during the competition, allowing me to send tailored messages to the contract and interact efficiently with the provided tasks.
Airdrop
- Source: https://github.com/TonBitSec/TonCTF-Challenges/tree/main/airdrop
- Score: 100
The first challenge in the competition was called Airdrop. As the name suggests, the contract allowed each user to claim an airdrop of 1 token. The initial supply of the contract was set to 30,000, and the goal of the challenge was to deplete this initial supply.
The vulnerability, however, wasn’t in the airdrop functionality itself, but in the deposit and withdrawal functions of the contract. A common pitfall when writing smart contracts is improper usage of signed integers, and this contract fell into that trap. Signed integers should only be used when absolutely necessary, but in this contract, they were used extensively, leading to a critical vulnerability.
The Vulnerability
When a user staked TON, the contract checked whether the provided amount
was less than the actual TON transferred. However, there was no check to ensure that amount
was a positive value. This opened the door for an attacker to pass a negative amount, which allowed them to manipulate the balance.
The Exploit
By analyzing the compiled contract, I found that the function responsible for staking, UserStake
, had a selector of f82b6291
. All I had to do was pass a negative value of -30,000
as the amount to exploit the vulnerability. This would cause the contract’s balance to be manipulated.
|
|
Random
- Source: https://github.com/TonBitSec/TonCTF-Challenges/tree/main/random
- Score: 100
The second challenge, Random, involved guessing a lucky number. The contract would take a user-provided luckynumber
parameter, and if it matched a value derived from the hash of the following cell:
|
|
The contract would then set a flag to 1
, indicating that the user had solved the challenge.
The Vulnerability
The contract didn’t use a native random function but rather constructed its own random number generator based on the hash of the aforementioned cell. This approach is inherently insecure since the value of now()
can be reasonably estimated, which makes the random number predictable.
However, given that the range of the generated random number was relatively small (from 0 to 99), instead of predicting the random number, I opted for a brute-force approach by repeatedly sending luckynumber
values until I got the correct one.
Eccs
- Source: https://github.com/TonBitSec/TonCTF-Challenges/tree/main/eccs
- Score: 100
The third challenge, Eccs, was added as a replacement for a previous on-chain data analysis challenge that had multiple issues in its description. The organizers eventually took that challenge down and replaced it with this one. Eccs was quite similar to the “Easy ECC” problem from the testing phase, as it implemented standard elliptic curve addition and multiplication. The goal was to solve the ECDLP.
Since the problem’s scale was relatively small, I could directly use SageMath to solve the ECDLP. Interestingly, although the code defined $b = 2$, the actual b
value used was 3
.
|
|
Running this script gave the solution 844279
.
Once Sage provided the solution, I simply had to send the following cell to solve the challenge:
|
|
Dex
- Source: https://github.com/TonBitSec/TonCTF-Challenges/tree/main/dex
- Score: 200
Unlike the previous mathematical challenge Eccs, the Dex challenge was much more engaging, combining a common issue in DeFi—rounding errors—with some unique features of the TON blockchain.
The Vulnerability
If you’re familiar with auditing or securing DeFi projects, you might have quickly spotted the vulnerability in the swap implementation. The contract incorrectly rounded the out_amount
up during a swap, which allowed users to exploit the rounding error for profit.
However, simply exploiting the rounding error wasn’t enough to solve the challenge. After taking advantage of the bug, the contract would only set the lock
variable to True
. To fully solve the challenge, a second condition needed to be met: the TON balance of the contract’s account had to drop below 0.5 TON.
Since the contract required a minimum of 0.14 TON for each operation, it was clear that performing just three swaps wouldn’t be enough to fully deplete the balance. This meant I had to trigger a withdrawal to reduce the contract’s balance further.
Bypassing the Withdrawal Check
In the contract’s withdraw
function, there was a balance check that required:
|
|
This check ensured that the balance of the contract, minus the amount the user wanted to withdraw, had to exceed 1 TON plus the storage reserve (0.1 TON).
At first glance, this seemed like a limitation that would prevent us from depleting the contract’s balance below 0.5 TON. However, there was a clever workaround. When calling the withdraw
function, if I attached a sufficiently large amount of TON, this amount would temporarily increase the myBalance()
that the contract checked against.
Since the actual withdrawal used the SendRemainingValue mode, the contract would return any excess TON back to me, bypassing the balance check while still allowing me to withdraw funds.
The Exploit
Here’s the code I used to perform the exploit:
|
|
Note:
- My approach to exploiting the rounding error was far from optimal. There’s room for improvement to reduce the number of swaps.
- The withdrawal amount I used in the code might need to be adjusted depending on the actual balance of the contract’s account at the time of the attack.
Puzzle
- Source: https://github.com/TonBitSec/TonCTF-Challenges/tree/main/puzzle
- Score: 200
As the name suggests, the Puzzle challenge was a straightforward puzzle, but it came with a subtle trap in the contract’s code. Specifically, all the bitwise shift operations in the contract didn’t include any assignment, rendering them ineffective. Once I realized this, solving the challenge became much simpler.
Ignoring the ineffective shift operations, the order of the operations wasn’t particularly important. The goal was to figure out how each operation affected the sum of the six member variables. After observing how each operation changed the sum, I could either manually adjust the values to meet the required conditions or use a solver like Z3 to automate the process.
Curve
- Source: https://github.com/TonBitSec/TonCTF-Challenges/tree/main/curve
- Score: 200
The Curve challenge was another mathematical puzzle, unrelated to the core features of the TON ecosystem. It revolved around solving a discrete logarithm problem on a given curve, where the contract implemented curve addition and multiplication.
Understanding the Curve
The curve’s behavior is defined in the challenge’s contract, specifically in the section that calculates the slope (m
) during the point addition process:
|
|
By analyzing this code, we observe that the slope calculation aligns with the properties of a quadratic curve in the form $y = ax^2 + bx + c$.
Curve Addition Implementation
The next part of the contract code handles the point addition on the curve:
|
|
Upon deeper inspection of the point addition implementation, we can see that the formula for $x_3$ simplifies to:
$$x_3 = x_1 + x_2 - x_0$$
This observation reveals that for repeated point addition (i.e., curve multiplication), the result follows the relation:
$$x_Q = k \cdot x_P - (k - 1) \cdot x_0$$ where $Q = k \cdot P$.
Solving the DLP
Given this relationship, we can simplify the DLP to doing integer division over $\mathbb{F}_p$. We are tasked with finding the multiplier k
that satisfies:
$$k = \frac{x_2 - x_0}{x_1 - x_0} \pmod{p}$$
The solution can be found using the following Python script:
|
|
Ecc
- Source: https://github.com/TonBitSec/TonCTF-Challenges/tree/main/ecc
- Score: 300
Similar to the previous Curve challenge, this problem implemented curve addition and multiplication, requiring participants to solve the DLP between two points.
Understanding the Curve
The curve’s slope calculation was defined in the contract as follows:
|
|
By analyzing the slope calculation and substituting the values provided in the challenge, we deduce that the equation of the curve is:
$$ y^2 = x^3 + 2x^2 + x = x(x + 1)^2 $$
This curve is singular, which is evident because it has a node. Singular curves behave differently from regular elliptic curves, and the DLP on such curves can be reduced to solving a DLP in an isomorphic group where solving DLP is much easier.
Solving the DLP
For this singular curve, we can calculate the two roots of the equation:
- The roots are 0 and 769908256801248 (which corresponds to x = -1).
- For the non-zero root, α = 769908256801248, we compute the square root t = 171237201247109.
With this information, we can transform the DLP on the curve into a simpler form in the isomorphic group:
|
|
MerkleAirdrop
- Source: https://github.com/TonBitSec/TonCTF-Challenges/tree/main/merkle_airdrop
- Score: 300
The MerkleAirdrop challenge implemented an airdrop system based on a Merkle Tree. The challenge initialized a Merkle Tree and allowed a specific address—EQDaypwc_Jr8by-alaK4mntRu35_EhlMz60AOeSJRawcrNM0
—to claim 614 tokens. The goal was to exploit the airdrop mechanism and forge a valid claim by tampering with the Merkle Tree proof.
The Vulnerability
Upon reviewing the verify
function, I noticed that the way the parent nodes in the Merkle Tree were computed was unusual. Instead of concatenating and hashing the child nodes together (as is typical for Merkle Trees), the function simply calculated the difference between the two node values.
|
|
Instead of the standard hashing approach, this implementation allowed us to manipulate the proof. By controlling the final input passed to the sha256
function, we could fabricate a valid proof based on a known hash.
The Exploit
To exploit the vulnerability, I only needed to modify the last proof element while keeping the earlier proofs intact. This adjustment let me control the input to the final sha256
operation, allowing me to bypass the verification process.
The challenge provided a set of data for the Merkle Tree verification:
|
|
Using the valid leaf, in the while loop, I dumped the values passed to the sha256
function to analyze the last round’s parameters.
To modify the claim amount, I changed the target leaf
value to represent a claim of 10,000 tokens. By adjusting the last proof value, I could ensure the sha256
input matched the expected value.
For example:
- Original final
sha256
input:90114452958426129291090095054266392995908099809161431530770587638743766783525
- Modified final leaf value for a 10,000 token claim:
108097698838845902137498555316715075862316788671405496171213270505370655211813
Given $leaf > target$, we can set the last entry of proofs to leaf-target
which is 17983245880419772846408460262448682866408688862244064640442682866626888428288
to bypass the verification.