发布时间:2026/9/8 7:37:26
off-by-null堆利用:单字节溢出到overlapping chunk的完整解析 off-by-null这个话题在pwn圈子里被聊了快十年但每过一阵都会有新人踩进同一个坑里。说它是堆利用的“入门必修课”一点都不夸张——它只溢出一个字节却能把glibc的堆管理机制搅得天翻地覆。这里说的“还没有涉及高版本”指的是一般glibc 2.23及以下的环境。这个版本没有tcache机制fastbin、unsorted bin、smallbin的检查相对朴素非常适合搞懂堆利用最底层的逻辑chunk结构、prev_inuse位、unlink、向前/向后合并。如果你能在这个版本下把off-by-null的原理和利用路径理清楚之后再去碰2.27以上带tcache的题目会顺很多。这篇文章适合已经对堆分配、malloc/free基本流程有概念的读者。如果你是零基础建议先把chunk结构、bins分类、top chunk这些补一补再回来看效果会好很多。1. off-by-null到底是什么从堆布局看单字节的杀伤力1.1 malloc chunk的二进制布局与prev_inuse位在glibc的实现里每个chunk都有一个16字节的头部64位环境下prev_size如果前一个chunk空闲这个字段保存前一个chunk的大小如果前一个chunk在使用这个字段可以被前一个chunk复用。size当前chunk的大小低3位是标志位其中最低位是prev_inuseP位表示前一个chunk是否正在使用。P位的作用非常关键。glibc在释放当前chunk时会先检查自己的P位P位为1说明前一个chunk在用不需要做向前合并P位为0说明前一个chunk空闲此时glibc会读取当前chunk的prev_size找到前一个空闲chunk把两个chunk合并成一个大的空闲块。off-by-null的攻击点就在这里。假设当前chunk的size是0x121内存里小端存放为21 01 00 00 00 00 00 00。如果你只向最低字节写入0x00size会变成0x100。注意不只是P位被清零整个size都变了——原本chunk的边界从0x120收缩到了0x100。这就是为什么off-by-null被称为“毒化单字节”它同时做了两件事伪造了前一个chunk为“空闲”并且把当前chunk的实际大小改小了。单字节写入0看似克制实际上是精准地命中了一个堆管理器的核心信任假设P位是由前一个chunk的分配/释放状态自动维护的glibc不会去反向验证。而利用者恰恰就是利用这个“不验证”把一个绝对不可能发生的状态喂给了堆管理器。1.2 常见触发场景off-by-null的触发点特别多常见的有read(fd, buf, size)读入实际数据刚好粘着下一个chunk的headerstrcpy/strcat/sprintf这类字符串函数拷贝后自动补一个\x00手动赋值时下标计算错误多写了一个字节的0某些自定义的序列化、编码逻辑在缓冲区末尾加结束符。看个典型伪代码char *a malloc(0x108); char *b malloc(0x110); ... read(0, a, 0x109); // 你以为自己只写0x108实际多了1字节malloc(0x108)实际拿到的user data长度是0x108从用户指针开始数最后一个可写字节刚好是下一个chunk的size字段的前一个字节。多写1字节就精确覆盖了B的size低1字节。写到这里顺便说一个很多人会搞混的点覆盖到的是“B的size首字节”而不是“prev_size首字节”。因为glibc分配时会把请求大小加SIZE_SZ后对齐实际usable size比理论上的“size - 0x10”大8字节。这个多出来的8字节正好能把下一chunk的prev_size和size都框在你的写范围内。如果你在调试时发现写完指针后看到的是prev_size变了多半是测试代码的malloc大小没选对。2. 低版本环境下为什么要用它没有tcache的利用思路2.1 为什么选2.23堆机制更“朴素”glibc 2.23是CTF里最常见的老牌环境Ubuntu 16.04原生用的就是它。这个版本没有tcache释放小chunk时要么进fastbin要么进unsorted bin要么进smallbin每个bin的管理逻辑都清清楚楚。对比一下2.27以上的环境tcache的引入给堆利用塞进了一条“快车道”一个空指针放松检查、一个没清理的key就能做tcache dup利用成本低很多。但问题也在这里——很多人在高版本上能轻松打穿题回到2.23却一头雾水。因为高版本里你绕过的是“附加规则”而2.23逼你把glibc原始的链表维护逻辑搞清楚。所以学off-by-null选2.23是合适的。先不看高版本那些花活把核心原理吃透。2.2 一切都是为了制造overlapping chunkoff-by-null本身不是最终目标最终目标基本都指向一个东西overlapping chunk也就是重叠内存块。有了重叠chunk你可以通过一个已分配chunk的合法读写去修改另一个正被程序引用chunk的内容把一个正在使用的大chunk的头部改掉释放后进入bins再通过分配重新拿回来修改某些关键函数指针比如__free_hook劫持控制流。一句话概括overlap是很多堆利用链的地基。off-by-null是打地基最经典的工具之一。2.3 两种实现路径unlink与chunk shrink低版本下利用off-by-null通常走两条路。第一条是unlink。通过P位清零触发向前合并让glibc去unlink一个位于当前chunk前面的“假chunk”。unlink过程中会对假chunk的fd和bk做检查即FD-bk P BK-fd P。如果你能在可控区域内伪造一个chunk让fd和bk都指向它自身就能绕过检查。这种思路一般用于把目标区域释放进unsorted bin或者配合全局指针做任意写。第二条是chunk shrink也就是把当前chunk的size改小让堆管理器认为当前chunk的结束地址比实际小从而在堆里留下一个“幽灵区域”——这个区域既没有被释放堆管理器也不认识它但程序仍然持有指针。实际题目中更多是把两条结合起来先用chunk shrink制造尺寸差再用fake chunk配合P位清零完成合并最终形成的重叠区域越大后续利用空间就越宽裕。2.4 一个容易被忽视的点prev_size怎么来很多人第一次试off-by-null都会踩同一个坑只记得把下一chunk的size清Pfree之后直接报错corrupted size vs. prev_size。原因很简单size被清P后glibc会认为前一个chunk空闲并去读当前chunk的prev_size。这个值如果不对合并逻辑立刻校验失败。所以利用off-by-null时prev_size同样需要你提前布置好。好消息是很多场景下prev_size恰好落在你能够正常写的范围内。比如malloc(0x108)时下一chunk的prev_size正好在a 0x100处而你的user data range覆盖到了a 0x107。也就是说正常情况下你就能控制prev_size只有size需要通过溢出来写。这就让整个利用链非常顺滑。3. 动手验证2.23环境下的最小可复现demo3.1 环境准备我用的环境是glibc 2.23可以直接起一个Ubuntu 16.04的docker搞定docker run -it --rm ubuntu:16.04 bash apt update apt install -y gcc gdb python-pip pip install pwntools调试工具强烈建议pwndbg看heap、bins、unsorted bin都非常直观。git clone https://github.com/pwndbg/pwndbg cd pwndbg ./setup.sh准备好这些就可以开始复现了。3.2 内存布局设计布局是这种利用手法的灵魂。我设计的测试代码如下#include stdio.h #include stdlib.h int main() { setbuf(stdout, NULL); void *a malloc(0x108); void *b malloc(0x110); void *c malloc(0x100); printf(a%p\nb%p\nc%p\n, a, b, c); printf(usable size of a: 0x%zx\n, malloc_usable_size(a)); // fake chunk 放在 a 用户区偏移 0x30 处 size_t fake_addr (size_t)a 0x30; printf(fake chunk addr: 0x%zx\n, fake_addr); // fake chunk header *(size_t*)(fake_addr 0x00) 0; // prev_size *(size_t*)(fake_addr 0x08) 0xd0; // size必须等于 b-prev_size *(size_t*)(fake_addr 0x10) fake_addr; // fd *(size_t*)(fake_addr 0x18) fake_addr; // bk // 在 a 的可写范围内设置 b-prev_size *(size_t*)((char*)a 0x100) 0xd0; // b 的 prev_size 位于 a0x100 // 触发 off-by-nulla 的 usable size 是 0x108a[0x108] 越界到 b-size ((char*)a)[0x108] 0; // b-size: 0x121 - 0x100清除 prev_inuse printf(before free, b size0x%zx\n, *((size_t*)b - 1)); free(b); printf(after free, unsorted chunk at 0x%zx\n, fake_addr); return 0; }这段代码的核心是把几个关键地址之间的关系理清楚。假设运行时a的地址为0x...010b的地址为0x...120c的地址为0x...240。关系如下表项地址说明a的用户数据区0x...010长度为0x108可写范围为a0x00到a0x107a的chunk起始0x...000a chunk 0x10b的chunk起始0x...110a的chunk size是0x110b的prev_size0x...110即a0x100在a的可写范围内b的size0x...118即a0x108是off-by-null的落点fake chunk0x...040即a0x30在a的用户数据区内fake到b的间距0x...110 - 0x...040 0xd0用于设置fake size和b-prev_size所以fake_addr 0x8的size设置成0xd0同时b的prev_size也设置成0xd0free(b)时glibc在向前合并时就会走到fake chunk的位置。3.3 关键代码与调试编译运行gcc -o demo demo.c ./demo如果一切顺利程序会输出after free不会有任何glibc报错。此时用gdb检查堆gdb ./demo b *free0x0 run或者直接跑完后趁进程没退出用pwndbg的heap bins看看unsorted bin的状态pwndbg heap bins正常情况下unsorted bin里会出现一个起始地址为fake_addr、大小为0x1d0左右的chunk。为什么是0x1d0因为合并后的大小是fake chunk的0xd0加上被收缩后的b的size0x100再加起来正好0x1d0。在堆管理器的视角里原来a末尾到b之间的这一整块区域都被回收了而实际上a的用户数据区还在程序手里。我们care一下关键的一次检查b-prev_size 0xd0则向前合并的目标是b_pos - 0xd0正好是fake chunkfake chunk size 0xd0通过chunksize(P) ! prev_size校验fake chunk的fd和bk都指向自身通过unlink的FD-bk P BK-fd P校验同时unlink写回过程中只是把自己写回自己不会破坏其他关键数据。这就是一个标准、干净、可复现的off-by-null合并demo。3.4 合并成功之后overlap验证与后续利用方向合并成功只是第一步重点在于合并之后造成的overlap。继续往下写void *d malloc(0x100); printf(d%p\nb%p\n, d, b);因为unsorted bin里现在有一个从fake chunk开始的0x1d0大小的空闲块malloc(0x100)会从这个大块里切出一个0x110大小的chunk返回。于是d的用户指针大约在fake_addr 0x10也就是a 0x40。而b的用户指针在a 0x110。这两个地址的关系是d的chunk范围覆盖了a 0x40到a 0x150左右而b的数据区从a 0x110开始。也就是说d完全有能力写到b的整个数据区甚至写到b的header。这就是重叠。有了overlap后续的利用路线就开放了如果能先leak libc可以d修改b的size把它改成fastbin大小再free(b)后配合fastbin dup把__free_hook的地址写进fastbin链表如果没有单独的leak原语可以通过unsorted bin的fd/bk读地址把主线程arena附近的地址dump出来再算libc基址也可以把合并后的大块二次分解制造更精细的布局去改__malloc_hook或者__free_hook。这些属于“拿到overlap之后的下半场”。作为初探我建议先把前半场——布局、溢出、合并、验证overlap——练到闭眼能写再往后推进。4. 常见问题与排错记录4.1 free之后直接crash怎么办这是最常见的失败。不同报错对应的原因不一样列一个速查表报错内容问题定位corrupted size vs. prev_size while consolidatingfake chunk的size和b-prev_size不匹配corrupted double-linked listfake chunk的fd/bk指向错误unlink检查没过free(): invalid pointerb的位置不对可能计算偏移时少算了prev_sizemalloc(): memory corruption覆盖size后b的结束地址和c的起始地址对不上调试思路很简单在free处打断点用gdb的x/20gx看b header、prev_size、fake chunk这三处的内存布局逐项对着校验。只要它们三者自洽这个free就一定能过。4.2 为什么unsorted bin里看不到合并chunk一种情况是b的大小其实小于fastbin阈值。比如把b的size改成了0x70以下free(b)后它不会进unsorted bin而是直接进fastbin。2.23没有tcache但这不代表所有off-by-null都一定会合并进unsorted bin需要保证合并后的目标chunk大于0x80。另一种情况是合并后紧接着与top chunk合并了。如果在b后面没有guard chunk也就是demo里的cfree(b)时glibc发现top chunk的prev_inuse为0会直接把合并后的chunk再并入top。这样你在bins里当然看不到它它变成top的一部分了。所以guard chunk是必须的除非你后续就是想通过这种方式控制top chunk的size那是另一种更进阶的玩法。4.3 高版本迁移的提示最后说一句版本。2.23的off-by-null是“裸奔”的你亲手处理每一个链表的校验。到了2.27以上有了tcacheoff-by-null往往可以直接配合tcache poisoning甚至可以绕过一些原本棘手的检查。但核心思想没变先通过溢出改size、伪造prev_size、完成合并、制造overlap。只要这个底层理解透了高版本只是多几层规则的问题。我个人在实际操作中的体会是off-by-null这东西纸上谈兵没有用必须自己动手把demo跑通、把gdb里的布局看清楚、把每个报错原因搞明白。建议就从本文这个最小demo开始跑通之后再去刷how2heap里的poison_null_byte和house_of_einherjar相关示例最后找几道glibc 2.23的老题练手。等你把“合并之前需要哪些条件”背都背不下来的时候说明你真的会了。

