この大会は2025/6/6 21:00(JST)~2025/6/8 21:00(JST)に開催されました。
今回もチームで参戦。結果は8698点で723チーム中46位でした。
自分で解けた問題をWriteupとして書いておきます。
discord (misc)
Discordに入り、#generalチャネルのトピックを見ると、フラグが書いてあった。
tjctf{wahooooooooooooooooooooooo_sanity_check}
guess-my-number (misc)
1以上1000以下のランダムな値を当てる必要がある。10回チャンスがあり、入力値が当てるべき値より大きいか小さいかわかるので、二分探索で求める。
#!/usr/bin/env python3 import socket def recvuntil(s, tail): data = b'' while True: if tail in data: return data.decode() data += s.recv(1) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('tjc.tf', 31700)) r = [1, 1000] for i in range(10): guess = (r[0] + r[1]) // 2 data = recvuntil(s, b': ') print(data + str(guess)) s.sendall(str(guess).encode() + b'\n') data = recvuntil(s, b'\n').rstrip() print(data) if data == 'Too high': r[1] = guess elif data == 'Too low': r[0] = guess else: break
実行結果は以下の通り。
Guess a number from 1 to 1000: 500
Too low
Guess a number from 1 to 1000: 750
Too high
Guess a number from 1 to 1000: 625
Too high
Guess a number from 1 to 1000: 562
Too low
Guess a number from 1 to 1000: 593
Too low
Guess a number from 1 to 1000: 609
Too high
Guess a number from 1 to 1000: 601
Too high
Guess a number from 1 to 1000: 597
Too high
Guess a number from 1 to 1000: 595
You won, the flag is tjctf{g0od_j0b_u_gu33sed_correct_998}
tjctf{g0od_j0b_u_gu33sed_correct_998}
mouse-trail (misc)
添付のテキストファイルに記載されている各座標をプロットすると、その結果フラグが浮き上がった。
#!/usr/bin/env python3 import matplotlib.pyplot as plt with open('mouse_movements.txt', 'r') as f: lines = f.read().splitlines() x_vals = [] y_vals = [] for line in lines: x = int(line.split(',')[0]) y = int(line.split(',')[1]) x_vals.append(x) y_vals.append(y) plt.scatter(x_vals, y_vals) plt.gca().invert_yaxis() plt.show()

