Androids Encryption (Crypto) - Pwn2Win CTF 2020
crypto 115 - 108 solves
We intercept an algorithm that is used among Androids. There are many hidden variables. Is it possible to recover the message?
Author: andre_smaira
Server: nc encryption.pwn2.win 1337
Challenge link
Challenge files
On connecting to the challenge service, we are given two options -
Also, in the server.py file, we see there are two functions, enc_plaintext
and enc_flag
. Both these functions call the encrypt
function.
The enc_plaintext
functions calls encrypt
with the plaintext supplied by the user (encoded in base64), key1 and iv1 (which are secret values) as arguments.
The enc_flag
fucntion takes as arguments the secret flag and iv2 and key2 which are actually derived from iv1, key1 and the flag.
iv2 = AES.new(key1, AES.MODE_ECB).decrypt(iv1)
key2 = xor(to_blocks(flag))
The encrypt
function returns the base64 encoding of the (iv+ciphertext) string.
Exploit
We see that every time encrypt
is called, the key2 and iv2 values are updated. iv2 becomes the AES-ECB decryption of the old iv2 and key2 is now the first block of the ciphertext (since the xor function with one argument simply returns the first block of the argument). So, our goal is now to recover the key2 and iv2 values so we can then reverse the encrypt_flag
function and recover the flag.
If we call encrypt_flag
(Choice 2) the first time, then the new key2 value will be the first block of the ciphertext. Nnow during this, the iv2 has also been updated but we don’t know that value, since the first part of the returned value, is the old iv2.
So, what we do next is call the encrypt_flag
(Choice 2) function again, then we get the new iv2 value along with the ciphertext. This means taht now we know the iv2 and the key2 value taht was used to encrypt the flag to obtain the ciphertext. What remains now, is just to reverse the encrypt
function and call it with our values of the ciphertext, key2 and iv2. This will get us the flag.
The decrypt
function can be written as -
assert len(key) == BLOCK_SIZE, f'Invalid key size'
assert len(iv) == BLOCK_SIZE, 'Invalid IV size'
assert len(txt) % BLOCK_SIZE == 0, 'Invalid plaintext size'
bs = len(key)
blocks = to_blocks(txt)
ctxt = b''
aes = AES.new(key, AES.MODE_ECB)
curr = iv
for block in blocks:
ctxt += xor(curr, aes.decrypt(block)) # Inverse of the encrypt function
curr = xor(ctxt[-bs:], block)
return ctxt
The complete exploit code is shown below -
from pwn import *
import base64
from Crypto.Cipher import AES
p = remote("encryption.pwn2.win", 1337)
# context.log_level = 'debug'
BUFF = 256
BLOCK_SIZE = 16
key2 = None
iv2 = None
def to_blocks(txt):
return [txt[i*BLOCK_SIZE:(i+1)*BLOCK_SIZE] for i in range(len(txt)//BLOCK_SIZE)]
def xor(b1, b2=None):
if isinstance(b1, list) and b2 is None:
assert len(set([len(b) for b in b1])) == 1, 'xor() - Invalid input size'
assert all([isinstance(b, bytes) for b in b1]), 'xor() - Invalid input type'
x = [len(b) for b in b1][0]*b'\x00'
for b in b1:
x = xor(x, b)
return x
assert isinstance(b1, bytes) and isinstance(b2, bytes), 'xor() - Invalid input type'
return bytes([a ^ b for a, b in zip(b1, b2)])
def decrypt(txt, key, iv):
assert len(key) == BLOCK_SIZE, f'Invalid key size'
assert len(iv) == BLOCK_SIZE, 'Invalid IV size'
assert len(txt) % BLOCK_SIZE == 0, 'Invalid plaintext size'
bs = len(key)
blocks = to_blocks(txt)
ctxt = b''
aes = AES.new(key, AES.MODE_ECB)
curr = iv
for block in blocks:
ctxt += xor(curr, aes.decrypt(block)) # Inverse of the encrypt function
curr = xor(ctxt[-bs:], block)
return ctxt
def encrypt_flag(p):
p.recvuntil(b"Choice: ")
p.sendline(b"2")
x = p.recvline().strip()
y = base64.b64decode(x)
return y[:16], y[16:]
def enc_plaintext(p, plaintext):
p.recvuntil(b"Choice: ")
p.sendline(b"1")
p.recvuntil(b"Plaintext: ")
p.sendline(plaintext)
x = p.recvline().strip()
y = base64.b64decode(x)
return y[:16], y[16:]
def getDecoding(s):
y = base64.b64decode(s)
return y[:16], y[16:]
iv2_orig, flag_enc_orig = encrypt_flag(p)
print(iv2_orig)
print(flag_enc_orig)
key2_new = xor(to_blocks(flag_enc_orig))
print(b"Key2 :" + key2_new)
iv2_new, flag_enc_cipher_new = encrypt_flag(p)
print(b"iv2 :", iv2_new)
print(b"flag_cipher_new :", flag_enc_cipher_new)
print(decrypt(flag_enc_cipher_new, key2_new, iv2_new).decode())
Running this, prints the flag - CTF-BR{kn3W_7h4T_7hEr3_4r3_Pc8C_r3pe471ti0ns?!?}
And yeah, after reading the flag, I realised it was actually AES in PCBC mode.