Points: Dynamic

Solves: 48

Description:

AES is unbreakable. Right?

`nc 52.193.157.19 9999`

flag format: hitcon{…} (use the one in GitHub as test)

Given:

secretserver-03f9e1472f1088fcf5571d3288e759e3.py

Solution:

(by L0rdComm4nder and goulov)

TLDR

This attack has several steps

1. Get the encrypted flag by IV manipulation
2. Get the 1st block by IV manipulation using the fact that `hitcon{` is as long as `get-sha` and use 128 average tries to find each byte of 1st block
3. Find the length of padding
4. Use 3. to get the 1st half of 2nd block using the fact that `.strip()` removes `\n` both at the beginning and at the end of a string
5. Repeat 2. to get the 2nd half of 2nd block.

Description

There might be some more efficient solutions but the one that follows is the first that came to our mind and there is minor effort involved. Some interesting facts we get by looking at the given file.

• Chall uses AES in CBC mode

• Notice that the padding function looks like PKCS7 but the unpadding function does not check that. In particular, the unpadding function drops as many characters as the last one. This will be usefull in steps 3. and 4.

``````def pad(msg):

return msg[:-ord(msg[-1])]
``````

• Send message uses always the same `iv` but the `recv_msg` decrypts with whatever `iv` is given.

``````def send_msg(msg):
iv = '2jpmLoSsOlQrqyqE'
encrypted = encrypt(iv,msg)
msg = iv+encrypted
msg = base64.b64encode(msg)
print msg
return

def recv_msg():
msg = raw_input()
try:
msg = base64.b64decode(msg)
assert len(msg)<500
decrypted = decrypt(msg[:16],msg[16:])
return decrypted
except:
print 'Error'
exit(0)
``````
• Messages are always sent encrypted but

a. we can get the encryption of some fixed messages `send_msg('Welcome!!')` and `send_msg('command not found')`

b. we can get the encryption of the uknown flag `send_msg(flag)`.

c. we can get the encryption of several different hash-functions

``````if __name__ == '__main__':
proof_of_work()
with open('flag.txt') as f:
assert flag.startswith('hitcon{') and flag.endswith('}')
send_msg('Welcome!!')
while True:
try:
msg = recv_msg().strip()
if msg.startswith('exit-here'):
exit(0)
elif msg.startswith('get-flag'):
send_msg(flag)
elif msg.startswith('get-md5'):
send_msg(MD5.new(msg[7:]).digest())
elif msg.startswith('get-time'):
send_msg(str(time.time()))
elif msg.startswith('get-sha1'):
send_msg(SHA.new(msg[8:]).digest())
elif msg.startswith('get-sha256'):
send_msg(SHA256.new(msg[10:]).digest())
elif msg.startswith('get-hmac'):
send_msg(HMAC.new(msg[8:]).digest())
else:
except:
exit(0)
``````

Step 1. Get the encrypted flag by IV manipulation

Recall the AES CBC mode of operation in particular the decryption.

Recall that `P_i = D(C_i) ^ C_{i-1}` where `C_0` is the `IV` block. It is known that changing the bit `j` of block `C_i` will affect bit `j` of block `P_{i+1}` while destroying block `P_i` as the decryption of `C_i` is now rubish. However, if we do it to `IV`, we are ok as `IV` is discarded in the end.

So after the proof of work we get `IV + E('Welcome!!' + '\x07' * 7)`. In order to get the encrypted flag we need to transform it into `IV2 + E('get-flag' + '\x08' * 8)`. This is simple to do.

One knows that `P_1 = D(C_1) ^ IV` and we know `P1`, `C1` and `IV`. We want to get `P1bar = 'get-flag' + '\x08' * 8`. Defining `IV2 = IV ^ P1 ^ P1bar` we get

``````    D(C1) ^ IV2 = D(C1) ^ IV ^ P1 ^ P1bar = P1 ^ P1 ^ P1bar = P1bar
``````

So done. We can get the flag ciphered.

``````target = 'get-flag' + '\x08'*8
source = 'Welcome!!' + '\x07'*7
iv2 = ''
for i in range(16):
iv2 += chr(ord(target[i]) ^ ord(source[i]) ^ ord(iv[i]))
``````

Step 2. Get the 1st block by IV manipulation

Now we have `IV + E(hitcon{......})` Using the fact that the flag starts with `hitcon{` we can get the 1st block iterating the technique of Step 1.

Notice that `hitcon{` is as long as `get-sha`, and so transform the `IV` as above in order to get `IV2 + E(get-sha......})`. Now rotate the 8th character of `IV` among the 256 possible chars. Notice that if you send these 256 messages to the server, all but one will return `send_msg('command not found')`. The only one that will succeed and will be different is the one where the 8th character of `D(C1) ^ IV2 = '1'` as it will call the `get-sha1` operation. This will allow us to get the 8th character of the flag