相关新闻

2026/9/8 7:37:26

MySQL日期维度表设计:公历农历双表构建200年日历数据

简介:面向MySQL开发者与数据统计人员的日历数据表资源,内含两个SQL脚本,分别创建公历表和农历表,完整覆盖1900—2100年共200年的日期数据。公历表设计有 week_day 、 is_weekend 、 is_holiday 等字段,农历表包含…

2026/9/8 7:37:26

MySQL 8.0高性能实战:索引、事务与主从复制全解析

高性能MySQL和普通MySQL的差别,很多时候不是版本高低,而是从“能跑”变成“知道它为什么快、为什么慢”。这几年我一直在做数据库相关的企业级应用,最深的体会是:面试题里背过的索引、事务、锁,到了生产环境每一个都会…

2026/9/8 7:37:26

MySQL从入门到精通:从环境搭建到SQL优化的完整学习指南

MySQL 是很多人接触到的第一套关系型数据库,也是后端项目里出现频率最高的数据存储方案。如果标题里的“从入门到精通”让你有点焦虑,先放轻松:这个目标不需要你背下所有命令,而是要把环境、SQL 基础、查询进阶、索引和事务这条主…

2026/9/8 8:32:30

PHP+MySQL社区系统开发与宝塔面板部署实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/8 8:32:30

