I briefly participated in DUCTF 2021, solving three crypto challenges and two simple rev challenges.

The reversing challenges were fun and hard enough for me, but they are labeled easy/medium so I will not explain them here.

Also, joseph (one of the problemsetters) has a very good writeup on https://jsur.in/posts/2021-09-26-ductf-2021-writeups, so check it out. 

 

yadlp

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
def G_add(A, B):
    x1, y1 = A
    x2, y2 = B
    return ((x1*x2 + D*y1*y2) % p, (x1*y2 + x2*y1 + 2*y1*y2) % p)
 
def G_mul(A, k):
    out = (10)
    while k > 0:
        if k & 1:
            out = G_add(out, A)
        A = G_add(A, A)
        k >>= 1
    return out
 
def rand_element():
    while True:
        x = randint(1, p-1)
        d = x^2 * (D + 1- D
        if (x & 1 == d & 1and kronecker(d, p) == 1:
            y = (x + sqrt(Zmod(p)(d))) * inverse_mod(D, p) % p
            return (x, y)
 
= 13337
= 17568142778435152362975498611159042138909402642078949814477371651322179417849164549408357464774644525711780515232117470272550677945089719112177956836141583
assert p.nbits() >= 512
assert ((p-1)//2).is_prime() # safe prime
 
FLAG = open('flag.txt''rb').read().strip()
assert len(FLAG) % 8 == 0
= [int.from_bytes(FLAG[i:i+8], 'big'for i in range(0len(FLAG), 8)]
 
= [rand_element() for _ in M]
= (10)
for m, gi in zip(M, G):
    c = G_add(c, G_mul(gi, m))
 
print(f'{D = }')
print(f'{p = }')
print(f'{G = }')
print(f'{c = }')
 
cs

 

Step 1 

In challenges like these, figuring out the curve we are on is usually the first step. To do this, we let $$y = \frac{x +\sqrt{(D+1)x^2 - D}}{D}$$ and work out some algebra to end up with $$(x+y)^2 - (D+1)y^2 = 1$$ which is a Pell's equation. Now we change $(x, y)$ to $(x+y, y)$ and change $D$ to $D+1$ and consider $x^2 - Dy^2 = 1$. 

 

The solutions of Pell's equation satisfy some good properties, which come from the identity $$(a^2-Db^2)(c^2-Dd^2) = (ac+Dbd)^2 - D(ad+bc)^2$$ which shows that if $(a, b), (c, d)$ are solutions of $x^2-Dy^2=1$, then $(ac+Dbd, ad+bc)$ is also such a solution. This turns out to be the group addition as well. The homomorphism to a multiplicative group of $\mathbb{F}_{p^2} = \mathbb{F}_p[x] / (x^2 - D)$ written as $$ (a, b) \rightarrow a + bx$$ is also notable. The results here so far can be studied from Pell's equation theory, so the point up to here were straightforward for me.

 

Step 2 

Now we have to look at the group structure. There are various methods to end up with the correct conclusion - that the group is a cyclic group of order $p+1$. You could just guess the group order, or give a solid mathematical proof. joseph gives a proof by using that the multiplicative group of $\mathbb{F}_{p^2}$ is cyclic and $\alpha^{p+1}=1$ for each $\alpha = a + bx$ for $(a, b)$ satisfying $a^2 - Db^2 = 1$. I think technically this proves that the group order is a divisor of $p+1$, but this is quite good as well. Here's another method that I used during my solve.

 

It suffices to show that there are exactly $p+1$ solutions for $a^2 - Db^2 \equiv 1 \pmod{p}$. This is equivalent to computing $$\sum_{b=0}^{p-1} 1 + \left( \frac{Db^2 + 1}{p} \right) = 2 + \sum_{b=1}^{p-1} 1 + \left( \frac{Db^2+1}{p} \right) $$ $$ = 2 + \sum_{b=1}^{p-1} 1+ \left( \frac{D + b^2}{p} \right)$$ which means the value is the number of solutions for $$a^2 - b^2 \equiv D \pmod{p}, \quad b \not\equiv 0 \pmod{p} $$ plus $2$. Of course, the given equation is equivalent to $$(a-b)(a+b) \equiv D \pmod{p}$$ and there are $p-1$ solutions of the form $$a-b \equiv i \pmod{p}, \quad a+b \equiv Di^{-1} \pmod{p}$$ for each $i \in \{1, \cdots , p-1\}$. The key is that none of the solutions have $b \equiv 0 \pmod{p}$ because $D$ is, in this problem, not a quadratic residue. Therefore, the number of solutions is $2 + (p-1) = p+1$, as desired. This concludes the proof.

 

Note that if $D$ was a quadratic residue, there would be $p-1$ solutions.

 

This is really the turning point of the challenge - if $D$ was a quadratic residue, the group order would be $p-1$ and the fact that $p$ is a safe prime will help greatly to make the cipher safe. However, $D$ is not a quadratic residue, which makes the fact that $p$ is "safe" mean absolutely nothing. In fact, $p+1$ is very smooth. I thought that the whole $p$ is "safe" thing in the given script was a very funny joke :)

 

Step 3

Now we compute the actual discrete logarithm. There are many ways to compute this - you could write a custom Pohlig-Hellman algorithm to account for different addition and multiplication formula, which is possible but a hassle. An easier way is to define $\mathbb{F}_{p^2} = \mathbb{F}_p[x] / (x^2 - D)$ and work discrete logarithm over this field. Of course, to find a discrete logarithm, we need a generator $g$ for the field. Theoretically, we can just find random points until we find a generator. In my solution, I just tried the given points.

 

Step 4

Now that we know all the discrete logarithms, all we need to do is compute the flag. We see that $$ \sum m_i \log_g(g_i) = \log_g(res) \pmod{p+1}$$ along with $0 \le m_i < 2^{64}$. This condition can be straightforwardly feeded into my CVP repository https://github.com/rkm0959/Inequality_Solving_with_CVP and we have the flag. This was a nice challenge combining a lot of fun math ideas :)

 

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
# Directly taken from rbtree's LLL repository
# From https://oddcoder.com/LOL-34c3/, https://hackmd.io/@hakatashi/B1OM7HFVI
def Babai_CVP(mat, target):
    M = IntegerLattice(mat, lll_reduce=True).reduced_basis
    G = M.gram_schmidt()[0]
    diff = target
    for i in reversed(range(G.nrows())):
        diff -=  M[i] * ((diff * G[i]) / (G[i] * G[i])).round()
    return target - diff
 
 
def solve(mat, lb, ub, weight = None):
    num_var  = mat.nrows()
    num_ineq = mat.ncols()
 
    max_element = 0 
    for i in range(num_var):
        for j in range(num_ineq):
            max_element = max(max_element, abs(mat[i, j]))
 
    if weight == None:
        weight = num_ineq * max_element
 
    # sanity checker
    if len(lb) != num_ineq:
        print("Fail: len(lb) != num_ineq")
        return
 
    if len(ub) != num_ineq:
        print("Fail: len(ub) != num_ineq")
        return
 
    for i in range(num_ineq):
        if lb[i] > ub[i]:
            print("Fail: lb[i] > ub[i] at index", i)
            return
 
        # heuristic for number of solutions
    DET = 0
 
    if num_var == num_ineq:
        DET = abs(mat.det())
        num_sol = 1
        for i in range(num_ineq):
            num_sol *= (ub[i] - lb[i])
        if DET == 0:
            print("Zero Determinant")
        else:
            num_sol //= DET
            # + 1 added in for the sake of not making it zero...
            print("Expected Number of Solutions : ", num_sol + 1)
 
    # scaling process begins
    max_diff = max([ub[i] - lb[i] for i in range(num_ineq)])
    applied_weights = []
 
    for i in range(num_ineq):
        ineq_weight = weight if lb[i] == ub[i] else max_diff // (ub[i] - lb[i])
        applied_weights.append(ineq_weight)
        for j in range(num_var):
            mat[j, i] *= ineq_weight
        lb[i] *= ineq_weight
        ub[i] *= ineq_weight
 
    # Solve CVP
    target = vector([(lb[i] + ub[i]) // 2 for i in range(num_ineq)])
    result = Babai_CVP(mat, target)
 
    for i in range(num_ineq):
        if (lb[i] <= result[i] <= ub[i]) == False:
            print("Fail : inequality does not hold after solving")
            break
    
        # recover x
    fin = None
 
    if DET != 0:
        mat = mat.transpose()
        fin = mat.solve_right(result)
    
    ## recover your result
    return result, applied_weights, fin
 
= 13337
= 17568142778435152362975498611159042138909402642078949814477371651322179417849164549408357464774644525711780515232117470272550677945089719112177956836141583
= [(824914940549535049134693493358510941451078743259825009611468757037905313350871186248512803517454757191925623544169989938841766683559931596350748072767428510151966144947987666795899106244951506314545969111450078363915090201899029695981970354886015549281568762501638756950135017679627954071369058817947706039379), (101486582544154755882799565747721968985757181546439671636266944003630091685296458602809598108730283939708536437234250236788574082203309291165264672955425073332426625916817700349475905733631656792492189677766534230576987725484499618918928882667666640821403823057239790395654518704427126712280655564669757208129), (183932668108693992521485398085562602312041460603947441945549962588535727427581518939988035699537651402132911882906207114481856245726889232477383971353397717502649671831125396398431215302241914145169143474764941575812028922929277656849105757332346628455059539582448544435155655055157181361580680672298566085040), (31659559589682038792373443499625336425984410444816927701478078393729427158560475807660732222976925740259222603744099204176656000696651625025144031884325799382092026348588885644924948782239369051861025018411316856012639637274661831713783735305424388410778778529413114167923397187236739639802371814632949741663), (85002940632911245271086232819802558705075497343626042596459840443706586203853513387110519988860262606571329443536753351788719347982001630351902784834916337641198814027309580920446604109217188703337221305342467525089149977505415741300885194767452232679123441594451455097533000754553745051816419202345186703390), (123526856735509864536970355600066326281947889029213985456688284373398735442238959974405852278389199689296697383935356101033820848429004040054320076371939432453949984320580417885537763124479618094084392655766673219227195157341323190069350175423869908524758510177197973709821798974003013596311361995273762475822)]
= (53885671676587869351584134016741684201444292771720647214726629135637756703202984619499793624021577642727627552363209890189894463607407200724886231027760157420389277336940268114831002964626027945367662485419944369852006741899961686908509331719915794976159062761271182318814519641566938538911041229521838799714)
 
print(len(G))
 
'''
D = 13338
p = 17568142778435152362975498611159042138909402642078949814477371651322179417849164549408357464774644525711780515232117470272550677945089719112177956836141583
G = [(8249149405495350491346934933585109414510787432598250096114687570379053133508711862485128035174547571919256235441699899388417666835599315963507480727674285, 10151966144947987666795899106244951506314545969111450078363915090201899029695981970354886015549281568762501638756950135017679627954071369058817947706039379), (10148658254415475588279956574772196898575718154643967163626694400363009168529645860280959810873028393970853643723425023678857408220330929116526467295542507, 3332426625916817700349475905733631656792492189677766534230576987725484499618918928882667666640821403823057239790395654518704427126712280655564669757208129), (1839326681086939925214853980855626023120414606039474419455499625885357274275815189399880356995376514021329118829062071144818562457268892324773839713533977, 17502649671831125396398431215302241914145169143474764941575812028922929277656849105757332346628455059539582448544435155655055157181361580680672298566085040), (3165955958968203879237344349962533642598441044481692770147807839372942715856047580766073222297692574025922260374409920417665600069665162502514403188432579, 9382092026348588885644924948782239369051861025018411316856012639637274661831713783735305424388410778778529413114167923397187236739639802371814632949741663), (8500294063291124527108623281980255870507549734362604259645984044370658620385351338711051998886026260657132944353675335178871934798200163035190278483491633, 7641198814027309580920446604109217188703337221305342467525089149977505415741300885194767452232679123441594451455097533000754553745051816419202345186703390), (12352685673550986453697035560006632628194788902921398545668828437339873544223895997440585227838919968929669738393535610103382084842900404005432007637193943, 2453949984320580417885537763124479618094084392655766673219227195157341323190069350175423869908524758510177197973709821798974003013596311361995273762475822)]
c = (5388567167658786935158413401674168420144429277172064721472662913563775670320298461949979362402157764272762755236320989018989446360740720072488623102776015, 7420389277336940268114831002964626027945367662485419944369852006741899961686908509331719915794976159062761271182318814519641566938538911041229521838799714)
GG = []
for a, b in G:
    GG.append((a+b, b))
a, b = c
cc = (a+b, b)
K.<z> = GF(p**2, name='z', modulus=x^2 - D)
tt = (p+1) // 432
L = list(factor(tt))
print(L)
base = (a+b) + b * z
for a, b in GG:
    v = a + b * z
    print(v.log(base))
'''
 
logs = [2816026164685113357819599161916784095343437608176866348054691853599389483035986857942301105700772017038015800856893756018342135280046636282509828459475264
,4454166524908585051122091699367767320812675170250549943320148456830102311369800005419960817723812062960747357335951038181643917358801852112440501578475705
,10592989590744873457884645785658581545224173008189907961231282661924610752981017572693574053983522152348945200152352773129758671274319626386160131388585752
,9412387853225306787772668180506806539830098501890523194264793947862715307266992454894401276064400106485711592745559238039738469223857458389407805014776300
,11705006563236210956123096549924041531532105228094197348728492717298448400356434417842060810409623716614227796999859505036258530445896800424164580386003096
,7526156655082313923417616532606029321209669119704767191155046511306556916835849234170663657892790215760669790225464654694145090868608505033102329054110089]
 
= Matrix(ZZ, 77)
lb = [0* 7
ub = [0* 7
for i in range(6):
    M[i, 0= logs[i]
M[60= p + 1
lb[0= 1
ub[0= 1
 
for i in range(6):
    M[i, i+1= 1
    lb[i+1= 0
    ub[i+1= (1 << 64)
 
result, applied_weights, fin = solve(M, lb, ub)
 
flag = b''
for i in range(6):
    flag += long_to_bytes(int(fin[i]))
 
print(flag)
cs

 

power sign

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
#!/usr/bin/env sage
 
proof.arithmetic(False# just makes things faster
 
def get_3m4_prime(N):
    while True:
        p = random_prime(2^N - 1, lbound=2^(N-1))
        if p % 4 == 3:
            return p
 
def generate_key(L, n, m):
    p = get_3m4_prime(L//2)
    q = get_3m4_prime(L//2)
    N = p*q
    r = next_prime(N)
    F.<x> = PolynomialRing(GF(r))
    K = F.quo(F.irreducible_element(n))
    return (K, m), N, (p, q)
 
def H(params, msg, u):
    K, m = params
    r, z = K.characteristic(), K.gens()[0]
    h = 0
    while msg > 0:
        h *= z
        h += msg % r
        msg //= r
    h += z*u
    for _ in range(m):
        h ^= r
    assert len(list(h)) != 0
    return int(h[0])
 
def sign(params, privkey, msg):
    p, q = privkey
    u = 1
    while True:
        c = H(params, msg, u) % (p*q)
        if legendre_symbol(c, p) == legendre_symbol(c, q) == 1:
            break
        u += 1
    xp = pow(c, (p+1)//4, p)
    xq = pow(c, (q+1)//4, q)
    x = crt([int(xp), int(xq)], [p, q])
    return x, u
 
def verify(params, pubkey, msg, sig):
    N = pubkey
    x, u = sig
    c = H(params, msg, u)
    return x^2 % N == c % N
 
def main():
    print('Welcome to the game. To get the flag, give me a message to sign, then sign a random message of mine!')
    FLAG = open('./flag.txt''r').read().strip()
 
    L, n, m = 1024153
    params, pubkey, privkey = generate_key(L, n, m)
    print('N:', pubkey)
 
    msg = int(input('message (in hex): '), 16)
    if msg < pubkey^m:
        print('That message is too small!')
        exit()
    if msg > pubkey^n:
        print('That message is too big!')
        exit()
    x, u = sign(params, privkey, msg)
    print('x:', x)
    print('u:', u)
 
    auth_msg = randint(1, pubkey^5)
    print('Now sign', hex(auth_msg))
    x = int(input('x: '))
    u = int(input('u: '))
 
    if verify(params, pubkey, auth_msg, (x, u)):
        print(FLAG)
    else:
        print('Incorrect!')
 
if __name__ == '__main__':
    main()
 
cs

 

There are a lot of approaches possible, and a lot of solutions are on joseph's writeup linked above. Here, I'll just write my solution. 

 

Ultimately, we need $x, u$ such that $$x^2 \equiv H(msg, u) \pmod{N}$$ holds. Obviously there must be some issue with the whole $H$, so let's take a look in that whole thing. 

 

In $H$, we write $msg$ as a polynomial in $GF(r^n)$ and then computes the constant part of $$(msg + zu)^{r^m}$$ My immediate idea was to utilize frobenius endomorphism to simplify $$(msg + zu)^{r^m} \equiv msg^{r^m} + z^{r^m} u^{r^m} \equiv msg^{r^m} + z^{r^m} u \pmod{r}$$ which was linear in $u$. This gave us $$H(msg, u) = H(msg, 0) + u H(0, 1) \pmod{r}$$ and both $H(msg, 0)$ and $H(0, 1)$ can be computed directly from the given data.

 

Therefore, we can just select $u$ appropriately to make $H(msg, u) \equiv 1 \pmod{r}$ and send $x=1$ to get the flag.

 

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
def H(params, msg, u):
    K, m = params
    r, z = K.characteristic(), K.gens()[0]
    h = 0
    while msg > 0:
        h *= z
        h += msg % r
        msg //= r
    h += z*u
    for _ in range(m):
        h = h ** r
    assert len(list(h)) != 0
    return int(h[0])
 
conn = remote('pwn-2021.duc.tf'31912)
conn.recvline()
 
= int(conn.recvline().split()[1])
 
= next_prime(N)
= PolynomialRing(GF(r), 'x')
= F.quo(F.irreducible_element(15))
params = (K, 3)
pubkey = N
 
conn.sendline(hex(pubkey ** 6)[2:].encode())
conn.recvline()
conn.recvline()
 
target = int(conn.recvline().split()[2][2:].decode(), 16)
 
val_1 = H(params, target, 0)
val_2 = H(params, 01)
 
= ((1 + r - val_1) * inverse(val_2, r)) % r
 
conn.sendline(str(1).encode())
conn.sendline(str(u).encode())
 
print(conn.recvline())
cs

 

l33tcrypt v2

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
from Crypto.Util.number import getPrime, bytes_to_long
 
flag = open('flag.txt''rb').read().strip()
 
p, q = getPrime(1337), getPrime(1337)
= p*q
 
K.<z> = NumberField((x-p)^2 + q^2)
hint1 = p^2 + q^2
hint2 = []
 
= 1+337
for _ in range(1*3*3-7):
    a, b = getrandbits(1337), getrandbits(1337)
    x = K(a + getrandbits(l)/2^l) + K(b + getrandbits(l)/2^l)*z
    y = x*x.conjugate()
    hint2.append((int(y), a, b))
 
Zn.<I> = (ZZ.quo(n*ZZ))[]
ZnI.<I> = Zn.quo(I^2 + 1)
 
= randrange(1, n) + bytes_to_long(flag) * I
= pow(m, 0x1337)
 
print(f'hint1 = {hint1}', f'hint2 = {hint2}', f'c = {c}', sep='\n')
 
cs

 

Hour 1-2

CCE, a local CTF, was going on, but I was done with my part and had some time on my hands. 

That whole number field thing looked very scary, but the "error part" getrandbits and the "error part" from rounding $y$ made me believe that this is not really about number fields, but more about "approximate stuff" like lattices.

 

Also, I saw that after we find $p, q$, the remaining part can be copied from TetCTF 2021. (https://rkm0959.tistory.com/192)

 

After looking at basic conjugate things like $\overline{x} = 2p-x$ and $x \overline{x} = p^2 + q^2$ and doing some calculations on paper, I ended up with $$(a+r_1)^2 + 2p(a+r_1)(b+r_2) + (b+r_2)^2 (p^2+q^2) = y + r_3$$ where $0 \le r_1, r_2, r_3 < 1$. To make everything integer, multiplying $4^l$ gives $$(2^la + r_1)^2 + 2p(2^la+r_1)(2^lb+r_2) + (2^lb+r_2)^2 (p^2+q^2) + 4^ly + r_3$$ where $0 \le r_1, r_2 < 2^l$ and $0 \le r_3 < 4^l$ are integers. We have two equations, so in total, 6 error terms. 

 

We know $p^2+q^2$, so we only want the $p$ part. I wanted to plug in CVP repo, but it's not possible in this form. This is because the equations have parts like $r_1r_2p$, which is a large unknown value. This situation happened to me before - it was in AeroCTF horcrux (https://rkm0959.tistory.com/211) and I had talked about my solution with joseph before on discord. So I thought this might be the way. The idea is to cancel out $p$, making everything about the 6 error terms only. In the end, I would have a 6-variable polynomial to solve for small roots. 

 

In AeroCTF horcrux, the bounds were good enough that I could finish with CVP repo, not using the full power of the polynomials. To be more exact, I wouldn't need to use that $r_1r_2$ is $r_1$ multiplied by $r_2$ - just the fact that $0 \le r_1r_2 < 2^{2l}$. However, that was not true for this problem, as my CVP repo failed to give a correct solution. This meant that I actually needed the full power of 6 variable polynomials. I tried defund's repo, but it killed my computer. Another idea I had in mind was to use bounding to find one of $r_1, r_2$. However, not knowing $p$ precisely made this impossible. Then I went to work on writing up CCE and had dinner. 

 

Hour 3

I started with the bounding idea again - the part that kept bugging me was that $r_1$ really didn't matter. For example, if I had known $p$, I could find $r_2$ without the knowledge of $r_1$ or $r_3$ as the "impact" of $r_2$ is far greater than $r_1$ or $r_3$ in the whole equation.

 

To be more detailed, consider the equation $$(a+r_1)^2 + 2p(a+r_1)(b+r_2) + (b+r_2)^2 (p^2+q^2) = y + r_3$$ with $0 \le r_1, r_2, r_3 < 1$. When $r_1$ changes from $0$ to $1$, the LHS increases about 1337 * 2 bits. When $r_3$ changes from $0$ to $1$, the RHS increases about 1 bit. When $r_2$ changes from $0$ to $1$, the LHS increases about 1337 * 3 bits. This made me think about ignoring all $r_1, r_3$ parts as "noises", simplifying the equations at the cost of a looser bound and loss of information. 

 

Consider $0 \le r_1, r_2 < 2^l$ and $0 \le r_3 < 4^l$, with $$\left( a+ \frac{r_1}{2^l} \right)^2 + 2p \left(a + \frac{r_1}{2^l} \right) \left( b + \frac{r_2}{2^l} \right) + \left(b+\frac{r_2}{2^l} \right)^2 (p^2+q^2) = y + \frac{r_3}{2^{2l}}$$ we will divide this by $p^2+q^2$, and denote anything that is around 1 or smaller as $O(1)$. This gives $$ \frac{1}{p^2+q^2} \left( a+ \frac{r_1}{2^l} \right)^2 + \frac{1}{p^2+q^2} 2p \left(a + \frac{r_1}{2^l} \right) \left( b + \frac{r_2}{2^l} \right) + \left(b+\frac{r_2}{2^l} \right)^2  = \frac{1}{p^2+q^2} y + \frac{1}{p^2+q^2} \cdot \frac{r_3}{2^{2l}}$$ First, $a^2$ is 1337 * 2 bits and so is $p^2 + q^2$, implying that the first part is $O(1)$. For the second part, if we fully expand the numerator, the part excluding $2pab$ are all around 1337 * 2 bits, implying that it is $O(1)$ after division by $p^2+q^2$.

We can also see that $(r_2/2^l)^2$ is $O(1)$. Finally, clearly $r_3/4^l$ is already $O(1)$. In conclusion, we have the stunning equation $$O(1) + \frac{2ab}{p^2+q^2} p + b^2 + \frac{2r_2b}{2^l} =  \frac{y}{p^2+q^2} $$ and in practice, the inequality $$ \frac{2ab}{p^2+q^2} p + b^2 + \frac{2r_2b}{2^l} \le \frac{y}{p^2+q^2} \le 3 + \frac{2ab}{p^2+q^2} p + b^2 + \frac{2r_2b}{2^l}$$ worked well, which was verified with multiple testing with generated data. After clearing denominators, this was the perfect inequality to plug into CVP repo. Everything was known except $p$ and one error term for each equation, and everything was linear in terms of unknown values. I plugged this into the CVP repository, but there were multiple solutions. It took a few minutes to figure out that my candidates for $p$ were all consecutive integers, and I just had to take the next prime number to find the actual $p$, giving the flag.

 

This was a really really good challenge that taught me a lot. It had an interesting tradeoff of complexity and precision.

 

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
 
def inthroot(a, n):
    return a.nth_root(n, truncate_mode=True)[0]
 
# Directly taken from rbtree's LLL repository
# From https://oddcoder.com/LOL-34c3/, https://hackmd.io/@hakatashi/B1OM7HFVI
def Babai_CVP(mat, target):
    M = IntegerLattice(mat, lll_reduce=True).reduced_basis
    G = M.gram_schmidt()[0]
    diff = target
    for i in reversed(range(G.nrows())):
        diff -=  M[i] * ((diff * G[i]) / (G[i] * G[i])).round()
    return target - diff
 
 
def solve(mat, lb, ub, weight = None):
    num_var  = mat.nrows()
    num_ineq = mat.ncols()
 
    max_element = 0 
    for i in range(num_var):
        for j in range(num_ineq):
            max_element = max(max_element, abs(mat[i, j]))
 
    if weight == None:
        weight = num_ineq * max_element
 
    # sanity checker
    if len(lb) != num_ineq:
        print("Fail: len(lb) != num_ineq")
        return
 
    if len(ub) != num_ineq:
        print("Fail: len(ub) != num_ineq")
        return
 
    for i in range(num_ineq):
        if lb[i] > ub[i]:
            print("Fail: lb[i] > ub[i] at index", i)
            return
 
    # heuristic for number of solutions
    DET = 0
 
    m = mat * mat.transpose()
    DET = inthroot(Integer(m.det()), 2)
    num_sol = 1
    for i in range(num_ineq):
        num_sol *= (ub[i] - lb[i])
    if DET == 0:
        print("Zero Determinant")
    else:
        num_sol //= DET
        # + 1 added in for the sake of not making it zero...
        # print("Expected Number of Solutions : ", num_sol + 1)
        # print(num_sol+1)
 
    # scaling process begins
    max_diff = max([ub[i] - lb[i] for i in range(num_ineq)])
    applied_weights = []
 
    for i in range(num_ineq):
        ineq_weight = weight if lb[i] == ub[i] else max_diff // (ub[i] - lb[i])
        applied_weights.append(ineq_weight)
        for j in range(num_var):
            mat[j, i] *= ineq_weight
        lb[i] *= ineq_weight
        ub[i] *= ineq_weight
 
    # Solve CVP
    target = vector([(lb[i] + ub[i]) // 2 for i in range(num_ineq)])
    result = Babai_CVP(mat, target)
 
    for i in range(num_ineq):
        if (lb[i] <= result[i] <= ub[i]) == False:
            # print("Fail : inequality does not hold after solving")
            return NoneNoneNone
            break
    
        # recover x
    fin = None
 
    mat = mat.transpose()
    fin = mat.solve_right(result)
    
    ## recover your result
    return result, applied_weights, fin
 
hint1 = 9474114456792673515431877947943819508105969635542281515830563486137327168661333703401850605206493227243522275742966516648683239582868203596838274343178793480973891177607186024817920580636526398124249438530268326706106084200991464054888550590630591134888356623254262308517945404542523082470069480353068223694311508861801523183513245279545596528410382012946385366699431483305340128165852371328697376926968446708099825429442186214124021086080153564030893750081986851543881131351556953787562383902563532279388285764786107301410609469955447888060120474186646198067390958666731078269768973485409216649415321100676385488676994332545669638759522053210552702128133338413305324201806761866164474398103381334477039479224259925086453057512169480806903881772813089475880157326511289464083589335128877565750510634618850
hint2 = [(732546382458694012527789828012658378094702844241795401426843533262220812770028735455197287163119092985574791815396084039371039996745646333674579318749988785972110472195404257454757344814442424493794011566431937242360848878285030166984149693743077380791443878087350475749876855340312637158554338528312647367781264029940556029591883509296327157870585105957528288777977912893801651347425601258985587330670953298765350018187069720989197539655266126608555068568433785671130844623376557756307700624321155540004939237651224429899583558320317618753120682478549822994855115229255313084828494729871333964260804633974731539710591594817406548163223896591957023800247556355776277803866504762994363248426088927388107745396346246364269053285330511504658050763844166012473791572894552857353549047649195624795947489180260979461063462063828112399114859364813704521025921163040319096254633472956309716047137099678802685979113668885601296025318218875502044310139823328365450929054463143898168438742821823709383772324178942782797995009228644990630313338092741812684409459706859981433655900653533420603781201785114071059836464490109704785152549161122683211537057829631974746267465106271003835623120675868336217957493577419646574917016786960763971105924313670864643113152080247596101761667919974877317059734430884155146853783807463171001928603696124835265895296591581488900411484570933812177545618929573489183920330986109459899431831015221573294655414274883012371358092609067769905057144741729285256290725546405447624734438623235233946301969480294721959115680856021696640872884026385162811995627855934793386619910476937700613082779, 1543824598221623810544208805328287421613768036151947874139391287620165680044273642997713684429385602196228549596953402195253771549624806908757244012787647676921740972118119690619160814867773301029620094485726996907379061963500831983371578370700427374066341469843386724329442101058337650126328673673103875588454070821764869582749550357448526278027673495765146251605639297850930361919443005623694632013303, 278066231056905142305104802223227529510107823582373063981133703297010715328189269872672545717708004188569473458656648620603642909190690880317856689557616399347538953740829444695466029704435732263108744727780955070759083273384356847390585190707938226562580173928509689922077486197504519480359398971760153208230160873276281036727480009843280457680277644007154868247812957266590582948212002647347579597965), (2403931462615300014912573083791207269120927941165936946270714147348589275302782927254153349406751900409737005652653965419293500156635383418370534229776798159799544655491444625223239629070116271649566531509751492007287556686610922243146922720689240769956444136141045389983487722510967341333296316124861289680453311483583713394680884118860739515747245777714794715741583447733115461680095467478792575220154609730018111981467052398055564596522296491712770565301016865613056558617018826541540034369006769600336871572325043701351770219492587170369815095344397680717819930616599208355365092147396589419063661351495695927808384696467907751578957796331366383391221037004742878990846671674314297157060496951084267550785175020195260374151822097301166545332365173007130667658669023236685137146454027881996654859397883138679620794416770836903293285267689375186032800923071081877797299448490161970114063577601307797826188431113415671273120884557490666872780316977507218882738147619244927641770516473541881268874031321080906148751514801419469523784406736104695838521768957591700470012632981881278541917424550592192054960270762406248837436552253011562745897713476468254217503340327771690937113342506707740878371307784138197357653258554320947445665637325825934844545591362876466794093523698691499245458580590045390833227060694376995243735824822120025136206595444317852707366827787780703456414342309744435791623414331744392284997640092463157849832124166944965921884701468913065315740226664124591175781629426506057267248659303310396227692666112080517619152746622372541984530565220811971391954114052398015660212010257113648848847, 1095271776133890944180744243712910546278746562392568011876271303881431329832093376635925871119446466987231888845639447279594831752278087660346437418402997861987783151298417098367490883057794520011454228160352569059415183920272078930149476192223454197039306461996527833603604938471295363667754643414137116188016150205603690898270274189826416464265752825558069478170856048769787582750749123139364050774900, 503722937364073120219172142862403799517933100630972300242952550817433776953552478694914014362767990852845981728951892566080540126804572527879551832311854342967959495035585119889481210539549990451963720802920003582648892828543694739151645941765335597098334960962085798952240875279321239403245884262371088975882015803767312665173035156867269826854927631774910426072984288933637519024572470691768302645082)]
= 605102888419960227209554020321928680123927287488855374705023014664876415212834812004489811277243459845970579234661036307883657755039678491432326372452841243321229852171896023071850921050801342321482817352489245753590859528134913389224781053767643398051814697896895555141791201522215484812840325201515942495422062613986130075684389632368804195932004897586630630338128732739336582396560286087433253207646470438321442025408149861408487622053608073870706373371068091196032798889074109310838175523351596714632562756238346928415386196418507587810202943766172435188745498284802679701385932344117910084137055452834777639122479568560833213921960536746001563295384313906616645378042076612734674897406388665158992742560848871345048141300064428115232029157294890916882227084289355920878535418217759491272196240289158*+ 4029058763453606238903897463441562834907249422434463474076515876575829847522130192389016587404930865750330557087612323698180248796377295462524538856886086398759909290696184205993616487194215215220788562122976961077906744617472657126002125275580071372330371116294688544107523683271239033324714979085342248973341926663953843846046960739886947485863993625451601367382937578510847100654709310703410238862000749364014455073705910890023184023421059288913438268024784676942709153616462894290327611033397199003399769315399282676125251796552761022560343255167268538619398474794249565056809687349530329863724341722260050648912909130315084736439621962322946883555665867960336853966064629555225967168666632969614488585135849531033105444785970372310095381763641123031809302021416125564151419362313796375570121725315170
= 338
 
offset = 3
 
= Matrix(ZZ, 34)
lb = [0* 4
ub = [0* 4
 
y1, a1, b1 = hint2[0]
y2, a2, b2 = hint2[1]
 
target1 = (y1 // hint1 - b1 * b1) * (1 << (l-1)) * hint1
fx = hint1 * (1 << (l-1))
 
M[00= a1 * b1 * (1 << l)
M[10= hint1 * b1 
lb[0= target1 - offset * fx 
ub[0= target1 
 
target2 = (y2 // hint1 - b2 * b2) * (1 << (l-1)) * hint1 
 
M[01= a2 * b2 * (1 << l)
M[21= hint1 * b2
lb[1= target2 - offset * fx
ub[1= target2
 
M[12= 1
lb[2= 0
ub[2= 1 << l
 
M[23= 1
lb[3= 0
ub[3= 1 << l
 
result, applied_weights, fin = solve(M, lb, ub)
 
= next_prime(int(fin[0]))
= inthroot(Integer(hint1 - p * p), 2)
 
print(p * p + q * q - hint1)
 
'''
phi = (p * p - 1) * (q * q - 1)
d = int(inverse_mod(0x1337, phi))
n = p * q
Zn.<I> = (ZZ.quo(n*ZZ))[]
ZnI.<I> = Zn.quo(I^2 + 1)
c = 605102888419960227209554020321928680123927287488855374705023014664876415212834812004489811277243459845970579234661036307883657755039678491432326372452841243321229852171896023071850921050801342321482817352489245753590859528134913389224781053767643398051814697896895555141791201522215484812840325201515942495422062613986130075684389632368804195932004897586630630338128732739336582396560286087433253207646470438321442025408149861408487622053608073870706373371068091196032798889074109310838175523351596714632562756238346928415386196418507587810202943766172435188745498284802679701385932344117910084137055452834777639122479568560833213921960536746001563295384313906616645378042076612734674897406388665158992742560848871345048141300064428115232029157294890916882227084289355920878535418217759491272196240289158*I + 4029058763453606238903897463441562834907249422434463474076515876575829847522130192389016587404930865750330557087612323698180248796377295462524538856886086398759909290696184205993616487194215215220788562122976961077906744617472657126002125275580071372330371116294688544107523683271239033324714979085342248973341926663953843846046960739886947485863993625451601367382937578510847100654709310703410238862000749364014455073705910890023184023421059288913438268024784676942709153616462894290327611033397199003399769315399282676125251796552761022560343255167268538619398474794249565056809687349530329863724341722260050648912909130315084736439621962322946883555665867960336853966064629555225967168666632969614488585135849531033105444785970372310095381763641123031809302021416125564151419362313796375570121725315170
print(pow(c, d))
'''
cs

 

'CTF' 카테고리의 다른 글

PBCTF 2021 Writeups  (0) 2021.10.13
TSGCTF 2021 Writeups  (0) 2021.10.03
ACSC Crypto Writeups  (0) 2021.09.26
CryptoCTF Writeups (DoRSA, Polish)  (0) 2021.08.05
GoogleCTF Quals 2021 Crypto Writeups  (1) 2021.07.19

This writeup is long overdue, sorry about that. I participated in ACSC finals, and solved nearly all crypto, as there was a cryptography challenge that was really more about SQL Injection than cryptography. To be more detailed, the exact same cryptogrpahic vulnerability is used in one of the cryptohack challenges (and it has very similar setups as well) so the cryptographic side of the challenge is next to none. I had some fun learning some basic SQL Injection and got close, but ultimately failed to solve the challenge. I did solve all others, including one that was solved only by me. This writeup will be relatively short and concise, as I do not have a lot of time on my hands.

 

The solutions codes are on the usual github, https://github.com/rkm0959/Cryptography_Writeups/tree/main/2021/ACSC  

 

RSA Stream

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
import gmpy2
from Crypto.Util.number import long_to_bytes, bytes_to_long, getStrongPrime, inverse
from Crypto.Util.Padding import pad
 
from flag import m
#m = b"ACSC{<REDACTED>}" # flag!
 
= open("chal.py","rb").read() # I'll encrypt myself!
print("len:",len(f))
= getStrongPrime(1024)
= getStrongPrime(1024)
 
= p * q
= 0x10001
print("n =",n)
print("e =",e)
print("# flag length:",len(m))
= pad(m, 255)
= bytes_to_long(m)
 
assert m < n
stream = pow(m,e,n)
cipher = b""
 
for a in range(0,len(f),256):
  q = f[a:a+256]
  if len(q) < 256:q = pad(q, 256)
  q = bytes_to_long(q)
  c = stream ^ q
  cipher += long_to_bytes(c,256)
  e = gmpy2.next_prime(e)
  stream = pow(m,e,n)
 
open("chal.enc","wb").write(cipher)
 
 
cs

 

We see that we are encrypting the given python file, so we know the plaintext and the ciphertext.

Since this is an XOR cipher, this implies that we know all the key streams used to encrypt the plaintext.

We note that the key streams are composed of $m^e \pmod{n}$ where $e$ are several primes. 

Of course, if we know $m^{e_1} \pmod{n}$ and $m^{e_2} \pmod{n}$ with $\gcd(e_1, e_2) = 1$, we can find $r_1, r_2$ such that $e_1r_1 + e_2r_2 = 1$.

Now we can finish calculating $m$ by simply using $$m \equiv m^{e_1 \cdot r_1} m^{e_2 \cdot r_2} \pmod{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
length = 723
= 30004084769852356813752671105440339608383648259855991408799224369989221653141334011858388637782175392790629156827256797420595802457583565986882788667881921499468599322171673433298609987641468458633972069634856384101309327514278697390639738321868622386439249269795058985584353709739777081110979765232599757976759602245965314332404529910828253037394397471102918877473504943490285635862702543408002577628022054766664695619542702081689509713681170425764579507127909155563775027797744930354455708003402706090094588522963730499563711811899945647475596034599946875728770617584380135377604299815872040514361551864698426189453
= 65537
 
= open("chal.py","rb")
inp = f.read()
f.close()
 
= open("chal.enc""rb")
outp = f.read()
f.close()
 
data = []
= 65537
 
for i in range(0768256):
    cc = inp[i:i+256]
    if len(cc) < 256:
        cc = pad(cc, 256)
    res = bytes_to_long(cc) ^ bytes_to_long(outp[i:i+256])
    data.append([res, e])
    e = nextprime(e)
 
= inverse(6553765539)
= (65537 * u - 1// 65539
 
= (pow(data[0][0], u, n) * inverse(pow(data[1][0], v, n), n)) % n
 
print(long_to_bytes(m))    
cs

 

CBCBC

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
#!/usr/bin/env python3
 
import base64
import json
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from secret import hidden_username, flag
 
key = os.urandom(16)
iv1 = os.urandom(16)
iv2 = os.urandom(16)
 
 
def encrypt(msg):
    aes1 = AES.new(key, AES.MODE_CBC, iv1)
    aes2 = AES.new(key, AES.MODE_CBC, iv2)
    enc = aes2.encrypt(aes1.encrypt(pad(msg, 16)))
    return iv1 + iv2 + enc
 
 
def decrypt(msg):
    iv1, iv2, enc = msg[:16], msg[16:32], msg[32:]
    aes1 = AES.new(key, AES.MODE_CBC, iv1)
    aes2 = AES.new(key, AES.MODE_CBC, iv2)
    msg = unpad(aes1.decrypt(aes2.decrypt(enc)), 16)
    return msg
 
 
def create_user():
    username = input("Your username: ")
    if username:
        data = {"username": username, "is_admin"False}
    else:
        # Default token
        data = {"username": hidden_username, "is_admin"True}
    token = encrypt(json.dumps(data).encode())
    print("Your token: ")
    print(base64.b64encode(token).decode())
 
 
def login():
    username = input("Your username: ")
    token = input("Your token: ").encode()
    try:
        data_raw = decrypt(base64.b64decode(token))
    except:
        print("Failed to login! Check your token again")
        return None
 
    try:
        data = json.loads(data_raw.decode())
    except:
        print("Failed to login! Your token is malformed")
        return None
 
    if "username" not in data or data["username"!= username:
        print("Failed to login! Check your username again")
        return None
 
    return data
 
 
def none_menu():
    print("1. Create user")
    print("2. Log in")
    print("3. Exit")
 
    try:
        inp = int(input("> "))
    except ValueError:
        print("Wrong choice!")
        return None
 
    if inp == 1:
        create_user()
        return None
    elif inp == 2:
        return login()
    elif inp == 3:
        exit(0)
    else:
        print("Wrong choice!")
        return None
 
 
def user_menu(user):
    print("1. Show flag")
    print("2. Log out")
    print("3. Exit")
 
    try:
        inp = int(input("> "))
    except ValueError:
        print("Wrong choice!")
        return None
 
    if inp == 1:
        if "is_admin" in user and user["is_admin"]:
            print(flag)
        else:
            print("No.")
        return user
    elif inp == 2:
        return None
    elif inp == 3:
        exit(0)
    else:
        print("Wrong choice!")
        return None
 
 
def main():
    user = None
 
    print("Welcome to CBCBC flag sharing service!")
    print("You can get the flag free!")
    print("This is super-duper safe from padding oracle attacks,")
    print("because it's using CBC twice!")
    print("=====================================================")
 
    while True:
        if user:
            user = user_menu(user)
        else:
            user = none_menu()
 
 
if __name__ == "__main__":
    main()
 
cs

 

We have a cipher that uses AES-CBC two times. From line 118 we see that padding oracle attacks still work.

If we write out the entire decryption process, we get that $$P_n = D_k(D_k(C_n) \oplus C_{n-1}) \oplus D_k(C_{n-1}) \oplus C_{n-2}$$ where $C_0 = IV_2$ and $C_{-1} = IV_1$. Note that $P_n$ can be easily controlled by $C_{n-2}$.

Therefore, the whole padding oracle attacks work, it's just that we need to change 2 blocks before the targeted block to perform the attack. The remaining parts are straightforward if you have solid understanding of padding oracle attack.

 

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
= remote('167.99.77.49'52171)
# r.interactive()
 
def read_menu(x):
    for _ in range(x):
        r.recvline()
 
read_menu(5)
 
read_menu(3)
r.sendline(b"1")
r.send(b"\n")
r.recvline()
token = r.recvline()
token = b64decode(token)
print(token)
print(len(token))
 
true_ptxt = [0* 80
 
for i in range(6416-16):
    for j in range(016):
        for k in tqdm(range(0256)):
            if i == 64 and j == 0 and k == 0:
                continue
            query_token = token[:i-j-17]
            query_token += bytes([token[i-j-17] ^ k])
            for u in range(j):
                query_token += bytes([token[i-j-16+u] ^ true_ptxt[i-j+16+u] ^ (j+1)])
            query_token += token[i-16:i+16]
            read_menu(3)
            r.sendline(b"2")
            r.sendline(b"abc")
            r.sendline(b64encode(query_token))
            res = r.recvline()
            if b"Check your token again" not in res:
                true_ptxt[i+15-j] = k ^ (j+1)
                break
        print(bytes(true_ptxt))
 
print(bytes(true_ptxt))
cs

 

Swap on Curve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from params import p, a, b, flag, y
 
= int.from_bytes(flag, "big")
 
assert 0 < x < p
assert 0 < y < p
assert x != y
 
EC = EllipticCurve(GF(p), [a, b])
 
assert EC(x,y)
assert EC(y,x)
 
print("p = {}".format(p))
print("a = {}".format(a))
print("b = {}".format(b))
 
cs

 

Basically we have to solve $$y^2 = x^3 + ax + b, \quad x^2 = y^3 + ay + b$$ which can be done by doing algebra to change it into a one variable polynomial equation, or using resultants to achieve the same goal. I used the latter option, also using what I learned from joseph (https://twitter.com/josep68_) in CryptoHack discord. Sometimes the resultant function on Sagemath doesn't work for some reason I don't fully understand, but you can get around this by using sylvester matrix trickery. 

 

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
'''
from sage.matrix.matrix2 import Matrix 
def resultant(f1, f2, var):
    return Matrix.determinant(f1.sylvester_matrix(f2, var))
p = 10224339405907703092027271021531545025590069329651203467716750905186360905870976608482239954157859974243721027388367833391620238905205324488863654155905507
a = 4497571717921592398955060922592201381291364158316041225609739861880668012419104521771916052114951221663782888917019515720822797673629101617287519628798278
b = 1147822627440179166862874039888124662334972701778333205963385274435770863246836847305423006003688412952676893584685957117091707234660746455918810395379096
# y^2 = x^3 + ax+b
# x^2 = y^3 + ay+b
POL.<x, y> = PolynomialRing(GF(p))
f = y * y - x * x * x - a * x - b
g = x * x - y * y * y - a * y - b 
res = resultant(f, g, y)
print(res.roots())
'''
 
 
= 10224339405907703092027271021531545025590069329651203467716750905186360905870976608482239954157859974243721027388367833391620238905205324488863654155905507
= 4497571717921592398955060922592201381291364158316041225609739861880668012419104521771916052114951221663782888917019515720822797673629101617287519628798278
= 1147822627440179166862874039888124662334972701778333205963385274435770863246836847305423006003688412952676893584685957117091707234660746455918810395379096
 
# y^2 = x^3 + ax+b
# x^2 = y^3 + ay+b
 
val = [(77010936765396008495452337756562661240243939011172848439081109615157875784261411855101635290874907070056020901709674867604250353250799895482424825163459961), (76777851164357276866499644465822802006538678473475083296658234236620122517398166826857547693953466022605068489266979760977164067377818930943400499575041621), (34181000969577773296415223858746017079573787698084416369226958825797414030997931351865933178274427186538927904848533932096678438894461559467254427322554481)]
 
for a, b in val:
    print(long_to_bytes(a))
cs

 

Two Rabin

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
import random
from Crypto.Util.number import *
from Crypto.Util.Padding import pad
 
from flag import flag
 
= getStrongPrime(512)
= getStrongPrime(512)
= p * q
= getStrongPrime(512)
 
= flag[0:len(flag)//2]
print("flag1_len =",len(m))
 
m1 = bytes_to_long(m)
m2 = bytes_to_long(pad(m,128))
 
assert m1 < n
assert m2 < n
 
c1 = (m1*(m1+B)) % n
c2 = (m2*(m2+B)) % n
 
print("n =",n)
print("B =",B)
print("c1 =",c1)
print("c2 =",c2)
 
# Harder!
 
= flag[len(flag)//2:]
print("flag2_len =",len(m))
 
m1 = bytes_to_long(m)
m1 <<= ( (128-len(m))*8 )
m1 += random.SystemRandom().getrandbits( (128-len(m))*8 )
 
m2 = bytes_to_long(m)
m2 <<= ( (128-len(m))*8 )
m2 += random.SystemRandom().getrandbits( (128-len(m))*8 )
 
assert m1 < n
assert m2 < n
 
c1 = (m1*(m1+B)) % n
c2 = (m2*(m2+B)) % n
 
print("hard_c1 =",c1)
print("hard_c2 =",c2)
 
 
cs

 

We have two separate problems, each of which needs to be solved to get the flag.

 

For the first problem, there is a known linear relation between the two messages since the pad is not random. Therefore, the two equations given can both be written as a polynomial equation of a single variable, which means that the system can be solved by polynomial GCD or basic algebra as both equations are only quadratic. I chose the latter option for some reason :P

 

For the second problem, the padding is small but they are random. Therefore, we can write the first message as $x$ and the second message as $x+y$ where $y$ is some small value. Since we have two quadratic equations on $x, y$, we can use resultants again to make a polynomial equation on $y$ only. Since $y$ is small, this polynomial equation can be solved via coppersmith's attack. After getting $y$, we now have two quadratic equations where $x$ is the only variable, which can be solved similarly as the first problem. 

 

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
flag1_len = 98
= 105663510238670420757255989578978162666434740162415948750279893317701612062865075870926559751210244886747509597507458509604874043682717453885668881354391379276091832437791327382673554621542363370695590872213882821916016679451005257003326444660295787578301365987666679013861017982035560204259777436442969488099
= 12408624070212894491872051808326026233625878902991556747856160971787460076467522269639429595067604541456868927539680514190186916845592948405088662144279471
c1 = 47149257341850631803344907793040624016460864394802627848277699824692112650262968210121452299581667376809654259561510658416826163949830223407035750286554940980726936799838074413937433800942520987785496915219844827204556044437125649495753599550708106983195864758161432571740109614959841908745488347057154186396
c2 = 38096143360064857625836039270668052307251843760085437365614169441559213241186400206703536344838144000472263634954875924378598171294646491844012132284477949793329427432803416979432652621257006572714223359085436237334735438682570204741205174909769464683299442221434350777366303691294099640097749346031264625862
 
flag2_len = 98
hard_c1 = 73091191827823774495468908722773206641492423784400072752465168109870542883199959598717050676487545742986091081315652284268136739187215026022065778742525832001516743913783423994796457270286069750481789982702001563824813913547627820131760747156379815528428547155422785084878636818919308472977926622234822351389
hard_c2 = 21303605284622657693928572452692917426184397648451262767916068031147685805357948196368866787751567262515163804299565902544134567172298465831142768549321228087238170761793574794991881327590118848547031077305045920819173332543516073028600540903504720606513570298252979409711977771956104783864344110894347670094
 
= PolynomialRing(Zmod(n), 'x')
= P.gen()
 
 
= x * (x + B) - c1
mul = (1 << (8 * (128 - flag1_len)))
cc = bytes_to_long(b"\x1e" * 30)
 
= (mul * x + cc) * (mul * x + cc + B) - c2
= g.monic()
 
= f - g 
cc = h.coefficients()
 
= (int(cc[0]) * inverse(int(n - cc[1]), n)) % n
flag1 = long_to_bytes(m)
 
'''
from sage.matrix.matrix2 import Matrix 
def resultant(f1, f2, var):
    return Matrix.determinant(f1.sylvester_matrix(f2, var))
n = 105663510238670420757255989578978162666434740162415948750279893317701612062865075870926559751210244886747509597507458509604874043682717453885668881354391379276091832437791327382673554621542363370695590872213882821916016679451005257003326444660295787578301365987666679013861017982035560204259777436442969488099
B = 12408624070212894491872051808326026233625878902991556747856160971787460076467522269639429595067604541456868927539680514190186916845592948405088662144279471
flag2_len = 98
hard_c1 = 73091191827823774495468908722773206641492423784400072752465168109870542883199959598717050676487545742986091081315652284268136739187215026022065778742525832001516743913783423994796457270286069750481789982702001563824813913547627820131760747156379815528428547155422785084878636818919308472977926622234822351389
hard_c2 = 21303605284622657693928572452692917426184397648451262767916068031147685805357948196368866787751567262515163804299565902544134567172298465831142768549321228087238170761793574794991881327590118848547031077305045920819173332543516073028600540903504720606513570298252979409711977771956104783864344110894347670094
POL.<x, y> = PolynomialRing(Zmod(n))
f = x * (x + B) - hard_c1
g = (x + y) * (x + y + B) - hard_c2
res = resultant(f, g, x)
print(res)
POL.<y> = PolynomialRing(Zmod(n))
h = y^4 + 79890495413921998317755749042148232336863396932303122279875240130974185840791225375990895444267582903006871773965303045933569843994868097491212523442101173551279596449914721209311144221088483103651821516943100935730831208926818636117783500717523025942791406004609937226219367532136778920138824547343785869589*y^2 + 51092857055466673249380987427595244393604870491102664532822637562859699012127822437087645886784256843354629922356917208150074797607882719481678015312190222601448915412917964775219154717370030324105055787501129672958587186322761301111111401381772704665208028130495822463025972061040003730113313298834567354767
print(h.small_roots(X = (1 << 240), beta = 1.0, epsilon = 0.025))''
y_cands = [1637558660573652475698054766420163959191730746581158985657024969935597275, 105663510238670420757255989578978162666434740162415948750279893317701612062865075870926559751210244886747509597507458509604874043682717453885668881354391379276091832437791327382673554621542363370695590872213882821916016679451005257003324807101635213925825667932900258849901826251288979045274120411473033890824]
POL.<x> = PolynomialRing(Zmod(n))
for y in y_cands:
    f = x * (x + B) - hard_c1
    g = (x + y) * (x + y + B) - hard_c2
    h = f - g
    print(h)
    cc = h.coefficients()
    print(cc)
    x = - cc[0] / cc[1]
    print(h(x))
    x = int(x)
    print("result", x)
    print(int(x * (x + B) - hard_c1) % n)
    print(int((x+y)*(x+y+B)-hard_c2) % n)
    break
'''
x1 = 37412309942286574006158913496010620267687663146876352767622106656129986496651165862840203148321069273733293624726376167460944865534151793748073347584719705531628535234167400567407324714477822390166015938266208084466510307154956915004073076813624952897284616411776573796324151099101617608303133521659321079317
x1 = n - B - x1
 
print((x1 * (x1+ B) - hard_c1) % n)
 
 
= x1 >> 240
flag2= long_to_bytes(m)
print(flag2)
 
print(flag1 + flag2)
cs

 

Wonderful Hash

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
import os
import string
from Crypto.Cipher import AES, ARC4, DES
 
BLOCK = 16
 
 
def bxor(a, b):
  res = [c1 ^ c2 for (c1, c2) in zip(a, b)]
  return bytes(res)
 
 
def block_hash(data):
  data = AES.new(data, AES.MODE_ECB).encrypt(b"\x00" * AES.block_size)
  data = ARC4.new(data).encrypt(b"\x00" * DES.key_size)
  data = DES.new(data, DES.MODE_ECB).encrypt(b"\x00" * DES.block_size)
  return data[:-2]
 
 
def hash(data):
  length = len(data)
  if length % BLOCK != 0:
    pad_len = BLOCK - length % BLOCK
    data += bytes([pad_len] * pad_len)
    length += pad_len
  block_cnt = length // BLOCK
  blocks = [data[i * BLOCK:(i + 1* BLOCK] for i in range(block_cnt)]
  res = b"\x00" * BLOCK
  for block in blocks:
    res = bxor(res, block_hash(block))
  return res
 
 
def check(cmd, new_cmd):
  if len(cmd) != len(new_cmd):
    return False
  if hash(cmd) != hash(new_cmd):
    return False
  for c in new_cmd:
    if chr(c) not in string.printable:
      return False
  return True
 
 
cmd = (b"echo 'There are a lot of Capture The Flag (CTF) competitions in "
       b"our days, some of them have excelent tasks, but in most cases "
       b"they're forgotten just after the CTF finished. We decided to make"
       b" some kind of CTF archive and of course, it'll be too boring to "
       b"have just an archive, so we made a place, where you can get some "
       b"another CTF-related info - current overall Capture The Flag team "
       b"rating, per-team statistics etc'")
 
 
def menu():
  print("[S]tore command")
  print("[E]xecute command")
  print("[F]iles")
  print("[L]eave")
  return input("> ")
 
 
while True:
  choice = menu()
  if choice[0== "S":
    new_cmd = input().encode()
    if check(cmd, new_cmd):
      cmd = new_cmd
    else:
      print("Oops!")
      exit(1)
  elif choice[0== "E":
    os.system(cmd)
  elif choice[0== "F":
    os.system(b"ls")
  elif choice[0== "L":
    break
  else:
    print("Command Unsupported")
    exit(1)
 
cs

 

We require some sort of a hash collision. Since the hash is simply XOR of the hash of each blocks, this problem is equivalent to finding $x_1, x_2, \cdots,  x_k$ given sets $X_1, X_2, \cdots, X_k$ such that the following holds. $$x_i \in X_i, \quad \oplus_{i=1}^k x_i = 0$$ Of course, if $k=2$ this is the well-known birthday problem. 

 

While I'm not sure if this was required for this challenge, this problem has been studied before. It's in the 2002 CRYPTO paper "A Generalized Birthday Problem", and it has been explained by barkingdog in his blog post https://www.secmem.org/blog/2020/08/19/A-Generalized-Birthday-Problem/. Using this, we can solve the problem in $\mathcal{O}(2^{n/(1+\log_2 k)})$. The basic idea is to make a binary tree and solve the problem "partially" (make LSBs zero) as we go upwards. For more details, consult the blog post above or the 2002 paper. 

 

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
= remote('wonderful-hash.chal.acsc.asia'10217)
 
for i in range(5):
    print(r.recvline())
ans = b'cat flag        DxriPBQeGgXmjlewgmpxnBnWbfoxGirrLUtUlukpPmjqMOlCBmaOIXXqKIGXoFJsdVGypWXGdXcPScZXFQPbnigusUZZdrxpCyMeGgKGqzJQIzbRqThOJXNgPPNXdPjIOtRhGKBXJDlYDGMfcnyAIojYUJaFyUhzjlsSQHZPasglvQOWIyVoYELtFwJQSsBPsNpvcKZYuKWBrEHwDQLkpCYWkbNIHPTHxbPqrzDgcXCnVvJKeIzkiMKqPhUwDRIe                                                                                                                                                 '
r.sendline("S")
r.sendline(ans)
for i in range(4):
    print(r.recvline())
 
r.sendline("E")
for i in range(10):
    print(r.recvline())
 
## main sol
BLOCK = 16
 
def bxor(a, b):
    res = [c1 ^ c2 for (c1, c2) in zip(a, b)]
    return bytes(res)
 
 
def block_hash(data):
    data = AES.new(data, AES.MODE_ECB).encrypt(b"\x00" * AES.block_size)
    data = ARC4.new(data).encrypt(b"\x00" * DES.key_size)
    data = DES.new(data, DES.MODE_ECB).encrypt(b"\x00" * DES.block_size)
    return data[:-2]
 
 
def hash(data):
    length = len(data)
    if length % BLOCK != 0:
        pad_len = BLOCK - length % BLOCK
        data += bytes([pad_len] * pad_len)
        length += pad_len
    block_cnt = length // BLOCK
    blocks = [data[i * BLOCK:(i + 1* BLOCK] for i in range(block_cnt)]
    res = b"\x00" * BLOCK
    for block in blocks:
        res = bxor(res, block_hash(block))
    return res
 
def get_random_block():
    res = "".join([rand.choice(string.ascii_letters) for _ in range(16)])
    return res.encode()
 
cmd = (b"echo 'There are a lot of Capture The Flag (CTF) competitions in "
       b"our days, some of them have excelent tasks, but in most cases "
       b"they're forgotten just after the CTF finished. We decided to make"
       b" some kind of CTF archive and of course, it'll be too boring to "
       b"have just an archive, so we made a place, where you can get some "
       b"another CTF-related info - current overall Capture The Flag team "
       b"rating, per-team statistics etc'")
 
# 27 
 
print(len(cmd))
 
target = bytes_to_long(hash(cmd))
 
fin_blocks = []
block0 = b"cat flag" + b" " * 8
fin_blocks.append(block0)
 
target ^= bytes_to_long(block_hash(block0))
 
back_blocks = []
for i in range(9):
    back_blocks.append(b" " * 16)
    target ^= bytes_to_long(block_hash(b" "*16))
back_blocks.append(b" ")
target ^= bytes_to_long(block_hash(b" " + bytes([15* 15)))
 
 
# now we need 16 blocks
 
grounds = []
for i in range(16):
    cur = []
    for j in range(6000):
        val = get_random_block()
        cc = block_hash(val)
        cc = bytes_to_long(cc)
        if i == 7:
            cc ^= target
        cur.append([[val], cc])
    grounds.append(cur)
 
def merger(l, r, tot): # merging [l, r) to have tot zeros
    print("WORKING", l, r, tot)
    global grounds
    if l + 1 == r:
        return grounds[l]
    cc1 = merger(l, (l+r)//2, tot - 12)
    cc2 = merger((l+r) // 2, r, tot - 12)
    print(l, (l+r)//2len(cc1))
    print((l+r)//2, r, len(cc2))
    LEFT = {}
    for i in range(len(cc1)):
        res = cc1[i][1] % (1 << tot)
        if res in LEFT.keys():
            arr = LEFT[res]
            arr.append(i)
            LEFT[res] = arr
        else:
            LEFT[res] = [i]
    ret = []
    for i in range(len(cc2)):
        res = cc2[i][1] % (1 << tot)
        if res in LEFT.keys():
            arr = LEFT[res]
            for idx in arr:
                xred = cc1[idx][1] ^ cc2[i][1]
                vals = cc1[idx][0+ cc2[i][0]
                ret.append([vals, xred])
    return ret
 
fin = merger(01648)
print(fin[0])
 
ret = fin[0][0]
 
sol = fin_blocks + ret + back_blocks
 
gogo = b""
for block in sol:
    gogo += block
 
print(gogo)
print(len(gogo))
 
print(hash(gogo))
print(hash(cmd))
cs

 

Share The Flag

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
#!/usr/bin/env python3
import os
import random
import string
 
 
with open('flag.txt''rb'as f:
    FLAG = f.read().strip()
assert FLAG.startswith(b'ACSC{')
assert FLAG.endswith(b'}')
SECRET = FLAG[5:-1]
assert len(SECRET) == 16
 
= 251
 
def random_letters(n):
    return ''.join(random.choices(string.ascii_lowercase, k=n)).encode()
 
print("""\
.----------------.
| Share the flag |
'----------------'
 
Welcome to our flag-sharing service.
We understand some of you couldn't resist sharing flags with others,
but it is STRICTLY PROHIBITED by the rules.
In order to satisfy your desire...
We made the official flag sharing service for you,
with a new algorithm inspired by Shamir Secret Sharing.
 
""")
 
# You'll need at least `threshold` shares to unlock the flag
threshold = 32
 
# Admin holds `len(SECRET) + 1` shares.
nshares = threshold - (len(SECRET) + 1)
 
# Splitting the flag
padding = random_letters(threshold - len(SECRET))
coeff = list(SECRET + padding)
 
xs = bytes(random.sample(range(1, p), k=nshares))
ys = bytes(sum(c * pow(x, i, p) for i, c in enumerate(coeff)) % p for x in xs)
print(f'X: {xs.hex()}')
print(f'Y: {ys.hex()}')
 
cs

 

This will be explained further later when I upload the CVP repository updates. 

 

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
def SVP_oracle(mat):
    M = mat.BKZ(block_size = 30)
    return M
 
def solve(mat, lb, ub):
    num_var  = mat.nrows()
    num_ineq = mat.ncols()
 
    max_element = 0 
    for i in range(num_var):
        for j in range(num_ineq):
            max_element = max(max_element, abs(mat[i, j]))
 
    # sanity checker
    if len(lb) != num_ineq:
        print("Fail: len(lb) != num_ineq")
        return NoneNone 
 
    if len(ub) != num_ineq:
        print("Fail: len(ub) != num_ineq")
        return NoneNone 
 
    for i in range(num_ineq):
        if lb[i] > ub[i]:
            print("Fail: lb[i] > ub[i] at index", i)
            return NoneNone 
 
    if num_var != num_ineq:
        print("Fail: This time, we require num_var = num_ineq")
        return NoneNone
    
    N = (num_var + num_ineq) // 2
    
    # heuristic for number of solutions
    DET = abs(mat.det())
    num_sol = 1
    for i in range(N):
        num_sol *= (ub[i] - lb[i] + 1)
    
    if DET == 0:
        print("Fail: Zero Determinant")
        return NoneNone
    else:
        num_sol //= DET
        # + 1 added in for the sake of not making it zero...
        print("Expected Number of Solutions : ", num_sol + 1)
 
    # recentering + scaling process begins
    max_diff = max([ub[i] - lb[i] for i in range(N)])
    applied_weights = []
 
    for i in range(N):
        ineq_weight = max_diff // (ub[i] - lb[i])
        applied_weights.append(ineq_weight)
        for j in range(N):
            mat[j, i] *= ineq_weight
        lb[i] *= ineq_weight
        ub[i] *= ineq_weight
 
    target = vector([(lb[i] + ub[i]) // 2 for i in range(N)])
 
    embedding = 251
 
    Kannan = Matrix(ZZ, N+1, N+1)
    for i in range(0, N):
        for j in range(0, N):
            Kannan[i, j] = mat[i, j]
        Kannan[i, N] = 0
    for i in range(0, N):
        Kannan[N, i] = target[i]
    Kannan[N, N] = embedding
 
    # SVP time
    result = SVP_oracle(Kannan)
 
    # finding solution
    fin_result = None 
    for i in range(N+1):
        isok = True
        if abs(result[i, N]) != embedding:
            isok = False
        result_vector = result[i]
        if result[i, N] == embedding:
            result_vector = -result_vector
            # now result = actual_vector - target
        for j in range(N):
            if (lb[j] <= result_vector[j] + target[j] <= ub[j]) == False:
                isok = False
        if isok == False:
            continue
        print("Found Vector!!")
        fin_result = result_vector[:N] + target
    
    if fin_result == None:
        print("Fail: could not solve...")
        return NoneNone
    
    return fin_result, applied_weights
 
 
 
# r = remote('share-the-flag.chal.acsc.asia', 37896)
# r.interactive()
 
= 251
= bytes.fromhex("02d4623be12c8f01cb2ebe5f837c1d")
= bytes.fromhex("bbdc06ceb34da7b16336b007dc5492")
X2 = bytes.fromhex("2fb9e753b237e68d35e266b0f01c9e")
Y2 = bytes.fromhex("20c0be9140f5a33d71b9e82f8f9409")
X3 = bytes.fromhex("f42e3ee10edeade0a3804a22e86a63")
Y3 = bytes.fromhex("c7224da73d9d96254f94136d9a65f1")
X4 = bytes.fromhex("37c9b07870283dd3f6198c46f027dd")
Y4 = bytes.fromhex("8101a88a365526e8faf417b79599a0")
X5 = bytes.fromhex("b0342cb7b3f5a022d927f9019a1bf3")
Y5 = bytes.fromhex("e2666d892955494775aa3c96c441f5")
X6 = bytes.fromhex("e56bf4f9e746252dbacb93a0a95087")
Y6 = bytes.fromhex("cbb43831857333b2c4663ba2c9189a")
X7 = bytes.fromhex("99ca36b1633cf3d903d8e6291f1bdc")
Y7 = bytes.fromhex("25180068651818171d10422dbdb395")
 
= Matrix(GF(p), 105128)
vec = []
for i in range(105):
    x, y = 00
    if i < 15:
        x = int(X[i])
        y = int(Y[i])
    elif i < 30:
        x = int(X2[i - 15])
        y = int(Y2[i - 15])
    elif i < 45:
        x = int(X3[i - 30])
        y = int(Y3[i - 30])
    elif i < 60:
        x = int(X4[i - 45])
        y = int(Y4[i - 45])
    elif i < 75:
        x = int(X5[i - 60])
        y = int(Y5[i - 60])
    elif i < 90:
        x = int(X6[i - 75])
        y = int(Y6[i - 75])
    elif i < 105:
        x = int(X6[i - 90])
        y = int(Y6[i - 90])
 
    vec.append(y)
    for j in range(16):
        M[i, j] = (x ** j) % p
    if i < 15:
        for j in range(16):
            M[i, j + 16= (x ** (j + 16)) % p
    elif i < 30:
        for j in range(16):
            M[i, j + 32= (x ** (j + 16)) % p
    elif i < 45:
        for j in range(16):
            M[i, j + 48= (x ** (j + 16)) % p
    elif i < 60:
        for j in range(16):
            M[i, j + 64= (x ** (j + 16)) % p
    elif i < 75:
        for j in range(16):
            M[i, j + 80= (x ** (j + 16)) % p
    elif i < 90:
        for j in range(16):
            M[i, j + 96= (x ** (j + 16)) % p
    elif i < 105:
        for j in range(16):
            M[i, j + 112= (x ** (j + 16)) % p
 
vec = vector(GF(p), vec)
 
bas = M.right_kernel().basis()
print(len(bas))
= M.solve_right(vec)
 
# v + bas -> all in 97 ~ 122
 
= Matrix(ZZ, 151151)
lb = [0* 151
ub = [0* 151
 
for i in range(23):
    for j in range(128):
        M[i, j] = int(bas[i][j])
    M[i, 128 + i] = 1
for i in range(128):
    M[23 + i, i] = p
for i in range(128):
    if i >= 16:
        lb[i] = int(97 - int(v[i]))
        ub[i] = int(122 - int(v[i]))
    else:
        lb[i] = int(32-int(v[i]))
        ub[i] = int(128-int(v[i]))
for i in range(23):
    lb[i + 128= 0
    ub[i + 128= p
 
fin_result, applied_weights = solve(M, lb, ub)
 
flag = ''
 
for i in range(16):
    flag += chr((fin_result[i] // applied_weights[i] + int(v[i]) + 251 * 30) % 251)
 
print("ACSC{" + flag + "}")
cs

 

'CTF' 카테고리의 다른 글

TSGCTF 2021 Writeups  (0) 2021.10.03
DUCTF 2021 Writeups  (0) 2021.09.26
CryptoCTF Writeups (DoRSA, Polish)  (0) 2021.08.05
GoogleCTF Quals 2021 Crypto Writeups  (1) 2021.07.19
zer0lfsr 2021 Full Writeup (0CTF/TCTF 2021)  (2) 2021.07.05

Guest Writeup : https://blog.cryptohack.org/cryptoctf2021-hard

'CTF' 카테고리의 다른 글

DUCTF 2021 Writeups  (0) 2021.09.26
ACSC Crypto Writeups  (0) 2021.09.26
GoogleCTF Quals 2021 Crypto Writeups  (1) 2021.07.19
zer0lfsr 2021 Full Writeup (0CTF/TCTF 2021)  (2) 2021.07.05
zer0lfsr 2021 Quick Solutions/Thoughts  (0) 2021.07.05

I played GoogleCTF Quals as a member of Super Guesser. We reached 9th place, which is good enough for a place in the Finals!

I solved all five cryptography problems, one (pythia) with barkingdog. While this was good work, I'm still sad that I couldn't solve the misc "cryptography" problem due to lack of time and creativity (and probably work ethic as well). Here's how we solved the cryptos.

 

NOTE : All challenges from GoogleCTF are open sourced, so check them here. https://github.com/google/google-ctf/tree/master/2021

exploit codes for all five challenges can be found on https://github.com/rkm0959/Cryptography_Writeups/tree/main/2021

 

tiramisu (28 solves)

Step 1 : Basic Analysis

We start by reading some golang codes. We see that the code roughly does the following 

  • We receive the server's ECDSA public key and the encrypted flag message.
  • The flag is encrypted with AES, using the key derived by HMAC with the ECDSA secret key.
  • We can send our ECDSA public key, which would normally allow us to compute the shared secret.
  • Using this shared secret, we send an authenticated encrypted message to the server.
  • The server checks if we sent a valid encryption, and replys with another valid encryption with different IV.

Step 2 : Reducing the Problem and Finding Attack Vector

First, note that HMAC with a strong hash like sha256 is very hard if not impossible to break. 

Therefore, we should probably look deeper into the inner workings of the ECDSA implementation.

Especially, since the flag is encrypted with a HMAC-derived key, we must recover ECDSA secret key at some point.

This immediately implies that the following message is a very scorching hot take.

 

hot take machine

 

After realizing that people are actually solving this problem, we look more into the ECDSA. 

We see that there are actually two curves supported in this implementation, as shown in this code.

 

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
func (server *Server) EstablishChannel(clientHello *pb.ClientHello) error {
    // Load peer key.
    peer, err := proto2ecdsaKey(clientHello.Key)
    if err != nil {
        return err
    }
 
    // Key sanity checks.
    if !peer.Curve.IsOnCurve(peer.X, peer.Y) {
        return fmt.Errorf("point (%X, %X) not on curve", peer.X, peer.Y)
    }
 
    // Compute shared secret.
    P := server.key.Params().P
    D := server.key.D.Bytes()
    sharedX, _ := server.key.ScalarMult(new(big.Int).Mod(peer.X, P), new(big.Int).Mod(peer.Y, P), D)
 
    masterSecret := make([]byte, server.key.Params().BitSize/8)
    sharedX.FillBytes(masterSecret)
 
    // Derive AES+MAC session keys.
    server.channel, err = newAuthCipher(masterSecret, channelCipherKdfInfo, channelMacKdfInfo)
    if err != nil {
        return fmt.Errorf("newAuthCipher()=%v, want nil error", err)
    }
    return nil
}
cs

 

Now we see the vulnerability! The server checks our point lies on our curve, but then takes the coordinates modulo $P$, which is the modulo from the server's curve. Since there are no restrictions on the coordinate size, by Chinese Remainder Theorem, we have a complete control over the final coordinates taken modulo $P$. The server takes this faulty point and multiply it by $D$, the ECDSA secret key.

 

Step 3 : The Invalid Curve Attack

Turns out that the problem now reduces to a known attack called the invalid curve attack. 

Basically, when we work on the Weierstrass curve $y^2 = x^3 + ax + b$, the standard implementations for adding and doubling points never uses the value of $b$. It only uses the value of $a$ and the one or two points of the input. 

Therefore, if the server considers a point $(X, Y)$ and considers it as a point on $y^2 = x^3 + ax + b$ while multiplying a scalar to it, it actually performs valid scalar multiplication, although the computation is on the point $y^2 = x^3 + ax + b'$, where $$b' \equiv Y^2 - X^3 - aX \pmod{p}$$

 

Since we have full control on $(X, Y)$, the result after the coordinates are taken modulo $P$, we can actually select any curves of the form $y^2 = x^3 - 3x + b$. To exploit this even further, we can use the small subgroup attack. Consider we take find a curve $y^2 = x^3 - 3x + b$ which has an order which is a multiple of a small prime $m$. We can then take a point $(X, Y)$ on this curve with order $m$. If we send this point, the shared secret will be the first coordinate of a scalar multiple of $(X, Y)$, which there are only at most $m$ candidates. 

 

Since we can easily determine if our guess for a shared secret is valid by checking whether we receive a reply or not, we can test all $m$ candidates. However, the shared secret is just the first coordinate of the elliptic curve point. This means we will actually get two possible candidates for $D \pmod{m}$, so something like $D \equiv \pm l \pmod{m}$, since the additive inverse of a point has the same first coordinate. 

 

If we gather a lot of such information, we will be able to find a sufficiently small candidates for $D$ by Chinese Remainder Theorem.

To do so, we choose the "signs" in each modulo relation, compute $D$ by Chinese Remainder Theorem, and repeat.

 

Step 4. Implementation and Finish

  • Choosing the Modulo $m$ - Prime : I chose the modulo $m$ to be prime, since they give a lot of information in a sense that $m$ will have GCD 1 with all the previous modulos I used. If you get $\pmod{m_1}$ and $\pmod{m_2}$ information, then you will get $\pmod{\text{lcm}(m_1, m_2)}$ information in total by Chinese Remainder Theorem. Therefore, having $\gcd(m_1, m_2) = 1$ usually helps.
  • Choosing the Modulo $m$ - Size : This is quite important. If we take larger $m$, the number of modulo relations we need to take becomes smaller. This implies that we have an easier offline bruteforce of choosing signs, computing Chinese Remainder parts, and either ECDSA public key verification or AES decryption. However, choosing larger $m$ also implies that we have to search more elliptic curves to find a appropriate one, and we also need to interact with the server more to decide what $D \pmod{m}$ can be. If we take small $m$, everything becomes the opposite. I chose $m$ to be a prime between $300$ and $1000$. In my case, 25 relations were sufficient to find $2^{25}$ candidates for $D$, which is definitely a feasible number of candidates for final offline bruteforce.
  • Multiprocessing : I used it to speedup both the server computation and the offline bruteforce. 
  • Trick from rbtree : Instead of bruteforcing the signs, one can just solve for $D^2$ by CRT and take sqrt. In this case, you would need twice more information on $D \pmod{m}$, but you don't need the extra bruteforce. Great idea from rbtree, shared after contest!

 

pythia (65 solves)

This was a tricky problem for me, as I found the solution nearly immediately but struggled to write the exploit. 

 

Step 1 : Basic Analysis

  • There are a total of $26^3$ possible passwords, each corresponding to a 16 bytes AES-GCM key.
  • There is a password (key) we have to find. We must do it using around 48 queries of the following type.
  • We can send some nonce/ciphertext/tag to be AES-GCM decrypted with the key without any associated data.
  • The only response we get from the queries are whether or not the decryption process was successful.

Note that there are actually 150 queries and 3 passwords, but since each three problems are independent, we can just focus on one.

 

Step 2 : Reducing the Problem and Finding Attack Vector

In these types of problems, it's best to halve the number of candidates by each query.

To do so, we have to figure out how to build some nonce/ciphertext/tag that accepts all of the keys from a given set.

If we can do this, we can simply perform a "binary search" to get each password. This is a good time to look at how AES-GCM works. 

 

Step 3 : AES-GCM and Polynomial Fun

Here, we only consider the case where there are no associated data and we have 12 byte nonce. Everything is in $GF(2^{128})$.

Let $K$ be the key, $C = C_0 || C_1 || \cdots || C_{n-1}$ the ciphertext, and $IV$ the nonce. We now calculate $$H = AES_K(0^{128}), \quad J_0 = IV || 0^{31} || 1, \quad L = 0^{64} || len(C)$$

The tag $T$ is now calculated as $T = C_0 H^{n+1} + C_1 H^n + \cdots + L \cdot H + AES_K(J_0)$. 

 

Let's fix $IV= 0^{96}$. If we know $K$ and $n$, then we can calculate $H, J_0, L, AES_K(J_0)$. 

Now we want the polynomial coefficients $C_0, C_1, \cdots, C_{n-1}, T$ such that $$C_0 H^{n+1} + C_1 H^n + \cdots + L \cdot H + T = AES_K(J_0)$$ for a given set of $K$. To be exact, if we have $K_1, K_2, \cdots , K_m$ as a target set of keys, we compute the corresponding $H_1, H_2, \cdots , H_m$.

Then, we will try to find $n, C_0, C_1, \cdots , C_{n-1}, T$ such that $$C_0 H_t^{n+1} + C_1 H_t^n + \cdots + C_{n-1} H_t^2 + L H_t + T = AES_{K_t}(J_0)$$ for each $1 \le t \le m$. This is starting to look like Lagrange interpolation, with the caveat that $L$ is determined by $n$.

 

Take $n=m$. Let $f(x) = C_0 x^{m+1} + \cdots + C_{m-1} x^2 + L x + T$, which is a polynomial we want to find.

Our desired $f$ must satisfy $f(H_t) = AES_{K_t}(J_0)$ for each $1 \le t \le m$.

This $f$ can be found directly with Lagrange interpolation, and $f$ will have degree at most $m-1$. However, we have to set $f$'s coefficient of $x$ to be equal to $L$. This can be done by adding some scalar multiple of $g = (x-H_1)(x-H_2) \cdots (x-H_m)$.

 

However, if this $g$ has the coefficient of $x$ equal to $0$, we cannot control this coefficient by adding $g$.

In this case, we have to control it by multiplying some random degree 1 polynomial.

Finally, note that we can put any number of zero blocks at the front of the ciphertext to meet the length requirement if $\deg f < m+1$. 

 

There are a lot of methods to handle these sepcial cases, (i.e. $g$ can't control the coefficient) and the solution writeen by barkingdog is a bit different from what I wrote as well. He handled these cases by simply adding a few random points for the interpolation part of $f$.

This makes $g$ and it's coefficients much more random, making it unlikely for the coefficient to be zero.

I would like to note that is error is a very unlikely case, so not error-handling these special cases won't hurt. 

 

Step 4 : Implementation Details

  • Fast Interpolation : If we try to find $C_i, T$ with a system of linear equations, the algorithm will take $\mathcal{O}(n^3)$ time which is quite slow. A relatively naive lagrange interpolation takes $\mathcal{O}(n^2)$ time. I suggest just using SageMath's lagrange polynomial function.
  • Binary Search : If we just use standard binary search, we would have to compute $f$ for a set of $26^3/2$ keys. This is quite large for us to handle, and it may not even fit the payload size. Since we have much more than $\log(26^3) < 15$ queries per password, we can try to make our interpolation computation easier at the cost of using more queries. Here is one way to do so - we divide the set of $26^3$ keys into $40$ batches of size around $440$. We find which batch the key is in, and then use standard binary search. This fits in the query limit quite tightly. Note that if we found the batch that has the key, we do not need to check the other batches. 
  • Cache : Note that the payload to test whether the key is in a certain set can be precomputed beforehand. Therefore, by using a cache or hashmap or whatever, we can save ourselves from repeating the same computation over and over again. This is especially useful when we are testing the $40$ batches - we will test the same batches for all three passwords, and computing the payload for all of them takes quite a while. By saving these payloads, we can save quite a lot of time on the exploit. 
  • AES-GCM : endian stuff are very confusing to figure out, so you have to really concentrate

 

h1 (23 solves)

Step 1 : Basic Analysis

  • We have some ECDSA looking stuff (it's actually just elliptic curve operations with Jacobians)
  • We have some signatures, but we do not know any public key.
  • Alice has sent two messages and signatures, but one of the messages contain the unknown flag.
  • Bob has sent two messages and signatures, and we know both messages and their hashes.
  • We also know the AES encrypted messages (one contains flag) with the shared secret as the key.
  • The ECDSA nonce is yelling loudly at us to attack it which we obviously have to do.

Step 2 : Reducing the Problem and Finding Attack Vector

Obviously there is something going on with the custom RNG, and it becomes apparent if we read the code. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
def RNG(nbits, a, b):
    nbytes = nbits // 8
    B = os.urandom(nbytes)
    return a * sum([B[i] * b ** i for i in range(len(B))]) % 2**nbits
 
def Sign(msg, d):
    h = hashlib.sha512(msg)
    z = Transform(int.from_bytes(h.digest(), 'big'), h.digest_size*8)
    k = RNG(n.bit_length(), 168430094294967296)
    x1, y1, z1 = Multiply(G, k)
    r = (x1 * pow(z1, -2, mod) % mod) % n
    s = pow(k, -1, n) * (z + r * d) % n
    return r, s
cs

 

We see that $n$ is $512$ bits, and our RNG call uses $b = 2^{32}$. Therefore, in the RNG call, the nonce can be written as $$ \sum_{i=0}^{15} a \cdot byte_i \cdot 2^{32i}$$ since all values from $i \ge 16$ becomes $0$ modulo $2^{512}$. Note that $255a < 2^{32}$, so no modulo operations are needed.

 

Since each $byte_i$ has 8 bit of "entropy", in total the RNG has only 128 bits of "entropy", which is horrible.

This also means that each complete message/signature pair gives us around 384 bits of information on the secret key.

Since we have two such pairs for Bob's secret key $db$, we should be able to extract this value.

 

Clearly, computing the shared secret solves the problem, so we just need the public key of Alice. 

This is not hard, as given one complete message/signature you can recover the public key. This is even in wikipedia.

NOTE : There may be more than one public key possible, so try all of them to find the flag.

 

Therefore, clearly getting the secret key $db$ is the hardest part of the challenge, unless you are rkm0959 who doesn't know fundamentals.

 

rkm0959 wonders how to get da (private) while he only needs Qa (public)

 

Step 3 : Yet Another Inequality Solving with CVP!

Consider the message, signature pair $(r_1, s_1, z_1)$ and $(r_2, s_2, z_2)$, where $z$'s are the hashes. We know $$ \left( \sum_{i=0}^{15} a \cdot byte_{1, i} \cdot 2^{32i} \right) \cdot s_1 \equiv z_1 + r_1 d \pmod{n}$$ $$ \left( \sum_{i=0}^{15} a \cdot byte_{2, i} \cdot 2^{32i} \right) \cdot s_2 \equiv z_2 + r_2 d \pmod{n}$$ Since we can, we will remove $d$ from this system by writing $$ \left( \sum_{i=0}^{15} a \cdot r_2 \cdot byte_{1, i} \cdot 2^{32i} \right) \cdot s_1 - \left( \sum_{i=0}^{15} a \cdot r_1 \cdot byte_{2, i} \cdot 2^{32i} \right) \cdot s_2 \equiv z_1r_2 - z_2r_1 \pmod{n}$$ Now we can consider $33$ variables $byte_{1, i}, byte_{2, i}, t$ for $0 \le i \le 15$, and use the $33$ inequalities $$\left( \sum_{i=0}^{15} a \cdot r_2 \cdot byte_{1, i} \cdot 2^{32i} \right) \cdot s_1 - \left( \sum_{i=0}^{15} a \cdot r_1 \cdot byte_{2, i} \cdot 2^{32i} \right) \cdot s_2 + tn =  z_1r_2 - z_2r_1 $$ $$ 0 \le byte_{1, i}, byte_{2, i} \le 255$$ and plug this into Inequality Solving with CVP repository. This gives us $db$ and it solves the problem. 

 

tonality (30 solves)

Step 1 : Basic Analysis

  • We get the ECDSA public key $P = dG$ and two messages.
  • We can send a integer $t$, and we get the signature of first message signed with private key $td$.
  • We now have to forge any signature of the second message with private key $d$. 

 

Step 2 : Finding Attack Vector, and "Wishful Thinking"

There's nothing really going on in this problem, so we just need to use the first query very well.

Denote the two messages $m_0, m_1$ and their hashes $z_0, z_1$. Denote the integer we will send as $t$. 

 

We will receive the signature $r_0, s_0$ such that the point $$ (z_0 / s_0) G + (r_0 / s_0) tP $$ has the first coordinate equal to $r_0$. Here, the integer division is done modulo $n$, the order of elliptic curve.

 

We now have to forge a signature $r_1, s_1$ such that the point $$ (z_1 / s_1) G + (r_1 / s_1)P$$ has the first coordinate equal to $r_1$. Here, the integer division is done modulo $n$, the order of elliptic curve. 

 

We wish to solve this in the easiest way possible - and this will happen when $$ z_0/s_0 = z_1/s_1 , \quad (r_0 / s_0)t = (r_1/s_1), \quad r_0 = r_1$$ which can be rearranged as $$t = z_0 / z_1, \quad r_1 = r_0, \quad s_1 = z_1s_0/z_0$$ and submitting this to the server solves the problem.

 

story (32 solves)

Step 1 : Basic Analysis

  • We have to send a sufficiently large UTF-8 string to the server.
  • We receive the CRC16, CRC32, CRC64 values of the sent string back.
  • We also receive the desired CRC16, CRC32, CRC64 values from the server.
  • We have to make a string with the desired CRC values, with the same lowercase text as the original string.

Step 2 : Finding Attack Vector

The obvious problem is that CRCs are not meant to be an encryption of some sort. They satisfy the powerful relation $$crc(x) \oplus crc(y)  \oplus crc(z) = crc(x \oplus y \oplus z)$$ which can be used to solve this problem relatively easily. 

 

There are several ways to solve this problem, maybe even using finite field representation of CRC. However, I still think the method in Step 3 is much easier to deal with than dealing endian issues and figuring out which polynomial is actually used to compute CRC.

 

Step 3 : Exploiting with Linearity

Denote $x_{16}, x_{32}, x_{64}$ be the CRC values of "a" * 256.

Also, for each $0 \le i \le 255$, we denote $x'_{i, 16}, x'_{i, 32}, x'_{i, 64}$ as the CRC values of "a" * 256 but the $i$th 'a' is changed to 'A'. 

These values can be computed by 257 server interactions, and the CRC computation is of course deterministic. 

 

Now we can build the CRC values of any length 256 string with 'a' and 'A' only. We start with $x_{16}, x_{32}, x_{64}$, and if the $i$th character is 'A' instead of 'a', we XOR the values $x'_{i, 16} \oplus x_{16}, x'_{i, 32} \oplus x_{32}, x'_{i, 64} \oplus x_{64}$ to our CRC result. 

 

Now changing 'a' to 'A' appropriately for the result CRC to match the target is something like a subset sum problem with bitstring XOR.

Of course, looking at each bit separately, this is nothing more than a system of linear equations over $\mathbb{F}_2$.

 

We solve this, find the locations where we have to change the lowercase/uppercase, and get the flag.

'CTF' 카테고리의 다른 글

ACSC Crypto Writeups  (0) 2021.09.26
CryptoCTF Writeups (DoRSA, Polish)  (0) 2021.08.05
zer0lfsr 2021 Full Writeup (0CTF/TCTF 2021)  (2) 2021.07.05
zer0lfsr 2021 Quick Solutions/Thoughts  (0) 2021.07.05
LINE CTF Crypto Writeups  (0) 2021.03.22

This writeup is a compilation of my thoughts and discussions with others.

My solutions are at https://github.com/rkm0959/Cryptography_Writeups/tree/main/2021/0ctfquals.

 

Problem 1 : zer0lfsr- (35 solves)

 

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
#!/usr/bin/env python3
 
import random
import signal
import socketserver
import string
from hashlib import sha256
from os import urandom
from secret import flag
 
def _prod(L):
    p = 1
    for x in L:
        p *= x
    return p
 
def _sum(L):
    s = 0
    for x in L:
        s ^= x
    return s
 
def n2l(x, l):
    return list(map(int'{{0:0{}b}}'.format(l).format(x)))
 
class Generator1:
    def __init__(self, key: list):
        assert len(key) == 64
        self.NFSR = key[: 48]
        self.LFSR = key[48: ]
        self.TAP = [011215]
        self.TAP2 = [[2], [5], [9], [15], [22], [26], [39], [2630], [59], [152226], [152239], [9222639]]
        self.h_IN = [2471527]
        self.h_OUT = [[1], [3], [03], [012], [023], [024], [0124]]
 
    def g(self):
        x = self.NFSR
        return _sum(_prod(x[i] for i in j) for j in self.TAP2)
 
    def h(self):
        x = [self.LFSR[i] for i in self.h_IN[:-1]] + [self.NFSR[self.h_IN[-1]]]
        return _sum(_prod(x[i] for i in j) for j in self.h_OUT)
 
    def f(self):
        return _sum([self.NFSR[0], self.h()])
 
    def clock(self):
        o = self.f()
        self.NFSR = self.NFSR[1: ] + [self.LFSR[0] ^ self.g()]
        self.LFSR = self.LFSR[1: ] + [_sum(self.LFSR[i] for i in self.TAP)]
        return o
 
class Generator2:
    def __init__(self, key):
        assert len(key) == 64
        self.NFSR = key[: 16]
        self.LFSR = key[16: ]
        self.TAP = [035]
        self.f_IN = [01020304047]
        self.f_OUT = [[0123], [01245], [0125], [012], [01345], [0135], [013], [014], [015], [02345], [
            023], [035], [12345], [1234], [1235], [12], [135], [13], [14], [1], [245], [24], [2], [34], [45], [4], [5]]
        self.TAP2 = [[037], [1111315], [29]]
        self.h_IN = [024681314]
        self.h_OUT = [[012345], [01246], [134]]
 
    def f(self):
        x = [self.LFSR[i] for i in self.f_IN]
        return _sum(_prod(x[i] for i in j) for j in self.f_OUT)
 
    def h(self):
        x = [self.NFSR[i] for i in self.h_IN]
        return _sum(_prod(x[i] for i in j) for j in self.h_OUT)        
 
    def g(self):
        x = self.NFSR
        return _sum(_prod(x[i] for i in j) for j in self.TAP2)  
 
    def clock(self):
        self.LFSR = self.LFSR[1: ] + [_sum(self.LFSR[i] for i in self.TAP)]
        self.NFSR = self.NFSR[1: ] + [self.LFSR[1] ^ self.g()]
        return self.f() ^ self.h()
 
class Generator3:
    def __init__(self, key: list):
        assert len(key) == 64
        self.LFSR = key
        self.TAP = [055]
        self.f_IN = [081624324063]
        self.f_OUT = [[1], [6], [012345], [01246]]
 
    def f(self):
        x = [self.LFSR[i] for i in self.f_IN]
        return _sum(_prod(x[i] for i in j) for j in self.f_OUT)
 
    def clock(self):
        self.LFSR = self.LFSR[1: ] + [_sum(self.LFSR[i] for i in self.TAP)]
        return self.f()
 
class zer0lfsr:
    def __init__(self, msk: int, t: int):
        if t == 1:
            self.g = Generator1(n2l(msk, 64))
        elif t == 2:
            self.g = Generator2(n2l(msk, 64))
        else:
            self.g = Generator3(n2l(msk, 64))
        self.t = t
 
    def next(self):
        for i in range(self.t):
            o = self.g.clock()
        return o
 
class Task(socketserver.BaseRequestHandler):
    def __init__(self*args, **kargs):
        super().__init__(*args, **kargs)
 
    def proof_of_work(self):
        random.seed(urandom(8))
        proof = ''.join([random.choice(string.ascii_letters + string.digits + '!#$%&*-?'for _ in range(20)])
        digest = sha256(proof.encode()).hexdigest()
        self.dosend('sha256(XXXX + {}) == {}'.format(proof[4: ], digest))
        self.dosend('Give me XXXX:')
        x = self.request.recv(10)
        x = (x.strip()).decode('utf-8'
        if len(x) != 4 or sha256((x + proof[4: ]).encode()).hexdigest() != digest: 
            return False
        return True
 
    def dosend(self, msg):
        try:
            self.request.sendall(msg.encode('latin-1'+ b'\n')
        except:
            pass
 
    def timeout_handler(self, signum, frame):
        raise TimeoutError
 
    def handle(self):
        try:
            signal.signal(signal.SIGALRM, self.timeout_handler)
            signal.alarm(30)
            if not self.proof_of_work():
                self.dosend('You must pass the PoW!')
                return
            signal.alarm(50)
            available = [123]
            for _ in range(2):
                self.dosend('which one: ')
                idx = int(self.request.recv(10).strip())
                assert idx in available
                available.remove(idx)
                msk = random.getrandbits(64)
                lfsr = zer0lfsr(msk, idx)
                for i in range(5):
                    keystream = ''
                    for j in range(1000):
                        b = 0
                        for k in range(8):
                            b = (b << 1+ lfsr.next()
                        keystream += chr(b)
                    self.dosend('start:::' + keystream + ':::end')
                hint = sha256(str(msk).encode()).hexdigest()
                self.dosend('hint: ' + hint)
                self.dosend('k: ')
                guess = int(self.request.recv(100).strip())
                if guess != msk:
                    self.dosend('Wrong ;(')
                    self.request.close()
                else:
                    self.dosend('Good :)')
            self.dosend(flag)
        except TimeoutError:
            self.dosend('Timeout!')
            self.request.close()
        except:
            self.dosend('Wtf?')
            self.request.close()
 
class ThreadedServer(socketserver.ForkingMixIn, socketserver.TCPServer):
    pass
 
if __name__ == "__main__":
    HOST, PORT = '0.0.0.0'31337
    server = ThreadedServer((HOST, PORT), Task)
    server.allow_reuse_address = True
    server.serve_forever()
 
cs

 

Introduction

There are three LFSR/NFSR based keystream generators. We are given

  • first 40000 bits of the keystream
  • SHA256 hash value of the key

We need to find the key for each generators, but actually we only need to do 2 out of 3. 

Note that the three problems are completely independent in this challenge. We will do #1, #3.

 

Solution for Generator3

Generator3 is a good target because it doesn't have any NFSR parts. However, it does have a "filter function" $f$. 

There are two methods to get around this filter function $f$, both which can be derived from classic approaches.

NOTE : Read https://doc.sagemath.org/html/en/reference/cryptography/sage/crypto/boolean_function.html

 

The first method computes the algebraic immunity of $f$. Sagemath gives us that $f$ has algebraic immunity of $2$, which means that there exists a polynomial $g$ of degree $2$ such that $fg = 0$ for all inputs. If $f = 1$, which we can directly verify using the output bit, we can immediately recover $g = 0$. Since $g$ has degree $2$ and every LFSR bit is a linear combination of the initial 64 bit key, each information $g = 0$ gives us a linear equation on $\displaystyle 64 + \binom{64}{2}$ monomials of degree no more than $2$. Note that $x^2 = x$ on $\mathbb{F}_2$. Since we have 40000 bits of keystream, we will get about 20000 linear equations. We can now solve the system of linear equation, and recover the 64 bit key as desired.  

 

The second method notes that for $$f_3(x_0, x_1, x_2, x_3, x_4, x_5, x_6) = x_1 + x_6 + x_0 x_1 x_2 x_3 x_4 x_5 + x_0 x_1 x_2 x_4 x_6$$ we have the incredibly precise linear approximation of $$\overline{f}_3(x_0, x_1, x_2, x_3, x_4, x_5, x_6) = x_1 + x_6$$ which matches $f_3$ for a ton of inputs, I believe $31/32$ of them.

 

This implies that we have a alternate keystream that directly follows the LFSR recurrence and matches the actual keystream quite well. This alternate keystream can now be found by fast correlation attack. The real key can be quickly derived from the alternate keystream. 

 

NOTE : Read https://iacr.org/archive/fse2011/67330055/67330055.pdf for the fast correlation attack.

 

I used the first method for solving this part, as I was not (and still not) familiar with the fast correlation attack.

 

Solution for Generator1

I chose Generator1 as a good target, because we can bruteforce all $16$ bits of the LFSR part of the key.

If we do this, we can derive all LFSR results by direct calculation. We now need to recover the NFSR part. 

 

The key idea here is that the function $h_1$, which is $$h_1(x_0, x_1, x_2, x_3, x_4) = x_1 + x_3 + x_0 x_3 + x_0 x_1 x_2 + x_0 x_2 x_3 + x_0 x_2 x_4 + x_0 x_1 x_2 x_4$$ has a close function independent of $x_4$, which is $$\overline{h}_1(x_0, x_1, x_2, x_3) = x_1 + x_3 + x_0 x_3 + x_0 x_1 x_2 + x_0 x_2 x_3$$ This matches $h_1$ for $15/16$ of the input. If we look at how the function $h$ is calculated, it takes the first four bits from the LFSR and the last bit from NFSR. The problem is that we don't really know much about the NFSR. However, we can now use $\overline{h}_1$ and the four bits from LFSR. 

 

This implies, after bruteforcing the 16 bits of the LFSR, we can calculate all $h$ values, each with probability $15/16$. Since we know the output bits $f$, which is the XOR of the first bit of NFSR and $h$, this means we can recover each NFSR bit with probability $15/16$.

We do this $48$ times to find all NFSR bits. The success probability is around 4%, which is pretty decent. This solves Problem 1.

 

Problem 2 : zer0lfsr+ (2 solves)

 

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
def b2n(x):
    return int.from_bytes(x, 'big')
 
def n2l(x, l):
    return list(map(int'{{0:0{}b}}'.format(l).format(x)))
 
def split(x, n, l):
    return [(x >> (i * l)) % 2**for i in range(n)][::-1]
 
def combine(x, n, l):
    return sum([x[i] << (l * (n - i - 1)) for i in range(n)])
 
class KDF:
    def __init__(self, key: int):
        self.msk = key
        self.SBOX = [1251271593013146810411]
        self.idx = [[03], [01], [23], [03]]
 
    def substitue(self, x):
        return [self.SBOX[i] for i in x]
 
    def expand(self):
        h = sha256(str(self.msk).encode()).digest()
        rnd_key = [h[: 2], h[24], h[24], h[46]]
        rnd_key = list(map(b2n, rnd_key))
        chunk = split(self.msk, 416)
        sub_key = [combine(self.substitue(split(chunk[self.idx[i][0]] ^ chunk[self.idx[i][1]] , 44)), 44for i in range(4)]
        final_key = [rnd_key[i] ^ sub_key[i] for i in range(4)]
        return combine(final_key, 416)
 
class zer0lfsr:
    def __init__(self, msk: int):
        self.key = []
        for i in range(3):
            msk = KDF(msk).expand()
            self.key.append(msk)
        self.g1 = Generator1(n2l(self.key[0], 64))
        self.g2 = Generator2(n2l(self.key[1], 64))
        self.g3 = Generator3(n2l(self.key[2], 64))
 
    def next(self):
        o1 = self.g1.clock()
        o2 = self.g2.clock()
        o2 = self.g2.clock()
        o3 = self.g3.clock()
        o3 = self.g3.clock()
        o3 = self.g3.clock()
        o = (o1 * o2) ^ (o2 * o3) ^ (o1 * o3)
        return o
 
class Task(socketserver.BaseRequestHandler):
    def __init__(self*args, **kargs):
        super().__init__(*args, **kargs)
 
    def proof_of_work(self):
        random.seed(urandom(8))
        proof = ''.join([random.choice(string.ascii_letters + string.digits + '!#$%&*-?'for _ in range(20)])
        digest = sha256(proof.encode()).hexdigest()
        self.dosend('sha256(XXXX + {}) == {}'.format(proof[4: ], digest))
        self.dosend('Give me XXXX:')
        x = self.request.recv(10)
        x = (x.strip()).decode('utf-8'
        if len(x) != 4 or sha256((x + proof[4: ]).encode()).hexdigest() != digest: 
            return False
        return True
 
    def dosend(self, msg):
        try:
            self.request.sendall(msg.encode('latin-1'+ b'\n')
        except:
            pass
 
    def timeout_handler(self, signum, frame):
        raise TimeoutError
 
    def handle(self):
        try:
            signal.signal(signal.SIGALRM, self.timeout_handler)
            signal.alarm(30)
            if not self.proof_of_work():
                self.dosend('You must pass the PoW!')
                return
            lfsr = zer0lfsr(random.getrandbits(64))
            for i in range(20):
                keystream = ''
                for j in range(1000):
                    b = 0
                    for k in range(8):
                        b = (b << 1+ lfsr.next()
                    keystream += chr(b)
                self.dosend('start:::' + keystream + ':::end')
            signal.alarm(100)
            self.dosend('k1: ')
            k1 = int(self.request.recv(100).strip())
            self.dosend('k2: ')
            k2 = int(self.request.recv(100).strip())
            self.dosend('k3: ')
            k3 = int(self.request.recv(100).strip())
            if lfsr.key == [k1, k2, k3]:
                self.dosend(flag)
            else:
                self.dosend('Wrong ;(')
        except TimeoutError:
            self.dosend('Timeout!')
            self.request.close()
        except:
            self.dosend('Wtf?')
            self.request.close()
cs

 

Introduction

The three Generators are identical to the first problem. The major differences here is 

  • We now have the "majority" of the three output bits from each generators. 
  • We now have 160000 bits, but we don't have the SHA256 hash of each key.
  • The three keys are related by the "KDF" function, i.e. $key_2 = KDF(key_1)$, $key_3 = KDF(key_2)$.
  • We have to find all three keys in 100 seconds to solve the problem.

Of course, the majority function has a 75% correlation with each generator's output bits. 

If we know one generator keystream, we can guarantee about 25% of the other generator keystream bits.

If we know two generator keystreams, we can guarantee about 50% of the other generator keystream bits.

 

A look at the KDF

After having a miserable day with this challenge, barkingdog noted an important property of the KDF function. 

His intuition was that the KDF function was way too complicated for it to be a "random function". Indeed, if we just wanted a random output, we can just return some bits of the SHA256 hash. Also, he noted that the repeated values in the round key and subkeys are suspicious.

These suspicions are indeed valid, as there is a important result on this KDF.

 

Claim : Let $K = K_0 || K_1 || K_2 || K_3$. Assume we know $KDF(K)$.

If we know one value out of $K_0 \oplus K_1$ and $K_2 \oplus K_3$, we can calculate the other value very fast. 

 

To explain this, note that the two round keys in the middle are the same, which means we can recover the XOR value of the two subkeys from $KDF(K)$. The two subkeys are some reversible and bijective transformations applied on $K_0 \oplus K_1$ and $K_2 \oplus K_3$.

This gives a straightforward method to accomplish our objective of the Claim. This result is very strong, and used heavily in my solution. 

 

Let's think about the order in which we are going to solve the problem. 

  • If we start from Generator1 and succeed in finding $key_1$, we can just use KDF function to directly finish.
  • If we start from Generator3 and succeed in finding $key_3$, we can use the KDF property to make solving Generator2 easier.

Of course, analyzing Generator1 with just 75% correlation seems very difficult, so we have to start from Generator3. 

Therefore, the natural idea is to go from 3 to 2 to 1, utilizing known keys and the KDF property to make things easier each step. 

 

Solution for Generator3

The fast correlation attack solution works here, since we still have nearly 75% correlation to a simple LFSR.

I had some time trying to make this solution stable, as the attack is probabilistic. I ended up doing

  • Find 64 bits that have highest probability of being correct, and assume at most one is incorrect.
  • We compute all possible values for $key_3$, and calculate the Generator3 keystream.
  • If it matches at least 7000 out of the first 10000 given output bits, then we found the key.

Honestly, I'm definitely not the person to ask how to write a reliable fast correlation attack :(

My attack for Generator3 succeeded in about 50% probabilty, and it worked in 8 seconds.

 

Solution for Generator2

From solving Generator3, we have some additional information at our hands, i.e.

  • We know around 40000 bits of the Generator1 output, and the same for Generator2.
  • We know $key_3 = KDF(key_2)$, so we can use the KDF property, giving easy 16 bits of information.

To solve Generator2, we need two properties of $f_2$ and $h_2$. First, note that $$h_2(x_0, x_1, x_2, x_3, x_4, x_5, x_6) = x_0 x_1x_2 x_3 x_4 x_5 + x_0 x_1 x_2 x_4 x_6 + x_1 x_3 x_4$$ is $0$ for $7/8$ of the inputs. Therefore, we can ignore the $h$ part of the clock output with high probability. Also, we note that $$f_2(x_0, x_1, x_2, x_3, x_4, x_5)$$ which is a very long polynomial defined by array f_OUT, has a linear approximation $$\overline{f}_2 (x_0, x_1, x_2, x_3, x_4, x_5) = x_1 + x_2 + x_5$$ which matches $f_2$ with a $3/4$ probability. This can be found by calculating the nonlinearity of $f_2$. 

 

Therefore, the output of Generator2 is close (matches with significantly higher probability than 0.5) to a keystream generated by a LFSR with the same recurrence. If we find this LFSR keystream, we can find the LFSR part of the $key_2$. The NFSR part of the $key_2$ can be directly derived by the KDF property. To find this LFSR keystream, we use yet another fast correlation attack. To be exact, I did

  • I only considered the approximately 40000 guaranteed Generator2 bits.
  • I found 32 bits that have the highest probability of being correct, and assumed at most one is incorrect.
  • Now we have around $32 \times 2^{16}$ possibilities for the LFSR part of the key.
  • Here, the $32$ part comes from assuming one bit is incorrect.
  • The $2^{16}$ part comes because the LFSR part is 48 bits, and we know 32 bits of the LFSR output.
  • If we know LFSR part of the key, the NFSR part of the key can be computed by the KDF property. 
  • For each possible key, we compute the KDF and see if it matches $key_3$. 

My attack for Generator2 succeeded in about 25% probability, and it worked in 20 seconds.

 

Solution for Generator1

From solving Generator2, we have some additional information at our hands, i.e.

  • We know around 80000 bits of the Generator1 output.
  • We know $key_2 = KDF(key_1)$, so we can use the KDF property, giving easy 16 bits of information.

Here's a rough idea. If we bruteforce LFSR part, using the same ideas as Problem 1, we can get 24 bits of the NFSR part. Also, we have the 16 bits of information from the KDF property. This adds up to 40 bits of extra information. Therefore, intuitively, we can find about $2^{24}$ candidates for the first key. After that, we can test each of them by either calculating the KDF or calculating the Generator1 output, and testing if it matches the guaranteed Generator1 bits. To be more detailed, I did as follows : 

  • I bruteforced the 16 bit LFSR part, and this gives approximately 24 bits of NFSR information
  • This means we have full $K_3$ and around 8 bits of each of $K_0, K_1, K_2$. 
  • We bruteforce the undecided bits of $K_2$. Now we know $K_2 \oplus K_3$, so $K_0 \oplus K_1$ as well.
  • We know around 16 bits of $K_0, K_1$ in total, and we know $K_0 \oplus K_1$.
  • This is enough to get a small number of candidates for $K_0, K_1$. 
  • In total, this is around $2^{24}$ level of bruteforce to find all candiates. 

I did this in C++ with 10 cores. I checked the validity of each key by testing if the Generator1 output matches the guranteed bits. 

My attack for Generator3 succeeded in about 20% probability because the code had to run in time. I set a timeout of 60 seconds.

 

Overall, my code found the 3 keys in about 25 trials, but I made a dumb programming mistake, forcing me to fix and try again.

The next time, the code found the 3 keys in about 90 trials, and I got the flag. I think the code works in about 2~3% probability.

 

Problem 3 : zer0lfsr++ (1 solve)

 

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
class zer0lfsr:
    def __init__(self):
        self.key = [random.getrandbits(64), random.getrandbits(64), random.getrandbits(64)]
        self.g1 = Generator1(n2l(self.key[0], 64))
        self.g2 = Generator2(n2l(self.key[1], 64))
        self.g3 = Generator3(n2l(self.key[2], 64))
 
    def next(self):
        o1 = self.g1.clock()
        o2 = self.g2.clock()
        o2 = self.g2.clock()
        o3 = self.g3.clock()
        o3 = self.g3.clock()
        o3 = self.g3.clock()
        o = (o1 * o2) ^ (o2 * o3) ^ (o1 * o3)
        return o
 
class Task(socketserver.BaseRequestHandler):
    def __init__(self*args, **kargs):
        super().__init__(*args, **kargs)
 
    def proof_of_work(self):
        random.seed(urandom(8))
        proof = ''.join([random.choice(string.ascii_letters + string.digits + '!#$%&*-?'for _ in range(20)])
        digest = sha256(proof.encode()).hexdigest()
        self.dosend('sha256(XXXX + {}) == {}'.format(proof[4: ], digest))
        self.dosend('Give me XXXX:')
        x = self.request.recv(10)
        x = (x.strip()).decode('utf-8'
        if len(x) != 4 or sha256((x + proof[4: ]).encode()).hexdigest() != digest: 
            return False
        return True
 
    def dosend(self, msg):
        try:
            self.request.sendall(msg.encode('latin-1'+ b'\n')
        except:
            pass
 
    def timeout_handler(self, signum, frame):
        raise TimeoutError
 
    def handle(self):
        try:
            signal.signal(signal.SIGALRM, self.timeout_handler)
            signal.alarm(5)
            self.dosend('Input the flag of zer0lfsr+: ')
            guess = self.request.recv(100).strip()
            assert guess == flag_plus
            signal.alarm(50)
            if not self.proof_of_work():
                self.dosend('You must pass the PoW!')
                return
            lfsr = zer0lfsr()
            for i in range(20):
                keystream = ''
                for j in range(1000):
                    b = 0
                    for k in range(8):
                        b = (b << 1+ lfsr.next()
                    keystream += chr(b)
                self.dosend('start:::' + keystream + ':::end')
            signal.alarm(180)
            self.dosend('hint: ')
            idx = int(self.request.recv(10).strip())
            assert idx in [012]
            self.dosend(str(lfsr.key[idx] >> 48))
            self.dosend('k1: ')
            k1 = int(self.request.recv(100).strip())
            self.dosend('k2: ')
            k2 = int(self.request.recv(100).strip())
            self.dosend('k3: ')
            k3 = int(self.request.recv(100).strip())
            if lfsr.key == [k1, k2, k3]:
                self.dosend(flag)
            else:
                self.dosend('Wrong ;(')
        except TimeoutError:
            self.dosend('Timeout!')
            self.request.close()
        except:
            self.dosend('Wtf?')
            self.request.close()
cs

 

Introduction

The change from lfsr+ to lfsr++ is

  • The three keys are independent of each other, so no KDF property to exploit.
  • We can ask for the first 16 bits of one of the three keys.
  • We have more time, 180 seconds instead of 100 seconds

It's clear that Generator3 can be solved in the exact same way. The problem is Generator2 and Generator1.

 

Solutions and Thoughts

 

Assume we use the hint on Generator2. Then we can get the NFSR part of the key. 

Therefore, it's clear that the solution from Problem 2 works in this challenge as well. 

Of course, we need to check the validity of the key differently (instead of using KDF) but this is not hard. 

We can just check if the Generator2 output matches the guaranteed Generator2 bits. 

 

If we use the hint on Generator2, we have nothing to use on Generator1, other than around 80000 bits of Generator1 guranteed.

This can be solved in two ways, as shown by the author and the only solver hellman : 

 

I thought about using the hint on Generator1 (which makes solving Generator1 relatively the same) and optimizing the fast correlation attack for Generator2, making it more reliable. For example, if I could find 48 bits that I can guarantee with high probability, allowing at most one mistake, I would be able to solve the problem. I was able to get 43 out of 44 bits correct, giving a complexity of around $44 \cdot 2^{20}$, which may be feasible. However, according to the author, the NFSR part of the key may not be uniquely determined. Therefore, this approach leads to a solution that is both slow (but probably within time limit with sufficient computational power) and has low success rate.

I was tired staying up all night solving lfsr+ and I didn't have a lot of time, so I did not implement my approach. 

 

Looking back, if I had just decided to use z3 on Generator1, I think I would've solved it. I need to practice using z3...

'CTF' 카테고리의 다른 글

CryptoCTF Writeups (DoRSA, Polish)  (0) 2021.08.05
GoogleCTF Quals 2021 Crypto Writeups  (1) 2021.07.19
zer0lfsr 2021 Quick Solutions/Thoughts  (0) 2021.07.05
LINE CTF Crypto Writeups  (0) 2021.03.22
zer0pts CTF 2021 Crypto Writeups  (1) 2021.03.07

This is an extremely rough sketch.

 

Problem 1 : zer0lfsr-

  • only need to solve 2 out of 3 generators

Generator1

  • h can be calculated without the last input with high probability
  • bruteforce LFSR 16 bits, then directly recover NFSR 48 bits with 48 output bits

Generator2

  • h is zero with high probability
  • f has nonlinearity 16, so f is equal to a linear function with 3/4 probability
  • therefore, a fast correlation attack can be used

Generator3

  • f is very close to a linear function, so a fast correlation attack can be used
  • f has a degree 2 annhilator, so we can use this as well to solve the problem

I solved Generator1 and Generator3 with annhilators.

 

Problem 2 : zer0lfsr+

 

We solve from Generator3 to Generator2 to Generator1. 

  • An important note in the KDF function (found by barkingdog)
  • If we know $KDF(K_0 || K_1 || K_2 || K_3)$ and $K_0 \oplus K_1$, then we know $K_2 \oplus K_3$
  • we also have a symmetry, if we know $K_2 \oplus K_3$ then we know $K_0 \oplus K_1$

Also, note that the output value is the majority of the three outputs, so we have 75% correlation.

 

Generator3 can be solved by a fast correlation attack, similar to Problem 1. This works around 50% of the time.

 

For Generator2, fast correlation attack on bits that we know for sure (i.e. bits where Generator3 and actual output are different, forcing Generator1 and Generator2 to be equal to the actual output) can be used. This is supposed to be used to find all 48 bits of the LFSR, but to do so we need all 48 bits to be perfectly sound, or have very few of them to be incorrect. This is hard to do, so what I did was find 32 bits that we can guarantee its value, assume that at most one of them is wrong. This gives us about $2^{16}$ possibilities for the initial LFSR. Then, we bruteforce all possibilities for LFSR. By the KDF property, we can uniquely recover the NFSR initial state.

This gives us an attack that works around 20 seconds in Python with success probability of around 20%. 

 

For Generator1, we do something like the following - 

  • bruteforce all 16 bits of the LFSR part
  • we know about 24 guaranteed bits of the Generator1
  • this gives about 24 bits of the NFSR part (by the solution of zer0lfsr-)
  • combined with the KDF property, we can get around $2^{24}$ possibilities for the key

For Generator1, I used C++ with multicore programming to fit in time. 

 

Problem 3 : zer0lfsr++

 

Generator3 is the same. We will use the 16bit hint on Generator1. This gives us something like

  • bruteforce all 16 bits of the LFSR part, and we know first 16 bits of the NFSR part
  • we know 8 guaranteed bits out of the first 16 bits of the Generator1 output
  • by checking this, around $2^8$ possibilities for the LFSR remain
  • out of the 32 remaining NFSR parts, we know 16 of them with the LFSR and the guaranteed Generator1 bits
  • so 16 of them remains, and bruteforcing gives us around $2^{24}$ keys

Therefore, we need to do Generator2 without any extra information. To do this,

  • A very careful fast correlation attack using all bits (including non-guaranteed bits) can be done
  • I was able to find 44 bits that we can guarantee with at most one incorrect bit
  • we brute force the remaining 20 bits, so something like $2^{20} \cdot 44$ possibilities, doable

I believe with C++ with multicore programming and some luck, we can definitely solve it with this strategy.

 

UPD : Apparently there are multiple solutions for Generator2, so we need to use the hint on Generator2.

'CTF' 카테고리의 다른 글

GoogleCTF Quals 2021 Crypto Writeups  (1) 2021.07.19
zer0lfsr 2021 Full Writeup (0CTF/TCTF 2021)  (2) 2021.07.05
LINE CTF Crypto Writeups  (0) 2021.03.22
zer0pts CTF 2021 Crypto Writeups  (1) 2021.03.07
AeroCTF 2021 Crypto Writeups  (0) 2021.02.28

We played as "rbtree fan club" and placed 3rd overall.

 

babycrypto 1

 

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
#!/usr/bin/env python
from base64 import b64decode
from base64 import b64encode
import socket
import multiprocessing
 
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
import hashlib
import sys
 
class AESCipher:
    def __init__(self, key):
        self.key = key
 
    def encrypt(self, data):
        iv = get_random_bytes(AES.block_size)
        self.cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return b64encode(iv + self.cipher.encrypt(pad(data, 
            AES.block_size)))
 
    def encrypt_iv(self, data, iv):
        self.cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return b64encode(iv + self.cipher.encrypt(pad(data, 
            AES.block_size)))
 
    def decrypt(self, data):
        raw = b64decode(data)
        self.cipher = AES.new(self.key, AES.MODE_CBC, raw[:AES.block_size])
        return unpad(self.cipher.decrypt(raw[AES.block_size:]), AES.block_size)
 
flag = open("flag""rb").read().strip()
 
COMMAND = [b'test',b'show']
 
def run_server(client, aes_key, token):
    client.send(b'test Command: ' + AESCipher(aes_key).encrypt(token+COMMAND[0]) + b'\n')
    client.send(b'**Cipher oracle**\n')
    client.send(b'IV...: ')
    iv = b64decode(client.recv(1024).decode().strip())
    client.send(b'Message...: ')
    msg = b64decode(client.recv(1024).decode().strip())
    client.send(b'Ciphertext:' + AESCipher(aes_key).encrypt_iv(msg,iv) + b'\n\n')
    while(True):
        client.send(b'Enter your command: ')
        tt = client.recv(1024).strip()
        tt2 = AESCipher(aes_key).decrypt(tt)
        client.send(tt2 + b'\n')
        if tt2 == token+COMMAND[1]:
            client.send(b'The flag is: ' + flag)
            client.close()
            break
 
if __name__ == '__main__':
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(('0.0.0.0'16001))
    server.listen(1)
 
    while True:
        client, address = server.accept()
 
        aes_key = get_random_bytes(AES.block_size)
        token = b64encode(get_random_bytes(AES.block_size*10))[:AES.block_size*10]
 
        process = multiprocessing.Process(target=run_server, args=(client, aes_key, token))
        process.daemon = True
        process.start()
cs

 

Here, we are given

  • an AES-CBC encrypted result of some token appended with "test"
  • access to an AES-CBC encryption oracle, with full control of IV and message

and we are supposed to find the AES-CBC encrypted result of the token appended with "show".

 

We can easily see that we just need to change the last block. Therefore, we only need to change the last block of the ciphertext as well. 

We can simply XOR the next to last block of the ciphertext and the padded "show" string, and send it to the encryption oracle with 0 as IV.

The result of this encryption can be used as the last block of our final ciphertext, and this solves the problem. Here's my dirty code.

 

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
def bytexor(a, b):
    assert len(a) == len(b)
    return bytes(x ^ y for x, y in zip(a, b))
 
= remote('35.200.115.41''16001')
 
def get_data():
    t = r.recvline()
    print(t)
    t = t.rstrip(b"\n")
    t = t.split()[-1]
    if t[:11== b"Ciphertext:":
        t = t[11:]
    print(t)
    return t
 
 
inp = b64decode(get_data())
IV = inp[:16]
CTXT = inp[16:]
 
print(CTXT)
print(r.recvline())
 
print(r.recv(1024))
 
ss = b64encode(b"\x00" * 16)
r.sendline(ss)
 
print(r.recv(1024))
 
val = bytexor(pad(b'show'16), CTXT[9*16:10*16])
val = b64encode(val)
r.sendline(val)
 
cc = b64decode(get_data())
cc = cc[16:32]
 
print(len(cc))
vv = IV + CTXT[:160+ cc
 
vv = b64encode(vv)
 
print(r.recv(1024))
 
r.sendline(vv)
 
print(r.recv(1024))
cs

 

babycrypto 2

 

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
#!/usr/bin/env python
from base64 import b64decode
from base64 import b64encode
import socket
import multiprocessing
 
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
import hashlib
import sys
 
class AESCipher:
    def __init__(self, key):
        self.key = key
 
    def encrypt(self, data):
        iv = get_random_bytes(AES.block_size)
        self.cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return b64encode(iv + self.cipher.encrypt(pad(data, 
            AES.block_size)))
 
    def encrypt_iv(self, data, iv):
        self.cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return b64encode(iv + self.cipher.encrypt(pad(data, 
            AES.block_size)))
 
    def decrypt(self, data):
        raw = b64decode(data)
        self.cipher = AES.new(self.key, AES.MODE_CBC, raw[:AES.block_size])
        return unpad(self.cipher.decrypt(raw[AES.block_size:]), AES.block_size)
 
flag = open("flag""rb").read().strip()
 
AES_KEY = get_random_bytes(AES.block_size)
TOKEN = b64encode(get_random_bytes(AES.block_size*10-1))
COMMAND = [b'test',b'show']
PREFIX = b'Command: '
 
def run_server(client):
    client.send(b'test Command: ' + AESCipher(AES_KEY).encrypt(PREFIX+COMMAND[0]+TOKEN) + b'\n')
    while(True):
        client.send(b'Enter your command: ')
        tt = client.recv(1024).strip()
        tt2 = AESCipher(AES_KEY).decrypt(tt)
        client.send(tt2 + b'\n')
        if tt2 == PREFIX+COMMAND[1]+TOKEN:
            client.send(b'The flag is: ' + flag)
            client.close()
            break
 
if __name__ == '__main__':
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(('0.0.0.0'16002))
    server.listen(1)
 
    while True:
        client, address = server.accept()
 
        process = multiprocessing.Process(target=run_server, args=(client, ))
        process.daemon = True
        process.start()
cs

 

This time, we don't have access to any oracle, but we now have to change the first block of the decrypted text.

This can be easily done by simply changing the IV as needed. Here's another dirty code that does the job.

 

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
def bytexor(a, b):
    assert len(a) == len(b)
    return bytes(x ^ y for x, y in zip(a, b))
 
= remote('35.200.39.68''16002')
 
def get_data():
    t = r.recvline()
    t = t.rstrip(b"\n")
    t = t.split()[-1]
    return t
 
 
inp = b64decode(get_data())
IV = inp[:16]
CTXT = inp[16:]
 
 
print(r.recv(1024))
 
IV = bytexor(IV, pad(b"Command: test"16))
IV = bytexor(IV, pad(b"Command: show"16))
 
vv = IV + CTXT
vv = b64encode(vv)
 
r.sendline(vv)
print(r.recv(1024))
 
print(r.recv(1024))
cs

 

babycrypto 3

 

The value of $n, e, c$ are given, and we have to decrypt it. 

 

Using RsaCTFTool, we can factorize $n$ (cm-factor is used) and RSA-decrypt the value of $c$. 

The resulting value of $m$, when converted to bytes, looks like a base64 encoded value of a string.

 

We take some suffix of the results and base64 decode it, to get "CLOSING THE DISTANCE.".

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
= 31864103015143373750025799158312253992115354944560440908105912458749205531455987590931871433911971516176954193675507337
= 65537
= 10879776433900426660843740332190892429769159527203392037251077478777616065501519198653853699716123394455804888854401574
 
# python3 RsaCtfTool.py -n 31864103015143373750025799158312253992115354944560440908105912458749205531455987590931871433911971516176954193675507337 -e 65537 --uncipher 10879776433900426660843740332190892429769159527203392037251077478777616065501519198653853699716123394455804888854401574 --private
 
val = 0x00026067ff851ecdcb61e50b83a515e3005130785055306c4f527942555345556752456c545645464f5130557543673d3d0a
= 291664785919250248097148750343149685985101
= 109249057662947381148470526527596255527988598887891132224092529799478353198637
 
val = long_to_bytes(val)
print(val)
 
tt = b'Q0xPU0lORyBUSEUgRElTVEFOQ0UuCg==\n' # remove prefix
print(b64decode(tt))
cs

 

babycrypto 4

 

We are given a secp160r1 ECDSA signature data, with 32-bit nonces. Also, the first 16 bits of nonces are leaked.

 

It's trivial from the ECDSA definition that given one "full" data of $r, s, k, h$ we can retrieve the value of the secret key.

Therefore, by brute forcing the remaining unleaked 16 bits, we can create a set of 65536 candidates of the secret key for each dataset. 

By taking the intersection of such datasets, we can find the secret key, solving the problem. Here's another code.

 

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
= [0* 20
= [0* 20
= [0* 20
= [0* 20
 
= open('output.txt''r')
for i in range(020):
    v = f.readline()
    cc = v.split()
    r[i] = int(cc[0][2:], 16)
    s[i] = int(cc[1][2:], 16)
    k[i] = int(cc[2][2:], 16)
    h[i] = int(cc[3][2:], 16
 
= 0x0100000000000000000001F4C8F927AED3CA752257
 
s1 = set()
s2 = set()
 
for i in range(01 << 16):
    # sk = h + r * pvk mod n 
    cc = ((s[0* (k[0+ i) - h[0]) * inverse(r[0], n)) % n
    s1.add(cc)
 
for i in range(01 << 16):
    cc = ((s[1* (k[1+ i) - h[1]) * inverse(r[1], n)) % n
    s2.add(cc)
 
print(s1 & s2)
 
cs

 

 

'CTF' 카테고리의 다른 글

zer0lfsr 2021 Full Writeup (0CTF/TCTF 2021)  (2) 2021.07.05
zer0lfsr 2021 Quick Solutions/Thoughts  (0) 2021.07.05
zer0pts CTF 2021 Crypto Writeups  (1) 2021.03.07
AeroCTF 2021 Crypto Writeups  (0) 2021.02.28
UnionCTF Crypto Writeups  (2) 2021.02.21

I participated in zer0pts CTF in Super Guesser, and we reached 1st place.

Since there are many challenges to cover, this writeup will be very concise.

The solution codes may be a bit messy, since I wrote this in a hurry :P

Also, r4j solved signme, so I'll leave that writeup to r4j :D

 

war(sa)mup

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
from Crypto.Util.number import getStrongPrime, GCD
from random import randint
from flag import flag
import os
 
def pad(m: int, n: int):
  # PKCS#1 v1.5 maybe
  ms = m.to_bytes((m.bit_length() + 7// 8"big")
  ns = n.to_bytes((n.bit_length() + 7// 8"big")
  assert len(ms) <= len(ns) - 11
 
  ps = b""
  while len(ps) < len(ns) - len(ms) - 3:
    p = os.urandom(1)
    if p != b"\x00":
      ps += p
  return int.from_bytes(b"\x00\x02" + ps + b"\x00" + ms, "big")
 
 
while True:
  p = getStrongPrime(512)
  q = getStrongPrime(512)
  n = p * q
  phi = (p-1)*(q-1)
  e = 1337
  if GCD(phi, e) == 1:
    break
 
= pad(int.from_bytes(flag, "big"), n)
c1 = pow(m, e, n)
c2 = pow(m // 2, e, n)
 
print("n =", n)
print("e =", e)
print("c1=", c1)
print("c2=", c2)
 
cs

 

Solution

Writing $\lfloor m/2 \rfloor = x$, we see that $m$ is either $2x$ or $2x+1$. If $m = 2x$, we can perform the related message attack with polynomials $$p_1(x) = (2x)^e - c_1, \quad p_2(x) = x^e - c_2$$ Of course, in this case we would have $c_1 \equiv 2^e c_2 \pmod{n}$ and polynomial GCD would be useless. If $m = 2x+1$, we can do it with $$p_1(x) = (2x+1)^e - c_1, \quad p_2(x) = x^e - c_2$$ which works and gives our desired value of $x$. $m$ can be recovered as $2x+1 \pmod{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
# sage
= 113135121314210337963205879392132245927891839184264376753001919135175107917692925687745642532400388405294058068119159052072165971868084999879938794441059047830758789602416617241611903275905693635535414333219575299357763227902178212895661490423647330568988131820052060534245914478223222846644042189866538583089
= 1337
c1 = 89077537464844217317838714274752275745737299140754457809311043026310485657525465380612019060271624958745477080123105341040804682893638929826256518881725504468857309066477953222053834586118046524148078925441309323863670353080908506037906892365564379678072687516738199061826782744188465569562164042809701387515
c2 = 18316499600532548540200088385321489533551929653850367414045951501351666430044325649693237350325761799191454032916563398349042002392547617043109953849020374952672554986583214658990393359680155263435896743098100256476711085394564818470798155739552647869415576747325109152123993105242982918456613831667423815762
 
P.<x> = PolynomialRing(Zmod(n))
 
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)
 
= (2 * x + 1)^e - c1
= x^e - c2
 
print(GCD(f, g, n))
 
# back to python
= -113131683468284119607964196541575421728552328779560800910707074969146190674728631210678514366174908879152073378298687396133007061413453069629241334690374397179449596372765780570923850759894857943624531609494119853246060991087070835093327088258517606688574536921269027653158032526646833175962482267255713310835
= 113135121314210337963205879392132245927891839184264376753001919135175107917692925687745642532400388405294058068119159052072165971868084999879938794441059047830758789602416617241611903275905693635535414333219575299357763227902178212895661490423647330568988131820052060534245914478223222846644042189866538583089
= 1337
c1= 89077537464844217317838714274752275745737299140754457809311043026310485657525465380612019060271624958745477080123105341040804682893638929826256518881725504468857309066477953222053834586118046524148078925441309323863670353080908506037906892365564379678072687516738199061826782744188465569562164042809701387515
c2= 18316499600532548540200088385321489533551929653850367414045951501351666430044325649693237350325761799191454032916563398349042002392547617043109953849020374952672554986583214658990393359680155263435896743098100256476711085394564818470798155739552647869415576747325109152123993105242982918456613831667423815762
 
print(n+m)
 
fin = (2 * (n+m) + 1) % n
print(long_to_bytes(fin))
cs

 

OT or NOT OT

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
import os
import signal
import random
from base64 import b64encode
from Crypto.Util.number import getStrongPrime, bytes_to_long
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
from flag import flag
 
= getStrongPrime(1024)
 
key = os.urandom(32)
iv = os.urandom(AES.block_size)
aes = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)
= aes.encrypt(pad(flag, AES.block_size))
 
key = bytes_to_long(key)
print("Encrypted flag: {}".format(b64encode(iv + c).decode()))
print("p = {}".format(p))
print("key.bit_length() = {}".format(key.bit_length()))
 
signal.alarm(600)
while key > 0:
    r = random.randint(2, p-1)
    s = random.randint(2, p-1)
    t = random.randint(2, p-1)
    print("t = {}".format(t))
 
    a = int(input("a = ")) % p
    b = int(input("b = ")) % p
    c = int(input("c = ")) % p
    d = int(input("d = ")) % p
    assert all([a > 1 , b > 1 , c > 1 , d > 1])
    assert len(set([a,b,c,d])) == 4
 
    u = pow(a, r, p) * pow(c, s, p) % p
    v = pow(b, r, p) * pow(c, s, p) % p
    x = u ^ (key & 1)
    y = v ^ ((key >> 1& 1)
    z = pow(d, r, p) * pow(t, s, p) % p
 
    key = key >> 2
 
    print("x = {}".format(x))
    print("y = {}".format(y))
    print("z = {}".format(z))
 
cs

 

Solution

The key idea is to make a small subgroup which the values of $x, y \in \mathbb{F}_p$ must belong to.

To do this, we wait until $p \equiv 1 \pmod{4}$, then take any $t \in \mathbb{F}_p$ with order $4$.

This can be done by taking some random $g \in \mathbb{F}_p$ and choosing $t = g^{(p-1)/4}$. 

Checking if order of $t$ is $4$ can now be done simply by verifying $t^2 \neq 1$ in $\mathbb{F}_p$.

 

Now we simply send $a = t$, $b = t^2$, $c = t^3$. Since $a^4 = b^4 = c^4 = 1$, we also have $u^4 = v^4 = 1$.

Therefore, we can see if $x = u$ and if $y= v$ by simply checking if $x^4=1$ and if $y^4=1$.

 

Of course, it should be noted that getStrongPrime does not generate a prime of the form $p=2q+1$ with $q$ prime. :wink:

 

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
= remote('crypto.ctf.zer0pts.com''10130')
 
def recvint():
    s = r.recvline()[:-1]
    s = s.split()[-1]
    return int(s)
 
= r.recvline()[:-1]
= S.split()[-1]
= base64.b64decode(S)
iv = V[:16]
ctxt = V[16:]
 
= recvint()
keylen = recvint()
 
if p % 4 != 1:
    print("BAD PRIME")
    exit()
 
= 0
for g in range(22000):
    t = pow(g, (p-1)//4, p)
    if (t * t) % p != 1:
        break
 
print("Begin Key Finding")
keyv = 0
add = 1
for i in tqdm(range(0128)):
    t = recvint()
    r.sendline(str(t))
    r.sendline(str((t * t) % p))
    r.sendline(str((t * t * t) % p))
    r.sendline(str(5))
    x = recvint()
    y = recvint()
    z = recvint()
    if pow(x, 4, p) != 1:
        keyv += add
    add = add * 2
    if pow(y, 4, p) != 1:
        keyv += add
    add = add * 2
    if i >= 126:
        keyv = long_to_bytes(keyv)
 
        aes = AES.new(key=keyv, mode=AES.MODE_CBC, iv=iv)
        ptxt = aes.decrypt(ctxt)
        print(ptxt)
cs

 

janken vs yoshiking

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
import random
import signal
from flag import flag
from Crypto.Util.number import getStrongPrime, inverse
 
HANDNAMES = {
    1"Rock",
    2"Scissors",
    3"Paper"
}
 
def commit(m, key):
    (g, p), (x, _) = key
    r = random.randint(2, p-1)
    c1 = pow(g, r, p)
    c2 = m * pow(g, r*x, p) % p
    return (c1, c2)
 
 
def decrypt(c, key):
    c1, c2 = c
    _, (x, p)= key
 
    m = c2 * inverse(pow(c1, x, p), p) % p
    return m
 
 
def keygen(size):
    p = getStrongPrime(size)
    g = random.randint(2, p-1)
    x = random.randint(2, p-1)
 
    return (g, p), (x, p)
 
 
signal.alarm(3600)
key = keygen(1024)
(g, p), _ = key
print("[yoshiking]: Hello! Let's play Janken(RPS)")
print("[yoshiking]: Here is g: {}, and p: {}".format(g, p))
 
round = 0
wins = 0
while True:
    round += 1
    print("[system]: ROUND {}".format(round))
 
    yoshiking_hand = random.randint(13)
    c = commit(yoshiking_hand, key)
    print("[yoshiking]: my commitment is={}".format(c))
 
    hand = input("[system]: your hand(1-3): ")
    print("")
    try:
        hand = int(hand)
        if not (1 <= hand <= 3):
            raise ValueError()
    except ValueError:
        print("[yoshiking]: Ohhhhhhhhhhhhhhhh no! :(")
        exit()
 
    yoshiking_hand = decrypt(c, key)
    print("[yoshiking]: My hand is ... {}".format(HANDNAMES[yoshiking_hand]))
    print("[yoshiking]: Your hand is ... {}".format(HANDNAMES[hand]))
    result = (yoshiking_hand - hand + 3) % 3
    if result == 0:
        print("[yoshiking]: Draw, draw, draw!!!")
    elif result == 1:
        print("[yoshiking]: Yo! You win!!! Ho!")
        wins += 1
        print("[system]: wins: {}".format(wins))
 
        if wins >= 100:
            break
    elif result == 2:
        print("[yoshiking]: Ahahahaha! I'm the winnnnnnner!!!!")
        print("[yoshiking]: You, good loser!")
        print("[system]: you can check that yoshiking doesn't cheat")
        print("[system]: here's the private key: {}".format(key[1][0]))
        exit()
 
print("[yoshiking]: Wow! You are the king of roshambo!")
print("[yoshiking]: suge- flag ageru")
print(flag)
 
cs

 

Solution

The first observation is that if $g$ is a quadratic residue (QR), the commitment leaks the legendre symbol of $m$.

Indeed, if $g$ is QR, then the legendre symbol of $m$ is simply the legendre symbol of $c_2$.

 

We generate instances until we find a prime $p$ that not all $1, 2, 3$ are QR modulo $p$. 

In this case, by leaking the legendre symbol of $m$, we can reduce the number of candidates for yoshiking's hand from 3 to 2.

Therefore, we can at least force a tie, and occasionally win. This is enough to solve the challenge.

 

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
= remote('crypto.ctf.zer0pts.com''10463')
r.recvline()
 
= r.recvline()
= s.split()
= int(T[4][:-1])
= int(T[-1].rstrip())
 
 
QR = []
NQR = []
 
if pow(g, (p-1)//2 , p) != 1:
    print("BAD PRIME 1")
    exit()
QR.append(1)
if pow(2, (p-1)//2, p-1== 1:
    QR.append(2)
else:
    NQR.append(2)
if pow(3, (p-1)//2, p-1== 1:
    QR.append(3)
else:
    NQR.append(3)
 
if len(QR) == 3:
    print("lol")
    exit()
 
def get_commit():
    s = r.recvline()
    t = s.split(b'(')[-1]
    c1 = int(t.split(b',')[0])
    c2 = int(t.split(b',')[1][1:-2])
    return c1, c2
 
for i in range(0500):
    print(r.recvline())
    c1, c2 = get_commit()
    if pow(c2, (p-1// 2, p) == 1:
        if len(QR) == 1:
            r.sendline(b"3")
        else:
            if 2 in QR:
                r.sendline(b"1")
            else:
                r.sendline(b"3")
    else:
        if len(NQR) == 1:
            if 2 in NQR:
                r.sendline(b"1")
            if 3 in NQR:
                r.sendline(b"2")
        else:
            r.sendline(b"2")
    r.recvline()
    r.recvline()
    r.recvline()
    s = r.recvline()
    if s.split()[-1].rstrip() == b"draw!!!":
        continue
    else:
        s = r.recvline()
        print(s)
        if s.split()[-1].rstrip() == b"100":
            for t in range(04):
                print(r.recvline())
cs

 

easy pseudorandom

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
from Crypto.Util.number import*
from flag import flag
 
nbits = 256
= random_prime(1 << nbits)
Fp = Zmod(p)
P.<v> = PolynomialRing(Fp)
 
= randrange(p)
= 2
= v^2 + b
 
v0 = randrange(p)
v1 = F(v0)
 
= ceil(nbits * (d / (d + 1)))
w0 = (v0 >> (nbits - k))
w1 = (v1 >> (nbits - k))
 
# encrypt
= bytes_to_long(flag)
= v1
for i in range(5):
    v = F(v)
    m ^^= int(v)
 
print(f"p = {p}")
print(f"b = {b}")
print(f"m = {m}")
print(f"w0 = {w0}")
print(f"w1 = {w1}")
 
cs

 

Solution

It's clear that we just need to find the value of $v_0$. Denote $l = 256$. Using the given info, we can write $$\begin{equation*} \begin{aligned} v_0 &= w_0 \cdot 2^{l - k} + x \\ v_1 &= w_1 \cdot 2^{l-k} + y \\ v_1 & \equiv v_0^2 + b \pmod{n} \end{aligned} \end{equation*}$$ Combining, this gives us the key relation $$ w_1 \cdot 2^{l-k} + y \equiv w_0^2 \cdot 2^{2l-2k} + b + w_0 \cdot 2^{l-k+1} \cdot x + x^2 \pmod{n}$$ The key idea is to remove the quadratic term $x^2$. Denote $$\begin{equation*} \begin{aligned} A &= w_0 \cdot 2^{l-k+1} \\ B &= w_1 \cdot 2^{l-k} - w_0^2 \cdot 2^{2l-2k} - b \end{aligned} \end{equation*}$$ Note that $A, B$ are constants that we know. Our relation is now $$ Ax \equiv B + y - x^2 \pmod{n}$$ Since $x, y$ are all below $2^{l-k}$, we can see that $$ B - 2^{2l-2k} \le Ax \pmod{n} \le B + 2^{l-k}$$ which is a type of relation that can be solved. Check out my writeups for PBCTF Special Gift and SECCON crypto01.

 

Note that there are $2^{l-k}$ possible values of $x$, and there are approximately $2^{2l-2k}$ possible values of $Ax \pmod{n}$. 

Since $l-k + 2l-2k = 3l-3k \approx l$, we can decide that there must be fairly small number of $x$ that satisfy the desired inequality.

This type of analysis was done in both of writeups above, and recently highlighted by Mystiz in AeroCTF writeup.

 

Now that there are three challenges I solved with this method, I think I'll add this to my CVP repository as well...

 

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
# notation a bit different from solution, but same idea
 
# simplify polynomial in sage
= 86160765871200393116432211865381287556448879131923154695356172713106176601077
= 71198163834256441900788553646474983932569411761091772746766420811695841423780
= 88219145192729480056743197897921789558305761774733086829638493717397473234815
w0 = 401052873479535541023317092941219339820731562526505
w1 = 994046339364774179650447057905749575131331863844814
 
nbits = 256
P.<x, y> = PolynomialRing(GF(p))
= (nbits * 2 + 2// 3
 
= w0 * 2^(nbits - k) + x
= w1 * 2^(nbits - k) + y
 
= g - f^2 - b
print(h)
 
# python
 
def ceil(n, m):
    return (n + m - 1// m
 
def optf(A, M, L, R):
    if L == 0:
        return 0
    if 2 * A > M:
        L, R = R, L
        A = M - A
        L = M - L
        R = 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)
 
= 18867904637006146022735447
= 19342813113834066795298816
= 86160765871200393116432211865381287556448879131923154695356172713106176601077
= 71198163834256441900788553646474983932569411761091772746766420811695841423780
= 88219145192729480056743197897921789558305761774733086829638493717397473234815
w0 = 401052873479535541023317092941219339820731562526505
w1 = 994046339364774179650447057905749575131331863844814
 
C1 = 55130802749277213576496911760053178817655787149958046010477129311148596128757
C2 = 78083221913223461198494116323396529665894773452683783127339675579334647310194
 
nbits = 256
= (nbits * 2 + 2// 3
 
delt = nbits - k
# C1 x + y + C2 - x^2 == 0 mod p
# C1 x == x^2 - y - C2 mod p
 
= (0 - (1 << (delt)) - C2 + p) % p
= ((1 << (2 * delt)) - C2 + p) % p
 
lst = 0
while lst <= (1 << delt):
    NL = (L - C1 * (lst + 1)) % p
    NR = (R - C1 * (lst + 1)) % p
    if NL > NR:
        lst = lst + 1
    else:
        cc = optf(C1, p, NL, NR)
        lst = lst + 1 + cc
    mm = m
    x = lst
    v0 = w0 * (1 << (nbits - k)) + x
    v1 = (v0 * v0 + b) % p
    v = v1
    # try out!
    for i in range(5):
        v = (v * v + b) % p
        mm ^= v
    print(long_to_bytes(mm))
cs

 

3-AES

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
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from binascii import hexlify, unhexlify
from hashlib import md5
import os
import signal
from flag import flag
 
keys = [md5(os.urandom(3)).digest() for _ in range(3)]
 
 
def get_ciphers(iv1, iv2):
    return [
        AES.new(keys[0], mode=AES.MODE_ECB),
        AES.new(keys[1], mode=AES.MODE_CBC, iv=iv1),
        AES.new(keys[2], mode=AES.MODE_CFB, iv=iv2, segment_size=8*16),
    ]
 
def encrypt(m: bytes, iv1: bytes, iv2: bytes) -> bytes:
    assert len(m) % 16 == 0
    ciphers = get_ciphers(iv1, iv2)
    c = m
    for cipher in ciphers:
        c = cipher.encrypt(c)
    return c
 
def decrypt(c: bytes, iv1: bytes, iv2: bytes) -> bytes:
    assert len(c) % 16 == 0
    ciphers = get_ciphers(iv1, iv2)
    m = c
    for cipher in ciphers[::-1]:
        m = cipher.decrypt(m)
    return m
 
signal.alarm(3600)
while True:
    print("==== MENU ====")
    print("1. Encrypt your plaintext")
    print("2. Decrypt your ciphertext")
    print("3. Get encrypted flag")
    choice = int(input("> "))
 
    if choice == 1:
        plaintext = unhexlify(input("your plaintext(hex): "))
        iv1, iv2 = get_random_bytes(16), get_random_bytes(16)
        ciphertext = encrypt(plaintext, iv1, iv2)
        ciphertext = b":".join([hexlify(x) for x in [iv1, iv2, ciphertext]]).decode()
        print("here's the ciphertext: {}".format(ciphertext))
 
    elif choice == 2:
        ciphertext = input("your ciphertext: ")
        iv1, iv2, ciphertext = [unhexlify(x) for x in ciphertext.strip().split(":")]
        plaintext = decrypt(ciphertext, iv1, iv2)
        print("here's the plaintext(hex): {}".format(hexlify(plaintext).decode()))
 
    elif choice == 3:
        plaintext = flag
        iv1, iv2 = get_random_bytes(16), get_random_bytes(16)
        ciphertext = encrypt(plaintext, iv1, iv2)
        ciphertext = b":".join([hexlify(x) for x in [iv1, iv2, ciphertext]]).decode()
        print("here's the encrypted flag: {}".format(ciphertext))
        exit()
 
    else:
        exit()
 
cs

 

Solution

Let's consider plaintexts/ciphertexts of one block first. Denote $E_k$, $D_k$ as ECB encryption/decryption with key $k$.

The encryption process can be written as $$ P \rightarrow E_{k_1}(P) \rightarrow E_{k_2}(E_{k_1}(P) \oplus IV_1) \rightarrow E_{k_2}(E_{k_1}(P) \oplus IV_1) \oplus E_{k_3}(IV_2) = C$$ The key relation is $$E_{k_1}(P) \oplus IV_1 = D_{k_2}(C \oplus E_{k_3}(IV_2))$$ For a fixed value of $C, IV_2$, the right hand side is fixed. Therefore, the left hand side is also fixed.

 

We start by using the decryption oracle with $C, IV_1, IV_2$ being all 16 bytes of \x00. Denote $P_0$ as the resulting plaintext. 

Then, we use the decryption oracle with $C, IV_1, IV_2$ all \x00, but $IV_1$ has the last byte \x01. Denote $P_1$ as the resulting plaintext.

 

Our observations lead to $$\text{bytes_to_long}(E_{k_1}(P_0) \oplus E_{k_1}(P_1)) = 1$$ This can be tested for all $2^{24}$ possible keys, and with that we find $k_1$. 

 

Now that we know $k_1$, we also know the value of $D_{k_2} \circ E_{k_3}$ applied to 16 bytes of \x00.

This is a perfect scenario for a meet-in-the-middle attack, and with this we can find $k_2$, $k_3$. 

 

retrieve data from server

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
# get data from server
= remote('crypto.ctf.zer0pts.com''10929')
 
def get_enc(ptxt):
    r.recvuntil(b">")
    r.sendline(b"1")
    ptxt = bytes.hex(ptxt)
    r.sendline(ptxt)
    s = r.recvline().split()[-1].rstrip()
    iv1, iv2, ctxt = s.split(b":")
    iv1 = bytes.fromhex(iv1.decode())
    iv2 = bytes.fromhex(iv2.decode())
    ctxt = bytes.fromhex(ctxt.decode())
    return iv1, iv2, ctxt
 
def get_dec(ctxt, iv1, iv2):
    r.recvuntil(b">")
    r.sendline(b"2")
    ctxt = bytes.hex(ctxt)
    iv1 = bytes.hex(iv1)
    iv2 = bytes.hex(iv2)
    goal = iv1 + ":" + iv2 + ":" + ctxt
    r.sendline(goal)
    s = r.recvline()
    print(s)
    s = s.split()[-1].rstrip()
    ptxt = bytes.fromhex(s.decode())
    return ptxt
 
def get_flag():
    r.recvuntil(b">")
    r.sendline(b"3")
    s = r.recvline().split()[-1].rstrip()
    iv1, iv2, ctxt = s.split(b":")
    iv1 = bytes.fromhex(iv1.decode())
    iv2 = bytes.fromhex(iv2.decode())
    ctxt = bytes.fromhex(ctxt.decode())
    assert len(iv1) == 16
    assert len(iv2) == 16
    assert len(ctxt) == 48
    return iv1, iv2, ctxt
 
 
= open("lol.txt""w")
ptxt_0 = get_dec(b"\x00" * 16, b"\x00" * 16, b"\x00" * 16)
ptxt_1 = get_dec(b"\x00" * 16, b"\x00" * 15 + b"\x01", b"\x00" * 16)
 
 
f.write(str(bytes_to_long(ptxt_0)) + "\n")
f.write(str(bytes_to_long(ptxt_1)) + "\n")
 
iv1, iv2, ctxt = get_flag()
 
f.write(str(bytes_to_long(iv1)) + "\n")
f.write(str(bytes_to_long(iv2)) + "\n")
f.write(str(bytes_to_long(ctxt)) + "\n")
 
cs

 

find the first key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def bytexor(a, b):
    assert len(a) == len(b)
    return bytes(x ^ y for x, y in zip(a, b))
 
for i in tqdm(range(01 << 24)):
    cc = long_to_bytes(i)
    if len(cc) < 3:
        cc = b"\x00" * (3 - len(cc)) + cc
    k1 = hashlib.md5(cc).digest()
    cipher = AES.new(k1, mode = AES.MODE_ECB)
    val_1 = cipher.encrypt(ptxt_0)
    val_2 = cipher.encrypt(ptxt_1)
    det = bytexor(val_1, val_2)
    if bytes_to_long(det) == 1:
        print(i)
cs

 

meet in the middle preparation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# we now know the first key
cipher = AES.new(key = key_1, mode = AES.MODE_ECB)
vv = cipher.encrypt(ptxt_0)
 
= open("Data1.txt""w")
for i in tqdm(range(01 << 24)):
    cc = long_to_bytes(i)
    if len(cc) < 3:
        cc = b"\x00" * (3 - len(cc)) + cc
        assert bytes_to_long(cc) == i
    k2 = hashlib.md5(cc).digest()
    cipher = AES.new(key = k2, mode = AES.MODE_ECB)
    res = cipher.encrypt(vv)
    f.write(str(bytes_to_long(res)) + "\n")
 
for i in tqdm(range(01 << 24)):
    cc = long_to_bytes(i)
    if len(cc) < 3:
        cc = b"\x00" * (3 - len(cc)) + cc
    k3 = hashlib.md5(cc).digest()
    cipher = AES.new(key = k3, mode = AES.MODE_ECB)
    res = cipher.encrypt(b"\x00" * 16)
    f.write(str(bytes_to_long(res)) + "\n")
f.close()
cs

 

meet in the middle in C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
map<stringint> M;
 
int main(void)
{
    fio; int i, j;
    freopen("Data1.txt""r", stdin);
    for(i=0 ; i < (1<<24) ;  i++)
    {
        if(i % 1000000 == 0cout << i / 1000000 << endl;
        string s; cin >> s;
        M[s] = i;
    }
    for(i=0 ; i<(1<<24) ; i++)
    {
        if(i % 1000000 == 0cout << i / 1000000 << endl;
        string s; cin >> s;
        if(M[s] != 0)
        {
            cout << M[s] << " " << i << endl;
            return 0;
        }
    }
    return 0;
}
cs

 

NOT Mordell primes

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
from Crypto.Util.number import bytes_to_long
from secrets import k, FLAG
 
 
= 13046889097521646369087469608188552207167764240347195472002158820809408567610092324592843361428437763328630003678802379234688335664907752858268976392979073
= 10043619664651911066883029686766120169131919507076163314397915307085965058341170072938120477911396027902856306859830431800181085603701181775623189478719241
= 12964455266041997431902182249246681423017590093048617091076729201020090112909200442573801636087298080179764338147888667898243288442212586190171993932442177
 
= EllipticCurve(GF(p),[a,b])
 
= E(1128360620302355288075151618990689693489224136092325178068938705418318741031525951872324247759313197901044260703591395247778139170748768869166170361843998012748862750577419812619234165922125135009793011470953429653398381275403229335519006908182956425430354120606424111151410237675942385465833703061487938776991)
= k*P
= (k+1)*P
 
= int(Q[0])
= int(R[0])
 
assert is_prime(p)
assert is_prime(q)
 
= 0x10001
= p*q
= bytes_to_long(FLAG)
= pow(m,e,N)
 
print(f'N = {N}')
print(f'c = {c}')
 
cs

 

Solution

Denote $P = (u, v)$, $Q = (x, y)$. We can utilize the sum of points formula to get the x-coordinate of $R$ as $$ \left( \frac{y-v}{x-u} \right)^2 - (x + u)$$ Therefore, we have the relation $$ g(x, y) = x(y-v)^2 - (x+u)(x-u)^2 - N(x-u)^2 \equiv 0 \pmod{p}$$ We now have to combine this relation with $$y^2 \equiv x^3 + ax + b \pmod{p}$$ Initially I tried to calculate the resultant, but it bugged out (maybe due to incorrect parameters given) so I changed my approach.

 

If we print out the monomials of $g(x, y)$, we see that $y$ appears in two monomials, $xy^2$ and $xy$.

We can change $y^2$ as $x^3 + ax + b$ in the first one. Now we can drag the $xy$ term into the right hand side, square the equation, and change $y^2$ to $x^3 + ax + b$ to obtain a equation with only one variable $x$. For more details, please refer to the solution code.

 

This polynomial equation can now be solved, which gives us the candidates for the prime divisor of $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
pytp = 13046889097521646369087469608188552207167764240347195472002158820809408567610092324592843361428437763328630003678802379234688335664907752858268976392979073
= 10043619664651911066883029686766120169131919507076163314397915307085965058341170072938120477911396027902856306859830431800181085603701181775623189478719241
= 12964455266041997431902182249246681423017590093048617091076729201020090112909200442573801636087298080179764338147888667898243288442212586190171993932442177
 
= EllipticCurve(GF(p),[a,b])
 
= E(1128360620302355288075151618990689693489224136092325178068938705418318741031525951872324247759313197901044260703591395247778139170748768869166170361843998012748862750577419812619234165922125135009793011470953429653398381275403229335519006908182956425430354120606424111151410237675942385465833703061487938776991)
= G[0]
= G[1]
 
= 22607234899418506929126001268361871457071114354768385952661316782742548112938224795906631400222949082488044126564531809419277303594848211922000498018284382244900831520857366772119155202621331079644609558409672584261968029536525583401488106146231216232578818115404806474812984250682928141729397248414221861387
= 15850849981973267982600456876579257471708532525108633915715902825196241000151529259632177065183069032967782114646012018721535909022877307131272587379284451827627191021621449090672315265556221217089055578013603281682705976215360078119427612168005716370941190233189775697324558168779779919848728188151630185987
 
P.<x, y> = PolynomialRing(GF(p))
= y ** 2 - x ** 3 - a * x - b
= x * ((y - v) * (y - v)  - (x + u) * (x-u) * (x-u)) - N * (x-u) * (x-u)
 
print(g.monomials())
# [x^4, x^3, xy^2, x^2, xy, x, 1]
 
= g.coefficients()
P.<x> = PolynomialRing(Zmod(p))
= 0
= 0
+= T[0* x^4
+= T[1* x^3 
+= T[2* x * (x^3 + a* x + b)
+= T[3* x^2 
+= T[5* x
+= T[6]
 
+= T[4* T[4* x^2 * (x^3 + a*+ b)
 
 
= f * f - g 
 
print(h.roots())
 
= 22607234899418506929126001268361871457071114354768385952661316782742548112938224795906631400222949082488044126564531809419277303594848211922000498018284382244900831520857366772119155202621331079644609558409672584261968029536525583401488106146231216232578818115404806474812984250682928141729397248414221861387
= 15850849981973267982600456876579257471708532525108633915715902825196241000151529259632177065183069032967782114646012018721535909022877307131272587379284451827627191021621449090672315265556221217089055578013603281682705976215360078119427612168005716370941190233189775697324558168779779919848728188151630185987
 
# cand = h.roots()
cand = [(52666479036523526653095613318351861523276271632713318115554199785641910004700605665354284976751168870025415689045359043450374250110154575852620226048974511), (42925282481368613878909113199174559468414118724732506754095097356205723116364073618588815566773856095001786294300257105174112147027045971030053962344407371), (112836062030235528807515161899068969348922413609232517806893870541831874103152595187232424775931319790104426070359139524777813917074876886916617036184399802)]
 
for p, t in cand:
    q = N // p
    phi = (p-1* (q-1)
    print(p * q - N)
    d = inverse(65537, phi)
    print(long_to_bytes(pow(c, d, N)))
cs

 

pure division

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from Crypto.Util.number import *
from flag import flag
 
assert len(flag) == 40
 
= 74894047922780452080480621188147614680859459381887703650502711169525598419741
a1 = 22457563127094032648529052905270083323161530718333104214029365341184039143821
a2 = 82792468191695528560800352263039950790995753333968972067250646020461455719312
Fp = Zmod(p^3)
= EllipticCurve(Fp, [a1, a2])
 
= bytes_to_long(flag)
= E(201395103510950985196528886887600944697931024970644444173327129750000389064102542826357168547230875812115987973230106228243893553395960867041978131850021580112077013996963515239128729448812815223970675917812499157323530103467271226217465854493032911836659600850860977113580889059985393999460199722148747745817726547235063418161407320876958474804964632767671151534736727858801825385939645586103320316229199221863893919847277366752070948157424716070737997662741835)
= m * S
 
with open("output.txt""w"as f:
    f.write("T: {}".format(T))
 
 
cs

 

Solution

The required paper for this challenge is https://arxiv.org/pdf/2010.15543.pdf.

Basically, (like a few challenges this year) we need a homomorphism to a group we can efficiently calculate discrete logarithm on.

It turns out that the given elliptic curve over $\mathbb{Z}_{p^3}$ has a homomorphism to $\mathbb{Z}_{p^2}$.

UPD : It should also be noted that the binary operator in $\mathbb{Z}_{p^2}$ is sum, not product.

 

To calculate this, we need to know how to perform point multiplication in a projective curve/division free setting.

The formula for this can be found online, for example, here. I wonder if this computation can be done in sage...

 

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
 
= 74894047922780452080480621188147614680853399736985793708596679454987247185378
# N is the order of the same elliptic curve, except the underlying field is GF(p) instead of ring Z/p^3Z
= 74894047922780452080480621188147614680859459381887703650502711169525598419741
= 22457563127094032648529052905270083323161530718333104214029365341184039143821
= 82792468191695528560800352263039950790995753333968972067250646020461455719312
mod = p * p * p
 
def point_double(x, y, z):
    t = (3 * x * x + a * z * z) % mod
    u = (2 * y * z) % mod
    v = (2 * u * x * y) % mod
    w = (t * t - 2 * v + mod * 3) % mod
    x2 = (u * w) % mod 
    y2 = (t * (v - w) - 2 * (u * u * y * y)) % mod
    y2 = (y2 + mod) % mod
    z2 = (u * u * u) % mod
    return x2, y2, z2
 
def point_add(x0, y0, z0, x1, y1, z1):
    if x0 == 0 and y0 == 0:
        return x1, y1, z1
    if x1 == 0 and y1 == 0:
        return x0, y0, z0
    t0 = (y0 * z1) % mod
    t1 = (y1 * z0) % mod
    t = (t0 - t1 + mod) % mod
    u0 = (x0 * z1) % mod
    u1 = (x1 * z0) % mod
    u = (u0 - u1 + mod) % mod
    u2 = (u * u) % mod
    v = (z0 * z1) % mod
    w = (t * t * v - u2 * (u0 + u1)) % mod 
    w = (w + mod) % mod
    u3 = (u * u2) % mod
    x2 = (u * w) % mod
    y2 = (t * (u0 * u2 - w) - t0 * u3) % mod
    y2 = (y2 + mod) % mod
    z2 = (u3 * v) % mod
    return x2, y2, z2
 
def point_mul(x, y, z, n):
    xr, yr, zr = 001
    while n:
        if n % 2 == 1:
            xr, yr, zr = point_add(xr, yr, zr, x, y, z)
            n -= 1
        n = n // 2
        x, y, z = point_double(x, y, z)
    return xr, yr, zr
 
x1 = 201395103510950985196528886887600944697931024970644444173327129750000389064102542826357168547230875812115987973230106228243893553395960867041978131850021580112077013996963515239128729448812815223970675917812499157323530103467271226
y1 = 217465854493032911836659600850860977113580889059985393999460199722148747745817726547235063418161407320876958474804964632767671151534736727858801825385939645586103320316229199221863893919847277366752070948157424716070737997662741835
 
x2 = 49376632602749543055345783411902198690599351794957124343389298933965298693663616388441379424236401744560279599744281594405742089477317921152802669021421009909184865835968088427615238677007575776072993333868804852765473010336459028 
y2 = 342987792080103175522504176026047089398654876852013925736156942540831035248585067987750805770826115548602899760190394686399806864247192767745458016875262322097116857255158318478943025083293316585095725393024663165264177646858125759
 
print((y1 ** 2 - x1 ** 3 - a * x1 - b) % mod)
print((y2 ** 2 - x2 ** 3 - a * x2 - b) % mod)
 
xf, yf, zf = point_mul(x1, y1, 1, N)
xs, ys, zs = point_mul(x2, y2, 1, N)
 
= xs * yf
= ys * xf
= GCD(A, B)
//= g
//= g
 
gg = (A * inverse(B, p * p)) % p
 
 
for i in range(02):
    cc = bytes_to_long(b"zer0pts{")
    val = (gg - cc * (1 << 256)) % p + i * p
    flag = b"zer0pts{" + long_to_bytes(val)
    print(flag)
cs

 

Tokyo Network

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
import base64
import random
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad
from qulacs import QuantumState, QuantumCircuit
from qulacs.gate import *
from secret import flag
 
GATES = {
    'CNOT': (CNOT, 2),
    'H': (H, 1), 'X': (X, 1), 'Y': (Y, 1), 'Z': (Z, 1),
    'S': (S, 1), 'SDAG': (Sdag, 1), 'T': (T, 1), 'TDAG': (Tdag, 1)
}
def parse_circuit(asm, qbits=9):
    """ Convert assembly into quantum circuit
    i.e.  q0 ---+--[Z]--
                |        <= CNOT 0,1; Z 0; H 1;
          q1 --[X]-[H]--
    """
    def apply(gate, args):
        return gate(*args)
 
    circuit = QuantumCircuit(qbits)
    cnt = 0
    for instruction in asm.replace('\n''').split(';'):
        t = instruction.strip().split()
        if t == []:
            continue
 
        if len(t) < 2:
            print("[-] Invalid instruction")
            exit(0)
 
        opecode, operand = t[0].upper(), t[1:]
        if opecode not in GATES:
            print("[-] Invalid gate")
            exit(0)
 
        operand = list(map(lambda x: int(x), ''.join(t[1:]).split(',')))
        if not all(map(lambda x: 0 <= x < qbits, operand)):
            print("[-] Invalid quantum bit specified")
            exit(0)
 
        if GATES[opecode][1!= len(operand):
            print("[-] Invalid number of operands")
            exit(0)
 
        gate = apply(GATES[opecode][0], operand)
        circuit.add_gate(gate)
 
        cnt += 1
        if cnt > 100:
            print("[-] Too large circuit")
            exit(0)
 
    return circuit
 
def transfer_and_measure(q):
    # Oops, noise might happen :(
    noise = QuantumCircuit(9)
    idx = random.randrange(09)
    noise.add_gate(DephasingNoise(idx, 0.31337))
    noise.add_gate(DepolarizingNoise(idx, 0.31337))
    noise.add_gate(BitFlipNoise(idx, 0.31337))
    noise.update_quantum_state(q)
 
    # Quantum arrived! You can update (or keep) the state :)
    circuit = parse_circuit(input('[?] Your Circuit: '))
    circuit.update_quantum_state(q)
 
    # Now you measure the quantum state :P
    return random.choices(range(2**9),
                          map(lambda x: abs(x), q.get_vector()))[0& 1
 
if __name__ == '__main__':
    N = 128
    xi, xip = 0.980.98
    p = (xi * (1 + xi))**0.5 - xi
    Np = int(N * (1 + 2*xi + 2*(xi*(1+xi))**0.5 + xip))
    print("[+] Np = " + str(Np))
 
    encoder = parse_circuit("""
    CNOT 0,3; CNOT 0,6;
    H 0; H 3; H 6;
    CNOT 0,1; CNOT 0,2;
    CNOT 3,4; CNOT 3,5;
    CNOT 6,7; CNOT 6,8;
    """)
 
    measured = 0
    ra, ba = 00
    for i in range(Np):
        ra = (ra << 1| random.choice([01])
        ba = (ba << 1| random.choices([10], [p, 1-p])[0]
        q = QuantumState(9)
        q.set_zero_state()
        if ra & 1:
            X(0).update_quantum_state(q)
        if ba & 1:
            H(0).update_quantum_state(q)
        encoder.update_quantum_state(q)
 
        measured = (measured << 1| transfer_and_measure(q)
        del q
 
    print("[+] Measured state: " + bin(measured))
    bb = int(input('[?] bb = '), 2)
    print("[+] ba = " + bin(ba))
 
    Nx, Nz = 00
    for i in range(Np):
        if (ba >> i) & 1 == (bb >> i) & 1 == 1:
            Nx += 1
        elif (ba >> i) & 1 == (bb >> i) & 1 == 0:
            Nz += 1
    if Nx < N * xi or Nz - N < N * xi:
        print("[-] Something went wrong :(")
        exit(0)
 
    xa = 0
    for i in range(Np):
        if (ba >> i) & 1 == (bb >> i) & 1 == 1:
            xa = (xa << 1| ((ra >> i) & 1)
    print("[+] xa = " + bin(xa))
 
    l = []
    for i in range(Np):
        if (ba >> i) & 1 == (bb >> i) & 1 == 0:
            l.append(i)
    chosen = random.sample(l, Nz - N)
    m = 0
    for i in chosen:
        m |= 1 << i
    print("[+] m = " + bin(m))
 
    k = 0
    for i in sorted(list(set(l) - set(chosen))):
        k = (k << 1| ((ra >> i) & 1)
 
    key = int.to_bytes(k, N // 8'big')
    iv = get_random_bytes(AES.block_size)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    ct = cipher.encrypt(pad(flag, AES.block_size))
    print("Result: " + base64.b64encode(iv + ct).decode())
 
cs

 

Solution

The context of this challenge is quite nice - it's quantum error correction with Shor and QKD with BB84.

I realized the BB84 part of the program after I read the flag, but if you look at it knowing it's BB84, it's quite clear.

 

Working backwards, we observe the following and eventually find the solution

  • our final goal is to find the AES key $k$
  • we know $ba$ and $m$, and we are free to choose $bb$, so we know the array $l$ and $chosen$
  • therefore, our goal is to find the bits of $ra$ in the indexes of $l \setminus chosen$
  • note that in those indexes, we have $ba$ and $bb$ bits all $0$
  • we see that this implies we don't need to worry about Hadamard gates
  • we only need to check whether or not X gate was applied at the 0th qubit
  • if there was no encoding and noise, we could just measure the 0th qubit to see whether 0 or 1 pops out
  • however, we have encoding and noise : how do we deal with this?
  • it turns out that the encoding part is simply the first part of Shor's quantum error correction code
  • therefore, we can just send the remaining part of the Shor's quantum error correction code and be done with it
  • note that our noise is single qubit, so we have no worries whatsoever
  • however, we need Toffoli gates to make the circuit : we don't have that in our arsenal of gates
  • this can be resolved by creating Toffoli gates with 2-qubit gates and single qubit gates : details are in Wikipedia.
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
from qulacs import QuantumCircuit, QuantumState
from qulacs.gate import *
import random
from pwn import *
from Crypto.Cipher import AES
import base64
 
 
# CNOT gate
def ADD_CNOT(a, b):
    return "CNOT " + str(a) + "," + str(b) + "; "
 
# single qubit gates
def ADD_single(s, a):
    return s + " " + str(a) + "; "
 
# toffoli
def ADD_toffoli(a, b, c): 
    ret = ""
    ret += ADD_single("H", c)
    ret += ADD_CNOT(b, c)
 
    ret += ADD_single("TDAG", c)
    ret += ADD_CNOT(a, c)
 
    ret += ADD_single("T", c)
    ret += ADD_CNOT(b, c)
 
    ret += ADD_single("TDAG", c)
    ret += ADD_CNOT(a, c)
 
    ret += ADD_single("T", b)
    ret += ADD_single("T", c)
 
    ret += ADD_CNOT(a, b)
    ret += ADD_single("H", c)
 
    ret += ADD_single("T", a)
    ret += ADD_single("TDAG", b)
 
    ret += ADD_CNOT(a, b)
 
    return ret 
 
 
 
= 128
xi, xip = 0.980.98
= (xi * (1 + xi))**0.5 - xi
Np = int(N * (1 + 2*xi + 2*(xi*(1+xi))**0.5 + xip))
 
# shor error correction
decoder = ""
decoder += ADD_CNOT(01)
decoder += ADD_CNOT(34)
decoder += ADD_CNOT(67)
decoder += ADD_CNOT(02)
decoder += ADD_CNOT(35)
decoder += ADD_CNOT(68)
decoder += ADD_toffoli(120)
decoder += ADD_toffoli(453)
decoder += ADD_toffoli(786)
decoder += ADD_single("H"0)
decoder += ADD_single("H"3)
decoder += ADD_single("H"6)
decoder += ADD_CNOT(03)
decoder += ADD_CNOT(06)
decoder += ADD_toffoli(360)
 
= remote('others.ctf.zer0pts.com''11099')
 
def get_bin():
    s = r.recvline()
    s = s.split()[-1].decode()
    return int(s, 2)
 
r.recvline()
for i in range(0860):
    r.sendline(decoder)
 
measure = get_bin()
bb = (1 << 400- 1
r.sendline(bin(bb))
 
ba = get_bin()
xa = get_bin()
= get_bin()
 
= []
for i in range(Np):
    if (ba >> i) & 1 == (bb >> i) & 1 == 0:
        l.append(i)
 
chosen = []
for i in range(Np):
    if (m & (1 << i)) > 0:
        chosen.append(i)
 
= 0
for i in sorted(list(set(l) - set(chosen))):
    k = (k << 1| ((measure >> i) & 1)
 
key = int.to_bytes(k, N // 8'big')
= r.recvline()
vv = s.split()[-1]
 
fin = base64.b64decode(vv)
iv = fin[:16]
ct = fin[16:]
 
cipher = AES.new(key, AES.MODE_CBC, iv)
print(cipher.decrypt(ct))
 
cs

'CTF' 카테고리의 다른 글

zer0lfsr 2021 Quick Solutions/Thoughts  (0) 2021.07.05
LINE CTF Crypto Writeups  (0) 2021.03.22
AeroCTF 2021 Crypto Writeups  (0) 2021.02.28
UnionCTF Crypto Writeups  (2) 2021.02.21
CryptoHack All Solve, Again  (10) 2021.01.31