security/암호학

AES 암호화 알고리즘 정리 및 예제 (feat SSFT 2023)

민사민서 2023. 8. 20. 02:53

AES 암호화 알고리즘

- Advanced Encryption Standard, 블록 암호

- Block Size: 16 Bytes

- Key size: 16 Bytes(AES128) / 24 Bytes(AES192) / 32 Bytes(AESE256)

- plaintext 길이가 16*k보다 작을 때 padding을 붙인다

    * 주로 PKCS#7 표준을 사용함 (padding 6개 필요하면 b'\x06'으로 채우고, 2개 필요하면 b'\x02'로 채우는)

    *  plaintext 길이가 16의 배수이면 padding b'\x10'*16 을 뒤에 붙인다

- plaintext의 길이가 block size보다 클 때 어떻게? => mode of operation

     * ECB(Electronic CodeBook) mode

      - 보안 취약점으로 사용 거의 안 함    

     * CBC(Cipher Block Chaining) mode

     - IV 필요, 제일 많이 사용되는 mode

     * OFB(Output FeedBack) mode

      - PT와 xor되는 key stream을 생성하는 느낌

     * CTR(CounTeR) mode

 

바이트 <-> 16진수 변환 유용한 파이썬 함수

* 바이트 문자열 <-> 16진수 문자열 

original_bytes = b'hello'
hex_string = original_bytes.hex() # '68656c6c6f'
converted_bytes = bytes.fromhex(hex_string) # b'hello'

* 바이트 문자열 <-> 16진수 바이트 문자열

from binascii import hexlify, unhexlify

original_bytes = b'hello'
hex_bytes = hexlify(original_bytes) # b'68656c6c6f'
converted_bytes = unhexlify(hex_bytes) # b'hello'

// p.recv() 하면 바이트 문자열로 받아와지므로 unhexlify가 편하더라

 

취약점 분석 예제 1 - ECB

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

key = open("/dev/urandom", "rb").read(16)
secret = open("secret.txt").read(32).encode() # 32bit key

while True:
	try:
		prefix = bytes.fromhex(input("Prefix(hex): "))
		if len(prefix) > 64: # <= 64bit prefix
			raise Exception
	except:
		print("Wrong prefix.")
		continue

	cipher = AES.new(key, AES.MODE_ECB)
	ciphertext = cipher.encrypt(pad(prefix + secret, 16))
	print("Encrypted Secret: ", ciphertext.hex())

- AES 암호화 알고리즘의 ECB mode로 암호화된다

- 각 블록별로 동일한 key를 가지고 암호화된다, PT에서 동일한 블록 = CT에서도 동일한 블록

 

- prefix 63바이트로 구성한다 = b'A'*31 + [추측값] + b'A'*31

- 처음 32바이트(b'A'*0x31+추측값)이후 32바이트(b'A'*31+비밀값의 첫바이트)의 암호화된 값을 비교해 leak 한다

- prefix를 63 ~ 32바이트까지 변화시키면서 secret 값 한 바이트씩 leak 한다

 

from binascii import hexlify, unhexlify
from pwn import *

p = remote("aes.sstf.site", 1335)

secret = b''

while len(secret)<32:
    for i in range(256):
        ch = bytes([i])
        payload = secret.rjust(31,b'A') + ch
        payload += b'A' * (31-len(secret))
        p.recvuntil("Prefix(hex): ")
        p.sendline(hexlify(payload))
        p.recvuntil("Encrypted Secret:  ")
        ct = unhexlify(p.recvline()[:-1])
        if ct[0:32] == ct[32:64]:
            secret += ch
            print(secret)
            break
        
p.interactive()

 

취약점 분석 예제 2 - OFB

from Crypto.Cipher import AES

with open("/dev/urandom", "rb") as f:
	key, iv = f.read(16), f.read(16)
secret = open("secret.txt").read(48).encode()

cipher = AES.new(key, AES.MODE_OFB, iv)
ciphertext = cipher.encrypt(secret)
print("Encrypted Secret: ", (iv + ciphertext).hex())

