beyondのblog

TON CTF 2024 Writeup

Ton CTF 2024

This article is a write-up of the TON CTF 2024 competition that I participated in with Leo. The TON CTF was sponsored by the TON Foundation and organized by Tonbit and TONX Studio. The competition encompassed multiple challenges related to the TON blockchain, all written in the Tact language. Tact is a high-level language that compiles to Func, which is then compiled to Fift, ultimately generating bytecode suitable for the TON virtual machine. This competition allowed me to gain a deeper understanding of the uniqueness of the TON ecosystem and its programming model.

This was my first time participating in a CTF-related competition, and the entire experience was both thrilling and unforgettable. The competition lasted only one day and consisted of 8 challenges. My teammate Leo and I completed this challenge together, ultimately achieving fourth place. You can view detailed descriptions of each challenge in this repository: TonCTF-Challenges.

Socreboard

Airdrop

Score: 100 The objective of the Airdrop challenge was to deplete the contract’s initial supply of 30,000 tokens. The contract allowed users to claim 1 token, but the vulnerability appeared in the implementation of the deposit and withdrawal functions. Because the contract didn’t strictly check if the input amount was positive, we could exploit negative values to manipulate the contract’s balance.

Solution Steps: We first constructed a message with a negative amount and sent it to the contract, thereby bypassing the contract’s balance check and reducing the contract’s supply, ultimately achieving the goal. The core code is as follows:

airdrop.send(provider.sender(),{ value: toNano("0.1") },{ $$type: "UserStake", amount: -30000n })

This code’s purpose is to construct a message containing a negative amount, calling the UserStake message. By passing a negative amount into the contract, it causes the contract to erroneously decrease its total supply.

Random

Score: 100 The Random challenge required us to guess a lucky number. If guessed correctly, the contract would set a flag to 1, indicating successful completion of the challenge. The contract used the current time and the user’s address hash to generate a random number, but due to the small range of the hash generation (0-99), we were able to solve this problem using a brute-force guessing method.

Solution Steps: We wrote a simple script that continuously sent different numbers to guess the lucky number until it matched the value generated by the contract, thus successfully passing the challenge.

Eccs

Score: 100 The Eccs challenge involved the Elliptic Curve Discrete Logarithm Problem (ECDLP). The contract implemented standard elliptic curve addition and multiplication operations, with the goal of calculating the multiplication factor given a specific curve point.

Solution Steps: We assumed that the range of k was relatively small and tried a brute-force approach. Alternatively, we could have used SageMath to solve it.

p = 738077334260605249
a = 1
b = 2

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.is_infinity = (x is None and y is None)
    
    def is_at_infinity(self):
        return self.is_infinity
    
    def __eq__(self, other):
        if self.is_at_infinity() and other.is_at_infinity():
            return True
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        if self.is_at_infinity():
            return "Point at infinity"
        return f"({self.x}, {self.y})"

O = Point(None, None)

def point_add(P, Q):
    if P.is_at_infinity():
        return Q
    if Q.is_at_infinity():
        return P
    if P.x == Q.x and (P.y + Q.y) % p == 0:
        return O
    if P == Q:
        lam = (3 * P.x * P.x + a) * pow(2 * P.y, -1, p) % p
    else:
        lam = (Q.y - P.y) * pow(Q.x - P.x, -1, p) % p
    x_r = (lam * lam - P.x - Q.x) % p
    y_r = (lam * (P.x - x_r) - P.y) % p
    return Point(x_r, y_r)

def scalar_mult(k, P):
    R = O
    N = P
    while k > 0:
        if k % 2 == 1:
            R = point_add(R, N)
        N = point_add(N, N)
        k = k // 2
    return R

P1 = Point(627636259244800403, 317346088871876181)
P2 = Point(456557409365020317, 354112687690635257)

def brute_force_k(max_range):
    for k in range(1, max_range):
        R = scalar_mult(k, P1)
        if R == P2:
            print(f"k: {k}")
            return k
        if k % 100000 == 0:
            print(f"try k = {k}")
    print("not found k")
    return None

max_range = 1000000
k = brute_force_k(max_range)

After running, we wait a while and get the result 844279

Dex

