Post

HackDay 2024 | Flip The Bit | [Crypto]

HackDay 2024 - Write-Up - Flip The Bit [Crypto]

Statement

1
2
3
4
5
6
Nos équipes ont réussi à identifier un serveur semblant appartenir au SYNDICAT. Malheureusement celui-ci est protégé … Nous devons faire appel à vos services en tant que crack en cybersécurité pour déjouer le système et avoir accès aux informations qu’il contient !
Our teams have successfully identified a server that appears to belong to SYNDICAT. Unfortunately, it is protected... We need to enlist your services as cybersecurity experts to bypass the system and gain access to the information it contains!

Connectez-vous à l'instance avec netcat et trouvez le moyen de vous faire passer pour un administrateur !
Connect to the instance with netcat and find a way to become an administrator ! 
nc challenges.hackday.fr 50398   

Author : Didouad
Points : 454
Solves : 18

Analysis of the statement

The statement tell us we need to bypass the connection system in order to get the flag. The conncetion system is on an instance : nc challenges.hackday.fr 50398.

Analysis of the connection system

After running netcat on the instance, we get the following prompt :

1
2
3
[>A] Name Yourself
[>B] Give your token
 > 

We deduce we can register ourself or login with a token, here is how the login works :

1
2
3
4
5
6
7
8
9
10
11
Network of SYNDICAT
[>A] Name Yourself
[>B] Give your token
 > A 
A
 > Username : skilo
skilo
 > Profession : hacker
hacker
username=skilo&agent=agence&profession=hacker&admin=false&time=1705829072.531827
Token : 8b8441779bb4dd7dd6da8028c576c856fcc5774e912237d96248c34425931cd20ee13eacdad2709068084aca3828105f5b6583712fd1a149964dec46ba1f8620e9a888f59447a2bc1d08cfb4ffed42352b9e813432b652e1b0c07ae4405096c977cf4e8092c281ff5cee1a7557b68f2e

According to the title and the output we immediately notice the possibility of attacking AES CBC with bit flipping. If you don’t know how works this attack, you can search on internet, this is very well explained.

About the token

With this simple python function :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BLOCK_SIZE = 16
def show_blocks(plaintext, token):
	for i in range(0, len(token), BLOCK_SIZE):
		print(plaintext[i:i+BLOCK_SIZE], token[i:i+BLOCK_SIZE])

>>> plaintext = b"username=skilooo&agent=agence&profession=hackerr&admin=false&time=1705829266.675812"
>>> token = bytes.fromhex("a56defc95685a44b2e29976e7fc5f3f7b87553c12210baf9955b8c2013ab1937ddb7fff35c0bd87275a2920a45dcfb38c864eea5fbf11313231b2983a7a768eaffa771a3feaf931d80ddedc431efcdd23e2c40c7d14f52670973451869f6f31d3beec2f167d2ca9c1d8325f742bceb31")
>>> show_blocks(plaintext, token)
b'username=skilooo' b'\xa5m\xef\xc9V\x85\xa4K.)\x97n\x7f\xc5\xf3\xf7'
b'&agent=agence&pr' b'\xb8uS\xc1"\x10\xba\xf9\x95[\x8c \x13\xab\x197'
b'ofession=hackerr' b'\xdd\xb7\xff\xf3\\\x0b\xd8ru\xa2\x92\nE\xdc\xfb8'
b'&admin=false&tim' b'\xc8d\xee\xa5\xfb\xf1\x13\x13#\x1b)\x83\xa7\xa7h\xea'
b'e=1705829266.675' b'\xff\xa7q\xa3\xfe\xaf\x93\x1d\x80\xdd\xed\xc41\xef\xcd\xd2'
b'812' b'>,@\xc7\xd1ORg\tsE\x18i\xf6\xf3\x1d'
b'' b';\xee\xc2\xf1g\xd2\xca\x9c\x1d\x83%\xf7B\xbc\xeb1'

We can see that there one more block in the token that in the plaintext, we can guess that the first block of the token is the IV. Having access in input/output to the IV during a bit flipping attack is a good thing because we can avoid destroying a ciphertext block by destroying the IV instead.

Now, let’s try to login we this token :

1
2
3
4
5
6
7
8
9
Network of SYNDICAT
[>A] Name Yourself
[>B] Give your token
 > B
B
 > Token : a56defc95685a44b2e29976e7fc5f3f7b87553c12210baf9955b8c2013ab1937ddb7fff35c0bd87275a2920a45dcfb38c864eea5fbf11313231b2983a7a768eaffa771a3feaf931d80ddedc431efcdd23e2c40c7d14f52670973451869f6f31d3beec2f167d2ca9c1d8325f742bceb31
a56defc95685a44b2e29976e7fc5f3f7b87553c12210baf9955b8c2013ab1937ddb7fff35c0bd87275a2920a45dcfb38c864eea5fbf11313231b2983a7a768eaffa771a3feaf931d80ddedc431efcdd23e2c40c7d14f52670973451869f6f31d3beec2f167d2ca9c1d8325f742bceb31
username=skilooo&agent=agence&profession=hackerr&admin=false&time=1705829266.675812
You are not admin nor from syndicat !

The output is clear : we have to change admin=true by admin=false and agent=agence by agent=syndicat.

Solving the chall

