objdump中的任意代码执行:一种无需地址泄露即可绕过现代防护的堆溢出奇技淫巧"

admin 2026-10-20 13:40:31 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

objdump 中的任意代码执行:一种无需地址泄露即可绕过现代防护的堆溢出奇技淫巧

原创

骨哥说事
骨哥说事

骨哥说事

2026年6月9日 09:23
上海

在小说阅读器读本章

去阅读

*声明:*文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。

防走失:https://gugesay.com/

不想错过任何消息?设置星标↓ ↓ ↓

objdump -g 这个功能理应很枯燥。它读取一个目标文件,打印调试信息,然后退出。但若使用特制的 FR30 目标文件,却能诱使其执行任意代码。

这个漏洞是 FR30 重定位处理器中缺失的一个边界检查。按今天的标准来看相当乏味。但酷炫之处在于,我们如何将这次简单的堆越界写入(OOB write)转化为一个漏洞利用,仅凭一份精心制作的输入,就能绕过 ASLR(地址空间布局随机化)、PIE(位置无关可执行文件)和堆加固等缓解措施。

该漏洞仅影响 objdump 一种罕见的构建配置。其父项目 binutils 的安全政策明确将此类问题排除在安全漏洞之外,并要求公开披露。我们遵循了这一流程,问题也得到了及时修复。

这个漏洞利用本身非常精彩。很少能看到一个堆溢出漏洞能被实现为一个真正的、一次性完成、并且同时绕过了 ASLR 的利用。

被遗忘的目标架构

FR30 是富士通在 20 世纪 90 年代末推出的一款嵌入式 RISC 核心,是专有的 32 位 FR 家族的一部分。Binutils 至今仍包含对它的支持,但主要针对主机的标准版 objdump 构建通常不启用该后端。实际受影响的是自定义或多目标构建:--enable-targets=all、明确的 fr30-*-elf 目标、SDK 工具链、CI 镜像,以及那些希望用一个工具识别所有格式的二进制分析环境。

为什么需要重定位?

你可能会想,为什么 objdump 需要对输入的目标文件执行重定位?它就不能直接按原样读取并打印字节吗?

利用中使用的 FR30 文件是一个可重定位的目标文件,而非最终的可执行文件。C 编译器会为每个源文件生成一个目标文件(.o),随后链接器将它们合并成一个可执行文件。由于编译器不知道每个节在最终程序中的具体位置,它会留下占位符值,并记录重定位条目来标记哪些位置需要修补。调试节的工作方式相同,而 objdump -g 读取的正是这些节。

在这个例子中,.debug_addr 节包含一个头部,后面跟着两个代码地址的零占位符条目:

当处理对应的重定位节(.rela.debug_addr)时,情况就变了:

在正常的构建过程中,链接器会查看重定位节,并将修补应用到它生成的二进制文件中。

但 objdump -g 运行在原始的目标文件上,周围没有链接器来执行修补。这个工作就落在了 binutils 的二进制文件描述符 (BFD) 库上,而我们的漏洞正藏身于此。

上面的重定位很简单,但真正的重定位格式要复杂得多。每个架构都定义了自己的重定位类型及其应用方式,这使得对于像 BFD 这样的多目标库来说,这个区域特别容易出现漏洞。

缺失的检查

这个漏洞由 Anthropic 发现并与我们分享。

FR30 的 R_FR30_48 重定位处理器是 bfd/elf32-fr30.c 中的 fr30_elf_i32_reloc 函数:

该函数首先计算 relocation,即要写入的值。攻击者控制了符号和加数项,而且此处的节状态是可预测的,因此我们控制了要写入的内容。

然后它调用 bfd_put_32 来应用补丁。写入操作发生在 data,即保存目标节内容的堆缓冲区中。在我们的利用中,这个节是 .debug_info。

其偏移量直接来自 reloc_entry->address,再加上两个字节以跳过 16 位指令前缀。没有任何东西检查该偏移量是否超出缓冲区大小。由于我们同时控制了要写入的值和偏移量,实现越界写入易如反掌。

每个重定位条目会触发该处理器运行一次,并且我们可以添加任意数量的条目,所以一个文件就给了我们任意次数的写入机会。

我们使用 .debug_info 节是因为 objdump 的 DWARF 读取器在解析内部的 DWARF 之前会加载并重定位该节。即使该节全是零,每次写入仍然会触发。

堆布局

虽然越界写入的能力很强大,但我们面前仍有两个障碍:

  1. 我们只能修改 data 缓冲区之后地址更高的内存,因为写入位置是 data + r_offset + 2,而 r_offset 是一个无符号偏移量,只能向前到达。
  2. 我们没有信息泄露,所以 PIE 和 libc 的基地址仍被 ASLR 隐藏。

幸运的是,data 在堆上并不孤单。附近有两个对象给了我们需要的一切。