Score: 200 The Dex challenge combined rounding error vulnerabilities in DeFi exchanges with some unique features of the TON blockchain. The contract incorrectly rounded up the out_amount during the exchange process, leading to an exploitable vulnerability.

Solution Steps: We first exploited the rounding error for arbitrage, but this alone was not enough to fully solve the challenge. To further reduce the contract balance, we needed to attach a sufficiently large TON balance to bypass the balance check during withdrawal, and then recover this part of TON after the operation, thereby successfully reducing the contract’s balance and ultimately solving the challenge.

Puzzle

Score: 200 The Puzzle challenge was a bit manipulation problem involving multiple variables, but the bitwise operations in the contract were not functioning correctly. This allowed us to solve the problem by adjusting the variable values to meet the contract’s target conditions.

Solution Steps: We can write a Depth-First Search (DFS) algorithm that recursively tries each combination of operations to find a solution where the sum of the variables equals the target value. Here’s the implementation code:

class PuzzleSolver:
    def __init__(self):
        self.a = 100
        self.b = 100
        self.c = 100
        self.d = 100
        self.e = 100
        self.f = 100
        self.pc = 0
        self.target = 638
        self.max_steps = 13
        self.solved = False
        self.operations = []

    def operate1(self):
        self.a -= 3
        self.d += 4
        self.f -= 3
        self.pc += 1
        self.operations.append('operate1')

    def operate2(self):
        self.a += 3
        self.c += 1
        self.d //= 2
        self.f *= 4
        self.pc += 1
        self.operations.append('operate2')

    def operate3(self):
        self.a *= 4
        self.b -= 2
        self.d += 3
        self.pc += 1
        self.operations.append('operate3')

    def operate4(self):
        self.a += 5
        self.c -= 1
        self.d += 4
        self.f += 1
        self.pc += 1
        self.operations.append('operate4')

    def operate5(self):
        self.a -= 4
        self.c += 2
        self.d -= 5
        self.f += 3
        self.pc += 1
        self.operations.append('operate5')

    def operate6(self):
        self.a *= 2
        self.b += 6
        self.c *= 8
        self.e += 4
        self.pc += 1
        self.operations.append('operate6')

    def operate7(self):
        self.a -= 1
        self.b //= 8
        self.c *= 4
        self.d += 1
        self.e -= 5
        self.f += 2
        self.pc += 1
        self.operations.append('operate7')

    def check_solution(self):
        return (self.a + self.b + self.c + self.d + self.e + self.f == self.target) and (self.pc <= self.max_steps)

    def dfs(self, depth=0):
        if self.check_solution():
            self.solved = True
            print("Solution found with operations:", self.operations)
            return
        if depth == self.max_steps or self.solved:
            return
        
        # Try each operation recursively
        backup = (self.a, self.b, self.c, self.d, self.e, self.f, self.pc, list(self.operations))
        
        for operate in [self.operate1, self.operate2, self.operate3, self.operate4, self.operate5, self.operate6, self.operate7]:
            operate()
            self.dfs(depth + 1)
            self.a, self.b, self.c, self.d, self.e, self.f, self.pc, self.operations = backup

solver = PuzzleSolver()
solver.dfs()
print("Solved:", solver.solved)

This code uses depth-first search to try every possible combination of operations until it finds a solution that satisfies the conditions.

MerkleAirdrop

Score: 300 The MerkleAirdrop challenge involved using a Merkle tree for airdrop verification. The vulnerability in the contract was in its verify function, which used a non-standard method for node calculation. This allowed us to forge valid proofs to bypass the verification.

Solution Steps: Observing the contract’s init function, the merkleRoot calculation logic starts from y0 = sha256('EQ...NM0'+'614'), then iterates three rounds using three integers from nodes (denoted as n0, n1, n2): y_(i+1) = sha256(string(diff(y_i, n_i))), taking the final y3 value.

Further observing the Claim message handling logic, it’s easy to see that we can succeed by replacing y0 with sha256(<recipient>+'10000') and using custom nodes to calculate the same merkleRoot through similar logic.

Clearly, the key lies in the step y3 = sha256(string(diff(y2, n2))) where diff(y2, n2) must be the same as the value calculated in init. To achieve this, first calculate the reference value d, then choose <recipient> and n0, n1, calculate the value of y2, and make n2 = (y2 > d) ? (y2 - d) : (y2 + d).

Ecc

