objdump -g 中的任意代码执行

Lobsters Hottest 新闻

摘要

objdump -g 中存在一个安全漏洞,由于 FR30 重定位处理程序缺少边界检查,通过精心构造的 FR30 目标文件可实现任意代码执行,单个漏洞利用即可绕过 ASLR 及其他缓解措施。

<p><a href="https://lobste.rs/s/sbcasc/arbitrary_code_execution_objdump_g">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/09 01:24

# OOBdump:基于重定向的编程 来源:https://blog.calif.io/p/oobdump-relocation-oriented-programming 我们有个爱好:在漏洞发现工具中找漏洞 (https://blog.calif.io/p/mad-bugs-all-your-reverse-engineering)。IDA Pro、Ghidra、Binja Sidekick 或 radare2。你叫得上名字的我们都黑过。朋友建议我们试试 objdump。于是就这么干了。 `objdump -g` 应该很无聊:读取目标文件,打印调试信息,然后退出。但只要找个合适的 FR30 目标文件,就能让它执行任意代码。 漏洞出在 FR30 重定位处理程序中缺少边界检查。按现在的标准看相当无聊。亮点在于我们如何把这个简单的堆 OOB 变成一个只用单个精心构造的输入就能击败 ASLR、PIE 和堆加固缓解措施的利用。 该漏洞仅影响 objdump 的一种罕见构建配置。其父项目 binutils 的安全策略明确将此类问题排除在安全漏洞之外,并要求公开披露。我们遵循了该流程,问题也得到了及时修复。 这个利用本身很漂亮。很少见到一个堆溢出能在单次攻击中既击败 ASLR 又保持真正的单次触发。 FR30 是 1990 年代末富士通的内嵌 RISC 核心,属于专有的 32 位 FR 家族 (https://en.wikipedia.org/wiki/Fujitsu_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` (https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=bfd/elf32-fr30.c;hb=7565cfd7ad2edc1f4ba6c88c6af86e78856c5b3f): 函数首先计算 `relocation`,即要写入的值。攻击者控制符号和加数项,这里的段状态是可预测的,因此我们控制要写入的内容。 然后它调用 `bfd_put_32` 来应用修补。写入操作落在 `data` 上,即存放目标段内容的堆缓冲区。在我们的利用中,该段是 `.debug_info`。 其偏移直接来自 `reloc_entry->address`,再加上两个字节来跳过 16 位的指令前缀。没有任何检查来验证偏移是否在缓冲区大小内。由于我们同时控制值和偏移,越界写入就变得轻而易举。 每个重定位条目都会执行一次该处理程序,我们可以添加任意数量的条目,所以一个文件就能提供我们想要任意次数的写入。 我们选择 `.debug_info` 是因为 objdump 的 DWARF 读取器在解析其中的 DWARF 数据之前会先加载并重定位该段。该段可以全为零,但每次写入仍然会触发。 虽然 OOB 写入很强大,但仍有两大障碍挡在我们面前: 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 缓冲区周围的堆布局 (https://substackcdn.com/image/fetch/$s_!0TOW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6678e2c1-1c7e-4cad-a58c-f4200ce562ab_932x884.png) 这个利用按顺序清除两个障碍。 磁盘上的 FR30 重定位偏移是 32 位,但 BFD 将其扩展为 64 位的 `arelent.address`: 由于 `arelent` 数组位于距离 `data` 一个已知的正偏移 `R` 处,一个重定位可以编辑后续的重定位。如果第 `n` 个重定位将 `0xFFFFFFFF` 写入第 `n+1` 个重定位 `address` 的高双字,那么第 `n+1` 个重定位就会计算 `data + 0xFFFFFFFF_xxxxxxxx + 2`,这个值在 64 位指针运算中会回绕到 `data` 下方。 这样我们就可以用两次重定位实现向后写入: 通过回绕64位arelent地址来访问缓冲区之前的内存 (https://substackcdn.com/image/fetch/$s_!6O1t!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6e75d94-a8d2-4609-b877-123e5d9f2755_480x452.gif) 这个利用是非交互式的:`objdump -g` 处理一个文件后直接返回,不输出任何内容。没有信息泄露,我们永远不知道堆或 libc 的地址,因此无法写入绝对指针。相反,我们将把 OOB 写入变成 OOB 递增,在不了解指针值的情况下就地编辑它们。 这需要两个改动: 1. 将 `bfd_put_32` 从大端改为小端。aarch64 是小端,因此大端写回会破坏指针而不是调整它。(本节) 2. 从另一个后端借用一种就地重定位类型,提供读-加-写递增功能。(第 3 步) 两者都依赖于同一个技巧。objdump 的 PIE 镜像加载在 64KB 边界上,因此二进制内任何指针的低 16 位在 ASLR 下是固定的。覆盖这两个字节,我们就可以将指针重定向到同一页面内的另一个对象,完全无需猜测。由于 OOB 写入一次修改 32 位,我们会破坏前一个字段的两个字节。在我们使用此技巧的地方,这些字节无关紧要。 对于第一个改动,我们修改 `bfd_put_32` 编码字节的方式。`bfd_put_32` 是一个宏,通过函数指针 `abfd->xvec->bfd_putx32` 进行分发,它决定写入是小端还是大端。 幸运的是,可以赋给 `abfd->xvec` 的 `bfd_target` 结构体都聚集在 `.data.rel.ro` 中。这个构建有九个位于 FR30 向量同一 64KB 页面内的小端 `bfd_target`。任何一个都行,但 `crx_elf32_vec` 在该页面中位于 `0x00b0`,所以我们选择了它。 一次2字节写入将 xvec 的低2字节从 FR30 向量重定向到小端 CRX 向量 (https://substackcdn.com/image/fetch/$s_!LkbF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febe3ceba-639f-4643-b3b3-04bc555401c8_760x340.gif) 第 2 步改变了 BFD 写入字节的方式。在第 3 步中,我们需要改变我们能用的重定位类型。 同样的部分覆盖在这里也适用,只是目标指向不同的指针。每个 `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 写入,我们可以人为地增大段大小来绕过检查。 好的,现在我们将堆 OOB 写入升级为 OOB 递增。然后呢? 还记得我们之前简单介绍过的 `bfd` 结构体中的 `FILE *iostream` 字段吗?原来这个 `FILE` 结构体实际上也是分配在堆上的! 这意味着我们可以使用 OOB 递增原语来修改 `FILE` 结构体中的选定字段,从而利用一种称为 House of Apple 2 (https://jia.je/ctf-writeups/2025-09-07-blackhat-mea-ctf-quals-2025/file101.html) 的文件流导向编程(FSOP)技术实现代码执行。 事实证明,只需要 4 次 OOB 递增: 前两次重新定向 FILE 中已有的 libc 指针,另外两次修改堆指针。由于 libc 和堆布局是固定的,这个操作是完全确定且可靠的。 通过四次部分重定位(PI)破坏 FILE (https://substackcdn.com/image/fetch/$s_!hlnd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09e08168-722e-49b0-aeda-a76cd02460f5_1760x680.png) `_lock` 和 `_wide_data` 的移动隐藏了一个技巧。我们将 `_wide_data` 指向 `fp-88`,这样它的 `_wide_vtable` 字段(偏移 224)就落在了 FILE 自身的 `_lock` 上(位于 `fp+136`)。 _wide_data 与 FILE 重叠,因此 _wide_vtable 和 _lock 共享同一个槽位 (https://substackcdn.com/image/fetch/$s_!EAOg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F476c14cb-36a8-488f-86d5-4b777d138b9e_1440x460.png) 现在这两个字段共享同一个堆指针。将其设置为 `fp+80`,则 `_lock` 获得一个零锁字,而 `_wide_vtable` 获得一个伪造的 vtable,其 `__doallocate` 是 `system`。 为什么要费心搞重叠?我们产生的每个值都是通过一个常数微调过的现有指针,所以我们不能凭空变出两个无关的堆地址,一个给 `_lock`,一个给 `_wide_vtable`。因此我们让布局只需要一个。选择 `fp-88` 使得 `_wide_vtable` 恰好落在 `_lock` 上,而那个单一的微调指针就完成了两个任务。 其他直接的 OOB 写入填充了所需的 `FILE` 状态:`write_ptr > write_base`、伪造的 wide-data 字段、`_flags` 中的命令字符串。 最后一次写入将 `abfd->iostream` 设为 `NULL`,这样 `bfd_close` 跳过 `fclose`,让该 FILE 仍然链接在 `_IO_list_all` 中。 当 `exit()` 执行时,glibc 遍历 `_IO_list_all` 并到达被破坏的 FILE。窄刷新检查(`_mode <= 0 && write_ptr > write_base`)选择它进行刷新,但由于 vtable 现在指向 `_IO_wfile_jumps`,`_IO_OVERFLOW` 分派到 wide 处理程序 `_IO_wfile_overflow`,该处理程序到达 `_IO_wdoallocbuf` 并通过伪造的 wide vtable 调用。`__doallocate` 槽位已被 OOB 递增为 `system`,因此调用变为 `system(fp)`,执行我们放置在 `FILE` 结构体开头的命令。 最后一个细节:我们将 `.debug_info` 的大小设置为 144 字节。更小的布局会将 tcache 元数据放置在必须保持为零的伪造 `_wide_data` 字段上,从而破坏利用。 上游的修复增加了处理程序自身本应执行的边界检查。在写入之前,FR30 处理程序现在会验证偏移并拒绝超出段范围的情况: 检查的是 `reloc_entry->address + 2`(实际写入偏移),并带有溢出保护。有了这个检查,崩溃 PoC 会使 `objdump` 拒绝该重定位并干净地退出,而不会越界写入。 我们从未真正击败 ASLR、PIE 或堆加固,只是避免给它们任何防守的机会。因为整个链条中没有任何东西依赖于绝对地址,所以既没有需要追查的泄漏,也没有需要猜测的基址;而 `xvec` 和 `howto` 的交换只需要触及 64KB 对齐已经固定的低位。 反过来,指针运算也只是在其自身区域内用恒定差值微调现有指针,因此 libc 指针留在 libc 中,堆指针留在堆上。普通利用会用泄露的地址伪造新的结构体,而我们只是重复利用了已经躺在附近的那些。 像这样的缓解措施是为正面交锋而构建的,并且往往能赢得那场战斗。但我们拒绝战斗,而是选择绕过它们。讽刺的是,完成绕过的正是 BFD 自己的重定位引擎,而正是同样的机制最初使得 ASLR 和 PIE 能够工作。 AI 生成的 PoC 和文章:https://github.com/califio/publications/tree/main/MADBugs/oobdump。 #### 关于本文的讨论 ### 想要更多内容?

相似文章

栈回溯技术能实现无泄漏的代码执行

Lobsters Hottest

本文探讨了在glibc中使用DWARF字节码进行栈回溯如何被利用来执行代码,并通过CTF挑战和对POSIX系统中线程取消机制的分析加以阐释。

突破防御:利用段错误绕过 Intel CET

Lobsters Hottest

该仓库提供了面向段错误编程(SFOP)的工件,这是一种利用信号处理器绕过 Intel CET 的新型利用技术。它包含针对 Nginx 和 Ladybird 的 PoC 利用和演示。

schrodingers-toctou: The binary you run is not the program you wrote

Lobsters Hottest

This research describes compiler-invented loads, where compiler optimizations create additional memory reads not present in source code, turning seemingly secure code into vulnerable binaries with TOCTOU races. It includes audits across kernels, hypervisors, enclaves, and firmware.