tjctf{we_love_cartesian_plane}
guess-again (rev)
マクロを見てみる。
$ olevba chall.xlsm olevba 0.60.2 on Python 3.11.9 - http://decalage.info/python/oletools =============================================================================== FILE: chall.xlsm Type: OpenXML WARNING For now, VBA stomping cannot be detected for files in memory ------------------------------------------------------------------------------- VBA MACRO ThisWorkbook.cls in file: xl/vbaProject.bin - OLE stream: 'VBA/ThisWorkbook' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (empty macro) ------------------------------------------------------------------------------- VBA MACRO Sheet1.cls in file: xl/vbaProject.bin - OLE stream: 'VBA/Sheet1' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (empty macro) ------------------------------------------------------------------------------- VBA MACRO Module1.bas in file: xl/vbaProject.bin - OLE stream: 'VBA/Module1' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Sub CheckFlag() Dim guess As String guess = ActiveSheet.Shapes("TextBox 1").TextFrame2.TextRange.Text If Len(guess) < 7 Then MsgBox "Incorrect" Exit Sub End If If Left(guess, 6) <> "tjctf{" Or Right(guess, 1) <> "}" Then MsgBox "Flag must start with tjctf{ and end with }" Exit Sub End If Dim inner As String inner = Mid(guess, 7, Len(guess) - 7) Dim expectedCodes As Variant expectedCodes = Array(98, 117, 116, 95, 99, 52, 110, 95, 49, 116, 95, 114, 117, 110, 95, 100, 48, 48, 109) Dim i As Long If Len(inner) <> (UBound(expectedCodes) - LBound(expectedCodes) + 1) Then MsgBox "Incorrect" Exit Sub End If For i = 1 To Len(inner) If Asc(Mid(inner, i, 1)) <> expectedCodes(i - 1) Then MsgBox "Incorrect" Exit Sub End If Next i MsgBox "Flag correct!" End Sub Function check(str, arr, idx1, idx2) As Boolean If Mid(str, idx1, 1) = Chr(arr(idx2)) Then check = True Else check = False End Function ------------------------------------------------------------------------------- VBA MACRO Module2.bas in file: xl/vbaProject.bin - OLE stream: 'VBA/Module2' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Function Validate(userInput As String) As Boolean Dim f As String f = "tjctf{fake_flag}" Validate = False End Function +----------+--------------------+---------------------------------------------+ |Type |Keyword |Description | +----------+--------------------+---------------------------------------------+ |Suspicious|Chr |May attempt to obfuscate specific strings | | | |(use option --deobf to deobfuscate) | |Suspicious|Hex Strings |Hex-encoded strings were detected, may be | | | |used to obfuscate strings (option --decode to| | | |see all) | +----------+--------------------+---------------------------------------------+
このことからフラグは以下の条件をみたすことがわかる。
・"tjctf{"から始まり、"}"で終わる。
・フラグの各文字のACIIコードは、98, 117, 116, 95, 99, 52, 110, 95, 49, 116, 95, 114, 117, 110, 95, 100, 48, 48, 109となる。このASCIIコードをデコードし、フラグの形式にすればフラグになる。
>>> s = [98, 117, 116, 95, 99, 52, 110, 95, 49, 116, 95, 114, 117, 110, 95, 100, 48, 48, 109]
>>> flag = ''.join([chr(c) for c in s])
>>> flag = 'tjctf{%s}' % flag
>>> flag
'tjctf{but_c4n_1t_run_d00m}'
tjctf{but_c4n_1t_run_d00m}
serpent (rev)
pickleとしてロードし、ソースコードにデコンパイルする。
#!/usr/bin/env python3 import pickle import ast with open('ast_dump.pickle', 'rb') as f: data = pickle.load(f) source_code = ast.unparse(data) print(source_code)
復元したソースコードは次の通り。
import random import base64 import hashlib from typing import List, Dict, Optional, Union, Any class DataProcessor: def __init__(self): self.data = [] self.cache = {} self.config = {'debug': False, 'verbose': True, 'timeout': 30, 'retries': 3} def process_data(self, input_data: Any) -> Optional[str]: if not input_data: return None transformed = self._transform(input_data) validated = self._validate(transformed) return self._encode(validated) if validated else None def _transform(self, data: Any) -> str: fake_secret = 'not_the_flag' decoy_1 = 'tjctf{h4ck3r_t1m3}' decoy_2 = 'tjctf{c0d3_br34k3r}' decoy_3 = 'tjctf{s3cur1ty_f41l}' decoy_4 = 'tjctf{3xpl01t_m0d3}' decoy_5 = 'tjctf{p4ssw0rd_cr4ck}' decoy_6 = 'tjctf{syst3m_h4ck}' decoy_7 = 'tjctf{d4t4_br34ch}' decoy_8 = 'tjctf{n3tw0rk_p3n3tr4t3}' decoy_9 = 'tjctf{cyb3r_w4rr10r}' decoy_10 = 'tjctf{c0mput3r_v1rus}' decoy_11 = 'tjctf{m4lw4r3_d3t3ct}' decoy_12 = 'tjctf{f1r3w4ll_byp4ss}' decoy_13 = 'tjctf{r00tk1t_1nst4ll}' decoy_14 = 'tjctf{k3yl0gg3r_r3c0rd}' decoy_15 = 'tjctf{sp4m_b0t_4ct1v3}' decoy_16 = 'tjctf{ph1sh1ng_4tt4ck}' decoy_17 = 'tjctf{r4ns0mw4r3_3ncrypt}' decoy_18 = 'tjctf{tr0j4n_h0rs3}' decoy_19 = 'tjctf{w0rm_spr34d}' decoy_20 = 'tjctf{sp00f1ng_1d3nt1ty}' dummy_var = 'placeholder' return str(data).upper() def _validate(self, data: str) -> bool: patterns = ['^[A-Z0-9_]+$', '\\w+', '.*'] return len(data) > 0 def _encode(self, data: str) -> str: encoded = base64.b64encode(data.encode()).decode() return encoded class OuterLayer: def __init__(self): self.data = [1, 2, 3, 4, 5] * 100 self.cache = {str(i): i ** 2 for i in range(50)} self.metadata = {'version': '1.0', 'author': 'unknown'} def process_data(self, input_data): def level_one_nested(): decoy_a = 'tjctf{m1n3cr4ft_w0rld}' decoy_b = 'tjctf{r0bl0x_g4m3}' decoy_c = 'tjctf{f0rtn1t3_v1ct0ry}' decoy_d = 'tjctf{l34gu3_0f_l3g3nds}' decoy_e = 'tjctf{c0d_w4rf4r3}' decoy_f = 'tjctf{4m0ng_us_sus}' decoy_g = 'tjctf{f4ll_guy5_w1n}' decoy_h = 'tjctf{r0ck3t_l34gu3}' decoy_i = 'tjctf{0v3rw4tch_h3r0}' decoy_j = 'tjctf{v4l0r4nt_4g3nt}' class MiddleLayer: def __init__(self): self.values = list(range(1000)) self.lookup = {chr(65 + i): i for i in range(26)} def deep_process(self): dummy_1 = 'tjctf{sp4c3_1nv4d3rs}' dummy_2 = 'tjctf{p4c_m4n_g4m3}' dummy_3 = 'tjctf{t3tr1s_bl0cks}' dummy_4 = 'tjctf{sup3r_m4r10}' dummy_5 = 'tjctf{d0nk3y_k0ng}' dummy_6 = 'tjctf{str33t_f1ght3r}' dummy_7 = 'tjctf{m0rt4l_k0mb4t}' dummy_8 = 'tjctf{z3ld4_l1nk}' dummy_9 = 'tjctf{p0k3m0n_c4tch}' dummy_10 = 'tjctf{f1n4l_f4nt4sy}' def level_two_nested(): red_herring_1 = 'tjctf{sk1b1d1}' class InnerLayer: def __init__(self): self.config = {'debug': False, 'verbose': True} self.settings = {'timeout': 30, 'retries': 5} def ultra_deep_process(self): fake_inner_1 = 'tjctf{days_b4_111}' def level_three_nested(): distraction_1 = 'tjctf{60mgs}' class CoreLayer: def __init__(self): self.core_data = [x ** 3 for x in range(100)] self.core_cache = {} def final_process(self): noise_1 = 'tjctf{4everbody}' def level_four_nested(): garbage_1 = 'tjctf{b4l3nc14ga_pr1nc3ss}' class DeepestLayer: def __init__(self): self.final_data = {'key': 'value'} def ultimate_process(self): junk_1 = 'tjctf{junk_1}' junk_2 = 'tjctf{junk_2}' junk_3 = 'tjctf{junk_3}' def final_nested(): secret = 'tjctf{f0ggy_d4ys}' return None return final_nested() return DeepestLayer() return level_four_nested() def another_method(self): more_noise_1 = 'tjctf{more_noise_1}' more_noise_2 = 'tjctf{more_noise_2}' return 'nothing' return CoreLayer() return level_three_nested() def secondary_method(self): secondary_fake_1 = 'tjctf{secondary_fake_1}' secondary_fake_2 = 'tjctf{secondary_fake_2}' return 'secondary' return InnerLayer() return level_two_nested() def alternate_method(self): alternate_fake_1 = 'tjctf{alternate_fake_1}' alternate_fake_2 = 'tjctf{alternate_fake_2}' return 'alternate' return MiddleLayer() return level_one_nested() def calculate_fibonacci(n: int) -> List[int]: if n <= 0: return [] elif n == 1: return [0] elif n == 2: return [0, 1] fib = [0, 1] for i in range(2, n): fib.append(fib[i - 1] + fib[i - 2]) return fib def hash_generator(text: str) -> Dict[str, str]: hashes = {} hashes['md5'] = hashlib.md5(text.encode()).hexdigest() hashes['sha1'] = hashlib.sha1(text.encode()).hexdigest() hashes['sha256'] = hashlib.sha256(text.encode()).hexdigest() fake_flag_1 = 'tjctf{5h4d0w_run3r}' fake_flag_2 = 'tjctf{d4rk_m4g1c}' fake_flag_3 = 'tjctf{n1ght_cr4wl3r}' fake_flag_4 = 'tjctf{3v1l_sp1r1t}' fake_flag_5 = 'tjctf{bl4ck_h0l3}' fake_flag_6 = 'tjctf{v01d_w4lk3r}' fake_flag_7 = 'tjctf{d34th_st4r}' fake_flag_8 = 'tjctf{ch40s_l0rd}' fake_flag_9 = 'tjctf{w1ck3d_m1nd}' fake_flag_10 = 'tjctf{d00m_br1ng3r}' fake_flag_11 = 'tjctf{3v1l_g3n1us}' fake_flag_12 = 'tjctf{h3ll_sp4wn}' fake_flag_13 = 'tjctf{d4rk_k1ng}' fake_flag_14 = 'tjctf{s4t4n1c_c0d3}' fake_flag_15 = 'tjctf{d3m0n_h4ck3r}' fake_flag_16 = 'tjctf{v4mp1r3_byt3s}' fake_flag_17 = 'tjctf{gh0st_1n_sh3ll}' fake_flag_18 = 'tjctf{z0mb13_c0d3}' fake_flag_19 = 'tjctf{sk3l3t0n_k3y}' fake_flag_20 = 'tjctf{wr41th_m0d3}' return hashes def obfuscated_function(): x = 42 y = 'hello' z = [1, 2, 3, 4, 5] ocean_flags = ['tjctf{d33p_s34s}', 'tjctf{blu3_w4v3s}', 'tjctf{0c34n_curr3nt}', 'tjctf{s4lty_w4t3r}', 'tjctf{m4r1n3_l1f3}', 'tjctf{c0r4l_r33f}', 'tjctf{wh4l3_s0ng}', 'tjctf{t1d4l_p00l}', 'tjctf{sh4rk_4tt4ck}', 'tjctf{s34_m0nst3r}'] space_flags = ['tjctf{st4r_d4nc3r}', 'tjctf{g4l4xy_r1d3r}', 'tjctf{c0sm1c_w1nd}', 'tjctf{pl4n3t_h0p}', 'tjctf{n3bul4_dr1ft}', 'tjctf{4st3r01d_b3lt}', 'tjctf{bl4ck_h0l3}', 'tjctf{sup3rn0v4}', 'tjctf{m3t30r_sh0w3r}', 'tjctf{sp4c3_d3br1s}'] for i in range(10): temp = i * 2 if temp % 3 == 0: temp += 1 nested_dict = {'level1': {'level2': {'level3': {'data': 'nothing important', 'flag': 'tjctf{d33p_f4k3}', 'ocean': ocean_flags, 'space': space_flags}}}} return nested_dict def complex_logic(): items = ['apple', 'banana', 'cherry', 'date'] processed = [] for item in items: if len(item) > 4: processed.append(item.upper()) else: processed.append(item.lower()) return processed class ConfigManager: def __init__(self): self.settings = {'api_key': 'fake_key_12345', 'endpoint': 'https://api.example.com', 'version': '1.0.0'} self.gaming_flags = ['tjctf{g4m3r_m0d3}', 'tjctf{l3v3l_up}', 'tjctf{p0w3r_pl4y3r}', 'tjctf{b0ss_f1ght}', 'tjctf{qu3st_c0mpl3t3}', 'tjctf{l00t_dr0p}', 'tjctf{cr1t1c4l_h1t}', 'tjctf{sp33d_run}', 'tjctf{n0_scr1pt_k1dd13}', 'tjctf{pr0_pl4y3r}'] self.music_flags = ['tjctf{b34t_dr0p}', 'tjctf{s0und_w4v3}', 'tjctf{m3l0dy_m4k3r}', 'tjctf{rh7thm_rush}', 'tjctf{4ud10_f1l3}', 'tjctf{d4nc3_fl00r}', 'tjctf{v0lum3_up}', 'tjctf{b4ss_b00st}', 'tjctf{t3mp0_ch4ng3}', 'tjctf{s0ng_qu3u3}'] def get_setting(self, key: str) -> Optional[str]: return self.settings.get(key) def update_setting(self, key: str, value: str) -> None: self.settings[key] = value def random_data_generator(size: int) -> List[int]: return [random.randint(1, 100) for _ in range(size)] def string_manipulator(text: str) -> str: operations = [lambda x: x.upper(), lambda x: x.lower(), lambda x: x[::-1], lambda x: x.replace('a', '@'), lambda x: x.replace('e', '3')] result = text for op in operations: result = op(result) return result def nested_loops_example(): matrix = [] for i in range(5): row = [] for j in range(5): value = i * j if value % 2 == 0: row.append(value) else: row.append(value + 1) matrix.append(row) return matrix def exception_handler(): try: risky_operation = 10 / 0 except ZeroDivisionError: fallback_value = 'error handled' return fallback_value except Exception as e: generic_error = str(e) return generic_error finally: cleanup_code = 'always executed' def recursive_function(n: int) -> int: if n <= 1: return 1 return n * recursive_function(n - 1) def massive_red_herring_factory(): sport_flags = ['tjctf{f00tb4ll_h3r0}', 'tjctf{b4sk3tb4ll_l3g3nd}', 'tjctf{s0cc3r_st4r}', 'tjctf{b4s3b4ll_ch4mp}', 'tjctf{t3nn1s_4c3}', 'tjctf{g0lf_m4st3r}', 'tjctf{sw1mm1ng_f4st}', 'tjctf{runn1ng_sp33d}', 'tjctf{cycl1ng_r4c3}', 'tjctf{sk41ng_tr1ck}'] vehicle_flags = ['tjctf{c4r_3ng1n3}', 'tjctf{m0t0rcycl3_r1d3}', 'tjctf{tr4in_st4t10n}', 'tjctf{4irpl4n3_fl1ght}', 'tjctf{b04t_s41l}', 'tjctf{subm4r1n3_d1v3}', 'tjctf{h3l1c0pt3r_s0und}', 'tjctf{r0ck3t_l4unch}', 'tjctf{sp4c3sh1p_0rb1t}', 'tjctf{b1cycl3_p3d4l}'] instrument_flags = ['tjctf{gu1t4r_s0l0}', 'tjctf{p14n0_k3ys}', 'tjctf{drum_b34t}', 'tjctf{v10l1n_str1ng}', 'tjctf{fl ut3_m3l0dy}', 'tjctf{s4x0ph0n3_j4zz}', 'tjctf{tr0mb0n3_sl1d3}', 'tjctf{cl4r1n3t_w00d}', 'tjctf{h4rp_4ng3l}', 'tjctf{0rg4n_p1p3}'] return sport_flags + vehicle_flags + instrument_flags def another_distraction_layer(): job_flags = ['tjctf{d0ct0r_h34l}', 'tjctf{t34ch3r_l34rn}', 'tjctf{3ng1n33r_bu1ld}', 'tjctf{l4wy3r_d3f3nd}', 'tjctf{ch3f_c00k}', 'tjctf{p1l0t_fly}', 'tjctf{n urs3_c4r3}', 'tjctf{f1r3f1ght3r_s4v3}', 'tjctf{p0l1c3_pr0t3ct}', 'tjctf{4rt1st_cr34t3}'] emotion_flags = ['tjctf{h4ppy_f33l}', 'tjctf{s4d_t34r}', 'tjctf{4ngry_r4g3}', 'tjctf{3xc1t3d_j0y}', 'tjctf{n3rv0us_w0rry}', 'tjctf{pr0ud_w1n}', 'tjctf{j34l0us_3nvy}', 'tjctf{c0nf1d3nt_str0ng}', 'tjctf{l0n3ly_4l0n3}', 'tjctf{c4lm_p34c3}'] country_flags = ['tjctf{4m3r1c4_us4}', 'tjctf{c4n4d4_m4pl3}', 'tjctf{m3x1c0_t4c0}', 'tjctf{br4z1l_s4mb4}', 'tjctf{3ngl4nd_t34}', 'tjctf{fr4nc3_b4gu3tt3}', 'tjctf{g3rm4ny_b33r}', 'tjctf{1t4ly_p4st4}', 'tjctf{j4p4n_sush1}', 'tjctf{ch1n4_dr4g0n}'] return job_flags + emotion_flags + country_flags def ultimate_confusion_generator(): element_flags = ['tjctf{f1r3_fl4m3}', 'tjctf{w4t3r_fl0w}', 'tjctf{34rth_s0l1d}', 'tjctf{41r_w1nd}', 'tjctf{1c3_c0ld}', 'tjctf{st34m_h0t}', 'tjctf{l1ght_br1ght}', 'tjctf{d4rkn3ss_bl4ck}', 'tjctf{3n3rgy_p0w3r}', 'tjctf{m4tt3r_4t0m}'] mythical_flags = ['tjctf{dr4g0n_f1r3}', 'tjctf{un1c0rn_m4g1c}', 'tjctf{ph03n1x_r1s3}', 'tjctf{gr1ff1n_fl1ght}', 'tjctf{k r4k3n_t3nt4cl3}', 'tjctf{m1n0t4ur_l4byrnth}', 'tjctf{p3g4sus_w1ng}', 'tjctf{hydr4_h34d}', 'tjctf{c3nt4ur_h0rs3}', 'tjctf{m3rm41d_0c34n}'] gem_flags = ['tjctf{d14m0nd_sp4rkl3}', 'tjctf{ruby_r3d}', 'tjctf{s4pph1r3_blu3}', 'tjctf{3m3r4ld_gr33n}', 'tjctf{t0p4z_y3ll0w}', 'tjctf{4m3thyst_purpl3}', 'tjctf{0p4l_r41nb0w}', 'tjctf{p34rl_wh1t3}', 'tjctf{qu4rtz_cl34r}', 'tjctf{0bs1d14n_bl4ck}'] return element_flags + mythical_flags + gem_flags def more_distractions(): alphabet = 'abcdefghijklmnopqrstuvwxyz' numbers = list(range(100)) food_flags = ['tjctf{p1zz4_t1m3}', 'tjctf{burr1t0_b0wl}', 'tjctf{c00k13_m0nst3r}', 'tjctf{c4k3_d4y}', 'tjctf{1c3_cr34m}', 'tjctf{d0nut_h0l3}', 'tjctf{sp4gh3tt1_c0d3}', 'tjctf{t4c0_tu3sd4y}', 'tjctf{s4ndw1ch_4rt}', 'tjctf{s0up_s34s0n}'] animal_flags = ['tjctf{c4t_l0v3r}', 'tjctf{d0g_w4lk3r}', 'tjctf{b1rd_w4tch3r}', 'tjctf{f1sh_t4nk}', 'tjctf{p4nd4_3y3s}', 'tjctf{t1g3r_str1p3s}', 'tjctf{3l3ph4nt_m3m0ry}', 'tjctf{d0lph1n_3ch0}', 'tjctf{p3ngu1n_w4ddl3}', 'tjctf{m0nk3y_bus1n3ss}'] weather_flags = ['tjctf{r41ny_d4ys}', 'tjctf{sn0wy_n1ghts}', 'tjctf{sunny_sk13s}', 'tjctf{cl0udy_m0rn1ng}', 'tjctf{st0rmy_s34s}', 'tjctf{w1ndy_h1lls}', 'tjctf{m15ty_m0unt41ns}', 'tjctf{h41l_st0rm}', 'tjctf{thu nd3r_r0ll}', 'tjctf{l1ghtn1ng_str1k3}'] combined = [] for letter in alphabet[:5]: for number in numbers[:5]: combined.append(f'{letter}{number}') return combined def final_distraction(): fake_secrets = ['tjctf{n0p3_try_4g41n}', 'tjctf{wr0ng_p4th}', 'tjctf{st1ll_l00k1ng}', 'tjctf{k33p_s34rch1ng}', 'tjctf{d34d_3nd}', 'tjctf{f4ls3_h0p3}', 'tjctf{m1sl34d1ng}', 'tjctf{r3d_h3rr1ng}', 'tjctf{w1ld_g00s3}', 'tjctf{bl1nd_4ll3y}'] tech_flags = ['tjctf{c0d3_n1nj4}', 'tjctf{h4ck_th3_pl4n3t}', 'tjctf{cyb3r_gh0st}', 'tjctf{d1g1t4l_w4rr10r}', 'tjctf{b1n4ry_b34st}', 'tjctf{4lgor1thm_k1ng}', 'tjctf{d4t4_dr4g0n}', 'tjctf{c0mpr3ss10n_k1ng}', 'tjctf{3ncrypt10n_l0rd}', 'tjctf{qu4ntum_cr4ck3r}'] color_flags = ['tjctf{r3d_4l3rt}', 'tjctf{blu3_scr33n}', 'tjctf{gr33n_c0d3}', 'tjctf{y3ll0w_w4rn1ng}', 'tjctf{purpl3_h4z3}', 'tjctf{0r4ng3_fl4m3}', 'tjctf{p1nk_p4nth3r}', 'tjctf{bl4ck_0ps}', 'tjctf{wh1t3_h4t}', 'tjctf{gr4y_4r34}'] number_flags = ['tjctf{z3r0_d4y}', 'tjctf{0n3_sh0t}', 'tjctf{tw0_f4ct0r}', 'tjctf{thr33_str1k3s}', 'tjctf{f0ur_tw3nty}', 'tjctf{f1v3_st4rs}', 'tjctf{s1x_s3ns3s}', 'tjctf{s3v3n_s34ls}', 'tjctf{31ght_b1ts}', 'tjctf{n1n3_l1v3s}'] for secret in fake_secrets: processed = secret.encode().decode() return 'end of distractions'
OuterLayerクラスのprocess_data関数の一番ネストの深いところにあるフラグで通った。
tjctf{f0ggy_d4ys}
loopy (web)
http://localhost:5000/adminを入力してSubmitすると、以下の結果となる。
Access denied. URL parameter included one or more of the following banned keywords: 127, local, 0.0.0.0, ::1, ffff, 017700000001, [::], 2130706433
16進数表現はブラックリストにないので、以下を入力してSubmitしてみる。
http://0x7F000001:5000/admin
この結果、フラグが表示された。
tjctf{i_l0v3_ssssSsrF_9o4a8}
front-door (web)
https://front-door.tjc.tf/account_creationで適当なユーザ名でアカウントを作成すると、クッキーのtokenに以下が設定されている。
eyJhbGciOiAiQURNSU5IQVNIIiwgInR5cCI6ICJKV1QifQ.eyJ1c2VybmFtZSI6ICJub3JhIiwgInBhc3N3b3JkIjogIm5lY28iLCAiYWRtaW4iOiAiZmFsc2UifQ.JZOAYHBBBBNBDDQABXBFJOABZBLBBSOBVLBWVBQRSJJBOJYXDQZBEIRQBSOOFFWB
他のユーザ名でアカウントを作成すると、クッキーのtokenに以下が設定されている。
eyJhbGciOiAiQURNSU5IQVNIIiwgInR5cCI6ICJKV1QifQ.eyJ1c2VybmFtZSI6ICJob2dlIiwgInBhc3N3b3JkIjogImZ1Z2EiLCAiYWRtaW4iOiAiZmFsc2UifQ.JZOAYHBBBBNBDDQABXBFJOABZBLBBSOBVLBWVBQRSJJBOJYXDQZBEIRQBSOOFFWB
signature部分はアカウントの情報を変更しても変わらない。
$ echo eyJhbGciOiAiQURNSU5IQVNIIiwgInR5cCI6ICJKV1QifQ | base64 -d {"alg": "ADMINHASH", "typ": "JWT"} $ echo eyJ1c2VybmFtZSI6ICJob2dlIiwgInBhc3N3b3JkIjogImZ1Z2EiLCAiYWRtaW4iOiAiZmFsc2UifQ | base64 -d {"username": "hoge", "password": "fuga", "admin": "false"}
adminをtrueにして、念のため、ユーザ名等もadminにした情報に変更する。
$ echo -n '{"username": "admin", "password": "admin", "admin": "true"}' | base64 eyJ1c2VybmFtZSI6ICJhZG1pbiIsICJwYXNzd29yZCI6ICJhZG1pbiIsICJhZG1pbiI6ICJ0cnVl In0=
クッキーのtokenに以下を設定し、リロードする。
eyJhbGciOiAiQURNSU5IQVNIIiwgInR5cCI6ICJKV1QifQ.eyJ1c2VybmFtZSI6ICJhZG1pbiIsICJwYXNzd29yZCI6ICJhZG1pbiIsICJhZG1pbiI6ICJ0cnVlIn0.JZOAYHBBBBNBDDQABXBFJOABZBLBBSOBVLBWVBQRSJJBOJYXDQZBEIRQBSOOFFWB
TODOボタンを押すと、以下のように表示された。
if you want to read my todo list you must be me and understand my secret code! [108, 67, 82, 10, 77, 70, 67, 94, 73, 66, 79, 89] [107, 78, 92, 79, 88, 94, 67, 89, 79, 10, 73, 69, 71, 90, 75, 68, 83] [105, 88, 79, 75, 94, 79, 10, 8, 72, 95, 89, 67, 68, 79, 89, 89, 117, 89, 79, 73, 88, 79, 94, 89, 8, 10, 90, 75, 77, 79, 10, 7, 7, 10, 71, 75, 78, 79, 10, 67, 94, 10, 72, 95, 94, 10, 68, 69, 10, 72, 95, 94, 94, 69, 68, 10, 94, 69, 10, 75, 73, 73, 79, 89, 89, 10, 83, 79, 94] [126, 75, 65, 79, 10, 69, 92, 79, 88, 10, 94, 66, 79, 10, 93, 69, 88, 70, 78, 10, 7, 7, 10, 75, 70, 71, 69, 89, 94, 10, 78, 69, 68, 79]
https://front-door.tjc.tf/robots.txtにアクセスすると、以下のように書いてある。
User-agent: * Disallow: # Gonna jot the encryption scheme I use down for later -The Incredible Admin # # def encrypt(inp): # enc = [13] # for i in range(len(inp)): # enc.append(ord(inp[i]) ^ 42) # return enc[1:]
この暗号アルゴリズムと推測し、表示されたコードを復号する。
>>> s = [108, 67, 82, 10, 77, 70, 67, 94, 73, 66, 79, 89] >>> ''.join([chr(c ^ 42) for c in s]) 'Fix glitches' >>> s = [107, 78, 92, 79, 88, 94, 67, 89, 79, 10, 73, 69, 71, 90, 75, 68, 83] >>> ''.join([chr(c ^ 42) for c in s]) 'Advertise company' >>> s = [105, 88, 79, 75, 94, 79, 10, 8, 72, 95, 89, 67, 68, 79, 89, 89, 117, 89, 79, 73, 88, 79, 94, 89, 8, 10, 90, 75, 77, 79, 10, 7, 7, 10, 71, 75, 78, 79, 10, 67, 94, 10, 72, 95, 94, 10, 68, 69, 10, 72, 95, 94, 94, 69, 68, 10, 94, 69, 10, 75, 73, 73, 79, 89, 89, 10, 83, 79, 94] >>> ''.join([chr(c ^ 42) for c in s]) 'Create "business_secrets" page -- made it but no button to access yet' >>> s = [126, 75, 65, 79, 10, 69, 92, 79, 88, 10, 94, 66, 79, 10, 93, 69, 88, 70, 78, 10, 7, 7, 10, 75, 70, 71, 69, 89, 94, 10, 78, 69, 68, 79] >>> ''.join([chr(c ^ 42) for c in s]) 'Take over the world -- almost done'
business_secretsページが作成されているが、アクセスするボタンは用意されていないということらしい。
https://front-door.tjc.tf/business_secretsにアクセスしたら、フラグが表示された。
tjctf{buy_h1gh_s3l1_l0w}
hidden-message (forensics)
添付されているpng画像のLSBに文字情報がないか確認してみる。
$ zsteg suspicious.png imagedata .. file: Targa image data - Map 1024 x 1023 x 1 +256 +259 "\002\003" b1,rgb,lsb,xy .. text: "tjctf{steganography_is_fun}###END###" b2,g,lsb,xy .. text: ["U" repeated 25 times] b2,g,msb,xy .. text: ["U" repeated 25 times] b4,g,lsb,xy .. file: 0420 Alliant virtual executable not stripped b4,g,msb,xy .. text: ["D" repeated 50 times] b4,bgr,lsb,xy .. file: 0421 Alliant compact executable
tjctf{steganography_is_fun}
deep-layers (forensics)
$ exiftool chall.png ExifTool Version Number : 13.00 File Name : chall.png Directory : . File Size : 370 bytes File Modification Date/Time : 2025:06:07 06:36:52+09:00 File Access Date/Time : 2025:06:07 09:54:11+09:00 File Inode Change Date/Time : 2025:06:07 06:36:52+09:00 File Permissions : -rwxrwxrwx File Type : PNG File Type Extension : png MIME Type : image/png Image Width : 1 Image Height : 1 Bit Depth : 8 Color Type : RGB with Alpha Compression : Deflate/Inflate Filter : Adaptive Interlace : Noninterlaced Password : cDBseWdsMHRwM3NzdzByZA== Warning : [minor] Trailer data after PNG IEND chunk Image Size : 1x1 Megapixels : 0.000001
Passwordにbase64文字列が設定されているので、デコードする。
$ echo cDBseWdsMHRwM3NzdzByZA== | base64 -d p0lygl0tp3ssw0rd $ binwalk chall.png DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 PNG image, 1 x 1, 8-bit/color RGBA, non-interlaced 90 0x5A Zlib compressed data, default compression 119 0x77 Zip archive data, encrypted at least v1.0 to extract, compressed size: 67, uncompressed size: 55, name: secret.gz 348 0x15C End of Zip archive, footer length: 22
pngの後ろにzipがあるので、切り出す。
$ dd bs=1 skip=119 if=chall.png of=chall.zip 251+0 records in 251+0 records out 251 bytes copied, 0.0263772 s, 9.5 kB/s
chall.zipを先ほどのパスワードで解凍し、内容を確認する。
$ unzip chall.zip Archive: chall.zip [chall.zip] secret.gz password: extracting: secret.gz $ gzip -d secret.gz gzip: secret: Value too large for defined data type $ cat secret tjctf{p0lygl0t_r3bb1t_h0l3}
tjctf{p0lygl0t_r3bb1t_h0l3}
footprint (forensics)
.DS_Storeから情報を抽出する。https://github.com/gehaxelt/Python-dsstore/のツールを使う。
$ git clone https://github.com/gehaxelt/Python-dsstore/ Cloning into 'Python-dsstore'... remote: Enumerating objects: 38, done. remote: Counting objects: 100% (8/8), done. remote: Compressing objects: 100% (4/4), done. remote: Total 38 (delta 1), reused 1 (delta 0), pack-reused 30 (from 1) Receiving objects: 100% (38/38), 12.33 KiB | 2.05 MiB/s, done. Resolving deltas: 100% (8/8), done. $ cd Python-dsstore $ python3 main.py ../.DS_Store Count: 100 -FumtF3yx-kSP11OD8mFPA 0wnNJd_pKKNtfhG-HL8iJw 1VmhSaBo9ymK5dUhB3cPEQ 1zp7dw6eF3co0VaPDKhUag 27bCy1Bt-9nnLG4W8oxkNA 4vsxjPs-c9hBNmmaE8HJ8Q 5rc69mw3DNjUvLolTrP3ew 6zed3-nVA008etbNxGTNEQ 78ICY1U_sI9qqF6vv97RhA 7AJTVqVtlVelnulrRMUCaQ 7P7nopvmQj2usona47YjSA 7v9Vn8ci1_C1tyjKpOrDjA 9lZ0k-7YFRkQu1QhA-d-DA _KxyLnw2LZxOc0Tk9U0cig A69F-dk9M1nQFfzi06gLPw abvFjWgNkHKQYbaMyCjdlw AFEvM2adjTHq0E8noIE0kw aNHzuom-c5UIGbW5ceGc9g aNPc9lG0gpLnRvGI2JPQMA aXNfdXNlZnVsP30gICAgIA b-PeUWwmlHzmh613ikEFWw b0HMxEHbs7pA9uHtxRPPTQ Bg9XKyNnYRSpUIYeK_2knA CE4CzpPMjNHuYeLi2dLNHg CPyvVdX_ynvUTdxXWYr7Mw D0FvxLGtoba2wKJQQEjUKA D3LWCMzIQyc12SQUw5uDnw DFhc0ROB762T-pZvtdPFqA dGpjdGZ7ZHNfc3RvcmVfIA dtiqv7O15HVT3k4aL1sTDA Dwv-qkn0QQLu8RlhwUNNeA e1xUP4GStK_lTs2W6gEkPA E6IuP9ouzpQDYXSNNdSyFw E8tEXrVCNAqQmSji8cDWqQ eNSX5RnqD95dCp7TNI2zOg eSRLCk2xqnpx1htFqlBAvw f0wkaX7NmMMATw1grKXIyw FsmKr0zCKawO9TVNgKkVvA FxWYnzxWEHMQGNLR_7uXRw g8azPwD-y-_2VU-dmRK7IA GQxeqKQR4yLZIz889h8awQ iHFj7XDSIesD-TJ-aSiTyA jg1spCuL4Vix9bgpToP5Hg JuARuIvr7ZvOOpeJ9LTOvA kAbgNCWwQGUZWFktiIIHeA kfAVM8pkh2jSSeuP0uWGog LWUOeeOqK7mlQOTJmSmwVA LxtZhpBRiU8PSE4eXQZV0w meMIlojtiqbBuHOGDYud_Q mLO_JmXEG0tcWougAWQ6Qw MnbgbJqhwFXlh_kKGIYLJQ N3lsvxkjSRnwIfz3Z7C5uw n8KZj1W3tx2WIXg8HqtF3g niZvI6zZ8yzoSl17d963mQ Ns2Tpp4fQxW5zhYLLGIVdA NUhhZkAZsgdvPVVF3KzZpA p7s4fwmK70UDkM_ApzmX3A PgbanHSdf0H3qDXVUrVAaA PGjsQh7wml99RXiA-gta6Q pJ_ampVpcIVGVZErPVONDQ pm_TeJmHmlL-5Mdv3R1YoA pMOW9YUc2Zrd-6B5G3-NSQ Po5jOoQ4HUssvLHuCmDj5g pOGlM0FXA5tvruLyZ5AVRA pPd8nFycxgq3SD67StjdCQ PqYOp2_ps2oErxR5U5uSXA PrceMk6k6v8-gPc6YUfuvQ prsJSTpQJJX5eKJQF3akDg PuAZy-41HFCOsKTCZkwDBw QJWXuKXCsnG2mjGYYbyoaA QLpbpFhcDb2oapdj3Ygutg Qs13PznBoQJC9yjgWm-clQ QzopSLFcXVCF5sII8C8jJA r4NAxKJ_RMhOLA468CAuLQ rhnhvlSsjRdNv35ZYwqSMg rkFwpUQRoffUQmfnqKFCNg sOcYOUIJIYjTYFNud5htCA sPEu3qqkuJDSVB6LZ0x82w tCSdwHZsNBvNS3h4qih6tA TibD2LWT-7Xua5Wmivc-6A TM0MhKzOCDKMYolGyoYc3g TxlnTHhAAyM7wIn3PGdLEg Ur4ktp41Mmf49_FANNugHQ uvIo5poX5D2dGKif1JiWIA uXcTlwm5yaS69kQd0YlYgQ V2XHU0KaptQFjruBnOeYJw V3JQfigsgWSgJ2bu8IuPOQ vNgO1oK2Ft-Q_OVtcjk7og VRU6bDaPkqPqFxsfBjrPQA VVa_NUjLLaMsO2_Jwko-SA w1oE_3GO5OTRADuEQ9Pqnw W62TUuIfC_ma91QdMu4ISA wzeP722FHEtirWHFJrgP2A xcTHd2ZbYtO9LQ2fmaQo1Q XfjqZgZvquXzdnfbcMKQMA Xnji8EXzCRLmKJvoAkZftA yfmS9_zOUIcxfSY-obWMhg ylFen4T5uMeqvJC6p8dfkA yx7GMqzc5YejMzOO5F087g Z3cpkGAUMlLgzQctzCo2Zg
各行をbase64デコードし、フラグの断片を抽出したものを構成する。
#!/usr/bin/env python3 from base64 import * def is_printable(s): for c in s: if c < 32 or c > 126: return False return True files = '''-FumtF3yx-kSP11OD8mFPA 0wnNJd_pKKNtfhG-HL8iJw 1VmhSaBo9ymK5dUhB3cPEQ 1zp7dw6eF3co0VaPDKhUag 27bCy1Bt-9nnLG4W8oxkNA 4vsxjPs-c9hBNmmaE8HJ8Q 5rc69mw3DNjUvLolTrP3ew 6zed3-nVA008etbNxGTNEQ 78ICY1U_sI9qqF6vv97RhA 7AJTVqVtlVelnulrRMUCaQ 7P7nopvmQj2usona47YjSA 7v9Vn8ci1_C1tyjKpOrDjA 9lZ0k-7YFRkQu1QhA-d-DA _KxyLnw2LZxOc0Tk9U0cig A69F-dk9M1nQFfzi06gLPw abvFjWgNkHKQYbaMyCjdlw AFEvM2adjTHq0E8noIE0kw aNHzuom-c5UIGbW5ceGc9g aNPc9lG0gpLnRvGI2JPQMA aXNfdXNlZnVsP30gICAgIA b-PeUWwmlHzmh613ikEFWw b0HMxEHbs7pA9uHtxRPPTQ Bg9XKyNnYRSpUIYeK_2knA CE4CzpPMjNHuYeLi2dLNHg CPyvVdX_ynvUTdxXWYr7Mw D0FvxLGtoba2wKJQQEjUKA D3LWCMzIQyc12SQUw5uDnw DFhc0ROB762T-pZvtdPFqA dGpjdGZ7ZHNfc3RvcmVfIA dtiqv7O15HVT3k4aL1sTDA Dwv-qkn0QQLu8RlhwUNNeA e1xUP4GStK_lTs2W6gEkPA E6IuP9ouzpQDYXSNNdSyFw E8tEXrVCNAqQmSji8cDWqQ eNSX5RnqD95dCp7TNI2zOg eSRLCk2xqnpx1htFqlBAvw f0wkaX7NmMMATw1grKXIyw FsmKr0zCKawO9TVNgKkVvA FxWYnzxWEHMQGNLR_7uXRw g8azPwD-y-_2VU-dmRK7IA GQxeqKQR4yLZIz889h8awQ iHFj7XDSIesD-TJ-aSiTyA jg1spCuL4Vix9bgpToP5Hg JuARuIvr7ZvOOpeJ9LTOvA kAbgNCWwQGUZWFktiIIHeA kfAVM8pkh2jSSeuP0uWGog LWUOeeOqK7mlQOTJmSmwVA LxtZhpBRiU8PSE4eXQZV0w meMIlojtiqbBuHOGDYud_Q mLO_JmXEG0tcWougAWQ6Qw MnbgbJqhwFXlh_kKGIYLJQ N3lsvxkjSRnwIfz3Z7C5uw n8KZj1W3tx2WIXg8HqtF3g niZvI6zZ8yzoSl17d963mQ Ns2Tpp4fQxW5zhYLLGIVdA NUhhZkAZsgdvPVVF3KzZpA p7s4fwmK70UDkM_ApzmX3A PgbanHSdf0H3qDXVUrVAaA PGjsQh7wml99RXiA-gta6Q pJ_ampVpcIVGVZErPVONDQ pm_TeJmHmlL-5Mdv3R1YoA pMOW9YUc2Zrd-6B5G3-NSQ Po5jOoQ4HUssvLHuCmDj5g pOGlM0FXA5tvruLyZ5AVRA pPd8nFycxgq3SD67StjdCQ PqYOp2_ps2oErxR5U5uSXA PrceMk6k6v8-gPc6YUfuvQ prsJSTpQJJX5eKJQF3akDg PuAZy-41HFCOsKTCZkwDBw QJWXuKXCsnG2mjGYYbyoaA QLpbpFhcDb2oapdj3Ygutg Qs13PznBoQJC9yjgWm-clQ QzopSLFcXVCF5sII8C8jJA r4NAxKJ_RMhOLA468CAuLQ rhnhvlSsjRdNv35ZYwqSMg rkFwpUQRoffUQmfnqKFCNg sOcYOUIJIYjTYFNud5htCA sPEu3qqkuJDSVB6LZ0x82w tCSdwHZsNBvNS3h4qih6tA TibD2LWT-7Xua5Wmivc-6A TM0MhKzOCDKMYolGyoYc3g TxlnTHhAAyM7wIn3PGdLEg Ur4ktp41Mmf49_FANNugHQ uvIo5poX5D2dGKif1JiWIA uXcTlwm5yaS69kQd0YlYgQ V2XHU0KaptQFjruBnOeYJw V3JQfigsgWSgJ2bu8IuPOQ vNgO1oK2Ft-Q_OVtcjk7og VRU6bDaPkqPqFxsfBjrPQA VVa_NUjLLaMsO2_Jwko-SA w1oE_3GO5OTRADuEQ9Pqnw W62TUuIfC_ma91QdMu4ISA wzeP722FHEtirWHFJrgP2A xcTHd2ZbYtO9LQ2fmaQo1Q XfjqZgZvquXzdnfbcMKQMA Xnji8EXzCRLmKJvoAkZftA yfmS9_zOUIcxfSY-obWMhg ylFen4T5uMeqvJC6p8dfkA yx7GMqzc5YejMzOO5F087g Z3cpkGAUMlLgzQctzCo2Zg''' flags = [] for file in files.splitlines(): while True: if len(file) % 4 == 0: break file += '=' d = urlsafe_b64decode(file) if is_printable(d): flags.append(d.decode().strip()) assert len(flags) == 2 assert '}' in flags[0] assert 'tjctf{' in flags[1] flag = flags[1] + flags[0] print(flag)
tjctf{ds_store_is_useful?}
packet-palette (forensics)
パケットを見ていくと、No.2~No.22のTCP payloadの一部を結合するとPNGデータになりそうであることがわかった。結合して画像を復元してみる。
#!/usr/bin/env python3 from scapy.all import * packets = rdpcap('chall.pcapng') png = b'' for i in range(len(packets)): data = packets[i][Raw].load head = data[:12] assert head[:4] == b'USBI' if data[4] == 0: assert data[5] == i - 1 png += data[12:] with open('flag.png', 'wb') as f: f.write(png)
復元した画像にフラグが書いてあった。