第一个是 bfd 结构体,这是 BFD 打开目标文件时分配的句柄。它包含许多重要的字段,引导 BFD 的所有操作,包括 xvec(指向 bfd_target 结构体的指针,里面充满了诱人的函数指针)和 iostream(指向打开的 FILE 结构体的指针)。这使它成为一个有价值的目标,但它位于 data 之前 8400 字节处,我们目前只能向前的写入还无法触及它。

第二个是 arelent 数组,它是文件中重定位记录的内存形式。它位于 data 之后 47440 字节处,在一个单独的分配块中,处于向前写入所能及的范围内。每次 objdump -g 运行时都会以相同的顺序分配相同的块,所以这些距离是确定性的。

环绕 .debug\_info 缓冲区的堆布局示意图

利用按顺序清除了这两个障碍。

第一步:让偏移量“回绕”

磁盘上的 FR30 重定位偏移量是 32 位的,但 BFD 会将其扩展为 64 位的 arelent.address:

因为 arelent 数组位于 data 之后一个已知的正偏移量 R 处,一个重定位可以编辑后面的另一个重定位条目。如果重定位 n 往重定位 n+1 的 address 的高32位写入 0xFFFFFFFF,那么重定位 n+1 计算 data + 0xFFFFFFFF_xxxxxxxx + 2 时,在 64 位指针运算中就会“回绕”到 data 之前。

这允许我们用两个重定位实现向后的写入:

通过回绕 64 位 arelent 地址来触及缓冲区之前的内存

第二步:翻转字节序

该利用是非交互式的:objdump -g 对一个文件运行,然后返回但不输出任何内容。在没有信息泄露的情况下,我们永远无法获知堆或 libc 的地址,因此无法写入绝对的指针。相反,我们将把越界写入(OOB write)转变为就地指针递增(OOB increment),无需知道其具体值即可就地编辑指针。

这需要两个改动:

  1. 将 bfd_put_32 从大端序改为小端序。aarch64 是小端序的,所以用大端序写回会破坏指针,而不是调整它。(本部分)
  2. 从另一个后端“借用”一个就地重定位类型,这能给我们一个“读取-加值-写入”的递增操作。(第三步)

这两个改动都依赖于同一个操作。objdump 的 PIE 镜像加载在 64KB 对齐的边界上,因此任何指向二进制内部的指针的低 16 位在 ASLR 下是固定的。覆写这两个字节,我们就能将指针重定向到同一内存页中的另一个对象,而无需任何猜测。由于 OOB 写入每次修改 32 位,我们会破坏前一个字段的两个字节。在我们使用此方法的地方,这些字节无关紧要。

对于第一个改动,我们改变了 bfd_put_32 编码字节的方式。bfd_put_32 是一个宏,它通过函数指针 abfd->xvec->bfd_putx32 分发,该指针决定写入是按小端序还是大端序进行。

对我们有利的是,可以分配给 abfd->xvec 的 bfd_target 结构体都聚集在 .data.rel.ro 段中。这个构建版本有九个小端序的 bfd_target 与 FR30 的向量表位于同一个 64KB 内存页中。其中任何一个都可以,但 crx_elf32_vec 在该页中位于最前面的 0x00b0 地址,所以我们选择了它。

一个 2 字节的写入操作将 xvec 的低 2 字节从 FR30 向量更改为小端序的 CRX 向量

第三步:借用更好的重定位类型

第二步改变了 BFD 写入字节的方式。在第三步,我们需要改变我们可用的重定位类型。

同样的部分覆写操作在这里也有效,只是指向一个不同的指针。每个 reloc_cache_entry 都有一个 howto 指针(reloc_howto_type *),描述如何应用该重定位:其宽度、写入位置以及执行此操作的处理器。

就像 bfd_target 向量表一样,各个后端的 reloc_howto_type 表也都在 .data.rel.ro 段中,所以只需要一次 2 字节的写入就可以将 howto 从一个表切换到另一个。

来自 i386 的 R_386_PC32 重定位类型正好给了我们想要的。它设置了 partial_inplace 标志,这使得 BFD 会累加到目标中已有的值上,而不是直接覆写它:

那么,唯一的问题是 i386 的重定位处理器实际会执行边界检查,而这正是原始漏洞代码所缺失的。因此,一旦我们切换到该处理器,我们的 OOB 写入将被拒绝。

不过有个简单的解决办法:由于节的大小信息位于堆上,而我们拥有堆 OOB 写入能力,我们可以直接人为地增加节的大小来绕过检查。

第四步:改写 FILE 结构体(House of Apple 2)

好了,我们已经把堆 OOB 写入升级为可控制的 OOB 递增。现在怎么办?

还记得我们之前简要介绍过的 bfd 结构体中的 FILE* iostream 字段吗?事实证明这个 FILE 结构体实际上是在堆上分配的!

这意味着我们可以使用 OOB 递增原语来修改 FILE 结构体内的选定字段,从而利用一种称为 House of Apple 2 的文件流导向编程(FSOP)技术实现代码执行。

事实证明,只需要 4 次 OOB 递增:

前两次重定向了 FILE 结构中已有的 libc 指针,而后两次修改了堆指针。由于 libc 和堆的布局是常量,这一操作完全确定且可靠。

