Tags: crypto
Rating: 3.0
In this challenge, we are given a Python file which generates 16 random bytes. It then prints those bytes in hex form and takes in a message in hex form. It then verifies that the message begins with "I solemnly swear that I am up to no good." followed by a null byte. If that check passes, the message is prepended with its length packed into 16 big-endian bytes. It is then padded according to PKCS#7. After being padded, it is encrypted with AES-CBC using a key and IV of 16 null bytes. If the last block is equal to the randomly generated bytes printed earlier, the flag is printed.
This is a construction known as CBC-HMAC, where data is signed by encrypting it with AES-CBC and using the last block as the signature. In CBC mode, every block is encrypted by XORing its plaintext value with the previous block (or the IV, in the case of the first block) and then encrypting using the AES primitive. As such, if we want to control the encryption of the last block, we need to know the original value of the plaintext of the last block and control the encrypted version of the second to last block, such that:
AES_decrypt(target_block) == last_block_plaintext XOR second_to_last_block_ciphertext
If we have a message that's a multiple of the block size in length, under PKCS#7 the last block will be the block size in bytes repeated for the entire block. In this case, '\x10' * 0x10. We also have the key and IV and are provided the target block value before we submit our message, so we can produce the decrypted version of the target block. Now we can rewrite our equation from above to the following:
second_to_last_block_ciphertext == last_block_plaintext XOR AES_decrypt(target_block)
So first, we connect to the service and receive the target block value. We decrypt that value with AES and XOR it with '\x10' * 0x10 to get the value we need to produce for the second to last block. Since we don't care what it decrypts to, we can take any valid message, encrypt it, and append our ciphertext block, then decrypt to get the desired value. We'll calculate our length field as:
<span>win_message = 'I solemnly swear that I am up to no good.\0'
length = len(win_message) + 16-(len(win_message) % 16) + 16 #extra 16 for junk block</span>
We then encrypt the length field and the win message padded with junk to a multiple of 16 bytes. After encryption, we take our desired second-to-last-block ciphertext value and append it to the ciphertext, then decrypt with AES-CBC. We now have the message we need to send, prepended with the length field. Since this will be added by the server, we need to remove it before sending our message, hex encoded.
A solver script which performs the task described above is as follows:
~~~
#!/usr/bin/env python3
from Crypto.Cipher import AES
import binascii, os, struct
import socket
def remote(host, port):
s = socket.socket()
s.connect(('104.198.243.170', 2501))
return s
def sxor(s1, s2):
return bytes([c1 ^ c2 for (c1, c2) in zip(s1, s2)])
def get_formatted_length(m):
return (len(bytearray(m))+16).to_bytes(0x10, 'big')
# Set up AES-ECB with same key for an easy AES primitive
derp_ecb = AES.new(bytes(0x10), AES.MODE_ECB)
# Set up AES-CBC with same key for calculating plaintext to send
derp_cbc = AES.new(bytes(0x10), AES.MODE_CBC, bytes(0x10))
full_block_padding = (b'\x10' * 0x10)
full_block_nulls = (b'\x00' * 0x10)
msg_prefix = b'I solemnly swear that I am up to no good.\0'
msg_prefix_padded = msg_prefix + b'A'*6
derp_cbc.encrypt(get_formatted_length(msg_prefix_padded)+msg_prefix_padded)
# get target and decrypt
r = remote('104.198.243.170', 2501)
target = r.recv(33).strip()
value_which_encrypts_to_target = derp_ecb.decrypt(binascii.unhexlify(target))
# xor to get payload
payload = sxor(value_which_encrypts_to_target, full_block_padding)
ciphertext = ciphertext_without_payload + payload
# Re-initialize AES-CBC decryptor
derp_cbc = AES.new(bytes(0x10), AES.MODE_CBC, bytes(0x10))
# remove formatted length from beginning
message = message[16:]
r.send(binascii.hexlify(message) + b'\n')
print(r.recv(1024))
~~~