tjctf{usb1p_f13g_1ns1d3_3_pr0t0c0l}
bacon-bits (crypto)
暗号化処理の概要は以下の通り。
・flag: フラグ ・text: 未知固定文字列 ・baconian: アルファベットとベコニアン暗号の対応表 ・text: textの各文字の配列 ・ciphertext = '' ・flagの各文字lとそのインデックスiについて以下を実行 ・lがアルファベットでない場合何もしない ・change = baconian[l] ・text[i*5:(i+1)*5]の各文字ltとそのインデックスixについて、change[ix]が"1"の場合ltを大文字で、"0"の場合ltを小文字にして、ciphertextに結合 ・out.txtにciphertextのASCIIコードで13引いたものの文字を連結したものを出力
逆算していけば、フラグを復号できる。
#!/usr/bin/env python3 baconian = { 'a': '00000', 'b': '00001', 'c': '00010', 'd': '00011', 'e': '00100', 'f': '00101', 'g': '00110', 'h': '00111', 'i': '01000', 'j': '01000', 'k': '01001', 'l': '01010', 'm': '01011', 'n': '01100', 'o': '01101', 'p': '01110', 'q': '01111', 'r': '10000', 's': '10001', 't': '10010', 'u': '10011', 'v': '10011', 'w': '10100', 'x': '10101', 'y': '10110', 'z': '10111'} with open('out.txt', 'r') as f: ct = f.read() ciphertext = ''.join([chr(ord(i) + 13) for i in ct]) baconian_ct = '' for c in ciphertext: if c.isupper(): baconian_ct += '1' else: baconian_ct += '0' flag = '' for i in range(0, len(baconian_ct), 5): change = baconian_ct[i:i+5] for k, v in baconian.items(): if v == change: flag += k break print(flag)
復号結果は以下の通り。
tictfoinkooinkoooinkooooink
フラグの形式に整形する。
tjctf{oinkooinkoooinkooooink}
alchemist-recipe (crypto)
暗号処理の概要は以下の通り。
・SNEEZE_FORK = "AurumPotabileEtChymicumSecretum" ・WUMBLE_BAG = 8 ・flag_content: flag.txtの内容 ・jellybean = glorbulate_sprockets_for_bamboozle(SNEEZE_FORK) ※固定データ ・encrypted_recipe = snizzle_bytegum(flag_content, jellybean) ・bubbles: flag_contentの8バイトパディング ・glomp = b'' ・bunnlesの8バイトブロックsplinterごとに以下を実行 ・zap = scrungle_crank(splinter, jellybean) ・zonked: splinterの各値をjellybean['drizzle'][x]に置換 ・quix = jellybean['twizzle'](16個の既知固定数値から構成する配列) ・splatted: zonkedとquixの8個の要素のXOR ・wiggle = jellybean['flibber'](8個の既知固定数値から構成する配列) ・waggly: iが0~7の(wiggle[i], i)をソートしたもの ・zort: wagglyの各要素の2つ目の値のみを配列にしたもの ・plunk = [0] * 8 ・0~7のyについて以下を実行 ・x = zort[y] ・plunk[y] = splatted[x] ・plunkのバイト文字列を返却 ・glompにzapを結合 ・glompを返却 ・encrypted.txtにencrypted_recipeの16進文字列化したものを出力
出力結果の16進数文字列をhexデコードし、8バイトごとに分け、以下のことに着目して復号する。
・jellybeanは既知固定データのため、quix、wiggleは既知固定データである。 ・wiggleが既知固定データであるため、waggly、zortは固定データである。
#!/usr/bin/env python3 import hashlib from Crypto.Util.Padding import unpad SNEEZE_FORK = "AurumPotabileEtChymicumSecretum" WUMBLE_BAG = 8 def glorbulate_sprockets_for_bamboozle(blorbo): zing = {} yarp = hashlib.sha256(blorbo.encode()).digest() zing['flibber'] = list(yarp[:WUMBLE_BAG]) zing['twizzle'] = list(yarp[WUMBLE_BAG:WUMBLE_BAG+16]) glimbo = list(yarp[WUMBLE_BAG+16:]) snorb = list(range(256)) sploop = 0 for _ in range(256): for z in glimbo: wob = (sploop + z) % 256 snorb[sploop], snorb[wob] = snorb[wob], snorb[sploop] sploop = (sploop + 1) % 256 zing['drizzle'] = snorb return zing jellybean = glorbulate_sprockets_for_bamboozle(SNEEZE_FORK) quix = jellybean['twizzle'] wiggle = jellybean['flibber'] waggly = sorted([(wiggle[i], i) for i in range(WUMBLE_BAG)]) zort = [oof for _, oof in waggly] with open('encrypted.txt', 'r') as f: encrypted_recipe = bytes.fromhex(f.read()) flag = b'' for i in range(0, len(encrypted_recipe), WUMBLE_BAG): zap = encrypted_recipe[i:i+WUMBLE_BAG] splatted = [0] * 8 for y in range(WUMBLE_BAG): x = zort[y] splatted[x] = zap[y] zonked = bytes([splatted[i] ^ quix[i] for i in range(WUMBLE_BAG)]) splinter = bytes([jellybean['drizzle'].index(x) for x in zonked]) flag += splinter flag = unpad(flag, WUMBLE_BAG).decode() print(flag)
tjctf{thank_you_for_making_me_normal_again_yay}
theartofwar (crypto)
暗号処理の概要は以下の通り。
・flag: flag.txtの内容 ・m: flagを数値化したもの ・e: 8ビット素数 ・eを出力 ・e回以下繰り返し ・n = generate_key() ・c = pow(m, e, n) ・nを出力 ・cを出力
Hastad's Broadcast Attackで復号する。
#!/usr/bin/env python3 from Crypto.Util.number import * from sympy.ntheory.modular import crt from gmpy2 import iroot with open('output.txt', 'r') as f: params = f.read().splitlines() e = int(params[0].split(' ')[-1]) ns = [] cs = [] for i in range(e): n = int(params[i * 2 + 1].split(' ')[-1]) c = int(params[i * 2 + 2].split(' ')[-1]) ns.append(n) cs.append(c) me, _ = crt(ns, cs) m, success = iroot(me, e) assert success flag = long_to_bytes(m).decode() print(flag)
tjctf{the_greatest_victory_is_that_which_require_no_battle}
seeds (crypto)
サーバの処理概要は以下の通り。
・randgen = RandomGenerator() ・randgen.seed: 現在時刻を文字表記したもの(例:Sat Jun 7 16:07:33 2025) ・randgen.seed: randgen.seedを数値化したもの ・randgen.m = 2 ** 32 ・randgen.a = 157 ・randgen.c = 1 ・cipher = AES.new(randgen.randbytes(32), AES.MODE_ECB) ・randgen.randbytes(32) = randgen.randint(32 * 8).to_bytes(len, "big") ・randgen.randint(32 * 8) ・randgen.seed = (randgen.a * randgen.seed + randgen.c) % randgen.m ・result: randgen.seedを4バイト文字列化したもの ・resultの長さが32になるまで、以下繰り返し ・randgen.seed = (randgen.a * randgen.seed + randgen.c) % randgen.m ・resultにrandgen.seedを4バイト文字列化したものを結合 ・resultを数値化したものを返却 ・flag: flag.txtの内容 ・ciphertext: flagをパディングし、cipherオブジェクトで暗号化したもの ・ciphertextを表示 ・以下繰り返し ・plaintext: 入力 ・plaintextが"quit"の場合、繰り返し終了 ・cipher = AES.new(randgen.randbytes(32), AES.MODE_ECB) ・ciphertext: plaintextをパディングし、cipherオブジェクトで暗号化したもの ・ciphertextを表示
最初のseedが決まれば、暗号鍵は決まる。またタイムゾーンはESTのようなので、夏時間ではJSTの13時間前を示すことに注意すれば、簡単に復号できる。
#!/usr/bin/env python3 import socket import time from datetime import datetime from zoneinfo import ZoneInfo from Crypto.Cipher import AES from Crypto.Util.Padding import unpad def recvuntil(s, tail): data = b'' while True: if tail in data: return data.decode() data += s.recv(1) class RandomGenerator: def __init__(self, seed = None, modulus = 2 ** 32, multiplier = 157, increment = 1): if seed is None: seed = time.asctime() if type(seed) is int: self.seed = seed if type(seed) is str: self.seed = int.from_bytes(seed.encode(), "big") if type(seed) is bytes: self.seed = int.from_bytes(seed, "big") self.m = modulus self.a = multiplier self.c = increment def randint(self, bits: int): self.seed = (self.a * self.seed + self.c) % self.m result = self.seed.to_bytes(4, "big") while len(result) < bits // 8: self.seed = (self.a * self.seed + self.c) % self.m result += self.seed.to_bytes(4, "big") return int.from_bytes(result, "big") % (2 ** bits) def randbytes(self, len: int): return self.randint(len * 8).to_bytes(len, "big") s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('tjc.tf', 31493)) data = recvuntil(s, b'\n').rstrip() print(data) dt = datetime.now(ZoneInfo('America/New_York')) seed = time.asctime(dt.timetuple()) randgen = RandomGenerator(seed=seed) data = recvuntil(s, b'\n').rstrip() print(data) enc_flag = eval(data.split(' = ')[1]) cipher = AES.new(randgen.randbytes(32), AES.MODE_ECB) flag = unpad(cipher.decrypt(enc_flag), AES.block_size).decode() print(flag)
実行結果は以下の通り。
Welcome to the AES Oracle
ciphertext = b'I<B\x8f7\x1a\x9d\xba\xcb=Dz8\x97\xe9c\xb7\xaf\x15\x01\xf4\xd9\xd9\xc2\x83jm\x1a\xa2\xda\x10\xb5'
tjctf{h4rv3st_t1me}
tjctf{h4rv3st_t1me}
close-secrets (crypto)
暗号処理の概要は以下の通り。
・flag_plaintext: flag.txtの内容 ・flag_bytes: flag_plaintextのバイト文字列エンコーディング ・p, g, u, v, shared_key = generate_dh_key() ・p: 1024ビット素数 ・g: 1024ビット素数 ・a: p - 10以上p以下の整数 ・b: g - 10以上g以下の整数 ・u = pow(g, a, p) ・v = pow(g, b, p) ・key = pow(v, a, p) ・b_key = pow(u, b, p) ・p, g, u, v, keyを返却 ・xor_key_str = hashlib.sha256(str(shared_key).encode()).hexdigest() ・xor_key_bytes: xor_key_strのバイト文字列エンコーディング ・intermediate_ords = dynamic_xor_encrypt(flag_bytes, xor_key_bytes) ・flag_bytesを逆順にしたものとxor_key_bytesの繰り返しとXORしたものを返却 ・final_cipher = encrypt_outer(intermediate_ords, shared_key) ・cipher = [] ・key_offset = shared_key % 256 ・intermediate_ordsの各値valに対して以下を実行 ・cipherにvalにkey_offsetをプラスしkeyを掛けたものを追加 ・cipherを返却 ・p, g, u, vをparams.txtに書き込み ・final_cipherをenc_flagに書き込み
a, bの範囲が狭いため、ブルートフォースでa, bを求めることができ、shared_keyがわかる。shared_keyがわかったら逆算していけば、フラグが得られる。
#!/usr/bin/env python3 import hashlib def decrypt_outer(ciphertext_ords, key): plain = [] key_offset = key % 256 for val in ciphertext_ords: if not isinstance(val, int): raise TypeError assert val % key == 0 plain.append(val // key - key_offset) return plain def dynamic_xor_decrypt(ciphertext_bytes, text_key_bytes): decrypted_ords = [] key_length = len(text_key_bytes) if not isinstance(ciphertext_bytes, bytes): raise TypeError for i, byte_val in enumerate(ciphertext_bytes): key_byte = text_key_bytes[i % key_length] decrypted_ords.append(byte_val ^ key_byte) return bytes(decrypted_ords[::-1]) with open('params.txt', 'r') as f: params = f.read().splitlines() p = int(params[0].split(' ')[-1]) g = int(params[1].split(' ')[-1]) u = int(params[2].split(' ')[-1]) v = int(params[3].split(' ')[-1]) for a in range(p - 10, p + 1): if pow(g, a, p) == u: break shared_key = pow(v, a, p) with open('enc_flag', 'r') as f: final_cipher = eval(f.read()) intermediate_ords = decrypt_outer(final_cipher, shared_key) intermediate_bytes = bytes(intermediate_ords) xor_key_str = hashlib.sha256(str(shared_key).encode()).hexdigest() xor_key_bytes = xor_key_str.encode('utf-8') flag_bytes = dynamic_xor_decrypt(intermediate_bytes, xor_key_bytes) flag = flag_bytes.decode() print(flag)
tjctf{sm4ll_r4ng3_sh0rt_s3cr3t}
double-trouble (crypto)
暗号処理の概要は以下の通り。
・k1, k2 = gen() ・myrandom: 42をシードとした乱数設定 ・k1: ランダム8バイト文字列 ・choices: ランダム6バイト文字列のリスト化したもの ・k2 = b'' ・8回以下繰り返し ・k2にchoicesのランダム0以上3以下のインデックスの値をバイト文字列として追加 ・k1, k2を返却 ・k3, k4 = gen() ・pt = b"example" ・ct1 = enc(pt, k1, k2, k3, k4) ・key1 = k1 + k2 ・ct1: ptをパディングしてkey1でAES ECBモード暗号化したもの ・key2 = k4 + k3 ・ct2 = ct1をパディングしてkey1でAES ECBモード暗号化したもの ・ct1を16進数表記でout.txtに書き込み ・ct2 = enc(flag, k1, k2, k3, k4) ・ct2を16進数表記でout.txtに書き込み
gen関数のk1は固定の8バイト文字列。またgen関数のchoicesは固定でインデックス0~3は4つの値しかない。k2はそれが8バイトであるため、全部で65536パターンしかない。
enc関数では2つの鍵それぞれが65536パターンあり、二重に暗号化している。平文と暗号文の組が1つわかっているので、平文を1回暗号化したものと暗号文を1回復号したものを突き合わせ、一致するものを探す。
あとはそれを使って、フラグを復号する。
#!/usr/bin/env python3 from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad import random import itertools with open("out.txt", "r") as f: params = f.read().splitlines() pt = b"example" ct = bytes.fromhex(params[0]) ct_flag = bytes.fromhex(params[1]) myrandom = random.Random(42) k1 = myrandom.randbytes(8) choices = list(myrandom.randbytes(6))[:4] k3 = k1 key1_list = [] ct1_list = [] for x in itertools.product(choices, repeat=8): k2 = bytes(x) key1 = k1 + k2 cipher = AES.new(key1, mode=AES.MODE_ECB) ct1 = cipher.encrypt(pad(pt, 16)) key1_list.append(key1) ct1_list.append(ct1) for x in itertools.product(choices, repeat=8): k4 = bytes(x) key2 = k4 + k3 cipher = AES.new(key2, mode=AES.MODE_ECB) ct1 = cipher.decrypt(ct) if ct1 in ct1_list: index = ct1_list.index(ct1) key1 = key1_list[index] break print('[+] key1:', key1) print('[+] key2:', key2) cipher = AES.new(key2, mode=AES.MODE_ECB) ct1 = cipher.decrypt(ct_flag) cipher = AES.new(key1, mode=AES.MODE_ECB) flag = unpad(cipher.decrypt(ct1), 16).decode() print('[*] flag:', flag)
実行結果は以下の通り。
[+] key1: b'\x9dy\xb1\xa3\x7f1\x80\x1c\x1a\x1a\x1a\x1agg\x1a\x06'
[+] key2: b'\x1ag\x1a\x1a\x06\xd1\x1a\x1a\x9dy\xb1\xa3\x7f1\x80\x1c'
[*] flag: tjctf{m33t_in_th3_middl3}
tjctf{m33t_in_th3_middl3}
survey (misc)
アンケートに答えたら、フラグが表示された。
tjctf{and_we_say_bye_bye_until_next_year}