Cryptography – 484 pts (35 solves) – Chall author: willwam845
A clean AES-CTR Padding Oracle Attack challenge, no hurdles, no bs. We can send cipher texts to the server and it will tell us whether or not it succeeded to unpad the decrypted cipher text. This allows us to straight up use the server as a padding oracle to decrypt the encrypted flag.
Check out write-ups by my teammates on K3RN3L4RMY.com
Exploration
The server we are allowed to connect to can do only a single thing for us, attempt decryption and unpadding of a given ciphertext. The server will only reveal whether or not there was an error during this process. This just reeks of a padding oracle attack. Let’s take a closer look at the server’s decrypt function.
def decrypt(ct):
try:
iv = ct[:16]
ct = ct[16:]
ctr = Counter.new(128, initial_value=bytes_to_long(iv))
cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
pt = cipher.decrypt(ct)
unpad(pt, 16)
return 1
except Exception as e:
return 0
Exploitation
The server uses AES in CTR mode, which behaves as a stream cipher and therefore is malleable. This means the padding oracle attack becomes quite simple as we can simply alter a single byte to affect the corresponding plaintext byte. I do not plan to go into a detailed description too much here, but here is the gist of the attack.
- Start at the back of an 16-byte block and XOR the ciphertext byte with some byte (iterate form 0 up to 255), until the server returns successfull unpadding.
- Because the padding is valid, this means the plaintext last byte must be ‘0x01’ (see PKCS padding). This allows us to deduce the original plaintext byte from the valid altered ciphertext byte as follows. \(\mathrm{pt\_byte} = \mathrm{altered\_byte} \oplus \mathrm{padding\_byte} \oplus \mathrm{original\_ct\_byte}\)
- Continue with the second-to-last byte and try to set the last two bytes to valid padding, so ‘0x02’.
- Continue this process for the entire 16-byte block and repeat for each 16-byte block, except for the IV as we do not need to recover that.
There are some nuances I have not touched upon, but they are included in the commented script below.
#!/usr/bin/env python3
#
# Polymero
#
def AES_CTR_POA(ENC, padding_oracle):
""" Perform AES-CTR Padding Oracle Attack on given ciphertext and oracle function. """
# Initliase decryption string
DEC = b''
# For every 16-byte block
while len(ENC) > 16:
BLOCK = b''
# For every byte in the block
for l in range(1,16+1):
# Padding value
padbyt = l
# Collect all valid bytes
valid_byts = []
for k in range(256):
# Alter a single byte of ciphertext
fuzz = ENC[:-l] + bytes([k]) + bytes([ENC[len(ENC)-(l-1):][i] ^ BLOCK[i] ^ padbyt for i in range(len(BLOCK))])
assert len(fuzz) == len(ENC)
# Pass to the oracle and recvieve unpadding success {0,1}
derr = padding_oracle(fuzz)
# If valid, the decrypted byte must be the padding value
if derr:
valid_byts += [k]
# Multiple hits, take the new one
if len(valid_byts) > 1:
if ENC[-l] in valid_byts:
valid_byts.remove(ENC[-l])
# Still multiple hits, error
if len(valid_byts) != 1:
print(valid_byts,l)
return 'ERROR'
# Recover plaintext byte and prepend to BLOCK
pt_byt = valid_byts[0] ^ padbyt ^ ENC[-l]
BLOCK = bytes([pt_byt]) + BLOCK
print(BLOCK+DEC)
# Prepend BLOCK to DEC string
DEC = BLOCK + DEC
# Remove block from ciphertext
ENC = ENC[:-16]
# Return
return DEC
# Imports
from pwn import *
# Remote connection
host = "babypad.be.ax"
port = 1337
s = remote(host,port)
# Retrieve the encrypted flag
ENCFLAG = bytes.fromhex(s.recvuntil(b'\n',drop=True).decode())
s.recv()
def request_decrypt(ciphertext):
""" Return unpad success of given ciphertext. """
s.sendline(ciphertext.hex().encode())
if s.recvuntil(b'\n',drop=True) == b'1':
s.recv()
return 1
else:
s.recv()
return 0
# FLAG!
print(AES_CTR_POA(ENCFLAG, request_decrypt))
Ta-da!
corctf{CTR_p4dd1ng?n0_n33d!}