Spring Boot家装项目管理系统:从需求到远程调试的完整实战

做装修公司信息化这行快十年,见过太多工地上“人盯人”的管理方式了。项目经理翻着手机找聊天记录报进度,老板想看一眼各工地资金占用情况得等财务月底拉Excel,客户三天两头问“我家装到哪一步了”却得不到准确答复——这些都是装修公司项目管…

2026/9/8 8:32:30

SSM+Vue乐器销售管理系统毕设全攻略:从数据库设计到答辩

2026届的毕设题目下来得比往年早,很多同学开题就领到了“乐器销售管理系统”这个题目,技术栈指定ssmvue,要求论文和程序一起交付。说实话,这类题目属于经典的“管理系统”家族,网上能搜到的代码很多,但真正…

2026/9/8 8:32:30

FLIR T600系列热像仪全解析:从参数读懂到现场实拍

干这行久了,会发现一个很有意思的现象:每逢设备采购季,总有人抱着FLIR T600系列的参数表来问我,指着640x480分辨率、40mK热灵敏度这些数字,反复确认到底哪款够用。参数摆在那里,谁都能对比,可真…

2026/9/8 7:15:10

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/8 7:15:15

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/8 7:15:10

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/8 0:01:49

踩多轮坑才跑通|OpenClaw 3.1.0 双平台本地 AI 自动化搭建实操实录

🔹 工具简述 OpenClaw 是一款备受开发者与办公人群青睐的开源本地智能工具,凭借离线本地运行、可视化图形面板、全流程自主任务处理三大核心特点,积累了众多忠实用户。与普通对话类 AI 产品不同,它能够直接调用电脑的软硬件操作权…

2026/9/8 0:01:50

拒绝复杂命令行,Hermes Agent 一键包快速解锁智能办公能力

🔍前言 不少想要体验 Hermes Agent 办公能力的使用者,往往会被复杂的环境配置拦住使用脚步。手动下载匹配依赖、反复调整系统目录、处理命令行持续报错、修复权限异常、补全丢失核心文件等一系列操作,对普通使用者而言门槛较高,很…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/7 22:45:59

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…