第四章 ARM下的堆调试技巧:GDB、heapinfo与堆块追踪
做ARM堆利用,调试工具就是你的眼睛。x86上有成熟的pwndbg、peda,但到了ARM平台,很多插件直接罢工。我刚开始转ARM pwn时,就吃过这个亏——明明感觉漏洞触发了,但堆块状态完全看不清。
这一章,我把自己踩过的坑和总结的方法都摊开来讲。你会学到:怎么在ARM上用GDB看堆,怎么让pwndbg/peda在ARM上干活,以及怎么手动追踪堆块的状态变化。
4.1 ARM GDB查看堆的基本操作
先别急着上插件。裸GDB也能干不少活,关键是知道看哪里。
4.1.1 查看堆基址与arena
ARM Linux的堆管理基于glibc的ptmalloc。每个线程有自己的arena,主线程的arena就是main_arena。我一般这样定位:
(gdb) info proc mappings
...
0xb6f00000 0xb6f01000 rw-p [heap]
...
(gdb) p &main_arena
$1 = (struct malloc_state *) 0xb6f00010
拿到main_arena地址后,就能看它的内部结构了:
(gdb) p main_arena
$2 = {mutex = 0, flags = 1, have_fastchunks = 0,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
top = 0xb6f00a80, last_remainder = 0x0,
bins = {0xb6f00010, 0xb6f00010, ...}, ...}
p &main_arena报错,试试p (struct malloc_state)*0xb6f00010直接强转。ARM上符号表有时不完整,得手动来。
4.1.2 查看堆块元数据
每个堆块前面有8字节的chunk header(32位ARM下)。结构很简单:
chunk -> +--------+
| prev_size| (4 bytes, 前一块大小)
+--------+
| size | (4 bytes, 本块大小 & 标志位)
+--------+
| user data|
+--------+
举个例子,假设分配了一个0x40大小的块:
(gdb) x/4wx 0xb6f00800
0xb6f00800: 0x00000000 0x00000041 0x41414141 0x41414141
这里0x41就是size,最低3位是标志位:
- PREV_INUSE (0x1):前一块正在使用
- IS_MMAPPED (0x2):mmap分配
- NON_MAIN_ARENA (0x4):非主线程arena
所以0x41 = 0x40 | 0x01,表示块大小0x40,前一块在用。
4.2 heapinfo命令与手动计算
pwndbg的heapinfo在ARM上经常失灵。为什么?因为插件硬编码了x86的寄存器名和结构体偏移。我建议你学会手动计算,再配合半自动脚本。
4.2.1 手动遍历fastbin
fastbin是单链表,头指针在main_arena.fastbinsY[i]里。每个bin对应一个size范围:
| fastbin索引 | size范围 | 实际块大小 |
|---|---|---|
| 0 | 0x10 - 0x18 | 0x10, 0x18 |
| 1 | 0x20 - 0x28 | 0x20, 0x28 |
| 2 | 0x30 - 0x38 | 0x30, 0x38 |
| 3 | 0x40 - 0x48 | 0x40, 0x48 |
| 4 | 0x50 - 0x58 | 0x50, 0x58 |
| 5 | 0x60 - 0x68 | 0x60, 0x68 |
| 6 | 0x70 - 0x78 | 0x70, 0x78 |
| 7 | 0x80 - 0x88 | 0x80, 0x88 |
查看fastbin[2](0x30大小)的链表:
(gdb) p main_arena.fastbinsY[2]
$3 = (mfastbinptr) 0xb6f00920
(gdb) x/gx 0xb6f00920
0xb6f00920: 0x00000000 0x00000031 // 第一个块,fd=0(链表尾)
(gdb) x/gx 0xb6f00920+8
0xb6f00928: 0x00000000 // fd指针为NULL
如果fd不为0,就继续往下跟。这就是fastbin的遍历方式。
4.2.2 查看unsorted bin
unsorted bin是双向链表,通过bins[0]和bins[1]访问。我习惯这样:
(gdb) p main_arena.bins[0]
$4 = 0xb6f00010
(gdb) p main_arena.bins[1]
$5 = 0xb6f00010
如果bins[0] == bins[1] == &main_arena.bins[0] - 0x10,说明链表为空。否则,bins[0]是第一个空闲块的fd,bins[1]是最后一个块的bk。
heapinfo的核心逻辑用纯Python重写了。核心就是:读main_arena偏移,然后按链表遍历。你可以在.gdbinit里加个armheap命令,省得每次手动算。
4.3 pwndbg/peda在ARM上的适配
说实话,pwndbg和peda对ARM的支持都是半成品。我试过好几个版本,总结出一套兼容方案。
4.3.1 常见问题与修复
| 问题 | 原因 | 修复方法 |
|---|---|---|
heapinfo报错 |
硬编码了x86寄存器名 | 修改pwndbg/commands/heap.py,把sp换成r13 |
chunk命令显示错误 |
结构体偏移不对 | 手动指定main_arena地址:set $arena=0xb6f00010 |
find_fake_fast失效 |
地址计算用了x86的页大小 | ARM页大小是4KB,改page_size常量 |
peda的heap命令 |
不支持ARM的堆结构 | 建议直接用pwndbg,peda放弃ARM |
4.3.2 我的推荐方案
我个人习惯用pwndbg + 手动补丁。具体步骤:
- 安装pwndbg(git clone最新版)
- 修改
pwndbg/glibc.py,把main_arena的查找逻辑改成从info proc mappings的heap段偏移计算 - 写一个
.gdbinit,启动时自动设置ARM架构:set architecture arm - 加一个自定义命令
armheap,封装手动遍历逻辑
heapinfo把堆块大小全读反了。后来发现是大小端问题——ARM默认小端,但pwndbg的某些代码假设了大端。加一行set endian little就解决了。
4.4 追踪堆块状态变化
调试堆利用,核心就是看堆块怎么从allocated变成freed,又怎么被重新分配。我一般用断点+条件打印来追踪。
4.4.1 设置malloc/free断点
(gdb) b *malloc
(gdb) commands
> printf "malloc(%zu) called\n", $r0
> continue
> end
(gdb) b *free
(gdb) commands
> printf "free(%p) called\n", $r0
> continue
> end
ARM上,参数通过r0-r3传递。所以$r0就是malloc的size或free的指针。
4.4.2 记录堆块生命周期
我写了个小脚本,每次malloc/free时记录堆块地址和状态:
define logheap
set $logfile = fopen("/tmp/heap.log", "a")
if $argc == 2
fprintf($logfile, "%s %p size=%zu\n", $arg0, $arg1, $arg2)
else
fprintf($logfile, "%s %p\n", $arg0, $arg1)
end
fclose($logfile)
end
然后在断点里调用:
(gdb) b *malloc+4
(gdb) commands
> logheap "malloc" $r0
> continue
> end
4.4.3 实战:追踪double free
假设我们怀疑一个double free漏洞。流程是这样的:
- 第一次free(ptr)后,堆块进入fastbin,fd指向NULL
- 第二次free(ptr)后,堆块再次进入fastbin,fd指向自己(形成循环)
- 两次malloc相同大小,会拿到同一个堆块
用GDB验证:
# 第一次free后
(gdb) x/2wx ptr-8
0xb6f00920: 0x00000000 0x00000031 # prev_size=0, size=0x31
(gdb) x/gx ptr
0xb6f00928: 0x00000000 # fd=NULL
# 第二次free后(double free)
(gdb) x/2wx ptr-8
0xb6f00920: 0x00000000 0x00000031
(gdb) x/gx ptr
0xb6f00928: 0xb6f00928 # fd指向自己!
看到fd == ptr,基本就实锤double free了。
ldd --version。
4.5 本章知识体系
下面这张图总结了ARM堆调试的核心流程和工具链:
4.6 本章小结
ARM堆调试没有x86那么舒服,但掌握了手动方法后,反而更踏实。你想想看,插件帮你做的事,你自己也能做,那还怕什么?
几个关键点再强调一下:
- 裸GDB是底线——
x/wx、p main_arena、info proc mappings这三板斧必须熟练 - 插件要适配——pwndbg能用,但得改几行代码。别指望开箱即用
- 追踪靠断点——malloc/free断点+条件打印,比任何可视化工具都可靠
- 日志是王道——把堆块状态变化写到文件里,回头分析时一目了然
嗯,这一章就到这。调试工具是基础,但真正的高手,是用工具的人,不是被工具用的人。