一道 protobuf 序列化的题目,打 setcontext 的方式也比较熟悉,或许打 environ 也可以,没尝试过。

程序分析:
main

进到 main 函数,找到该程序反序列化的入口 sub_192D,该函数主要的作用是为了把读进来的原始字节按 protobuf 消息 pwn 解析成一个对象,进去看一看:
sub_192D
1 | char *__fastcall sub_192D(_UNKNOWN **a1, unsigned __int64 n5, unsigned __int8 *s) |

找到 protobuf

查找字符串可以看到 msg 开头的在.rodata 的字符串,猜测这个就是我们需要逆向的 protobuf


这里 0x9B70 (+0x8)的地方就是 idx,+0x10 偏移的地方就是结构体元素类型:0xF 对应的是 bytes
1 | message pwn { |
| 内存数值 (Dec) | 内存数值 (Hex) | 对应 Proto 类型 | 底层 Wire Type | 实战内存占用特征 |
|---|---|---|---|---|
| 0 | 0x00 |
int32 |
0 (Varint) | 占 4 字节(有符号) |
| 1 | 0x01 |
sint32 |
0 (Varint) | 占 4 字节(ZigZag 压缩编码) |
| 2 | 0x02 |
sfixed32 |
5 (32-bit) | 占 4 字节(固定长度) |
| 3 | 0x03 |
int64 |
0 (Varint) | 占 8 字节 |
| 4 | 0x04 |
sint64 |
0 (Varint) | 占 8 字节 |
| 5 | 0x05 |
sfixed64 |
1 (64-bit) | 占 8 字节(固定长度) |
| 6 | 0x06 |
uint32 |
0 (Varint) | 占 4 字节(无符号) |
| 7 | 0x07 |
fixed32 |
5 (32-bit) | 占 4 字节 |
| 8 | 0x08 |
uint64 |
0 (Varint) | 占 8 字节 |
| 9 | 0x09 |
fixed64 |
1 (64-bit) | 占 8 字节 |
| 10 | 0x0A |
float |
5 (32-bit) | 占 4 字节 |
| 11 | 0x0B |
double |
1 (64-bit) | 占 8 字节 |
| 12 | 0x0C |
bool |
0 (Varint) | 占 4 字节(实际存 0 或 1) |
| 13 | 0x0D |
enum |
0 (Varint) | 占 4 字节 |
| 14 | 0x0E |
string |
2 (Length-del) | 占 8 字节(仅存指向字符串的指针 char*) |
| 15 | 0x0F |
bytes |
2 (Length-del) | 占 16 字节(复合结构体 ProtobufCBinaryData) |
| 16 | 0x10 |
message |
2 (Length-del) | 占 8 字节(指向子 Message 实例的指针) |
在二进制文件同级目录下创建 bot.proto 文件:
1 | syntax = "proto2"; |
1 | protoc --python_out=./ ./bot.proto |
命令执行成功后,会在 --python_out 当前目录下生成一个专属的 Python 模块文件。
生成的文件名有严格的固定格式:原文件名 _pb2.py
之后我们就可以导入模块:import ez_buf_pb2 编写 exp
sub_a55D
这个就是我们的指令菜单了,简单逆向修改了一下函数名和变量名,增加一下可读性,感觉 protobuf 的变量名哪里还是有一点弯弯绕绕,需要仔细分析一下:
1 | unsigned __int8 *__fastcall sub_155D( |
add
1 | void *__fastcall add(__int64 idx, size_t size, size_t size_1, const void *content) |
这里是创建,规定大小不超过 0xF0,qword_A560[idx] 数组存入的是 size
delete:
1 | unsigned __int8 *__fastcall delete(__int64 idx) |
show
1 | ssize_t __fastcall show(__int64 idx) |
edit:
1 | void *__fastcall edit(__int64 idx, signed __int64 size, const void *content) |
EXP 思路:
泄露 libc_base
泄露 libc_base 比较常用的就是 UnSortedbin 泄露 main_arena 减去固定偏移的方法找到 libc_base:
1 | for i in range(8): |
这里需要先挂满 Tcachebin 才可以把 bin 挂到 UnSortedbin 上,也是比较常见的手法了:

Fastbin Double Free
1 | for i in range(8, 17): |
- 思路:申请 9 个
0x60的 chunk。先释放 7 个(10到16)把0x60大小的 Tcache 填满。 - Double Free 绕过:glibc 2.31 引入了 Tcache key 机制来防止 Tcache 的 Double Free。由于 Tcache 已经被我们填满了,接下来释放的 8 和 9 会进入 Fastbin。Fastbin 只检查连续释放的两个 chunk 是否相同(
top != p),所以按照8 -> 9 -> 8的顺序释放,可以绕过检查。
Tcache_Poisoning 将 __free_hook 申请出来
1 | for i in range(24, 27): |
- 由于上一步的 double_free,我们可以借此申请处 free_hook 道原本 chunk8 的地址,修改 fd 指针
- 当我们申请 chunk 27 的时候就会申请到 _free_hook
泄露 heap_base
1 | show_chunk(14) |
泄露 fd 指针然后 vmmap 调一下 heap_base 减去固定差值即可:

布置 ORW
1 | orw = p64(pop_rdi) + p64(fake) + p64(pop_rsi) + p64(0) + p64(libc.sym["open"]) |
这里直接先布置 ORW,再说之后的 setcontext + 61 的事情,这样子清晰一点。
setcontext+61
先放这一部分的 exp:
1 | free_hook = libc.sym["__free_hook"] |
之后是要打 setcontext+61 迁移到栈上,除此之外还需要一段能够跳转到 setcontext 的 gadgets,通常是:
1 | mov rdx, [rdi+8] |

来看一下这道题目的setcontext:
1 | Dump of assembler code for function setcontext: |
如果我们可以返回到 setcontext+61 的位置, 且成功控制 rdx 及周边区域, 我们就可以控制各个寄存器的值, 同时我们可以发现 , setcontext+107 的跳转语句均成立, 因此程序真正的执行流如下, 其中 mov rcx,QWORD PTR [rdx+0xa8] ; push rcx 是设置 rcx 并将其值压入栈中, 最后由 ret 将这个值作为返回地址, 实际作用就是设置 rip
1 | 0x00007ffff7e279ad <+61>: mov rsp,QWORD PTR [rdx+0xa0] |
在这道题中, 由于 malloc 的大小是恒定的, 其 rdi 不能由我们控制, 因此我们考虑利用 free 函数, 因为 free 函数的 rdi 是目标堆块的 user data 地址, 只要我们布置好堆块就能够利用
而 gadgets 的内容是 :
1 | mov rdx, [rdi+8] |
这也就意味着在触发 free 的时候,参数 rdi 来自的是被 free 的那个指针,因此我们就可以提前在将要 free 的那个 chunk 的 data 区进行布置。当调用 free 时就会 从 [rdi+0x8] 取出一个地址到 rdx;再调用 [rdx+0x20],通过这个 gadget 我们可以通过 rdi 控制 rdx , 并通过设置好的 rdx 调用 setcontext+61。
依旧根据我们上面所说:画一个 chunk28 的 data 域的结构图吧:
1 | 0x00: "./flag\x00\x00" |
在 delete 触发 free 的时候
进入 hook_call执行 gadgetsmov rdx, [rdi+0x8]:rdx = p64(fake)call qword ptr [rdx+0x20]:call ptr [setcontext+61]- 执行 setcontext,下面只说最重要的
rsp,QWORD PTR [rdx+0xa0]:rsp = p64(fake + 0xb0)ret- 而
p64(fake + 0xb0)就是 ORW 链,执行 ORW 获取 flag
1 | free(fake_chunk) |
总 EXP:
1 | from pwn import * |
说些什么吧!