babycrypto
Crypto, 300 points.

Description

Start with this one! nc 13.233.66.116 5000
1
#!/usr/bin/python3 -u
2
import os
3
from binascii import hexlify, unhexlify
4
5
flag = open("./flag","rb").read()
6
7
class bb(object):
8
def __init__(self, key):
9
self.meh = [x for x in range(256)]
10
j = 0
11
for i in range(256):
12
j = (j + self.meh[i] + key[i%len(key)])&0xff
13
self.meh[i], self.meh[j] = self.meh[j], self.meh[i]
14
self.cat = 0
15
self.mouse = 0
16
17
def crypt(self, string):
18
out = []
19
for c in string:
20
self.cat = (self.cat+1)&0xff
21
self.mouse = (self.cat+self.meh[self.cat])&0xff
22
self.meh[self.cat], self.meh[self.mouse] = self.meh[self.mouse], self.meh[self.cat]
23
k = self.meh[ (self.meh[self.cat]+self.meh[self.mouse])&0xff ]//2
24
out.append((c+k)&0xff)
25
return bytearray(out)
26
27
28
cipher = bb(os.urandom(32))
29
30
while True:
31
print("Commands: \n(e)ncrypt msg or (p)rint flag")
32
choice = input()
33
34
if choice == 'e':
35
message = input()
36
print(hexlify(cipher.crypt(unhexlify(message))))
37
elif choice == 'p':
38
print(hexlify(cipher.crypt(flag)))
39
else:
40
print("meh!")
Copied!

Solution

