PWN2WIN2020] Androids Encryption

노션으로 옮김·2020년 6월 1일
1

wargame

목록 보기
56/59
post-thumbnail

서버에 접속하면 메뉴를 선택할 수 있다.

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에 대해서 암호화가 진행된다.

먼저 ivxor()시킨 뒤, 그 결과를 AES.encrypt()시킨 결과 ctxt를 생성한다.
그것을 다시 blockxor() 시킨 결과를 다음 IV(curr)로 사용한다

그리고 iv2key2를 새롭게 초기화 한다.

🎇Key, IV 구하기

AES 암호화를 복호화하기 위해서는 사용된 KeyInitial Vector, 그리고 Encrypted Data를 알아야 한다.

flag 암호화에 사용된 iv는 쉽게 구할 수 있다.
encrypt()가 마지막에 반환해주는 값에 포함되기 때문이다.

 return str(base64.b64encode(iv+ctxt), encoding='utf8')

하지만 같은 암호화 연산에 사용된 key를 모른다면 의미가 없어진다.

다행히 같은 암호화 연산에 사용된 Key, IV, Encrypted Data를 알 수 있는 방법이 있다.

암호화의 마지막에 key2iv2는 다시 설정된다.

iv2 = AES.new(key2, AES.MODE_ECB).decrypt(iv2)
key2 = xor(to_blocks(ctxt))

Encrypted Data(ctxt)를 알고 있으므로 key2를 구할 수 있다.
그리고 key2encrypt_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?!?}

Full Source Code

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))

0개의 댓글