try:
	iv = bytes.fromhex(input("IV(hex): "))
	if len(iv) != 16:
		raise Exception
	msg = bytes.fromhex(input("Message(hex): "))
except:
	print("Wrong input.")
	exit()

cipher = AES.new(key, AES.MODE_OFB, iv)
ciphertext = cipher.encrypt(msg)
print("Encrypted Message: ", ciphertext.hex())

- 16B IV 값과 48B CT 값이 제공된다 (unhexlify만 하면 되겠네)

- 원하는 IV와 PT를 입력하여 CT를 1회 얻을 수 있다 // 동일한 key를 사용해 암호화 해준다

- secret 암호화에 사용된 IV48바이트 길이의 널바이트를 PT로 넣어주면(PT1=PT2=PT3=0), x xor 0 = x 이므로 생성된 CT를 통해 키 스트림 K1, K2, K3를 파악할 수 있다

- 각각의 키스트림과 CT1, CT2, CT3 의 xor 연산을 통해 secret 문자열을 구할 수 있겠다

from binascii import hexlify, unhexlify
from pwn import *

p = remote("aes.sstf.site", 1336)

p.recvuntil("Encrypted Secret:  ")
res = unhexlify(p.recvline()[:-1])

iv = res[0:16] # 16B
ct = res[16:] # 48B

p.recvuntil("IV(hex): ")
p.sendline(hexlify(iv))
p.recvuntil("Message(hex): ")
p.sendline(hexlify(b'\x00'*48))

p.recvuntil("Encrypted Message:  ")
keystream = unhexlify(p.recvline()[:-1])

secret = b''
for c, s in zip(ct,keystream):
    secret += bytes([c^s])
print(secret)

 

취약점 분석 예제 3 - CBC

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from secret import key, flag

while True:
	try:
		iv = bytes.fromhex(input("IV(hex): "))
		if len(iv) != 16:
			raise Exception
		msg = bytes.fromhex(input("CipherText(hex): "))
		if len(msg) % 16:
			raise Exception
	except:
		print("Wrong input.")
		continue

	cipher = AES.new(key, AES.MODE_CBC, iv)
	plaintext = cipher.decrypt(msg)
	try:
		plaintext = unpad(plaintext, 16)
	except: # check padding - vulnerable
		print("Try again.")
		continue

	if plaintext == b"CBC Magic!":
		print(flag)
		break
	else:
		print("Wrong CipherText.")

- 원하는 IV와 CT를 입력받아 복호화를 진행한다

- 패딩이 올바르지 않은 경우 "Try again" 출력하고, 복호화된 값이 "CBC Magic!"과 같으면 플래그 출력 및 성공

 

Oracle Padding Attack

- 블록 암호 모드 중 padding을 필요로 하는 mode에서 발생하는 공격 기법

- 복호화시스템에 암호문을 넣었을 때 padding 형식이 맞는지 틀린지 유무를 응답해주는 경우 특정 plaintext를 도출하는 IV를 구하거나, 특정 IV로부터 plaintext를 도출할 수 있다.

대략적인 개념은 다음과 같다 (아래 쉬운설명 링크부터 보고 오는게 좋을듯)

- 어차피 encryption algorithm과 key는 모른다. 어떠어떠한 알고리즘에 의해 CT가 암호화되어 Intermediate Plaintext(IP)로 변해있을 것이다.

- IP의 값만 알면 PT로부터 IV를 구하거나, IV로부터 PT를 구할 수 있다

- PT = IP(고정된 값이다) xor IV 이므로 IV의 값에 따라 복호화시스템의 응답이 달라질 것이다

(padding 형식이 틀림 / padding 형식은 맞는데 원래 PT랑 다름 / padding 형식도 맞고 원래 PT랑도 같음)

=> blind SQLI 처럼 응답을 통해 IP를 LSB부터 한 바이트씩 구해나가자

 

- IV의 값을 LSB부터 한 바이트씩 0~255 브포 돌려가며 "padding 형식이 맞음" 응답이 나오는 케이스를 찾는다

