The first two crypto challenges of CTF101 were about encoding.
sv-encoder
We are given a Python script encoding the flag:
1import base64 as b64
2
3flag = b'sv{FAKE_FLAG}'
4flag = b64.b16encode(flag)
5for i in range(20):
6 flag = b64.b64encode(flag)
7for j in range(5):
8 flag = b64.b16encode(flag)
9flag = flag.decode()
10with open('out.txt', 'w') as fh:
11 fh.write(flag)
As we can see, the flag is encoded with base16, then base64 20 times, then again 5 times base16. We are also given the output in out.txt. The solution consists of reversing the process, step by step:
1import base64 as b64
2
3with open('out.txt') as fh:
4 flag = fh.read().strip()
5flag = flag.encode()
6for j in range(5):
7 flag = b64.b16decode(flag)
8for i in range(20):
9 flag = b64.b64decode(flag)
10flag = b64.b16decode(flag)
11print(f'{flag = }')
12# flag = b'sv{enc0d1ng_1s_n0t_3ncrypt10n}'
sv-vs-encoder
The second challenge was a bit more involved. This time we have a custom encoding function, magic_shuffle:
1def magic_shuffle(poor_string):
2 assert type(poor_string) == bytes, 'Magicians only use bytestrings'
3
4 magic_string = b''
5
6 for c in poor_string:
7
8 mask = 0b11
9 b0 = c & mask
10 b1 = (c >> 2) & mask
11 b2 = (c >> 4) & mask
12 b3 = (c >> 6) & mask
13
14 magic_char = bytes([bi + 32 for bi in [b1, b3, b0, b2] ])
15 # print(f'{magic_char = }')
16
17 magic_string += magic_char
18
19 return magic_string
The flag is shuffled using this function and then base16 encoded.
1flag = b'sv{FAKE_FLAG}'
2
3flag = magic_shuffle(flag)
4flag = b64.b16encode(flag)
Let’s look closer at magic_shuffle. For each character c in our string we do three things:
- first we derive values
b0tob3by using amask = 0b11and a binary shift: this means thatb0are the last two significant bits ofc,b1the next two and so on; for instance,ain binary is0b1100001, and hence we would getb0 = 0b01,b1 = 0b00,b2=0b10andb3 = 0b01; - then we add
32to each one of thebi; - finally, they are shuffled, and a bytestring made by
[b1, b3, b0, b2]is returned; for instance, if we try to encodeb'a'we getb' !!"'.
The key observation here is that for each single byte we encode, we get four bytes, and that these four bytes entirely define the starting byte. All we have to do is once again go backwards: for each tuple of four bytes we take them, we unshuffle them, we subtract 32 from each one of them and then put them one next to each other to rebuild the original byte.
1def magic_unshuffle(shuffled_string):
2 assert type(shuffled_string) == bytes
3
4 assert len(shuffled_string) % 4 == 0 # Each char is encoded in 4 chars
5
6 out = []
7
8 while shuffled_string != b'':
9 # Take the first 4 chars
10 next_chars, shuffled_string = shuffled_string[:4], shuffled_string[4:]
11
12 # Unshuffle
13 b1 = next_chars[0] - 32
14 b3 = next_chars[1] - 32
15 b0 = next_chars[2] - 32
16 b2 = next_chars[3] - 32
17
18 # Reconstruct
19 c = b0 + (b1 << 2) + (b2 << 4) + (b3 << 6)
20
21 out.append(c)
22
23 return bytes(out)
Before applying this, we have to base16 decode our input, and we are done:
1flag = b'20212323212122232221232320212323202020232321232122212123202020232121212323212321212020232021222320202323232123212120202323212321232121222120202321212322202021232021232220202123212020232321222223212321212120232020202320202023202021222020212223212123'
2flag = b64.b16decode(flag)
3flag = magic_unshuffle(flag)
4# flag = b'sv{s0_y0u_4r3_4_m4g1c14n_t00!!}'