서버에 접속하면 메뉴를 선택할 수 있다.
Let's see if you are good enough in symmetric cryptography! MENU 1 - Encrypt your secret 2 - Encrypt my secret 3 - Exit Choice: 2 Ff9XXv18Ddcl48dMOYkBAlYAtANzxMApPxdagQ9M2Nb22gm1uHP5tyUdkTnz3/FvKxdk6PvKLzF5AHuUcvWb9g==
1번을 입력하면 base64 인코딩된 평문을 입력받는데, 이것을 AES 암호화한 결과를 출력해준다.
2번을 입력하면 저장되어 있는 flag 값을 암호화한 결과를 출력해준다.
먼저 소스를 확인해본다.
server.py
#!/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()
flag를 암호화할 때는 encrypt_flag
함수가 사용되며
입력값을 암호화할 때는 encrypt_plaintext
함수가 사용된다.
encrypt_plaintext
def enc_plaintext(): print('Plaintext: ', end='') txt = base64.b64decode(input().rstrip()) print(encrypt(txt, key1, iv1))
암호화에 사용되는 키와 벡터는 각각 key1
, iv1
이다.
이것은 코드 시작부분에서 secrets.py
에서 임포트하는 값이다.
from secrets import flag, key1, iv1
반대로 flag를 암호화할 때는 key2
, iv2
가 사용되는데
encrypt_flag
def enc_flag(): print(encrypt(flag, key2, iv2))
이 값은 초기에 key1
, iv1
을 이용하여 계산한 결과값이다.
iv2 = AES.new(key1, AES.MODE_ECB).decrypt(iv1) key2 = xor(to_blocks(flag))
암호화하는 루틴을 확인해본다.
encrypt
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))
전달된 평문(txt
)은 to_blocks
함수에 의해 16바이트 크기로 블록화된다.
블록화 된 결과(blocks
)는 각각의 block
에 대해서 암호화가 진행된다.
먼저 iv
와 xor()
시킨 뒤, 그 결과를 AES.encrypt()
시킨 결과 ctxt
를 생성한다.
그것을 다시 block
과 xor()
시킨 결과를 다음 IV(curr
)로 사용한다
그리고 iv2
와 key2
를 새롭게 초기화 한다.
AES 암호화를 복호화하기 위해서는 사용된 Key와 Initial Vector, 그리고 Encrypted Data를 알아야 한다.
flag 암호화에 사용된 iv
는 쉽게 구할 수 있다.
encrypt()
가 마지막에 반환해주는 값에 포함되기 때문이다.
return str(base64.b64encode(iv+ctxt), encoding='utf8')
하지만 같은 암호화 연산에 사용된 key
를 모른다면 의미가 없어진다.
다행히 같은 암호화 연산에 사용된 Key, IV, Encrypted Data를 알 수 있는 방법이 있다.
암호화의 마지막에 key2
와 iv2
는 다시 설정된다.
iv2 = AES.new(key2, AES.MODE_ECB).decrypt(iv2) key2 = xor(to_blocks(ctxt))
Encrypted Data(ctxt
)를 알고 있으므로 key2
를 구할 수 있다.
그리고 key2
는 encrypt_flag()
를 다시 호출하여 진행되는 encrypt()
에서 Key로 사용된다.
iv2
는 앞서 봤던 것처럼 encrypt
마지막에 반환해주므로 key2
를 구했다면 iv2
, ctxt
를 모두 알고 있는 셈이다.
이제 남은 것은 구한 값을 이용해 암호화를 역연산하여 플래그를 구하는 것이다.
첫 번째 encrypt_flag()
의 결과로 iv
, ctxt
, key2
를 구한다.
enc_flag = base64.b64decode('Ff9XXv18Ddcl48dMOYkBAlYAtANzxMApPxdagQ9M2Nb22gm1uHP5tyUdkTnz3/FvKxdk6PvKLzF5AHuUcvWb9g==')
iv = enc_flag[:16]
ctxt = enc_flag[16:]
key2 = xor(to_blocks(ctxt))
두 번째 encrypt_flag()
의 결과로 iv2
, ctxt2
를 구한다.
enc_flag2 = base64.b64decode('qyGvHzTkXK15raxzgXMU/ICNo5Bx8CwqI/ZXWDeaeUyOJ+HGu3kN0O0sJ4Cn7rRhyOLxBg7Au16TvsTEadG9Nw==')
iv2 = enc_flag2[:16]
ctxt2 = enc_flag2[16:]
암호화를 역연산하는 decrypt()
를 정의하고 iv2
, ctxt2
, key2
를 전달하여 flag를 구한다.
def decrypt(enc, key, iv):
global key2, iv2
assert len(key) == BLOCK_SIZE, f'Invalid key size'
assert len(iv) == BLOCK_SIZE, 'Invalid IV size'
assert len(enc) % BLOCK_SIZE == 0, 'Invalid plaintext size'
bs = len(key)
blocks = to_blocks(enc)
dtxt = b''
aes = AES.new(key, AES.MODE_ECB)
curr = iv
for block in blocks:
dtxt += xor(aes.decrypt(block), curr)
curr = xor(block, dtxt[-bs:])
return str(dtxt, encoding='utf8')
print(decrypt(ctxt2, key2, iv2))
복호화된 플래그가 출력되는 것을 확인할 수 있다.
root@ubuntu:/work/ctf/PWN2WIN2020/andriod_encrypt/androids_encryption# python3 ex.py
CTF-BR{kn3W_7h4T_7hEr3_4r3_Pc8C_r3pe471ti0ns?!?}
import base64
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)])
def decrypt(enc, key, iv):
global key2, iv2
assert len(key) == BLOCK_SIZE, f'Invalid key size'
assert len(iv) == BLOCK_SIZE, 'Invalid IV size'
assert len(enc) % BLOCK_SIZE == 0, 'Invalid plaintext size'
bs = len(key)
blocks = to_blocks(enc)
dtxt = b''
aes = AES.new(key, AES.MODE_ECB)
curr = iv
for block in blocks:
dtxt += xor(aes.decrypt(block), curr)
curr = xor(block, dtxt[-bs:])
return str(dtxt, encoding='utf8')
enc_flag = base64.b64decode('Ff9XXv18Ddcl48dMOYkBAlYAtANzxMApPxdagQ9M2Nb22gm1uHP5tyUdkTnz3/FvKxdk6PvKLzF5AHuUcvWb9g==')
iv = enc_flag[:16]
ctxt = enc_flag[16:]
key2 = xor(to_blocks(ctxt))
enc_flag2 = base64.b64decode('qyGvHzTkXK15raxzgXMU/ICNo5Bx8CwqI/ZXWDeaeUyOJ+HGu3kN0O0sJ4Cn7rRhyOLxBg7Au16TvsTEadG9Nw==')
iv2 = enc_flag2[:16]
ctxt2 = enc_flag2[16:]
print(decrypt(ctxt2, key2, iv2))