For this challenge, I don’t have the time to do the full write-up, so I’ll just give some elements to understand the trick below :

  1. We realize that we need to change admin=false to admin=true and agent=agence to agent=syndicat.
  2. We notice that the entire decrypted token is passed through the Python .decode() method.
  3. We decide to use the Initialization Vector (IV) to keep the ciphertext unchanged. -> However, there’s a problem: the text to be modified is more than 16 bytes, so we must necessarily change a cipher block other than the IV.
  4. Stroke of genius: thanks to the Python error generated by .decode(), we have access to the plaintext byte by byte of the first block.
    Example: When we get the error: “can’t decode 0xfa at position 0,” we know that the byte at position 0 is 0xfa. We just need to modify the IV at position 0 to get what we want, to then get an error at a more advanced position and continue until we know the entire block.

In summary:

  1. I edit the first cipher block, so my second plaintext block is what I want.
  2. I send the token as is to the authentication system; it gives me one by one the problematic bytes of the first plaintext block.
  3. After having just decodable chars in block 0 I will know the full plaintext of block 0
  4. I adjust my IV to put what I want in the first plaintext block.
  5. It’s flag!

Here is my (horrible) python script :

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
from pwn import remote, xor
from time import sleep

BLOCK_SIZE = 16

host = "challenges.hackday.fr" 
port = 50398
conn = remote(host, port)

username = b"ski"
profession = b"hacker&username=skilo"

def get_token(username, profession):
	# Choix du register
	res = conn.recvuntil(b">")
	# print(res.decode())
	conn.sendline(b"A")
	
	# Choix du username
	res = conn.recvuntil(b":")
	# print(res.decode())
	conn.sendline(username)
	
	# Choix de la profession
	res = conn.recvuntil(b":")
	# print(res.decode())
	conn.sendline(profession)

	# Choix de la profession
	res = conn.recvuntil(b"[>A]")
	# print(res.decode())

	token = bytes.fromhex(res.split(b"Token :")[1].split(b"\r\n")[0].split(b" ")[1].decode())
	payload = res.split(b"Token :")[0].strip().split(b"\r\n")[1]

	return (payload, token)

def edit_token(payload, token):
	print(len(payload), len(token))
	# assert len(payload) + 16 == len(token)
	# Parsing tu token raw
	iv = token[:BLOCK_SIZE]
	token = token[BLOCK_SIZE:]

	# Reparsing (plus précis)
	expected_block = b"dmin=true&profes"
	current_cblock = token[:BLOCK_SIZE]
	current_pblock = payload[BLOCK_SIZE:BLOCK_SIZE*2]

	# Bit flipping pour edit le deuxieme block
	z_0 = xor(current_cblock, current_pblock)
	new_cblock = xor(z_0, expected_block)

	# Construction du nouveau token
	resultat = b""
	resultat += iv
	resultat += new_cblock
	resultat += token[BLOCK_SIZE:]

	# Retour du nouveau token
	return resultat

def send_token(token):
	# Conversion hex + bytes + on conserve l'iv
	iv = token[:BLOCK_SIZE]
	token = token.hex().encode()

	# Envoie du token
	conn.sendline(b"B")
	conn.sendline(token)

	# Reception de la réponse + print dans tous les cas
	res = conn.recvuntil(b"Network of SYNDICAT")
	print("---------------------------------------------")
	print(res)
	print("---------------------------------------------")

	if b"You are not admin nor from syndicat" in res:
		last_chance(bytes.fromhex(token.decode()), res)
		exit(1)

	if b"can't decode" not in res:
		clean_res = res # TODO
		return_value = {
			"status": "can_decode",
			"res": clean_res,
			"iv": iv
		}

	# On récup le byte qui a rendu du non imprimable
	error_byte = res.split(b"decode byte ")[1].split(b" ")[0]
	error_pos  = res.split(b"position ")[1].split(b":")[0]

	return_value = {
		"status": "cant_decode",
		"error_byte": int(error_byte.decode(), 16),
		"error_pos": int(error_pos.decode()),
		"iv": iv,
		"token": token
	}

	return return_value

def edit_iv(error_infos):
	if error_infos["status"] != "cant_decode":
		print("Ça n'avait pas échoué")
		conn.interactive()

	# Parsing des données
	iv = error_infos["iv"]
	token = error_infos["token"]
	char_pos = error_infos["error_pos"]
	char_pt = error_infos["error_byte"]

	# Infos cools
	expected_pt = b"agent=syndicat&a"

	z_i = iv[char_pos] ^ char_pt
	temp_iv = list(iv)
	temp_iv[char_pos] = z_i ^ expected_pt[char_pos]

	return bytes(temp_iv)

def last_chance(token, res):
	store_v = len(token)
	iv = token[:BLOCK_SIZE]
	token = token[BLOCK_SIZE:]

	current_pblock = res.split(b"Token :")[1].split(b"\r\n")[1][:BLOCK_SIZE]
	expected_pt = b"agent=syndicat&a"
	assert len(current_pblock) == len(expected_pt)

	z = xor(current_pblock, iv)
	new_iv = xor(z, expected_pt)
	assert len(new_iv) == BLOCK_SIZE
	new_token = new_iv + token
	print(len(new_iv), len(token), len(new_token))
	print(len(new_token))
	assert len(new_token) == store_v
	print("LAST CHANCE WAS CALLED")
	send_token(new_token)

def show_blocks(p_input, token):
	for i in range(0, len(token), BLOCK_SIZE):
		print(p_input[i:i+BLOCK_SIZE], token[i:i+BLOCK_SIZE])

payload, token = get_token(username, profession)

show_blocks(payload, token)
# exit(1)

new_token = edit_token(payload, token)

while True:
	sleep(0.5)
	return_value = send_token(new_token)
	print(return_value)
	new_iv = edit_iv(return_value)
	new_token = new_iv + new_token[BLOCK_SIZE:]

Flag : HACKDAY{This_15_SH0wTime}

This post is licensed under CC BY 4.0 by the author.