用四个 PI 重定位损坏 FILE 结构

_lock 和 _wide_data 的调整隐藏了一个技巧。我们将 _wide_data 指向 fp-88,这样它的 _wide_vtable 字段(偏移量 224)就会落在 FILE 自身的 _lock 字段上(fp+136)。

_wide_data 与 FILE 重叠,使得 _wide_vtable 和 _lock 共享一个位置

现在这两个字段共享同一个堆指针。将其设置为 fp+80,_lock 得到一个零锁字,而 _wide_vtable 得到伪造的虚函数表,其 __doallocate 条目指向 system 函数。

为什么要采用重叠设计?我们生成的每个值都是一个现有指针加上一个常量偏移,因此我们无法凭空捏造两个不相关的堆地址,一个给 _lock,一个给 _wide_vtable。所以我们设计了一种只需要一个地址的布局。选择 fp-88 使得 _wide_vtable 恰好落在 _lock 上,这一个调整过的指针就两用了。

通过其他直接的 OOB 写入来填充必需的 FILE 状态:write_ptr > write_base、假的宽数据字段、放在 _flags 中的命令字符串。

最后一次写入设置 abfd->iostream = NULL,使 bfd_close 跳过 fclose,让该 FILE 结构体保留在 _IO_list_all 链表中。

在 exit() 时,glibc 会遍历 _IO_list_all,找到被破坏的 FILE 结构体。窄流的刷新检查(_mode <= 0 && write_ptr > write_base)选中了它进行刷新,但由于虚函数表现在指向 _IO_wfile_jumps,_IO_OVERFLOW 会分发到宽处理器 _IO_wfile_overflow,这进而调用 _IO_wdoallocbuf,并通过伪造的宽虚函数表进行调用。__doallocate 槽位已被 OOB 递增为 system,所以这次调用变成了 system(fp),执行了我们放在 FILE 结构体开头的命令。

最后一个细节:我们将 .debug_info 的大小设置为 144 字节。更小的布局会将 tcache 元数据放在需要保持为零的假 _wide_data 字段之上,从而破坏利用。

修复方案

上游的修复补丁增加了该处理器本应自己执行的边界检查。在写入之前,FR30 处理器现在验证偏移量,并拒绝任何超出节范围的写入:

检查是针对 reloc_entry->address + 2,即实际的写入偏移量,并带有防止溢出的保护。有了这个检查,使崩溃的 PoC(概念验证)会导致 objdump 拒绝重定位并干净退出,而不是越界写入。

经验与启示

我们并非真正“击败”了 ASLR、PIE 或堆加固,而是更多地绕过了它们,没有给它们任何防御的机会。由于整个利用链不依赖于绝对地址,所以根本不需要泄露或猜测基地址,而 xvec 和 howto 的切换只需要触及低 16 位,这些位在 64KB 对齐下已经固定。

相应地,指针运算也只是在它们自己的区域内,对现有指针加上常量偏移,所以 libc 指针保持在 libc 中,堆指针保持在堆上。在普通的漏洞利用中,需要通过泄露的地址伪造新的结构体,而我们只是重复利用了附近已有的结构体。

这类缓解措施是为正面交锋而设计的,并且往往能在正面交锋中获胜。但我们拒绝正面交锋,直接绕道而行。具有讽刺意味的是,执行绕道的工具正是 BFD 自己的重定位引擎,而这类引擎正是 ASLR 和 PIE 最初得以工作的基础。

AI 生成的 PoC 和漏洞分析文章:

https://github.com/califio/publications/tree/main/MADBugs/oobdump。

  • END -

感谢阅读,如果觉得还不错的话,动动手指给个三连吧~


objdump中的任意代码执行:一种无需地址泄露即可绕过现代防护的堆溢出奇技淫巧" 网络安全文章

objdump中的任意代码执行:一种无需地址泄露即可绕过现代防护的堆溢出奇技淫巧"

本文详细分析了objdump工具中FR30架构重定位处理器的堆溢出漏洞,该漏洞源于缺失边界检查导致越界写入。文章展示了如何通过64位地址回绕、字节序切换和重定位类型借用等技术,将简单OOB写入升级为可控的OOB递增操作,最终利用HouseofApple2技术实现无需地址泄露的任意代码执行,成功绕过ASLR、PIE等现代防护机制。
FlutterBridge行动利用虚假谷歌广告传播macOS后门" 网络安全文章

FlutterBridge行动利用虚假谷歌广告传播macOS后门"

FlutterBridge行动是CL-CRI-1089组织自2025年底发起的恶意广告活动,通过虚假谷歌广告诱骗Mac用户下载伪装成播客播放器或PDF浏览器的FlutterShell后门。该恶意软件具备任意命令执行、文件窃取和浏览器劫持能力,攻击者使用空壳公司购买广告并频繁更新变种。研究显示该恶意软件仍在积极开发中,建议用户警惕不明来源的应用程序下载。
评论:0   参与:  0