=> padding 형식이 맞다는 것은 \x01, \x02\x02, \x03\x03\x03 ... 이런 형태라는 것.

=> 즉 끝에서 i개의 바이트가 \x0i\x0i ... \x0i 형태라는 것

cf) 여기서 든 의문

IV 맨 마지막 바이트 \x53에서 padding 형식 맞게 나왔다고 해보자.

근데 이게 ~~~\x01 로 성공한건지 ~~~\x02\x02 로 성공한건지 어케 알지

왜 PT의 마지막바이트가 \x01임을 확신하지

=> 내가 내린 결론: 그래서 IV payload를 구성할 때 보통 널로 나머지를 채워서 공격하나?

=> "IV[17-i] = 브포로 구한 값" 과 "PT[17-i] = '\x{i}' " 을 xor 하면 IT[17-i] 값을 구할 수 있다.

=> i = 1 ~ 16 에 대해 반복

=> 각 반복마다 IT 한 바이트씩 구하고, 구한 IT를 기반으로 IV를 재구성한다

 

쉬운 설명은

https://bperhaps.tistory.com/entry/%EC%98%A4%EB%9D%BC%ED%81%B4-%ED%8C%A8%EB%94%A9-%EA%B3%B5%EA%B2%A9-%EA%B8%B0%EC%B4%88-%EC%84%A4%EB%AA%85-Oracle-Padding-Attack

여기 보세요

 

확장: 여러 블록이고 주어진 PT를 바탕으로 IV & CT 구해야 한다면?

- CT_n으로 임의의 값 사용, 맨 오른쪽 블록부터 IP_n 을 구한 뒤 PT_n을 바탕으로 CT_n-1을 구한다

- CT_n-1로 구한 값 사용, IP_n-1 구한 뒤 PT_n-1을 바탕으로 CT_n-2 구한다

...

- CT_1로 구한 값 사용, IP_1 구한 뒤 PT_1 바탕으로 IV 구한다

// CT_n만 임의의 값 사용, 그 뒤로 CT_n-1 차례차례 구해나가고 IV도 구한다

 

확장: 여러 블록이고 주어진 CT, IV 바탕으로 PT 구해야 한다면?

- 얘는 순서 상관 없음

- 맨 오른쪽 블록에서 IP_n 구한 뒤 CT_n-1 바탕으로 PT_n 구한다

- IP_n-1 구한 뒤 CT_n-2 바탕으로 PT_n-1 구한다

...

- IP_1 구한 뒤 IV 바탕으로 PT_1 구한다

 

 

문제 풀이로 돌아와서

- 다행히 블록 한 개이고, PT = b'CBC Magic!\x06\x06\x06\x06\x06\x06' 으로 주어져있다

- CT = b'AAAAAAAAAAAAAAAA' 으로 고정시키고 (아무 값이나 상관없음)

- IV = b'\x00'*0xf + [브포 값]  부터 시작해서 IP를 한 바이트씩 구해나가면 되겠다

from pwn import *
from binascii import hexlify, unhexlify

p = remote("aes.sstf.site", 1337)
IP = b''
CT = b'A'*16 # Anything
PT = b'CBC Magic!' + b'\x06'*6

for i in range(1,17):
    pay = b'\x00'*(16-1-len(IP))
    for ch in range(256):
        IV = pay + bytes([ch]) + bytes([ c^i for c in IP ])
        p.recvuntil("IV(hex): ")
        p.sendline(hexlify(IV))
        p.recvuntil("CipherText(hex): ")
        p.sendline(hexlify(CT))

        if b"Try again" not in p.recvline(): # padding error
            IP = bytes([ch^i]) + IP
            print(len(IP))
            break

IV = bytes([ pt^ip for pt, ip in zip(PT,IP) ])
p.recvuntil("IV(hex): ")
p.sendline(hexlify(IV))
p.recvuntil("CipherText(hex): ")
p.sendline(hexlify(CT))

p.interactive()