Score: 300 The Ecc challenge was similar to Eccs, also involving elliptic curve addition and multiplication operations, but with higher difficulty, requiring solving the discrete logarithm problem between two points. The key to this challenge was understanding the slope calculation method of the curve and using it to find the correct multiplication factor.

Solution Steps: Attempt to use the Baby-step Giant-step algorithm for brute-force cracking.

import math

# Prime modulus for the elliptic curve
p = 769908256801249

def invMod(x, p):
    # Calculate modular inverse
    return pow(x % p, -1, p)

def point_add(P, Q):
    x1, y1 = P
    x2, y2 = Q
    if (x1 == 0 and y1 == 0):
        return (x2 % p, y2 % p)
    if (x2 == 0 and y2 == 0):
        return (x1 % p, y1 % p)
    if x1 == x2 and y1 == y2:
        numerator = (3 * x1 * x2 + 4 * x1 + 1) % p
        denominator = (y1 + y2) % p
        if denominator == 0:
            return (0, 0)
        m = (numerator * invMod(denominator, p)) % p
    else:
        numerator = (y2 - y1) % p
        denominator = (x2 - x1) % p
        if denominator == 0:
            return (0, 0)
        m = (numerator * invMod(denominator, p)) % p
    x3 = (m * m - x1 - x2 - 2) % p
    y3 = (m * (x1 - x3) - y1) % p
    return (x3 % p, y3 % p)

def point_neg(P):
    # Negate a point
    x, y = P
    return (x % p, (-y) % p)

def point_sub(P, Q):
    # Point subtraction P - Q
    return point_add(P, point_neg(Q))

def scalar_mul(P, n):
    # Scalar multiplication
    R = (0, 0)
    Q = P
    while n > 0:
        if n % 2 == 1:
            R = point_add(R, Q)
        Q = point_add(Q, Q)
        n = n // 2
    return R

def BSGS(P, Q, m):
    # Baby-step Giant-step algorithm
    print("Starting Baby-step Giant-step algorithm")
    # Precompute Baby steps
    baby_steps = {}
    R = (0, 0)
    print("Calculating Baby steps...")
    for j in range(m):
        if R not in baby_steps:
            baby_steps[R] = j
        R = point_add(R, P)
        if j % (m // 10) == 0 and j > 0:
            print(f"Baby steps progress: {j}/{m}")
    # Calculate s = m * P
    s = scalar_mul(P, m)
    # Giant steps
    print("Calculating Giant steps...")
    R = Q
    for i in range(m):
        if R in baby_steps:
            j = baby_steps[R]
            k = i * m + j
            print(f"Solution found: k = {k}")
            return k
        R = point_sub(R, s)
        if i % (m // 10) == 0 and i > 0:
            print(f"Giant steps progress: {i}/{m}")
    print("No solution found. Please increase the value of m and try again.")
    return None

# Given points
p1 = (232682398601200, 266866051136644)
p2 = (565954914175128, 196353530004690)

# Set the value of m, can be adjusted based on memory and time constraints
m = 2 ** 25  # Approximately 1,048,576

k = BSGS(p1, p2, m)
if k is not None:
    print(f"Found k: {k}")
    # Verify the result
    check = scalar_mul(p1, k)
    if check == p2:
        print("Verification successful, the value of k is correct.")
    else:
        print("Verification failed, the value of k is incorrect.")
else:
    print("Unable to find the value of k. Please try increasing the value of m.")

Found k: 45514416202853

Curve

Score: 200 The Curve challenge involved a mathematical problem, specifically solving the discrete logarithm problem on a given curve. The curve’s behavior was defined in the contract, with the focus on understanding the implementation of point addition and how to solve the discrete logarithm through the relationship between points.

Solution Steps: Observing the implementation of the contract’s add function, after simplification, we find that the defined point operation satisfies (under modulo $p$) $$x_{P_{1}+P_2}=x_{P_1}+x_{P_2}-x_{P_0}$$

Selecting the base point $G$, we obviously have $$x_{k\cdot G}=k\cdot x_G-(k-1)\cdot x_{P_0}$$

That is, $$k=(x_{k\cdot G}-x_{P_0})(x_G-x_{P_0})^{-1}$$

Substitute zero.x, p1.x, p2.x, and p to solve for k.