waniCTFと呼ばれる、大阪大学 CTF サークル Wani Hackase が開催する初心者向けの CTF大会に参加しました。
全体60位/433人でした。
結果は↓な感じで、ほとんどCTFしたことない自分からしたら割と頑張れたのでは、と思います。
ほとんどの分野でEasyぐらいまでしか解けなかったのですが、唯一cryptoは全完出来たので嬉しかったです!(競プロで殴れただけ)
以下、writeupです
Crypto
[Begginer] Simple Conversion
convert.pyでフラグを整数に変換した後のoutput.txtが与えられるので、それを逆変換してくださいという問題でした。
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))
709088550902439876921359662969011490817828244100611994507393920171782905026859712405088781429996152122943882490614543229
int.to_bytes()を使って元に戻せばよいです。引数のlengthは適当にガチャして64を入れたらフラグが出てきました。
with open('output.txt') as f: n = int(f.read()) print(n.to_bytes(64, byteorder="big"))
フラグは、FLAG{7h1s_i5_h0w_we_c0nvert_m3ss@ges_1nt0_num63rs}
[Easy] Easy
前問と同じ、encrypt.pyで変換後のoutput.txtが与えられるから逆変換しろという問題。
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)
読むと、文字'A'~'Z'を0~25に直して、その数字を$x$として$Ax+B (mod \space 26)$に変換していることが分かります。 しかし変換するのに使っている1次式の係数は隠されてしまっています。
そこで、output.txtを見ます。
HLIM{OCLSAQCZASPYFZASRILLCVMC}
最初の4文字はFLAG
となっているはずなので、ここから係数$A, B$がある程度分かるはずです。
実際は2つ分かればよくて、連立合同式を立てて計算してみると、$A=5, B=8$になることが分かります。
よってそれを元に適当に変換するコードを書けばよいです。
with open("output.txt") as f: S = f.read().strip() A = 5 B = 8 def decrypt(s: str, a: int, b: int) -> str: res = "" for x in s: if "A" <= x <= "Z": x = ord(x) - ord("A") x = (x - b + 26) % 26 for i in range(26): if a * i % 26 == x: x = i break x = chr(x + ord("A")) res += x return res if __name__ == '__main__': res = decrypt(S, A, B) print(res)
フラグは、FLAG{WELCOMETOCRYPTOCHALLENGE}
[Hard] Can't restore the flag?
与えられたコマンドnc crt.cry.wanictf.org 50000
でサーバーにアクセスすると、Modを入力するように要求されます。
siro53@MyComputer:~/CTF/waniCTF2021/crypto$ nc crt.cry.wanictf.org 50000 Mod >
サーバーのコードは次のようになっています。(適宜コメントを入れています)
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 > ")) # modを入力する if mod > upper_bound: # mod は 300以下じゃないとだめ print("Don't cheat 🤪") continue result = flag % mod print(result) # flagをbytesから整数に直したものを、入力したmodであまりを計算し出力 except Exception: print("Bye 👋") break
要するにフラグの300以下のmodを取った値なら教えてあげるから、そこからフラグの値を復元してねということです。
こんなのCRT(中国剰余定理)じゃん...ということで、手打ちでmodを埋め込んでCRTをすればフラグが分かります。完全に競プロ
コードは長いので省略します。実装はここを参考にしました
フラグはFLAG{Ch1n3s3_r3m@1nd3r_7h30r3m__so_u5eful}
[Normal] Extra
encrypt.pyとoutput.txtが与えられます。そこからフラグを復元しろという問題です。
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 = }")
N = 22255382023772668851018179427844169178508638456713544208965498667359965716247243217931028270320680101854437928939452335472153643094266035953797432826168426002458800906764442624308120284177094975740468163835305872963635678413995878812492729432260346481442092245748885202467992527408086207041964831622724073720751839241897580988210971776031098476500998975223039782371635291859483569580516707907602619018780393060215756966917504096971372578145138070121288608502379649804953835336933545368863853793291348412017384228807171466141787383764812064465152885522264261710104646819565161405416285530129398700414912821358924882993 M = 455054308184393892678058040417894434538147052966484655368629806848690951585316383741818991249942897131402174931069148907410409095241197004639436085265522674198117934494409967755516107042868190564732371162423204135770802585390754508661199283919569348449653439331457503898545517122035939648918370853985174413495 e = 65537 c = 17228720052381175899005296327529228647857019551986416863927209013417483505116054978735086007753554984554590706212543316457002993598203960172630351581308428981923248377333772786232057445880572046104706039330059467410587857287022959518047526287362946817619717880614820138792149370198936936857422116461146587380005750298216662907558653796277806259062461884502203484610534512552197338982682870358910558302016481352035443274153409114492025483995668048818103066011831955626539382173160900595378864729936791103356604330731386911513668727994911216530875480647283550078311836214338646991447576725034118526046292574067040720093
問題文は「いつものRSA?」と書いていて、実際encrypt.pyを眺めるとそんな感じのことをしています。
しかし少し異なるのは$M=2p+q$が与えられている点です。これと$N=pq$を連立して2次方程式を解けば、解の公式から$p$の候補が2つに絞れます。
よって$p$を2通り試してwikiを参考にそこから復号すればよいです。
実装にあたり先程のPythonのACL記事を参考にしています。
from Crypto.Util.number import long_to_bytes from math_lib import * N, M, e, c = map(int, input().split()) def isqrt(n: int) -> int: ok = 0 ng = n while ng - ok > 1: mid = (ok + ng) // 2 if mid * mid <= n: ok = mid else: ng = mid return ok s = isqrt(M*M-8*N) assert (s * s == M*M-8*N) p = (M + isqrt(M*M - 8*N)) // 4 # p = (M - isqrt(M*M - 8*N)) // 4 q = N // p assert (p * q == N) phi = lcm(p-1, q-1) d = inv_mod(e, phi) m = pow(c, d, N) print(long_to_bytes(m))
フラグは、FLAG{@n_ex7ra_param3ter_ru1n5_3very7h1n9}
[Very Hard] OUCS
与えられたコマンドnc oucs.cry.wanictf.org 50010
でサーバーにアクセスすると、次の5つのコマンドを使えます。
サーバーのコードも与えられています。(適宜コメントを入れています)
import random from Crypto.Util.number import bytes_to_long, getPrime, long_to_bytes from const import description, flag, logo # nc oucs.cry.wanictf.org 50010 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 # フラグを暗号化して、暗号文(int)を返す if choice == 1: ciphertext = cipher.encrypt(flag) ciphertext = bytes_to_long(ciphertext) print(f"{ciphertext = :#x}") # 2. Encrypt # 平文(int)を入れると、暗号文(int)が出てくる 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 # 暗号文(int)を入れると、平文(int)が出てくる # ただし平文がフラグと一致した場合、何も表示されない 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
これを用いてフラグを得る問題です。
とりあえず見たことない計算をしているので、クラス名を頼りに岡本 内山 暗号
とかで検索してみます。するとWikipediaのページがヒットします。
wikiの式とserver.pyをにらめっこして比べると、どうやらこの暗号で間違いないようです。秘密鍵は素数$p, q$なので、これらが分からないことには復号できません。
しばらく考えましたが素数$p, q$が分からないことにはどうしようもないので、色々writeupを漁ってみます。
すると、どうやら岡本・内山暗号は準同型暗号であることを知りました。(そういえば問題文にも「OUによるHomomorphicなCryptoSystemです」と書いてあったな...とwriteupを書きながら気づきました)
この暗号は特に、暗号化する関数を$E$とすると、平文$a, b$に対し$E(a)E(b) = E(a+b)$が成り立つようです。これはめちゃくちゃ使えそうです。
実際、次のような手順で解けます。
- コマンド1で、フラグを暗号化した値$C$を教えてもらう。
- コマンド2で、適当に決めた定数$T$(私は1にしました)を暗号化した値$E_T$を教えてもらう。
- コマンド4で、公開鍵$n, g, h$を教えてもらう。
- $C * E_T (mod \space n)$を計算し、それをコマンド3でdecryptしてもらう。
- 4で得た値から定数$T$を引いた値がフラグ!
よって、5で得た値をbytes型に直すことでフラグが得られました。
フラグは、FLAG{OU_d0es_n0t_repre53nt_Osaka_University_but_Okamoto-Uchiyama}
Forensics
[Beginner] presentation
zipを解凍すると中にはpowerpointファイルが入っています。開いてみるとこうなっています。 よってこの四角い図形をどければフラグが分かります。
[Very Hard] MixedUSB
与えられたMixedUSB.imgというイメージファイルをダウンロードします。とりあえずマウントしようとしてみると失敗します。
このイメージファイル壊れてそう...という気持ちになるのでなんとか中身を見る方法はないか調べると、TestDiskというツールがあることを知りました。
より詳しい使い方はこのサイトがわかりやすかったです。rootまでイメージファイルを移動させてからやらないといけなくて面倒だった。
すると、dummyファイルが沢山ある中にflag.txtを発見することが出来ました。これを適当なディレクトリにコピーして中身を見ればフラグゲットです。
フラグはFLAG{mixed_file_allocation_table}
[Easy] secure document
zipファイルにはパスワード付きのzipファイルと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, ^ac{Esc}{home}password{:} {end} return
zipファイルの中にはflag.jpgが入っており、パスワードを得ることが課題のようです。
とりあえずpassword-generatorが何なのか理解する必要があるので、必死にググると(自分はFormatTimeが関数っぽいなと思ってこれでググりました。)これはAutoHotKeyというキーボードの割当をカスタマイズするための言語で書かれたスクリプトであることが分かります。
問題には次のような説明文があったので、このスクリプトを動かしてメモ帳などに「the password for today is nani」とタイプすると、password: Wan1_20210502_C7F!na!
と変換されます。
日付の部分はzipファイル名と同じ20210428
に直してセミコロン以降をパスワードに入力してみると、zipファイルが空きました。
フラグは↓
ブラウザのURL入力するところで試していたらスクリプトが変な挙動をして一生パスワードが出てこず、大量に時間を溶かしてしまった...
[Normal] slow
zipファイルには.wavファイルが入っています。audacityとかで波形を見てみても何もわかりませんでした。
色々writeupを漁ってみると、どうやらSSTV(低速度走査テレビ)ってやつに聞かせてみる方法もあるらしい
実際RX-SSTVをインストールして試してみると...
...うわあああなんか浮かび上がってきたあああああああああああ
というわけでフラグは、FLAG{encoded_by_slow-scan_television}
でした。
(ちなみに音は結構不快なので注意)
Misc
[Beginner] binary
sample.pyと01だけ書かれたbinary.csvが与えられるので複号せよという問題。
s = "WANI" bits = [] for i in range(len(s)): val = s[i] for j in range(8): b = (ord(val) >> (7 - j)) & 0x01 bits.append(b)print(bits)
s = "" c = 0 for i in range(len(bits)): val = int(bits[i]) c = (c << 1) | val if i % 8 == 7: s = s + chr(c) c = 0
print(s)
bits = []フラグは、with open('binary.csv') as f: for line in f: line = line.rstrip('\r\n') bits.append(int(line))
s = "" c = 0 for i in range(len(bits)): val = int(bits[i]) c = (c << 1) | val if i % 8 == 7: s = s + chr(c) c = 0
print(s)
FLAG{the_basic_knowledge_of_communication}
Pwn
[Beginner] 01 netcat
与えられたコマンドnc netcat.pwn.wanictf.org 9001
で問題サーバにアクセスするとls
コマンドを打てと言われます。
仰せのままに実行すると当該ディレクトリにflag.txtがあることがわかるので、cat flag.txt
を実行することでフラグを得ます。
[Easy] 03 rop machine easy
nc rop-easy.pwn.wanictf.org 9003
で問題サーバにアクセスします。
rop machineという予め用意されたプログラムを使ってROP (Return Oriented Programming)を行い、system("/bin/sh")
を実行する問題のようです。
ROPを僕は知らなかったので色々調べました。↓のwriteupがわかりやすかったです。
要するにこのrop machineというやつは命令列を上から順に実行します。pop rdi; ret
という命令は、stackのtopにある値をpopしてrdiレジスタにセットし、次にstackのtopにある値をリターンアドレスとみなして飛びます。
よってstackに"/bin/sh"
のアドレス、system関数のアドレスを順にpushしておき、pop rdi; ret
を実行することでsystem("/bin/sh")
が実行できます。
[Easy] 04 rop machine normal
全問に加えて実行できる命令が増えています。
目標はexecve("/bin/sh", 0, 0)
を実行することであり、execveのsyscall番号が0x3bであることがわかっています。
やることは前問とほぼ同じで、第1引数に渡す値をrdiレジスタにセット、第2引数に渡す値をrsiレジスタにセット、第3引数に渡す値をrdxレジスタにセットし、syscall命令に渡すsyscall番号をraxレジスタにセットし、実行してあげればよいです。
フラグは、FLAG{now-you-can-call-any-system-calls-with-syscall}
reversing
[Beginner] secret
zipファイルには実行ファイルのみが入っています。
実行権限を与えて実行してみると、パスワードを要求されます。
そこでヒントにある表層解析というのを試してみると、
$ strings -a secret > out
なんかwani_is_the_coolest_animals_in_the_world!
というのがパスワードっぽく見えます。実際正しくて、これを入れるとフラグが出てきました。
[Easy] execute
zipを解凍するとファイル構成は次のようになっています。 説明文に「コマンドを間違えてソースコードを消しちゃった!」とある通り、Makefileを見てみると
all: gcc --version > version.txt gcc -S main.c gcc -shared -o libprint.so print.c rm main.c rm print.c
ガッツリ消してて笑う
main.sとlibprint.soというやつがあるなら普通になんらかの方法でコンパイルできるんじゃないか?と思い必死にググると、ありました。
これを参考に次のようにコンパイルして実行すると、フラグを得ることが出来ました。
$ export LD_LIBRARY_PATH=. $ gcc -I./ -L./ main.s -o main -lprint $ ./main Flag is "FLAG{c4n_y0u_execu4e_th1s_fi1e}"
Web
[Beginner] fake
https://fake.web.wanictf.org/ というサイトからフラグを得る問題です。デベロッパーツール開けばなんかあるでしょと思い開くと、謎のhtmlファイルがありました。 これを開くとフラグを得ます。
[Easy] Wani Request 1
https://request1.web.wanictf.org/
↑のwebサイトが与えられています。
自前のサーバを用意もしくはRequestBinなどのサービスを使えと言われているので、RequestBinを使ってみます。
与えられたサイトに発行されたURLを入れて送信し、httpヘッダーを見るとそれっぽいリンクがあります。 実際アクセスするとフラグをゲットできます。