# ret2dlresolve ###### tags: `pwn`, `ctf` # はじめに 使用する環境はUbuntu20.04 LTS ```shell= root@Ubu2004x64:now# uname -a Linux Ubu2004x64 5.15.0-46-generic #49~20.04.1-Ubuntu SMP Thu Aug 4 19:15:44 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux root@Ubu2004x64:now# ``` ## チャレンジプログラム Exploitする対象のプログラムは以下の通りだ. ```c= // vuln.c #define _GNU_SOURCE #include <stdio.h> #include <unistd.h> int main(int argc, char **argv) { char buf[0x100]; read(0, buf, 0x200); return 0; } __attribute__((constructor)) void setup(){ setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); } ``` これを, Stack Smash Protector無し. Partial-RELROでコンパイルする. ```shell= gcc -o vuln vuln.c -fno-stack-protector -no-pie ``` `gef`の`checksec`コマンドを見ると, + `Canary`無し + `NX`有効 + `Partial-RELRO`/非`PIE` でビルドされているのがわかる. ```shell= root@Ubu2004x64:now# alias checksec alias checksec='gdb -ex "checksec" -ex "quit"' root@Ubu2004x64:now# checksec ./vuln [!] Missing file `rp-lin-x64-v2` GEF for linux ready, type `gef' to start, `gef config' to configure 197 commands loaded for GDB 9.2 using Python engine 3.8 Reading symbols from ./vuln... (No debugging symbols found in ./vuln) [+] checksec for '/tmp/now/vuln' Static/Dynamic : Dynamic Stripped : No (The symbol remains) Canary : Disabled★ NX : Enabled★ PIE : Disabled★ RELRO : Partial RELRO★ Fortify : Not Found Intel CET : Found (endbr64/endbr32 is found) RPATH : Not Found RUNPATH : Not Found System ASLR : Enabled (randomize_va_space: 2) root@Ubu2004x64:now# ``` # PLT/GOT Ubunt20.04からは, Dynamic Linkな関数呼び出しは, `.plt.sec`セクション/`.got.plt`セクションを経由して呼び出すようになっている. 例えば, 関数`hogehoge`をPartial RELROなPLT経由で呼び出す際のコールは, 以下のようになる. ``` call hogehoge@plt    ┗ plt.secセクションへジャンプ endbr64 bnd jmp hogehoge@.plt ┗ .pltセクションへジャンプ endbr64 push reloc_arg(関数毎に異なる整数値) bnd jmp .pltセクションの先頭 ┗ .pltセクション(の先頭) push binaryのlink_map bnd jmp _dl_runtime_resolve_xsavec ┗ _dl_fixup(link_map, reloc_arg) <-解決したシンボルのアドレスを返す. ┗ _dl_lookup_symbol_x <- 実際にシンボル解決を行っている関数. ``` `.plt`セクションの先頭のコードを見てみよう. 逆アセすると次のようなコードを見て取れる. ``` root@Ubu2004x64:now# objdump -M intel --no-show-raw-insn -d ./vuln -j.plt ./vuln: ファイル形式 elf64-x86-64 セクション .plt の逆アセンブル: 0000000000401020 <.plt>: 401020: push QWORD PTR [rip+0x2fe2] # 404008 <_GLOBAL_OFFSET_TABLE_+0x8>★ 401026: bnd jmp QWORD PTR [rip+0x2fe3] # 404010 <_GLOBAL_OFFSET_TABLE_+0x10>★ ~ 401030: endbr64 401034: push 0x0 401039: bnd jmp 401020 <.plt> ~ 401040: endbr64 401044: push 0x1 401049: bnd jmp 401020 <.plt> ~ root@Ubu2004x64:now# ``` 重要なのは最初の2つの命令だ. 1. `*0x404008`を`push` - `0x404008`には, バイナリ自身の`link_map`のアドレスが入っている. 2. `*0x404010`にジャンプ - `0x404010`には, `_dl_runtime_resolve_xsavec`のアドレスが入っている. ## `_dl_runtime_resolve_xsavec`関数 この関数アセンブリ直書きで定義された`x86_64`用関数(サブルーチン)である. `_dl_runtime_resolve_xsavec`のソースコードはマクロが多用されていて, 読むのが辛いので ディスアセンブルしたものを見ていく. ```asm! gef> disas _dl_runtime_resolve_xsavec Dump of assembler code for function _dl_runtime_resolve_xsavec: 0x00007ffff7fe7bc0 <+0>: endbr64 0x00007ffff7fe7bc4 <+4>: push rbx★(1) 0x00007ffff7fe7bc5 <+5>: mov rbx,rsp★(2) 0x00007ffff7fe7bc8 <+8>: and rsp,0xffffffffffffffc0 0x00007ffff7fe7bcc <+12>: sub rsp,QWORD PTR [rip+0x14b35] # 0x7ffff7ffc708 <_rtld_global_ro+232> 0x00007ffff7fe7bd3 <+19>: mov QWORD PTR [rsp],rax 0x00007ffff7fe7bd7 <+23>: mov QWORD PTR [rsp+0x8],rcx 0x00007ffff7fe7bdc <+28>: mov QWORD PTR [rsp+0x10],rdx 0x00007ffff7fe7be1 <+33>: mov QWORD PTR [rsp+0x18],rsi 0x00007ffff7fe7be6 <+38>: mov QWORD PTR [rsp+0x20],rdi 0x00007ffff7fe7beb <+43>: mov QWORD PTR [rsp+0x28],r8 0x00007ffff7fe7bf0 <+48>: mov QWORD PTR [rsp+0x30],r9 0x00007ffff7fe7bf5 <+53>: mov eax,0xee 0x00007ffff7fe7bfa <+58>: xor edx,edx 0x00007ffff7fe7bfc <+60>: mov QWORD PTR [rsp+0x250],rdx 0x00007ffff7fe7c04 <+68>: mov QWORD PTR [rsp+0x258],rdx 0x00007ffff7fe7c0c <+76>: mov QWORD PTR [rsp+0x260],rdx 0x00007ffff7fe7c14 <+84>: mov QWORD PTR [rsp+0x268],rdx 0x00007ffff7fe7c1c <+92>: mov QWORD PTR [rsp+0x270],rdx 0x00007ffff7fe7c24 <+100>: mov QWORD PTR [rsp+0x278],rdx 0x00007ffff7fe7c2c <+108>: xsavec [rsp+0x40] 0x00007ffff7fe7c31 <+113>: mov rsi,QWORD PTR [rbx+0x10]★() 0x00007ffff7fe7c35 <+117>: mov rdi,QWORD PTR [rbx+0x8]★() 0x00007ffff7fe7c39 <+121>: call 0x7ffff7fe00c0 <_dl_fixup>★ 0x00007ffff7fe7c3e <+126>: mov r11,rax 0x00007ffff7fe7c41 <+129>: mov eax,0xee 0x00007ffff7fe7c46 <+134>: xor edx,edx 0x00007ffff7fe7c48 <+136>: xrstor [rsp+0x40] 0x00007ffff7fe7c4d <+141>: mov r9,QWORD PTR [rsp+0x30] 0x00007ffff7fe7c52 <+146>: mov r8,QWORD PTR [rsp+0x28] 0x00007ffff7fe7c57 <+151>: mov rdi,QWORD PTR [rsp+0x20] 0x00007ffff7fe7c5c <+156>: mov rsi,QWORD PTR [rsp+0x18] 0x00007ffff7fe7c61 <+161>: mov rdx,QWORD PTR [rsp+0x10] 0x00007ffff7fe7c66 <+166>: mov rcx,QWORD PTR [rsp+0x8] 0x00007ffff7fe7c6b <+171>: mov rax,QWORD PTR [rsp] 0x00007ffff7fe7c6f <+175>: mov rsp,rbx 0x00007ffff7fe7c72 <+178>: mov rbx,QWORD PTR [rsp] 0x00007ffff7fe7c76 <+182>: add rsp,0x18 0x00007ffff7fe7c7a <+186>: bnd jmp r11 End of assembler dump. gef> ``` `_dl_runtime_resolve_xsavec`関数をざっくり説明すると以下のような処理が行われる. 1. rbxを使ってスタックを退避 2. `_rtld_global_ro.xsave_state_size`分スタックを確保 - `_rtld_global_ro`は`ld-linux.so.2`のグローバル変数. 4. 2で確保したスタックに各種レジスタの現在の値を退避 5. `.plt`セクション実行時, スタックに積んだ`link_map`と`reloc_arg`をそれぞれ`rdi`,`rsi`に入れて`_dl_fixup`関数を呼び出し. - 戻り値としてシンボルのアドレスが, `rax`に格納される. - `_dl_fixup`関数内部で, `.got.plt`にシンボルのアドレスが書き込まれる. 6. スタックを戻して, 解決したアドレスにジャンプ. - `call`した関数の初回実行 ## `_dl_fixup` `_dl_fixup`関数では以下のような処理が行われる. コード的には次のような定義. > マクロを展開している. ```clike! /* 解決したシンボルのアドレスを返す. * さらにこの関数の中で.got.pltにアドレスを書き込んでいる. */ void *_dl_fixup( struct link_map *l, /* 対象のlinkmapへのポインタ(先頭は_rtld_global._ns_dl[0]._ns_loaded) */ unsigned int reloc_arg /* Elf64_Word : unsigned int */ ) { const Elf64_Sym *const symtab = (const void *)D_PTR(l, l_info[DT_SYMTAB]); // (1)これを制御可能なRWなアドレスに向ける const char *strtab = (const void *)D_PTR(l, l_info[DT_STRTAB]); // ★(1) const uintptr_t pltgot = (uintptr_t) D_PTR(l, l_info[DT_PLTGOT]); ``` 構造体, マクロが多用されていて非常に難解だが一つづつ見ていく. 構造体で利用するのは次の4つだ. 1. `link_map`構造体  1. `Elf64_Sym`構造体 2. `Elf64_Dyn`構造体 3. `PLTREL`構造体 またマクロは, `D_PTR`マクロを読み解く必要がある. ```clike #define D_PTR(map, i) ((map)->i->d_un.d_ptr + (dl_relocate_ld (map) ? 0 : (map)->l_addr)) ``` ソースコード上, `PLTREL`型は`#ifdef`で分岐して定義が異なり, + `Elf64_Rel` + `Elf64_Rela` のどちらかの型を取る. ただし, `x64`の`Ubuntu`では, `Elf64_Rela`構造体を使用する. `Elf64_Rela`構造体の定義は以下の通り. ```clike typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; Elf64_Sxword r_addend; } Elf64_Rela; ``` // r_sym : r_info >> 32, r_type : r_info&0xffff_ffff 読み進めると, `_dl_lookup_symbol_x`関数を以下のように呼んでいるのが確認できる. ``` // sysdeps/generic/ldsodefs.h result = _dl_lookup_symbol_x(strtab + sym->st_name,//シンボル文字列のアドレス l, // _dl_fixupの第一引数 &sym, // .dynamicセクションのElf64_Sym[]   l->l_scope, // 解決対象シンボルの探索範囲フラグ   version,   ELF_RTYPE_CLASS_PLT,   flags,   NULL); ``` 実際に解決処理を行うのは, `_dl_lookup_symbol_x`関数なので この関数を呼び出す時の引数を偽装するというのが本質である. 偽装する対象として + `link_map`の指し先(`l`) + シンボルの文字列解決(`strtab + sym->st_name`) の2つが効果的なターゲットだろう. 今回の問題のように, libcやldのアドレスがわからない状態では`reloc_arg`を偽装することが多いだろう. ## `reloc_arg`の偽装 `readelf`コマンドなどでELFのセクションヘッダを見ると次のようになっている. > 重要なものだけ抜粋した. ```shell= root@Ubu2004x64:now# readelf -SW ./vuln ~ snipped ~ [番] 名前 型 アドレス Off サイズ ES Flg Lk Inf Al [ 6] .dynsym DYNSYM 00000000004003c8 0003c8 0000a8 18 A 7 1 8 [ 7] .dynstr STRTAB 0000000000400470 000470 000052 00 A 0 0 1 [10] .rela.dyn RELA 00000000004004f0 0004f0 000060 18 A 6 0 8 [11] .rela.plt RELA 0000000000400550 000550 000030 18 AI 6 24 8 [12] .init PROGBITS 0000000000401000 001000 00001b 00 AX 0 0 4 [13] .plt PROGBITS 0000000000401020 001020 000030 10 AX 0 0 16 [14] .plt.sec PROGBITS 0000000000401050 001050 000020 10 AX 0 0 16 [15] .text PROGBITS 0000000000401070 001070 0001e5 00 AX 0 0 16 [16] .fini PROGBITS 0000000000401258 001258 00000d 00 AX 0 0 4 [22] .dynamic DYNAMIC 0000000000403e20 002e20 0001d0 10 WA 7 0 8 [23] .got PROGBITS 0000000000403ff0 002ff0 000010 08 WA 0 0 8 [24] .got.plt PROGBITS 0000000000404000 003000 000028 08 WA 0 0 8 [25] .data PROGBITS 0000000000404028 003028 000010 00 WA 0 0 8 [26] .bss NOBITS 0000000000404040 003038 000020 00 WA 0 0 16 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), l (large), p (processor specific) root@Ubu2004x64:now# ``` まずELFはライブラリのような外部の関数を使いたい., ダイナミックリンクの場合, 原理上関数名は実行時に解決されるため, 関数名(文字列データ)をメモリにマップされるセクションのどこかに保持しているはずだ. 一般的なELF構成では, `.dynstr`セクションがそれだ. このセクションにはバイナリが実行時に必要になるシンボル名を文字列テーブルとして確保している. > 各文字列は, `\0`(NULL)区切りになっている. そして, この文字列テーブルのデータは2つの構造体から参照される. 1. `Elf64_Sym`構造体. 2. `Elf64_Dyn`構造体 の2つだ. `Elf64_Sym`は64bit ELFの中でシンボル情報を扱うための構造体だ. 定義は以下の通り. ```clike root@Ubu2004x64:now# pahole -C 'Elf64_Sym' typedef struct elf64_sym Elf64_Sym; root@Ubu2004x64:now# pahole -C 'elf64_sym' struct elf64_sym { Elf64_Word st_name; /* 0 4 */ unsigned char st_info; /* 4 1 */ unsigned char st_other; /* 5 1 */ Elf64_Half st_shndx; /* 6 2 */ Elf64_Addr st_value; /* 8 8 */ Elf64_Xword st_size; /* 16 8 */ /* size: 24, cachelines: 1, members: 6 */ /* last cacheline: 24 bytes */ }; root@Ubu2004x64:now# ``` ELFの`.dynsym`セクションには, この構造体の配列が格納されている. そしてこれも`.dynstr`セクションと同じく, 実行時メモリにロードされ, 配列のエントリは`plt`の関数のシンボル情報を保持している. `Elf64_Sym.st_name`は, `.dynstr`の文字列テーブルのオフセットを示している. つまりその`Elf64_Sym`構造体で管理したい関数の文字列は, `.dynstr`セクションから`Elf64_Sym.st_name`バイト進んだところに存在する. link_map構造体と, `reloc_arg`の関係を図にすると以下のように描ける. ![image](https://hackmd.io/_uploads/SJYLSJAna.png) # Exploit {%gist 1u991yu24k1/edd8a6bd0b53995500a560173083552f %} + 補足1: + このバイナリでは, `pop rdx; ret`(もしくはそれに等価なROPガジェット)が存在しない.しかし`main`関数で`read`の`size`を`0x200`でセットしているため,`rdx`を壊さないようにROPすれば任意書き込みのサイズとしては十分である. + 実際の問題では`read`サイズが十分か実際に調べて, 長さが足りないときは代替の方法を探す必要がある. + 補足2: + `system`関数の引数の文字列を`stager`で直接指定している. + そのため,`setvbuf@.got.plt`偽装後, 最初に`setvbuf@plt`に飛ばしたときには, `system(NULL)`として呼び出されるため, 子プロセスは即`exit`する. + 偽装した`setvbuf`を呼び出したあと, 一度戻って引数をセットし直してから再度` setvbuf@plt`を呼び出す. + ピボットされたスタックに`"/bin/sh"`を配置しておき, `pop rdi`する. # 参考文献 + ももテク記事 - [ROP stager + Return-to-dl-resolveによるASLR+DEP回避](https://inaz2.hatenablog.com/entry/2014/07/15/023406) - [ROP stager + Return-to-dl-resolve + DT_DEBUG readによるASLR+DEP+RELRO回避](https://inaz2.hatenablog.com/entry/2014/07/20/161106) - [x64でROP stager + Return-to-dl-resolve + DT_DEBUG readによるASLR+DEP+RELRO回避をやってみる](https://inaz2.hatenablog.com/entry/2014/07/29/020112) - [x64でROP stager + Return-to-dl-resolve + DT_DEBUG read + __libc_csu_init gadgetsによるASLR+DEP+RELRO回避をやってみる](https://inaz2.hatenablog.com/entry/2014/10/12/191047)