``````    (for char 8 of P1)   D(C1) ^ IV2 = '1'   and   D(C1) ^ IV = P1   ==>   P1 = IV ^ IV2 ^ '1'
``````

You will get it on average 128 calls.

How to get the 9th character? The idea is to shift the `get-sha` one position to the right (using chars 2 to 8 instead of 1 to 7) and apply the same technique to the 9th char. Notice that at this stage we know chars 1 to 8 of the flag and so we can do it.

The problem is that if we do it then the message will not begin with `get-sha1` as required in `main`. The trick here is to pad with `\n` to the left. Since `main` does `strip()` after decryption it will drop the `\n` at the beginning. (honestly do not know if this was the intended solution but it worked for us).

Iterate this and get the 1st block of flag: `hitcon{Paddin9_1`

Step3. Find the length of padding

For the next steps it will be important to find the length of the padding. Notice that if you control the last byte of the last block (in this case `C3`) you can discard as many bytes as you want while unpadding as unpadding only checks the very last byte of the plaintext to assess padding. So we apply Step 1. to transform the ciphered flag to `get-sha1` and rotate the last byte of `C2` among all the possible 256 values. This in turn will rotate the last byte of `C3` among the possible 256 values and since

• the length of the message is 48 (3 blocks)
• the length of `get-sha1` is 8

it will return

• 40 different ciphers (we can remove up to 40 chars and still get a message that starts with `get-sha1`), each of them once.
• one cipher repeated 216 times, the cipher of `command not found`, that is when we remove more than 40 chars.

Repeat for `get-md5`. Since `get-md5` is of length 7 it will return

• 41 different ciphers (we can remove up to 41 chars and still get a message that starts with `get-md5`), each of them once.
• one cipher repeated 215 times, the cipher of `command not found`, that is when we remove more than 41 chars.

The char that is in MD5 and not in SHA1 is the one that gives us the length of the flag.

``````    (for char 16 of P3)   D(C3) ^ C2bar = '41'   and   D(C3) ^ C2 = pad_value   ==>   pad_value = C2 ^ C2bar ^ '41'
``````

This requires 2 * 256 calls to the server, and in this case we got `pad_value = 16`.

Notice that the decryption of `C2` will be garbage, hence it will not return `P2` but in this case we do not care about it.

Step 4. Get the 1st half of 2nd block

This step is a mix of previous ones.

a. From `IV + E(hitcon{Paddin9_1)` (notice we know the 16 chars of `P1`), prepare `IV2 + E('\n' * 9 + 'get-md5' + C2 + C3)`. From this we can manipulate the last byte of `C2`, to affect the last byte of `P3`, so that when `unpad` is done everything is discarded except the 1st byte of `P2` and so we otain `E(MD5(1st byte of P2))`. If you want to keep just 1 byte of `P2` (that is 17 of the whole message) just change the last byte of `C2` to

``````    newchar = C2[-1] ^ (48 - 17) ^ pad_value  ==>  new_padding = D(C3) ^ C2bar = D(C3) ^ C2[-1] ^ (48 - 17) ^ pad_value = pad_value ^ (48 - 17) ^ pad_value = (48 -17)
``````

The problem with this trick is that by changing the last byte of `C2` we are damaging its decryption and so we do not get the original `P2`. The way we sorted this was to add a dummy block in the middle and prepare `IV2 + E('\n' * 9 + 'get-md5' + C2 + C2 + C3)` instead. By applying the tricks on the 2nd copy of `C2`, we do not damage the decryption of the 1st `C2` and consequently can get the original `P2` there. Notice that you need to discard 16 more chars in this case.

b. Now from `IV + E('Welcome!!' + '\x07' * 7)` prepare `IV2 + E('get-md5' + char + '\x08' * 8)` for all possible 256 chars and compare this to the cipher obtained above. One of them will match, and this will give us the 1st char of `P2`.

Use this char and prepare

a. with dropping one less char.

b. with `IV2 + E('get-md5' + 1st char of P2 + char + '\x08' * 8)`

This will return the 2nd char of `P2`. Iterate up to 9 chars (this is the most you can extend in b.)

We got `P2 = '5_ve3y_h4r'`

Step 5. Get the 2nd half of 2nd block

This is just the repetition of Step 2. discarding the 1st block of the encrypted flag and considering `5_ve3y_` instead of `hitcon{`. We will use in this case `C1` as the `IV`.

This got us the remaining 9 chars: `hitcon{Paddin9_15_ve3y_h4rd__!!}`

VERY IMPORTANT: You can do all these decryption tricks with CBC only if you know what is the plaintext you want to match!