Pwn2Win Androids Encryption Write up
Pwn2Win CTF was held on May 30-31 2020. this challenge is the easiest problem and the only one I solve in this competition.
Discreption
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
Source Code
We were provided source code as below.
#!/usr/bin/python3 -u
# *-* coding: latin1 -*-
import sys
import base64
from Crypto.Cipher import AES
from secrets import flag, key1, iv1
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)])
BUFF = 256
BLOCK_SIZE = 16
iv2 = AES.new(key1, AES.MODE_ECB).decrypt(iv1)
key2 = xor(to_blocks(flag))
def encrypt(txt, key, iv):
global key2, iv2
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 += aes.encrypt(xor(block, curr))
curr = xor(ctxt[-bs:], block)
iv2 = AES.new(key2, AES.MODE_ECB).decrypt(iv2)
key2 = xor(to_blocks(ctxt))
return str(base64.b64encode(iv+ctxt), encoding='utf8')
def enc_plaintext():
print('Plaintext: ', end='')
txt = base64.b64decode(input().rstrip())
print(encrypt(txt, key1, iv1))
def enc_flag():
print(encrypt(flag, key2, iv2))
def menu():
while True:
print('MENU')
options = [('Encrypt your secret', enc_plaintext),
('Encrypt my secret', enc_flag),
('Exit', sys.exit)
]
for i, (op, _) in enumerate(options):
print(f'{i+1} - {op}')
print('Choice: ', end='')
op = input().strip()
assert op in ['1', '2', '3'], 'Invalid option'
options[ord(op)-ord('1')][1]()
def main():
print('Let\'s see if you are good enough in symmetric cryptography!\n')
try:
menu()
except Exception as err:
sys.exit(f'ERROR: {err}')
if __name__ == '__main__':
main()
Challenge Summary
In summary, this encryption process is like the following image.
After encryption, IV2 was decoded with AES-ECB using key2 which was regenerated using the encrypted text. we were given the encrypted text and previous key2.
As you can see, we can generate an arbitrary key2 by inputting an arbitrary plain text. With this key2 and previous IV2, we can newly presume IV2. Once we can get key2 and IV2, we can decrypt the flag.
Exploit
the exploit code is below.
import base64
from pwn import *
from Crypto.Cipher import AES
BUFF = 256
BLOCK_SIZE = 16
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)])
p = remote("encryption.pwn2.win", 1337)
_ = p.recvuntil("Choice: ")
# firstly, i'll make an arbitrary key2
p.sendline("1")
_ = p.recvuntil("Plaintext: ")
p.sendline(base64.b64encode(b"a"*16))
enc_m = str(p.recvline()).replace("\\n","")[2:-1]
enc_m = base64.b64decode(enc_m)
iv= enc_m[:BLOCK_SIZE]
ctxt = enc_m[BLOCK_SIZE:]
key2 = xor(to_blocks(ctxt))
# Secondly, get iv2
_ = p.recvuntil("Choice: ")
p.sendline("2")
enc_flag = str(p.recvline()).replace("\\n","")[2:-1]
enc_flag = base64.b64decode(enc_flag)
iv2= enc_flag[:BLOCK_SIZE]
ctxt = enc_flag[BLOCK_SIZE:]
# Next, culculate newly iv2,key2 using ctxt, key2 and iv2 above
iv2 = AES.new(key2, AES.MODE_ECB).decrypt(iv2)
key2 = xor(to_blocks(ctxt))
# Now we have iv2 and key2. let's decrypt the flag
_ = p.recvuntil("Choice: ")
p.sendline("2")
enc_flag = str(p.recvline()).replace("\\n","")[2:-1]
enc_flag = base64.b64decode(enc_flag)
ctxt = enc_flag[BLOCK_SIZE:]
b1 = xor(AES.new(key2, AES.MODE_ECB).decrypt(ctxt[:BLOCK_SIZE]) , iv2)
curr = xor(b1, ctxt[:BLOCK_SIZE])
b2 = xor(AES.new(key2, AES.MODE_ECB).decrypt(ctxt[BLOCK_SIZE:BLOCK_SIZE*2]) , curr)
curr = xor(b2, ctxt[BLOCK_SIZE:BLOCK_SIZE*2])
b3 = xor(AES.new(key2, AES.MODE_ECB).decrypt(ctxt[BLOCK_SIZE*2:BLOCK_SIZE*3]) , curr)
print(b1+b2+b3)