
HMAC의 간단한 버전을 만들었고, 이게 merkle-damgard 구조의 문제와 같은 취약점을 가지지 않기를 바란다고 한다. 그 말은 즉슨 merkle-damgard 구조와 같은 취약점, 즉 length extension attack이 가능하다는 것을 시사하는 것 아닐까? 일단 문제 코드를 보자.
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os
from utils import listener
FLAG = "crypto{???????????????}"
def bxor(a, b):
return bytes(x ^ y for x, y in zip(a, b))
def hash(data):
data = pad(data, 16)
out = b"x00" * 16
for i in range(0, len(data), 16):
blk = data[i:i+16]
out = bxor(AES.new(blk, AES.MODE_ECB).encrypt(out), out)
return out
class Challenge():
def __init__(self):
self.before_input = "You'll never forge my signatures!n"
self.key = os.urandom(16)
def challenge(self, msg):
if "option" not in msg:
return {"error": "You must send an option to this server."}
elif msg["option"] == "sign":
data = bytes.fromhex(msg["message"])
if b"admin=True" in data:
return {"error": "Unauthorized to sign message"}
sig = hash(self.key + data)
return {"signature": sig.hex()}
elif msg["option"] == "get_flag":
sent_sig = bytes.fromhex(msg["signature"])
data = bytes.fromhex(msg["message"])
real_sig = hash(self.key + data)
if real_sig != sent_sig:
return {"error": "Invalid signature"}
if b"admin=True" in data:
return {"flag": FLAG}
else:
return {"error": "Unauthorized to get flag"}
else:
return {"error": "Invalid option"}
"""
When you connect, the 'challenge' function will be called on your JSON
input.
"""
listener.start_server(port=13388)
먼저 hash를 보자. data를 16의 배수가 되게끔 패딩해주고 16byte씩 끊어서 key로 삼아 out을 암호화하고, 이를 이전의 암호화된 out(초기에는 0x00 * 16 byte)과 xor 해준다. 이를 계속 반복한다.
그리고 challenge 함수에서는 두 가지 옵션이 있다. 먼저 sign 옵션에서는 data에 admin=True가 있다면 error를 반환하고 아니라면 위의 hash 함수에 key와 data를 연결해서 보낸다. 이렇게 서명을 얻어낸다.
get_flag 옵션에서는 data에 admin=True가 있으며 data의 서명값과 함께 전송한 서명값이 동일하다면 flag를 출력해준다.
위의 sign 옵션에서 admin=True가 data에 존재한다면 error를 반환하는 부분에 주목할 수 있다. 만약 이를 허용한다면 그렇게 얻은 서명값으로 그대로 get_flag에서 flag를 얻어낼 수 있기 때문에 필터링을 넣어준 것이다. 우리는 이 부분을 공략할 수 있다.
먼저 sign에 message로 길이가 0인 문자열을 보낸다. 그러면 자동으로 패딩이 채워져서 0x10 * 16byte가 되고, 이를 hash 함수에 넣은게 서명값으로 나온다. 그 다음에 admin=True와 패딩 0x06 * 6 byted의 문자열을 hash 함수 내부에서 이루어지는 것처럼 AES ECB mode로 encrypt해준다. 이렇게 되면 결과적으로 hash 함수에서 1 round가 더 이루어진 것과 동일한 결과를 얻을 수 있고, 성공적으로 필터링을 무시한 것이다.
(0x10 * 16byte + ‘admin=True’ + 0x06 * 6byte ⇒ 총 32byte 문자열에 대한 서명값)
해당 서명값을 signature로 하고, data는 위에서 합친 것을 한번에 보내서 get_flag 옵션을 실행하면 real_sig와 sent_sig가 동일하고 data에는 admin=True가 있기에 flag를 반환하게 된다.
위의 과정을 정리한 exploit 코드는 다음과 같다.
from Crypto.Cipher import AES
import pwn
import json
p = pwn.connect('socket.cryptohack.org', 13388)
p.readline()
data={
"option":"sign",
"message":""
}
p.sendline(json.dumps(data))
sig=bytes.fromhex(json.loads(p.readline().strip().decode())["signature"])
d=b'admin=True' + b'x06'*6
my_sign = pwn.xor(AES.new(d, AES.MODE_ECB).encrypt(sig), sig).hex()
d=(b'x10'*16+b'admin=True').hex()
data={
"option":"get_flag",
"signature":my_sign,
"message":d
}
p.sendline(json.dumps(data))
print(p.readline())
json 형태로 통신을 주고 받기에 json.loads를 통해 signature를 얻어냈다. 그리고 from pwn import * 형태로 이전과 같이 했더니 xor이 잘 안 돼서 import pwn으로 바꾸면서 몇 부분을 조금 수정해주었다.

함수 실행 결과이다. flag를 잘 얻어오고 있다 ㅎㅎ
🚩 crypto{l3ngth_3xT3nd3r}
