libc.soはld-linux-x86-64.soの構造体を参照している。関数ではなく構造体なので、メンバの配置が一致している必要があり、異なるオプションでビルドされていると、誤ったメンバを参照してしまう。これをどうにかするのは難しそう。
libc.soとld-linux-x86-64.soの整合性の問題なので、libcと一緒にld-linux-x86-64.soが配布されているならば、動かすことができる。追記を参照。
詳細
CTFのうち、サーバーで動作している攻撃対象プログラムの脆弱性を突いてフラグを盗み出せという問題(PwnやExploitと呼ばれる)では、攻撃対象プログラムとともに、サーバーで使われてるlibc.soが提供されることが多い。
Pwnでは、バッファオーバーフローなどの脆弱性を経由してシェルを動かし、サーバー内のflag.txtなどのファイルを読む。攻撃対象のプログラムのコードだけではシェルを実行することができない場合には、libcのsystem関数などを使ってシェルを実行する。systemなどのアドレスはlibcによって異なるので、サーバーで実際に使われているlibcが必要になる。
単にsystemを呼び出すだけならば、systemのアドレスが分かれば良いのだが、ASLRによって攻撃をする度にアドレスが変わるので、実際には、
- 攻撃対象プログラムが使用しているprintfなどの関数のアドレスを得る
- ASLRでは関数の相対位置はずれないので、printfからsystemのアドレスを計算する
- systemを呼び出す
という流れになる。また、system関数の引数に渡したい/bin/sh
という文字列も、攻撃中に与えることが無理ならば、libcの中にあるものを使うことになる。攻撃するスクリプトをいきなり完成させることは難しいので、与えられたプログラムを手元で動かしてデバッグする。このときは、プログラムは手元のlibcで動作するから、リモートのサーバーを攻撃するときには各関数や文字列のアドレスを調節することになる。この作業が面倒なので、手元のlibcを与えられたlibcに差し替えて、プログラムを動かせると楽。
これをやりたいのだけど、何か方法は無いのだろうか。
— kusanoさん@がんばらない (@kusano_k) 2016年12月12日
"与えられたlibcで手元で動かしたかったのでいろいろ試行錯誤していたが結局だめだった"
SECCON 2016 online のwriteup - 忖度https://t.co/3uxMBBlYKt
この前のSECCON 2016 オンライン予選のjmperで試してみる。x64。この問題はsetjmpのjmp_bufをヒープオーバーフローで書き換える必要があるらしい。単にsystem関数を呼び出すよりも、libcへの依存が大きそうなので、特に与えられたlibcでデバッグしたい(setjmpやmallocの動作はそうそう変わらないと思うけど)。
とりあえず、バイナリエディタでjmper中のlibc.so.6
を./bc.so.6
に書き換え、与えられたlibcをbc.so.6
にリネームして同じディレクトリに置いた。
わざわざこんなことをしなくても、LD_PRELOAD
を使うという手もあるらしい。
@kusano_k 普通にLD_PRELOADじゃだめですかね
— れっくす (@xrekkusu) 2016年12月12日
動かしてみたところ、Segmentation fault。残念。
kusano@ubuntu:~/ctf/seccon2016q$ ./jmper_bc
Segmentation fault (コアダンプ)
gdbで動かして、どこで落ちたかを見てみると、__libc_start_main。
[----------------------------------registers-----------------------------------]
RAX: 0xec8148ff8941d589
RBX: 0x0
RCX: 0x0
RDX: 0x470
RSI: 0x7fffffffe5d8 --> 0x7fffffffe7f7 ("/home/kusano/ctf/seccon2016q/jmper_bc")
RDI: 0x7ffff7ffe5d8 --> 0x7ffff7ffe168 --> 0x0
RBP: 0xf7de9950
RSP: 0x7fffffffe500 --> 0x7fffffffe5d8 --> 0x7fffffffe7f7 ("/home/kusano/ctf/seccon2016q/jmper_bc")
RIP: 0x7ffff7a33fdf (<__libc_start_main+399>: call rax)
R8 : 0x7ffff7dd1e80 --> 0x0
R9 : 0x7ffff7de78e0 (push rbp)
R10: 0xd ('\r')
R11: 0x1
R12: 0x0
R13: 0x7ffff7de70f0 (push r15)
R14: 0x7ffff7ffe168 --> 0x0
R15: 0x0
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x7ffff7a33fd3 <__libc_start_main+387>: add rdx,0x47
0x7ffff7a33fd7 <__libc_start_main+391>: shl rdx,0x4
0x7ffff7a33fdb <__libc_start_main+395>: lea rdi,[r14+rdx*1]
=> 0x7ffff7a33fdf <__libc_start_main+399>: call rax
0x7ffff7a33fe1 <__libc_start_main+401>: add r12d,0x1
0x7ffff7a33fe5 <__libc_start_main+405>: mov r13,QWORD PTR [r13+0x40]
0x7ffff7a33fe9 <__libc_start_main+409>: cmp ebp,r12d
0x7ffff7a33fec <__libc_start_main+412>: jne 0x7ffff7a33fc7 <__libc_start_main+375>
めちゅくちゃなアドレスをcallしようとしている。対応するlibcのソースコードはこの部分。
248 struct audit_ifaces *afct = GLRO(dl_audit);
249 struct link_map *head = GL(dl_ns)[LM_ID_BASE]._ns_loaded;
250 for (unsigned int cnt = 0; cnt < GLRO(dl_naudit); ++cnt)
251 {
252 if (afct->preinit != NULL)
253 afct->preinit (&head->l_audit[cnt].cookie);
254
255 afct = afct->next;
256 }
GLROマクロなどの定義はldsodefs.hにあって、afct
はextern struct rtld_global_ro _rtld_global_ro
のメンバを指すようになっている。
libcのインポートテーブルなどを確認すると、ld-linux-x86-64.soを参照している。#ifdef
マクロが大量にあって、メンバの有無が変わっているので、どれかが異なっていると誤ったメンバを参照してしまう。
kusano@ubuntu:~/ctf/seccon2016q$ ldd bc.so.6
/lib64/ld-linux-x86-64.so.2 (0x0000559a127d8000)
linux-vdso.so.1 => (0x00007fff52dba000)
kusano@ubuntu:~/ctf/seccon2016q$ readelf -s bc.so.6 | grep _rtld_global_ro
6: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND _rtld_global_ro@GLIBC_PRIVATE (24)
kusano@ubuntu:~/ctf/seccon2016q$ readelf -s /lib64/ld-linux-x86-64.so.2 | grep _rtld_global_ro
26: 0000000000225ca0 368 OBJECT GLOBAL DEFAULT 17 _rtld_global_ro@@GLIBC_PRIVATE
gdbでafctの値を確認してみても、ld-2.23.soのメモリになっている。
[-------------------------------------code-------------------------------------]
0x7ffff7a33fa8 <__libc_start_main+344>: call QWORD PTR [rdx+0xd0]
0x7ffff7a33fae <__libc_start_main+350>: jmp 0x7ffff7a33ef3 <__libc_start_main+163>
0x7ffff7a33fb3 <__libc_start_main+355>: mov r13,QWORD PTR [rax+0x120]
=> 0x7ffff7a33fba <__libc_start_main+362>: mov rax,QWORD PTR [rip+0x39be27] # 0x7ffff7dcfde8
0x7ffff7a33fc1 <__libc_start_main+369>: xor r12d,r12d
0x7ffff7a33fc4 <__libc_start_main+372>: mov r14,QWORD PTR [rax]
0x7ffff7a33fc7 <__libc_start_main+375>: mov rax,QWORD PTR [r13+0x18]
0x7ffff7a33fcb <__libc_start_main+379>: test rax,rax
gdb-peda$ i proc map
process 30812
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x400000 0x401000 0x1000 0x0 /home/kusano/ctf/seccon2016q/jmper_bc
0x601000 0x602000 0x1000 0x1000 /home/kusano/ctf/seccon2016q/jmper_bc
0x602000 0x603000 0x1000 0x2000 /home/kusano/ctf/seccon2016q/jmper_bc
0x7ffff7a12000 0x7ffff7bcc000 0x1ba000 0x0 /home/kusano/ctf/seccon2016q/bc.so.6
0x7ffff7bcc000 0x7ffff7dcc000 0x200000 0x1ba000 /home/kusano/ctf/seccon2016q/bc.so.6
0x7ffff7dcc000 0x7ffff7dd0000 0x4000 0x1ba000 /home/kusano/ctf/seccon2016q/bc.so.6
0x7ffff7dd0000 0x7ffff7dd2000 0x2000 0x1be000 /home/kusano/ctf/seccon2016q/bc.so.6
0x7ffff7dd2000 0x7ffff7dd7000 0x5000 0x0
0x7ffff7dd7000 0x7ffff7dfd000 0x26000 0x0 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ff3000 0x7ffff7ff8000 0x5000 0x0
0x7ffff7ff8000 0x7ffff7ffa000 0x2000 0x0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 0x2000 0x0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 0x1000 0x25000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffd000 0x7ffff7ffe000 0x1000 0x26000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x0
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 [stack]
0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]
ビルドオプションごとのld-linux-x86-64.soを用意しておくという手もあるかもしれないが、そんなことをするよりは、作問者が使っているライブラリみたいなものを作っておくほうが良さそうである。
Win32 APIのcbSizeのように、構造体の中身が異なっても動く仕組みがあれば良いのに……。
追記
ld-linux-x86-64.soを特に使うわけでもないのに、libc.soと一緒に配布している問題をちらほら見る。例えばこの問題。
同じ環境のld-linux-x86-64.soとlibc.soがあれば、動かすことができる。そもそもld-linux-x86-64.soが何かというとプログラムをロードするためのプログラムである。配布されたld-linux-x86-64.soで問題プログラムをロードすれば良い。
$ LD_PRELOAD=./libc-2.31.so ./ld-2.31.so ./pwn08
Coins : 1500
Gacha stack : 0
1. do_gacha!
2. view_history
3. clear_history
4. ceiling
9. exit
ld-linux-x86-64.soはELFファイルに絶対パスが埋め込まれている。上のように起動するとデバッガなどで扱いづらいなら、書き換えれば良い。それを行ってくれるpatchelf
というコマンドがある。また、patchelf
では共有ライブラリを探すパスも書き換えることができる。
$ cp pwn08 pwn08_2
$ cp libc-2.31.so libc.so.6
$ patchelf --set-rpath . --set-interpreter ./ld-2.31.so ./pwn08_2
$ ldd pwn08_2
linux-vdso.so.1 (0x00007ffd31f64000)
libc.so.6 => ./libc.so.6 (0x00007ff73398a000)
./ld-2.31.so => /lib64/ld-linux-x86-64.so.2 (0x00007ff73395d000)
$ ./pwn08_2
Coins : 1500
Gacha stack : 0
1. do_gacha!
2. view_history
3. clear_history
4. ceiling
9. exit
カレントディレクトリが変わっても動くようにしたいならば、相対パスではなく絶対パスで指定しましょう。
この記事を最初に書いたときには使っていなかったpwntoolsを使うようになった。各種シンボルのアドレスはpwntoolsの関数で取れるから、ローカルとリモートでpwntoolsに読み込ませるlibcを変えるだけ。この記事のようなシチュエーションだとそもそもあまり困っていない。
今でも差し替えたいのはヒープ問題を解くときである。malloc
周りの挙動はlibcのバージョン間で大きく変わる。ヒープ問ではPwngdbが便利で、ヒープの状況が見られる。
で、libcを差し替えると、差し替えたlibcのデバッグ情報が無くてmain_arena
のアドレスが取れないため、この機能が動かない。
gdb-peda$ parseheap
Can not get libc version
Cannot get main_arena's symbol address. Make sure you install libc debug file (libc6-dbg & libc6-dbg:i386 for debian package).
can't find heap info
libcのバージョンごとにVMを用意しておくのが一番かもしれない。