So I'm not sure about this task's title and description since only 4 teams managed to solve it, and I'm also not sure why there were only 4 solves since it was pretty simple.
We are given a service which runs the given Python script. We are able to encrypt messages and print an encrypted version of the flag:
1
Commands:
2
(e)ncrypt msg or (p)rint flag
3
e
4
61626364
5
b'dba4abb9'
6
Commands:
7
(e)ncrypt msg or (p)rint flag
8
p
9
b'73bf75c1d4a8ac5fd1cc9bd9388290906dadc7388298789b97879291598bd3d58582c8787c89c1d6af882b'
Copied!
It looks like the service initializes some kind of cryptographic stream with a random 32-byte key.
The algorithm looks a lot like RC4, except for two (not so) small details: addition is used instead of XOR, and the keystream byte is divided by 2 before being used.
Knowing about RC4 was not needed to solve this challenge, you just have to understand that the random key is used to produce a pseudo-random infinite stream of bytes that is used to encrypt plaintexts, here using addition modulo 256 instead of XOR.
Asking for the flag twice will encrypt it twice, but the keystream will be at a different position so the output is different.
1
Commands:
2
(e)ncrypt msg or (p)rint flag
3
p
4
b'ba9acd8cfab0c47b8ebaf4af6dc450656ba8798f7cadd6c47acebaae9789c3b777d2bd8f697bcff2add369'
5
Commands:
6
(e)ncrypt msg or (p)rint flag
7
p
8
b'6c767a8cb94c9848957dc282a6c75aa2cc8fdf9b9cb2f1788c698ca8b08b6dc24da1d87b5371a0a7ea9e3f'
Copied!
Obviously, the weakness lies in the division by 2 of k in the crypt method. What this means is that k can only take values in 0, ..., 127 before it is added to our plaintext byte.
Since we know the flag will be a readable ASCII string, this reduces the amount of possibilities for a plaintext character. For instance, if we consider this encrypted version of the flag:
1
ba9acd8cfab0c47b8ebaf4af6dc450656ba8798f7cadd6c47acebaae9789c3b777d2bd8f697bcff2add369
Copied!
...it starts with 0xBA, and if we note p[0] the first character of the flag, we have p[0] + k = 0xBA.
As k can only take values between 0 and 127, we know p[0] can only take values between 0x3B and 0xBA, and since we assume it is readable ASCII, we know p[0] is somewhere between 0x3B and 0x7F.
Now we can do this for every byte of the plaintext but there's still too many possibilities... We cannot retrieve the flag like that.
Of course, the idea was to exploit the fact that we can encrypt the flag several times. For each byte, the higher its encrypted value is, the smallest the space of possibilities for the associated plaintext character is.
Therefore, each time we ask for a new encrypted flag, there is a probability that we're reducing the number of flag candidates. If we're asking for enough encrypted flags, we can thus reconstruct it with a high probability.
Here's the exploit:
1
from binascii import unhexlify as unhex
2
import socket
3
4
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
5
s.connect(('13.233.66.116', 5000))
6
s.recv(4096)
7
8
L = []
9
charset = b'{}ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
10
11
count = 0
12
while True:
13
s.send(b'p\n')
14
res = s.recv(4096)
15
try:
16
res = unhex(res.split(b'\n')[0].split(b"'")[1])
17
except:
18
continue
19
L.append(res)
20
21
plaintext = b''
22
for i in range(43):
23
for o in charset:
24
good = True
25
for k in range(len(L)):
26
q = (L[k][i] - o) & 255
27
if q >= 128:
28
good = False
29
break
30
if good:
31
plaintext += bytes([o])
32
break
33
34
print(count, plaintext)
35
count += 1
Copied!
The flag is retrieved in about 400 requests:
1
0 b'{{{{{{A{{{{{0{A{A{{{{{{{{A{{0{{{{{A{AA{{{{A'
2
1 b'{{{{{AA0{{{{0{A{H{{A{A{{{G{{0{{{A{A{AA{{{{0'
3
2 b'{{{A{AA0{{{{0{A{H{{A{A{{{G{{0{{{0{AAAA{{{{0'
4
3 b'{{{a{AA0{{{a0{A{M{VA{A{{{Z{{0{{I0{SAAA{{{{0'
5
4 b'{{{a{AA0{{{a0{A{M{VA{A{{{Z{{0{{I0{SAAA{{{{0'
6
5 b'd{Za{0A0{{{a0{A{M{VA{A{{aZ{{0{{I0{SYAA{{{{0'
7
6 b'd{Za{0A0{{{a0{A{M{VA{AnJaZ{{0{AI0{SYAA{{{{0'
8
7 b'dAZa{0W0{Q{a0{A{M{VA{AnJaZ{{0{TU0{SYAA{{{{'
9
8 b'eDZa{0W0{Q{a0{0{M{V0{AnJaZ{{0{TU0{aYAA{{{{'
10
9 b'eDZa{0W0{Q{c0{0{M{V0{AnJaZ{{0{TU0baYAA{{{{'
11
10 b'eDZa{0b0YQ{c0{0{M{V0{AnJaZ{{0{Xa0baY0A{{{{'
12
11 b'eDZa{0b0YQ{c2{0{M{V0{AqJaZ{{0{Xa0baY0A{{{{'
13
14
[...]
15
16
130 b'flaf{3r2_dyn3l1c_pb0x0s_a_sh0nga0f_a14utx}'
17
131 b'flaf{3r2_dyn3l1c_pb0x0s_a_sh0nga0f_a14utx}'
18
132 b'flaf{3r2_dyn3l1c_pb0x0s_a_sh0nga0f_a14utx}'
19
133 b'flaf{3r2_dyn3l1c_pb0x0s_a_sh0nga0f_a14utx}'
20
134 b'flaf{3r2_dyn3l1c_pb0x0s_a_sh0nga0f_a14utx}'
21
135 b'flaf{3r2_dyn3l1c_pb0x0s_a_sh0nga0f_a14utx}'
22
136 b'flaf{3r2_dyn3l1c_pb0x0s_a_sh0nga0f_a14utx}'
23
137 b'flaf{3r2_dyn3l1c_pb0x0s_a_sh0nga0f_a14utx}'
24
138 b'flaf{3r2_dyn3l1c_pb0x0s_a_sh0nga0f_a14utx}'
25
139 b'flaf{3r2_dyn3l1c_pb0x0s_a_sh0nga0f_a14utx}'
26
27
[...]
28
29
444 b'flag{4r3_dyn4m1c_sb0x3s_a_th0ng_0f_b34uty}'
30
445 b'flag{4r3_dyn4m1c_sb0x3s_a_th0ng_0f_b34uty}'
31
446 b'flag{4r3_dyn4m1c_sb0x3s_a_th1ng_0f_b34uty}'
Copied!
Enjoy!
Last modified 2yr ago
Copy link