#1. はじめに
2021/4/30 10:00 JST ~ 2021/5/2 20:00 JST に開催された「WaniCTF'21-spring」に「N30Z30N総帥」として参加しました。33問中25問を解き、5098pt 25位でした。
以下、Crypto、Forensics、Reversing の Writeup です。
※2021/07/22 はてなブログより移転。
#2. Writeup (Crypto)
##2-1. Simple conversion (Beginner)
戻し方を忘れました…
ファイルは「convert.py」「output.txt」の 2 つが提示されます。
ファイル:「convert.py」
from const import flag
def bytes_to_integer(x: bytes) -> int:
x = int.from_bytes(x, byteorder="big")
return x
print(bytes_to_integer(flag))
ファイル:「output.txt」
709088550902439876921359662969011490817828244100611994507393920171782905026859712405088781429996152122943882490614543229
整数値を文字列に直す問題です。やり方は色々ありますが、普段使いの「long_to_bytes」を使いました。
ソルバ「solve.py」
from Crypto.Util.number import *
with open("output.txt","r") as f:
x = int(f.read())
print(long_to_bytes(x).decode())
FLAG{7h1s_i5_h0w_we_c0nvert_m3ss@ges_1nt0_num63rs}
※long_to_bytes を使いたくなければ、
import math
print(x.to_bytes(math.ceil(x.bit_length()/8), "big").decode())
とかでもよいかと思います。
##2-2. Easy (Easy)
手始めに
ファイルは「encrypt.py」「output.txt」の 2 つが提示されます。
ファイル:「encrypt.py」
with open("flag.txt") as f:
flag = f.read().strip()
A = REDACTED
B = REDACTED
plaintext_space = "ABCDEFGHIJKLMNOPQRSTUVWXYZ_{}"
assert all(x in plaintext_space for x in flag)
def encrypt(plaintext: str, a: int, b: int) -> str:
ciphertext = ""
for x in plaintext:
if "A" <= x <= "Z":
x = ord(x) - ord("A")
x = (a * x + b) % 26
x = chr(x + ord("A"))
ciphertext += x
return ciphertext
if __name__ == "__main__":
ciphertext = encrypt(flag, a=A, b=B)
print(ciphertext)
ファイル:「output.txt」
HLIM{OCLSAQCZASPYFZASRILLCVMC}
A、B の値を鍵とする換字式暗号ですが、最大でも 26×26 通りしかないので真面目に分析するのはやめて総当たりして、「FLAG{」で始まるものを捜しました。
ソルバ「solve.py」
ct = "HLIM{OCLSAQCZASPYFZASRILLCVMC}"
plaintext_space = "ABCDEFGHIJKLMNOPQRSTUVWXYZ_{}"
def decrypt(ciphertext: str, a: int, b: int) -> str:
plaintext = ""
for x in ciphertext:
if "A" <= x <= "Z":
x = ord(x) - ord("A")
x = (a * x + b) % 26
x = chr(x + ord("A"))
plaintext += x
return plaintext
if __name__ == "__main__":
for a in range(26):
for b in range(26):
plaintext = decrypt(ct, a, b)
if plaintext[0:5] == "FLAG{":
print(plaintext)
exit()
FLAG{WELCOMETOCRYPTOCHALLENGE}
##2-3. Extra (Normal)
いつものRSA?
ファイルは「encrypt.py」「output.txt」の 2 つが提示されます。
ファイル:「encrypt.py」
from Crypto.Util.number import getPrime, bytes_to_long
with open("flag.txt", "rb") as f:
flag = f.read()
p, q = getPrime(1024), getPrime(1024)
N = p * q
M = 2 * p + q
e = 0x10001
def encrypt(plaintext: bytes) -> int:
plaintext = bytes_to_long(plaintext)
c = pow(plaintext, e, N)
return c
if __name__ == "__main__":
c = encrypt(flag)
print(f"{N = }")
print(f"{M = }")
print(f"{e = }")
print(f"{c = }")
ファイル:「output.txt」
N = 22255382023772668851018179427844169178508638456713544208965498667359965716247243217931028270320680101854437928939452335472153643094266035953797432826168426002458800906764442624308120284177094975740468163835305872963635678413995878812492729432260346481442092245748885202467992527408086207041964831622724073720751839241897580988210971776031098476500998975223039782371635291859483569580516707907602619018780393060215756966917504096971372578145138070121288608502379649804953835336933545368863853793291348412017384228807171466141787383764812064465152885522264261710104646819565161405416285530129398700414912821358924882993
M = 455054308184393892678058040417894434538147052966484655368629806848690951585316383741818991249942897131402174931069148907410409095241197004639436085265522674198117934494409967755516107042868190564732371162423204135770802585390754508661199283919569348449653439331457503898545517122035939648918370853985174413495
e = 65537
c = 17228720052381175899005296327529228647857019551986416863927209013417483505116054978735086007753554984554590706212543316457002993598203960172630351581308428981923248377333772786232057445880572046104706039330059467410587857287022959518047526287362946817619717880614820138792149370198936936857422116461146587380005750298216662907558653796277806259062461884502203484610534512552197338982682870358910558302016481352035443274153409114492025483995668048818103066011831955626539382173160900595378864729936791103356604330731386911513668727994911216530875480647283550078311836214338646991447576725034118526046292574067040720093
高校数学などで扱う「解と係数の関係」を想起できれば p、q が容易にリークでき、復号可能です。
具体的に、2p、q は X^2 - (2p+q)X + 2pq = 0 の解で、M = 2p+q、2N = 2pq ですから二次方程式の解の公式にあてはめて計算すれば OK です。
ソルバ「solve.py」
from Crypto.Util.number import *
import gmpy2
N = 22255382023772668851018179427844169178508638456713544208965498667359965716247243217931028270320680101854437928939452335472153643094266035953797432826168426002458800906764442624308120284177094975740468163835305872963635678413995878812492729432260346481442092245748885202467992527408086207041964831622724073720751839241897580988210971776031098476500998975223039782371635291859483569580516707907602619018780393060215756966917504096971372578145138070121288608502379649804953835336933545368863853793291348412017384228807171466141787383764812064465152885522264261710104646819565161405416285530129398700414912821358924882993
M = 455054308184393892678058040417894434538147052966484655368629806848690951585316383741818991249942897131402174931069148907410409095241197004639436085265522674198117934494409967755516107042868190564732371162423204135770802585390754508661199283919569348449653439331457503898545517122035939648918370853985174413495
e = 65537
c = 17228720052381175899005296327529228647857019551986416863927209013417483505116054978735086007753554984554590706212543316457002993598203960172630351581308428981923248377333772786232057445880572046104706039330059467410587857287022959518047526287362946817619717880614820138792149370198936936857422116461146587380005750298216662907558653796277806259062461884502203484610534512552197338982682870358910558302016481352035443274153409114492025483995668048818103066011831955626539382173160900595378864729936791103356604330731386911513668727994911216530875480647283550078311836214338646991447576725034118526046292574067040720093
#pとqは入れ替わっても一般性を失わないので気にしない
D = M*M - 8*N
SQRT_D = gmpy2.iroot(D,2)[0]
p = (M + SQRT_D) // 2
q = (M - SQRT_D) // 2
if p % 2 == 0:
p = p // 2
if q % 2 == 0:
q = q // 2
assert p*q == N
phi = (p-1)*(q-1)
d = inverse(e,phi)
pt = pow(c,d,N)
print(long_to_bytes(pt).decode())
** FLAG{@n_ex7ra_param3ter_ru1n5_3very7h1n9}**
##2-4. Can't restore the flag? (Hard)
ちりつもですよ
ファイル「server.py」
from Crypto.Util.number import bytes_to_long
with open("flag.txt", "rb") as f:
flag = f.read()
flag = bytes_to_long(flag)
assert flag <= 10 ** 103
upper_bound = 300
while True:
try:
mod = int(input("Mod > "))
if mod > upper_bound:
print("Don't cheat 🤪")
continue
result = flag % mod
print(result)
except Exception:
print("Bye 👋")
break
サジェストに従って、いくつかの m (最大300) でフラグを mod.m した値を入手し、「中国式剰余定理」を使って復元しました。
modulus は互いに素に取るのが効率的なのでとりあえず小さい順に素数を列挙しましたがそれで十分足りたのでヨシ!
ソルバ「solve.py」
from pwn import *
from sympy import sieve
from sympy.ntheory.modular import crt
from Crypto.Util.number import long_to_bytes
r = remote("crt.cry.wanictf.org","50000")
i = 1
xs = []
mods = []
mod = 1
while(True):
p = sieve[i]
r.recvuntil("Mod > ")
r.sendline(str(p).encode())
x = int(r.recvline().decode())
xs.append(x)
mods.append(p)
mod = mod * p
i = i + 1
if mod >= 10 ** 103:
break
print(mods)
print(xs)
print(crt(mods,xs))
print(long_to_bytes(crt(mods,xs)[0]).decode())
FLAG{Ch1n3s3_r3m@1nd3r_7h30r3m__so_u5eful}
##2-5. OUCS (Very Hard)
OUによるHomomorphicなCryptoSystemです
ファイル:「server.py」
import random
from Crypto.Util.number import bytes_to_long, getPrime, long_to_bytes
from const import description, flag, logo
class OkamotoUchiyamaCryptoSystem:
def __init__(self, bits: int):
p, q = getPrime(bits), getPrime(bits)
n = p * p * q
while g := random.randrange(2, n):
if pow(g, p - 1, p * p) != 1:
break
h = pow(g, n, n)
self.p = p
self.n = n
self.g = g
self.h = h
def encrypt(self, plaintext: bytes) -> bytes:
plaintext = bytes_to_long(plaintext)
n, g, h = self.n, self.g, self.h
r = random.randrange(2, n)
ciphertext = pow(g, plaintext, n) * pow(h, r, n) % n
ciphertext = long_to_bytes(ciphertext)
return ciphertext
def decrypt(self, ciphertext: bytes) -> bytes:
ciphertext = bytes_to_long(ciphertext)
p, g = self.p, self.g
a = (pow(ciphertext, p - 1, p ** 2) - 1) // p
b = (pow(g, p - 1, p * p) - 1) // p
b_ = pow(b, -1, p)
plaintext = a * b_ % p
plaintext = long_to_bytes(plaintext)
return plaintext
def get_publickey(self) -> tuple[int, int, int]:
return self.n, self.g, self.h
if __name__ == "__main__":
print(logo)
cipher = OkamotoUchiyamaCryptoSystem(bits=1024)
while True:
print()
print(description)
while not (choice := input("> ")) in "12345":
print("Invalid choice.")
choice = int(choice)
# 1. Encrypt the flag
if choice == 1:
ciphertext = cipher.encrypt(flag)
ciphertext = bytes_to_long(ciphertext)
print(f"{ciphertext = :#x}")
# 2. Encrypt
elif choice == 2:
print("Enter your plaintext")
plaintext = int(input("> "), 0)
plaintext = long_to_bytes(plaintext)
ciphertext = cipher.encrypt(plaintext)
ciphertext = bytes_to_long(ciphertext)
print(f"{ciphertext = :#x}")
# 3. Decrypt
elif choice == 3:
print("Enter your ciphertext")
ciphertext = int(input("> "), 0)
ciphertext = long_to_bytes(ciphertext)
# ... except for the flag
plaintext = cipher.decrypt(ciphertext)
if flag == plaintext:
print("Decryption succeeded, but we won't tell you the result :P")
continue
plaintext = bytes_to_long(plaintext)
print(f"{plaintext = :#x}")
# 4. Show public key
elif choice == 4:
n, g, h = cipher.get_publickey()
print(f"{n = :#x}")
print(f"{g = :#x}")
print(f"{h = :#x}")
# 5. Exit
else:
print("Bye :)")
break
提示されたプログラムからわかる通り、サーバでは、1. 暗号化したフラグの参照、2. 任意の値の暗号化、3. 任意の値の復号(復号結果がフラグと一致した場合には結果を表示しない)、4. 公開鍵の表示、が可能です。
ここで実装されている暗号は「Okamoto-Uchiyama 暗号」と呼ばれる暗号で、平文、暗号文とも演算を含め Z/nZ(環)に埋め込んで考えることができ、平文での和が暗号文の積になる(f を暗号化写像とすると、平文 x、y に対しf(x+y) = f(x)*f(y))という、準同型性が特徴です。この準同型性を利用すると、フラグをリークできます。
具体的には、平文「1」(0 でなければ何でもよいので、シンプルに 1 とした)を暗号化した値とフラグを暗号化した値の積(mod.n)を復号し、1 を引けばフラグ(整数値)を得られます。これを long_to_bytes で可読化すれば OK です。
手入力+手元計算でやったのでソルバはありませんが、参考までに入出力メモを掲示します。
.oooooo. ooooo ooo .oooooo. .oooooo..o
d8P' `Y8b `888' `8' d8P' `Y8b d8P' `Y8
888 888 888 8 888 Y88bo.
888 888 888 8 888 `"Y8888o.
888 888 888 8 888 `"Y88b
`88b d88' `88. .8' `88b ooo oo .d8P
`Y8bood8P' `YbodP' `Y8bood8P' 8""88888P'
?─────────────────────?
│ 1. Encrypt flag │
│ 2. Encrypt │
│ 3. Decrypt │
│ 4. Show public key │
│ 5. Exit │
?─────────────────────?
> 2
Enter your plaintext
> 1
ciphertext = 0xfcd810e81a536741fac341f90ab54633462b9ea6b1a0ca00aa36a525d1e571badbced74012c8335ea8a30a49c23922809d3b3293b1ce0a5a5eeb367410e1313563ae44b98f9e6fd8cd712409dd8874182c64a967741c1404762d07f3b7909b66236e92b368f99929c23adaa29510336e9c13754e00574f34d01618ac08799f3f2f6aa87066879f294d0c2109105abbff3fe79aec9fa4a150db3df0a506ec8302aeac80f4f04eb6553d3e7b5eb023c77791da47234b9f357d95151f6cdfe4fbd4abb8e366e6449450f7d90accb64538f5c2414a4c7d4a7ee5405a9600bddb73dbd289f62ba9d86f6e0d1efb47d49cc4b840b8cd45b78d9e712c716ff997d8127eb75db16850d3f4332df921730087be1f3e28df69440e8b18dd308301944085b3be4679683207ba69976ea9a77b396977469476e3432c5c494ba9952e6ab2015fb265bf019cb2a1970e28f8d312fb2b1651564ff0d31cd2b6363d23240d3a498a45923edb55d8999a0824dd3d6850a32372ba69e4c944dca6ee3034de9096473
?─────────────────────?
│ 1. Encrypt flag │
│ 2. Encrypt │
│ 3. Decrypt │
│ 4. Show public key │
│ 5. Exit │
?─────────────────────?
> 1
ciphertext = 0x7d2fda13f8251a64947a78bc6d25dff38d02692fb8468ec1e6f684bf41b7aceb57b24e8a0ba918529b081fcd858ec80fa2e71395bd78b333c777fb15e16c5e3ab132163f93cbfe49b517c7897432bbae7317042739683d395e4033d936f2de98ef54375ef3a1db3f103aa7715c8a2e2b9ee74e74b769f038675cd32eed346dd42e64acbd18ef5be97a1d316e3ff8459e351df99a4f0411e24b2c07f064d28eca18945af058330d29c0e57e6ee8c77d88639d05f062bcbd5a24b36822b30e7c1a35b80c673ebd7dec00e892927ec87c7d7f1f326adca8b7fba090ed2e0443ebf786d3229cf46ea4c7d93d500ad0b0d20e60415a31380c1d003a55e023c046b274535fc3553abda094959e234072fb5685006b134df168bb6cc998cece775d9155f6e221cdf2de5f6ac265a6c85bccc81ec32d5699f4fa4c0c37392893b6e4f71d8deff57b3b493c1a5c33bd65bfed3e577a113a1cf0e0b42553a850d89a8900834d8a46e5cec941e79bd32dd1d9df0d84c2e6f2b6b08815bd2d9b774a48a50fd
?─────────────────────?
│ 1. Encrypt flag │
│ 2. Encrypt │
│ 3. Decrypt │
│ 4. Show public key │
│ 5. Exit │
?─────────────────────?
> 4
n = 0x2909b09c7189d7ab4decc38e5b07e7ee1ae20dcf3c6b1a6c50838ac57692fd3136930e0e12c90263ddaeb17409432a77c3443b7ebb741dd868aeb309193afb55551e6d3371486dea0d58807b22da8845d228b9b82e9dcbfd820463bc1218976bc9f8e005673023da2b81c2b4cc59cae3f4756e48deb4368a3bda657a4d3b3e289c55f12f544a000f9735ffe4690db644f50bcfef918acae6a2bec8efa2894bb884de6eb7d5a40f9c22a57c4ed5eeb166b69e64995c15fe8b9c452857f5e7492428cc6ad7e329279866bf83df9dcd30657e3765633b680f4851af784db29db0215d136598fcd77758bc8e6648fbc3e33f934f778bae33b85195ed26e54b32257cb2b3ffcdaff98142155eea7cb4fe68e49eb5f2c873d8fcf7f702dd5c0e418f6489727385de65fa0080a71e0d7dd999326d799701de508ed50deb169c3ad6b708730b593097c98fda99c649343b0fda0a2db01130b136b21e8f5cb8e14cf4c9dd62550e0866c83195033aa3735a54c4ad71ca110b2e986d0fab97ffac92ecaea3
g = 0x1a83aadf1cb2cd808c809899f295c16f03873901a9ca74966bd160b0fca3acac7e3506583a72fb661ffd9707bb9b7aaf9eb848b883cbb97e9142685bbd70b16b011ab5293b2f0253fc12659419960c20ea8e130f6528bc0ede9558c2a4e3d4ae60101a679e289e12841e821879054333873594117aa91732de6a07c8bfa332b5f55c7ee79bb6a46988e5cea762f07f0be9c46bbe32bbe236b0ac34eb19eb872d1182779d67b3ad910b4ba352a9b738923f72476fb041ef6ccc5a9df0ce14ca2ca1738c68d4dfdc45215accef534353173a8f8c158a7bd11a1de64cb132323956e9a9960151b3edde80d5e475e06c21d19f5e44ae5bf53b5327fb058812dcc016ee2a1c5b5fa2e91cec2938ac8ddb32dbd8ec6c80170089cc149f39302e34c1d518b8978429a259b333190b51f2b1a3099db0c9f9b6a4ebe91494aa590d7770efb8a00402a1d6f5d9391c8fa321723dfcf9b70e65c3070363b555d45098b25a130f032983de71e6b2b1eb45a83b4472a8ea952960ba42005aca7d4489189b0ca
h = 0x205d5cf49c6e184fa98a71030864b08a2182400c5f423cdde8337081809e95cdd3ff40db7dee7501de311d52a7147d8f940ad3dcc84570b9e2e7fe83e66e4fc6025ce73fb0e467ab863f2cdcb8db8a72bc3e68459d66cd90845b034305104ecb8b13fc85ebfcc01bf81f6a68706b82d57c704ed22e9815f4b52516bb2e790d2a10ea3f7811bb304ee2a746d593c06b0b05b9cde19f530c85118cdcf388a14311bd3d915bdd580d3eeb8bf131ff385f687778ed19b1f9c363eee5aa82a5a768671c4e19d10e6b25c017be8dbb2f711ae9a780b5b91c84cea64b3bf5789e9c5c3f38bafa7be89c32d93ac70333ef3096e9149cfec06348b4007543e005f201d5e8593c3224dab7200f294f2637bb932de9b994c9170b76c3f3a44543f8108207a8e055b851d5c0487ca0b93f034d18bc76bc4f488bbc52f93e06f07959222d778e5ebc24303c1ced2d7de696dc2cb2b58596bb635e058eaee579694a8a2c7ebc396dbf749bfc5011562408853cb7c8406ab147b091e02e00ea1a6a92801d583629
?─────────────────────?
│ 1. Encrypt flag │
│ 2. Encrypt │
│ 3. Decrypt │
│ 4. Show public key │
│ 5. Exit │
?─────────────────────?
> 3
Enter your ciphertext
> 704570859981873367736030171387222778046461504246410396308256213320044324020626713393851452887648445696112810344403527263793737668080305570075248701579206561128306953907528753710991694556917100435224961970715765189681202067634876851667906580092550268128222279201549562622069415953944695852242670774889317171712797476511449816506544287314097283817634529360542698429484193862330355698473206683905216957521676875460109820225536460528756581346771158033200440794165892838757402347751980238773855741532793748898317137383775888980456189519176442066266712100285202775923052318359624399829687070784882141486964144228344331130619712880404813283890605223738334817991182645972351769631008106710761204537251366081995271099308613914196776046971452665422074405536364632895724381867012918135567757241187084128314603206569454202911924095905685535342461258048888579271599202558893947827759461129710410741445779906129531308222891148399148043101
plaintext = 0x464c41477b4f555f643065735f6e30745f726570726535336e745f4f73616b615f556e69766572736974795f6275745f4f6b616d6f746f2d5563686979616d617e
?─────────────────────?
│ 1. Encrypt flag │
│ 2. Encrypt │
│ 3. Decrypt │
│ 4. Show public key │
│ 5. Exit │
?─────────────────────?
> 5
Bye :)
この後、plaintext - 1 を long_to_bytes すればフラグを得られます。
FLAG{OU_d0es_n0t_repre53nt_Osaka_University_but_Okamoto-Uchiyama}
「OU は Osaka University ではなく Okamoto-Uchiyama」、なるほど。
#3. Writeup (Forensics)
##3-1. presentation (Beginner)
このままじゃFLAGをプレゼンできない...
ファイル:「presentation.ppsx」
このファイルは zip アーカイブなので、解凍して全ファイル対象に「Flag{」で grep かけたらフラグが出てきました。
ちなみに、フラグは \ppt\slides\slide1.xml の中にありました。
FLAG{you_know_how_to_edit_ppsx}
##3-2. secure document (Easy)
本日の資料は以下を入力して圧縮しました。
the password for today is nani
ファイルは「flag_20210428.zip」(パスワード付きzip)と「password-generator」の 2 つが提示されます。
ファイル:「password-generator」
::the::
Send, +wani
return
::password::
Send, +c+t+f
return
::for::
Send, {home}{right 3}{del}1{end}{left 2}{del}7
return
::today::
FormatTime , x,, yyyyMMdd
SendInput, {home}{right 4}_%x%
return
::is::
Send, _{end}{!}{!}{left}
return
:*?:ni::
Send, ^a^c{Esc}{home}password{:} {end}
return
「Send」「SendInput」「FormatTime」などでググると、後者は AutoHotKey っぽいことが判明。AutoHotkey Wiki などを参考に解読すると、パスワードは「Wan1_20210428_C7F!na!」であることが分かったので、前者をこのパスワードで解凍し、出現した JPG ファイルを目視するフラグを得られます。
FLAG{Nani!very_strong_password!}
##3-3. slow (Normal)
宇宙からメッセージが届きました。(※音量注意!)
音声ファイル「slow.wav」が提示されます。
「ここは不思議な第三惑星だから、どうせヘビメタ聞き取れないのさ」などとワケワカランことを思いながら Audacity で再生するも、途中違和感のある箇所があるがよくわからん。再生速度を変えてみてもダメ、スペクトログラムからも何もワカラン。
途方に暮れていたが、気を取り直して色々な CTF の Writeup を漁ってみると、ピーヒャラ系の音声を映像に変換する「SSTV」なるものがあることが判明。SSTVツール「RX-SSTV」をインストールし、これに先ほどの音声ファイル(怪しい部分だけ抜粋)を食わせた(実際には、RX-SSTVを稼働中に前述の音声ファイルを再生した)ところ、フラグを含む画像が生成されました。
FLAG{encoded_by_slow-scan_television}
##3-4. illegal image (Hard)
裏で画像がやり取りされているらしい
パケットデータ「illegal_image.pcap」が提示されます。
とりあえず WireShark で開いて TCP ストリームとかいろいろ見てもよくわからないので、pcap ファイルをバイナリエディタで覗くと、JPEG ファイルのヘッダ(FFD8)やら文字列「JFIF」やらを発見!
ということで、このファイルを送受信しているところ(ICMPプロトコル、192.168.0.133=>192.168.0.158)だけ手で切り出して、各パケットのデータ部だけを切り出して結合、JPEG 画像ファイルを再構築して目視しました。
※個人的には、この手作業部分をプログラムで実現できるようになりたいです。
FLAG{ICMP_Exfiltrate_image}
##3-5. MixedUSB (Very Hard)
USBにパーティションを設定したら、どこにFLAGを保存しているのかわからなくなってしまいました...
USB メモリのイメージファイル「MixedUSB.img」が提示されます。
FTK Imager で開くも、Dummy のフラグしかなさげ。
しかし、バイナリディタで開いて「FLAG{」で検索かけたらすぐ出てきてしまいました。(実は、断片でも出てくればいいなーくらいな気分でやってみました。)
FLAG{mixed_file_allocation_table}
#4. Writeup (Reversing)
##4-1. secret (Beginner)
この問題では Linux の ELF 実行ファイル(バイナリ)である「secret」が配布されています。
このバイナリを実行すると secret key を入力するように表示されます。
試しに「abcde」と入力してみると「Incorrect」と言われました。
$ file secret
secret: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64>.so.2, BuildID[sha1]=1daf4ab43cfa357911806c3ccae34a1b6e027913, for GNU/Linux 3.2.0, not stripped$ sudo chmod +x secret
$ ./secret
...
Input secret key : abcde
Incorrect$ ./secret
...
Input secret key : ??????
Correct! Flag is ??????
このバイナリが正しいと判断する secret key を見つけて読み込んでみましょう!(secret key とフラグは別の文字列です)
(このファイルを実行するためには Linux 環境が必要となりますので WSL や VirtualBox で用意してください)
ヒント :「表層解析」や「静的解析」を行うことで secret key が見つかるかも...?
表層解析ツール strings
静的解析ツール Ghidra
バイナリエディtで中を見ると、キーワード的な文字列が「wani_is_the_coolest_animals_in_the_world!」が見つかります。(表層解析)。
この文字列を secret key として、プログラムを実行するとフラグを得られます(動的解析)。
FLAG{ana1yze_4nd_strin6s_and_execu7e_6in}
##4-2. execute (Easy)
コマンドを間違えて、ソースコードも消しちゃった!
今残っているファイルだけで実行して頂けますか?
(reverse engineeringすれば、実行しなくても中身は分かるみたいです。)
「libprint.so」「main.s」「Makefile」「version.txt」の 4 ファイルが提供されます。
本来はこれらのファイルから実行形式ファイルを作成して実行するのでしょうが、サジェストにあるとおり main.s からフラグと思われる部分を抽出して、可読化するソルバを作り実行しました。
「main.s」(抜粋)
.file "main.c"
(中略)
xorl %eax, %eax
movabsq $7941081424088616006, %rax
movabsq $7311705455698409823, %rdx
movq %rax, -48(%rbp)
movq %rdx, -40(%rbp)
movabsq $3560223458505028963, %rax
movabsq $35295634984951667, %rdx
movq %rax, -32(%rbp)
movq %rdx, -24(%rbp)
leaq -48(%rbp), %rax
(中略)
4:
ソルバ「solve.py」
from Crypto.Util.number import *
x1 = 7941081424088616006
x2 = 7311705455698409823
x3 = 3560223458505028963
x4 = 35295634984951667
pt = long_to_bytes(x4)+long_to_bytes(x3)+long_to_bytes(x2)+long_to_bytes(x1)
print(pt[::-1].decode())
FLAG{c4n_y0u_execu4e_th1s_fi1e}
No, I couldn't ... but I got the flag!
##4-3. timer (Hard)
フラグが出てくるまで待てますか?
super_complex_flag_print_function 関数でフラグを表示しているようですが、難読化されているため静的解析でフラグを特定するのは難しそうです...
GDBを使って動的解析してみるのはいかがでしょうか?
バイナリファイル「licence」とが提供されます。
問題文には「GDB を使ってみたら」というサジェストがありましたが、今回は別法で解きましました。
このファイルをIDA free版で開いてタイマーのセット値の場所を確認すると 14AC (値は3F480がリトルエンディアンで入っている)だったので、
その部分を「1」(1秒)に改ざんしたバイナリを作成、
これを実行してフラグをゲットしました。
FLAG{S0rry_Hav3_you_b3en_wai7ing_lon6?_No_I_ju5t_g0t_her3}
##4-4. license (Very Hard)
このプログラムは非常に強力なライセンス確認処理が実装されています。
ただ、今持っているライセンスファイルは間違っているようです。
正しいライセンスファイルを見つけて頂けますか?
$ ./licence key.dat
Failed to activate.
複雑な処理をシンボリック実行で解析してくれるツール「angr」を使えば簡単に解けるかも。
バイナリファイル「licence」と、ダミーのライセンスファイル「key.dat」が提供されます。
IDA free 版で開き、通過すべきアドレス(find)と回避すべきアドレス(avoid)を確認し、angr を使ったソルバを作って実行しました。
シンボリックファイルを使う方法を知らなかったので、結構手間取りました(いろいろ勉強になりました)。実際は angr がいろいろ勝手にやってくれたので回り道だったらしいのですが…ソルバは冗長な部分がありますがそのまま掲載します。
ソルバ「solve.py」
import angr
import claripy
import logging
p = angr.Project('licence')
base_addr = 0x400000
find_addr = base_addr+0x5e57
avoid_addr = (base_addr+0x5ce4, base_addr+0x5d28, base_addr+0x5d5b, base_addr+0x5d88, base_addr+0x5dbb, base_addr+0x5de1, base_addr+0x5e17, base_addr+0x5e44)
data = claripy.BVS('data', 128*8)
filename = 'key.dat'
simfile = angr.SimFile(filename, content=data)
state = p.factory.full_init_state(
args=['licence', filename],
fs={filename: simfile},
add_options={angr.options.LAZY_SOLVES})
sim = p.factory.simulation_manager(state)
logging.getLogger('angr.sim_manager').setLevel('INFO')
sim.explore(find=find_addr, avoid=avoid_addr)
ans = sim.one_found.fs.get(filename).concretize()
print(repr(ans))
FLAG{4n6r_15_4_5up3r_p0w3rfu1_5ymb0l1c_3x3cu710n_4n4ly515_700l}
#5. おわりに
Web がほぼ出来なかったので次回(秋?)までに修行を積んでおこうと思います。