team-logo
MindCrafters
Published on

THEM?!CTF 2026 - Forensics Writeups

Authors

Introduction

From May 29 to May 31, 2026, there was a great CTF: THEM?!CTF 2026. We solved 9 out of 12 forensics challenges. Below you will find detailed writeups for each of the solved tasks.

00

Radio Chaos

01

Description

The challenge featured an audio file. Analyzing the audio signal or listening to it reveals that it represents an SSTV (Slow Scan Television) transmission.

By using an SSTV decoder tool (such as QSSTV on Linux or Robot36 on mobile) and selecting the PD120 mode, the audio signal is decoded into an image displaying the flag.

Flag

THEM?!CTF{YOU_ARE_A_SSTV_CHAMPION}

Solution author: Grzechu

pixel-drift

02

Description

We were provided a PCAP capture (output (1).pcapng) containing network traffic on port 7777. The packet data transmits spatial coordinate logs of moving objects.

After shifting the UDP payload bitstream by 3 bits to the left, we can parse RPC-like records representing MoveObject commands. Each record contains:

  • Object ID (between 1 and 10000)
  • Starting XYZ coordinates
  • Destination XYZ coordinates
  • Animation speed

By computing the midpoints between the starting and ending positions for each object: midpoint=source+destination2\text{midpoint} = \frac{\text{source} + \text{destination}}{2} and plotting these coordinates in 2D (either as ASCII output or an image), we reconstruct the hidden horizontal text that reveals the flag.

Solution Script

#!/usr/bin/env python3
"""Reconstruct the hidden midpoint message from the pixel-drift PCAP.

Usage:
    python3 solve_pixel_drift.py 'output (1).pcapng'

Dependencies:
    pip install dpkt matplotlib
"""
from __future__ import annotations

import argparse
import math
import struct
from pathlib import Path

import dpkt
import matplotlib.pyplot as plt
import numpy as np

# After shifting the server bitstream by three bits, each MoveObject-like RPC
# after the first record in a datagram begins with this byte signature.
LONG_PREFIX = bytes.fromhex("730100a31e5001")
# The first record has a shorter visible prefix because of datagram framing.
SHORT_PREFIX = bytes.fromhex("a31e5001")


def shift_left_bits(data: bytes, count: int) -> bytes:
    bits = "".join(f"{value:08b}" for value in data)[count:]
    bits += "0" * (-len(bits) % 8)
    return bytes(int(bits[i : i + 8], 2) for i in range(0, len(bits), 8))


def decode_record(stream: bytes, offset: int):
    """Decode: uint16 id + start xyz + destination xyz + speed."""
    if offset + 2 + 7 * 4 > len(stream):
        return None
    object_id = struct.unpack_from("<H", stream, offset)[0]
    values = struct.unpack_from("<7f", stream, offset + 2)
    if not (1 <= object_id <= 10000):
        return None
    if not all(math.isfinite(v) and abs(v) < 100000 for v in values):
        return None
    sx, sy, sz, dx, dy, dz, speed = values
    if speed <= 0:
        return None
    distance = math.dist((sx, sy, sz), (dx, dy, dz))
    duration = distance / speed
    # The actual animation in this challenge lasts 45 seconds.
    if not (44.9 <= duration <= 45.1):
        return None
    return object_id, np.array([sx, sy, sz]), np.array([dx, dy, dz]), duration


def extract_moves(pcap_path: Path):
    latest = {}
    with pcap_path.open("rb") as handle:
        for frame_no, (_timestamp, raw) in enumerate(dpkt.pcapng.Reader(handle), 1):
            eth = dpkt.ethernet.Ethernet(raw)
            ip = eth.data
            if not isinstance(ip, dpkt.ip.IP):
                continue
            udp = ip.data
            if not isinstance(udp, dpkt.udp.UDP) or udp.sport != 7777:
                continue

            stream = shift_left_bits(bytes(udp.data), 3)
            record_offsets = []

            # First record in a datagram.
            first = stream.find(SHORT_PREFIX, 0, 25)
            if first >= 0:
                record_offsets.append(first + len(SHORT_PREFIX))

            # Remaining records in the same datagram.
            start = 0
            while True:
                pos = stream.find(LONG_PREFIX, start)
                if pos < 0:
                    break
                record_offsets.append(pos + len(LONG_PREFIX))
                start = pos + 1

            for offset in record_offsets:
                decoded = decode_record(stream, offset)
                if decoded is not None:
                    object_id, source, destination, duration = decoded
                    latest[object_id] = (frame_no, source, destination, duration)
    return latest


def render(midpoints: np.ndarray, output_path: Path) -> None:
    plt.figure(figsize=(28, 3))
    plt.scatter(midpoints[:, 0], midpoints[:, 1], s=28, marker="s")
    plt.gca().set_aspect("equal", adjustable="box")
    plt.axis("off")
    plt.margins(0.01)
    plt.tight_layout(pad=0)
    plt.savefig(output_path, dpi=300, bbox_inches="tight", pad_inches=0)
    plt.close()


def print_ascii(midpoints: np.ndarray) -> None:
    xs = np.rint(midpoints[:, 0]).astype(int)
    ys = np.rint(midpoints[:, 1]).astype(int)
    print("\nMidpoint projection (read horizontally):\n")
    for y in sorted(set(ys), reverse=True):
        row = "".join("##" if np.any((xs == x) & (ys == y)) else "  " for x in range(xs.min(), xs.max() + 1))
        print(row.rstrip())


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("pcap", type=Path)
    parser.add_argument("-o", "--output", type=Path, default=Path("pixel_drift_reconstructed.png"))
    args = parser.parse_args()

    moves = extract_moves(args.pcap)
    if not moves:
        raise SystemExit("No matching movement records found.")

    ordered = [moves[object_id] for object_id in sorted(moves)]
    midpoints = np.array([(source + destination) / 2 for _frame, source, destination, _duration in ordered])
    durations = np.array([duration for _frame, _source, _destination, duration in ordered])

    print(f"Recovered movement records: {len(ordered)}")
    print(f"Duration range: {durations.min():.6f} .. {durations.max():.6f} seconds")
    print(f"Midpoint z range: {midpoints[:, 2].min():.6f} .. {midpoints[:, 2].max():.6f}")
    print_ascii(midpoints)
    render(midpoints, args.output)
    print(f"\nSaved reconstruction: {args.output}")


if __name__ == "__main__":
    main()

Flag

THEM?!CTF{SH3_IS_S0_PERF3FCT_BL4H_BLAH_BL4H}

Solution author: JohnDoers

bite

bite

I do have the full solution script, but going straight to it would be boring and monotonous, so I will walk through the challenge step by step.

First of all, I was given an .ad1 file and an nc connection. After researching .ad1 files, I downloaded Exterro FTK Imager to open it. Once I connected to the nc server, I received the first question:

Q1: What is the malware download URL?

The answer was located in \User\felisa\AppData\Roaming\Thunderbird\Profiles\Mail\192.168.18.2, in the Inbox file, which contains all of the messages sent to felisa.

The answer is https://mega.nz/folder/N3lBVQQT#AeiSi9X_pkYU29Xxz4tAzg. The file hosted there is bite.exe, the ransomware.

Q2: At what UTC time was the phishing email received?

After using Ctrl+F to find the message, I got all the details:

From - Fri May 29 12:38:06 2026
X-Account-Key: account1
X-UIDL: 4towMCuU4hdy3HYgXvCkHA
X-Mozilla-Status: 0001
X-Mozilla-Status2: 00000000
X-Mozilla-Keys:
Return-Path: <[email protected]>
Received: from vermithor.localdomain (unknown [172.17.0.1])
        by 7a7109a92306 (Mailpit) with SMTP
        for <[email protected]>; Fri, 29 May 2026 12:05:56 +0000 (UTC)
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
From: GameMaster Pro <[email protected]>
To: [email protected]
Subject: Your FREE Aimbot License Key Inside!
Date: Mon, 25 May 2026 07:15:00 +0000
Message-ID: <[email protected]>

SGV5IGdhbWVyLAoKQ29uZ3JhdHMhIFlvdSd2ZSBiZWVuIHNlbGVjdGVkIGZvciBvdXIgZXhjbHVz
aXZlIGJldGEgdGVzdGluZyBwcm9ncmFtLgpZb3VyIEJpdGUgQWltYm90IHYyIGxpY2Vuc2UgaXMg
cmVhZHkuCgpEb3dubG9hZCBoZXJlOgpodHRwczovL21lZ2EubnovZm9sZGVyL04zbEJWUVFUI0Fl
aVNpOVhfcGtZVTI5WHh6NHRBemcKCkxpY2Vuc2Uga2V5OiBTTkstRUIxVDMtQUxQSEEtMjAyNgoK
UmVtZW1iZXIg4oCUIHRoaXMgaXMgYSBwcml2YXRlIGJldGEuIERvIG5vdCBzaGFyZSB0aGUgbGlu
ayB3aXRoIGFueW9uZS4KTmV3IGZlYXR1cmVzOiB3YWxsaGFjaywgc2lsZW50IGFpbSwgYXV0by10
cmlnZ2VyLCBFU1Agb3ZlcmxheS4KClN0YXkgc2hhcnAsCuKAlCBUaGUgQml0ZSBUZWFt

The answer is 2026-05-25 07:15:00.

Q3: What is the sender's email address of the phishing email?

This question is also tied to that email: [email protected].

Q4: What is the subject line of the phishing email?

Also from that email: Your FREE Aimbot License Key Inside!.

Q5: What email client was the victim using?

This can be read from the email headers and folder layout: thunderbird. We can tell because the log files are located in the Thunderbird folders.

Q6: What is the filename of the malware as saved to disk?

Also from that message: bite.exe.

00

Q7: At what UTC time was the malware downloaded in Edge?

For this I had to look at the download history. After some research, I found the History file at AppData\Local\Microsoft\Edge\Default\. I downloaded it and tried to read it, but my small Python script reported disk is malformed. So I had to download the SQLite binaries for Windows and try to recover it, which worked:

PS D:\code> .\sqlite3.exe History ".recover" | Out-File -Encoding utf8 recovered.sql
PS D:\code> .\sqlite3.exe History_recovered.db ".read recovered.sql"

Then, with the following Python program, I extracted the download records:

import sqlite3

con = sqlite3.connect("History_recovered.db")
for row in con.execute("""
    SELECT target_path, total_bytes,
           datetime(start_time/1000000 - 11644473600, 'unixepoch') AS started,
           datetime(end_time/1000000 - 11644473600, 'unixepoch')   AS finished,
           tab_url
    FROM downloads ORDER BY start_time DESC"""):
    print(row)
con.close()

The first row was:

('C:\\Users\\felisa\\Downloads\\bite.zip', 2217619, '2026-05-29 12:40:02', '2026-05-29 12:40:05', 'https://mega.nz/folder/N3lBVQQT#AeiSi9X_pkYU29Xxz4tAzg')

The answer is 2026-05-29 12:40:05.

Q8: What is the absolute path the browser saved the malware to?

As shown above: C:\Users\felisa\Downloads\bite.zip.

Q9: What is the username of the victim account on the infected machine?

felisa.

Q10: What is the MachineGuid of the infected machine?

This one was interesting, because on felisa's Desktop there was a README_DECRYPT.txt:

YOUR FILES HAVE BEEN ENCRYPTED
==============================

To recover your files, send 0.05 BTC to:
bc1qsnek55m3l0v3r1337deadbeef00000000000

Then contact [email protected] with your MachineGuid.
Your MachineGuid: 2ec8f83b-8ec8-453b-8c2f-5a6a1773fe8b

Encrypted files:
  - Notes.txt
  - Passwords.txt
  - Project Alpha.docx
  - screenshot.png

The answer is 2ec8f83b-8ec8-453b-8c2f-5a6a1773fe8b.

Q11: What is the full registry key path where MachineGuid is stored?

I had to make a couple of attempts and guesses here, because I had no immediate idea. The answer is SOFTWARE\Microsoft\Cryptography.

Q12: What is the SHA-256 hash of the dropper binary?

I went to my Kali RDP session, downloaded bite.exe, and uploaded it to VirusTotal. The hash is fba69a6f8d51e9cf32db3b8f5dc7750c80745b0865e4d22dcd0cb8223a98b6ab.

01

Q13: What Windows API does the dropper use to locate the embedded resource?

For this we go to Ghidra analysis. As is standard on Windows, it would be something like FindResource, so I opened it in Ghidra.

02

Q14: What resource ID does the dropper use to load the encrypted payload?

This took more research in Ghidra:

MOV R8D, 0xa     ; lpType = 10 = RT_RCDATA
MOV EDX, 0x64    ; lpName = 0x64 = 100  <- RESOURCE ID
XOR ECX, ECX     ; hModule = NULL (current process)
CALL FindResourceA

The answer is 100.

Q15: What resource type is the payload stored as?

As we know from the question above: RCDATA.

Q16: What is the RC4 decryption key embedded in the dropper?

After some research in Ghidra:

...
1400010b4 48 8d 2d        LEA        RBP,[s_e456bac6661a5c29_140002010]  = "e456bac6661a5c29"
          55 0f 00 00
                       LAB_1400010bb                   XREF[1]: 1400010f8(j)
1400010bb 89 c8           MOV        EAX,ECX
1400010bd 45 0f b6 08     MOVZX      R9D,byte ptr [R8]=>local_12c
1400010c1 ff c1           INC        ECX
1400010c3 49 ff c0        INC        R8
1400010c6 83 e0 0f        AND        EAX,0xf
1400010c9 0f b6 44        MOVZX      EAX,byte ptr [RBP + RAX*0x1]=>s_e456bac6661a5c  = "e456bac6661a5c29"
...

The answer is e456bac6661a5c29.

Q17: After RC4 decryption, what filename does the dropper write the payload as in %TEMP%?

...
1400011a3 b9 04 01        MOV        ECX,0x104
          00 00
1400011a8 ff 15 3a        CALL       qword ptr [->KERNEL32.DLL::GetTempPathA]  = 0000518c
          3f 00 00
1400011ae 4c 8d 84        LEA        R8=>local_230,[RSP + 0xd8]
          24 d8 00
          00 00
1400011b6 48 8d 15        LEA        RDX,[s_%s\svchost.exe_140002000]  = "%s\\svchost.exe"
          43 0e 00 00
1400011bd 48 89 f1        MOV        RCX,RSI
1400011c0 ff 15 6a        CALL       qword ptr [->USER32.DLL::wsprintfA]  = 0000520e
...

The answer is svchost.exe.

Q18: What is the SHA-256 hash of the recovered ransomware payload?

For this I used an python program to compute it:

#!/usr/bin/env python3
import pefile, hashlib

def rc4(key, data):
    key = key.encode()
    S = list(range(256))
    j = 0
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]
    i = j = 0
    out = []
    for byte in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        out.append(byte ^ S[(S[i] + S[j]) % 256])
    return bytes(out)

pe = pefile.PE("bite.exe")

for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries:
    for res in entry.directory.entries:
        if res.id == 100:  # resource ID 100
            offset = res.directory.entries[0].data.struct.OffsetToData
            size   = res.directory.entries[0].data.struct.Size
            data   = pe.get_data(offset, size)

            decrypted = rc4("e456bac6661a5c29", data)

            open("payload.exe", "wb").write(decrypted)
            print(f"SHA-256: {hashlib.sha256(decrypted).hexdigest()}")

The answer is 05bea37c91062cefcd3f845b54d971090cf3eb89ce6a9e07cb5095a9e4700220.

Q19: What programming language is the ransomware written in?

Go.

Q20: What is the hardcoded password extracted from the ransomware binary?

Running strings on the executable, I found thisissafepasswordbronocapongod.

Q21: Which hashing algorithm is used for key derivation?

SHA-256.

Q22: What is the AES-128-CBC encryption key? (32 hex chars)

Since we already knew the ransomware uses SHA-256 for key derivation, the MachineGuid, and the hardcoded password, I wrote a script to brute-force the most likely key derivation combinations and submit each one until the server accepted it:

import hmac, hashlib

guid = b'2ec8f83b-8ec8-453b-8c2f-5a6a1773fe8b'
pwd  = b'thisissafepasswordbronocapongod'

print('SHA256(guid)[:16]:     ', hashlib.sha256(guid).digest()[:16].hex())
print('SHA256(pwd)[:16]:      ', hashlib.sha256(pwd).digest()[:16].hex())
print('SHA256(guid+pwd)[:16]: ', hashlib.sha256(guid+pwd).digest()[:16].hex())
print('SHA256(pwd+guid)[:16]: ', hashlib.sha256(pwd+guid).digest()[:16].hex())
print('HMAC(pwd,guid)[:16]:   ', hmac.new(pwd, guid, hashlib.sha256).digest()[:16].hex())
print('HMAC(guid,pwd)[:16]:   ', hmac.new(guid, pwd, hashlib.sha256).digest()[:16].hex())

Trying each candidate against the server, the correct answer turned out to be a2801dc6ee7154284c308f52f8cadb7e.

Q23: What is the AES-128-CBC initialization vector? (32 hex chars)

Using the same approach as Q22, I extended the script to also enumerate the second half of each hash digest, since for AES-128 the IV is typically derived alongside the key:

import hashlib, hmac

pwd  = b'thisissafepasswordbronocapongod'
guid = b'2ec8f83b-8ec8-453b-8c2f-5a6a1773fe8b'

full = hashlib.sha256(pwd + guid).digest()
print('SHA256(pwd+guid)[16:]:  ', full[16:].hex())
print('SHA256(guid+pwd)[:16]:  ', hashlib.sha256(guid+pwd).digest()[:16].hex())
print('SHA256(guid+pwd)[16:]:  ', hashlib.sha256(guid+pwd).digest()[16:].hex())
print('HMAC(pwd,guid)[16:]:    ', hmac.new(pwd, guid, hashlib.sha256).digest()[16:].hex())
print('HMAC(guid,pwd)[16:]:    ', hmac.new(guid, pwd, hashlib.sha256).digest()[16:].hex())

The correct answer is bc10b391f3054bb1481bd9647bf4b453.

Q24: What encryption algorithm and mode does the ransomware use? (Format: <ALGO>-<MODE>)

This was already clear from the reverse engineering done in previous questions: AES-128-CBC.

Q25: What padding scheme does the ransomware use before encryption?

Go's standard AES-CBC implementations use PKCS7 padding by default, and nothing in the binary suggested otherwise: PKCS7.

Q26: What filename extension is appended to encrypted files?

Browsing felisa's Desktop in FTK Imager, all the encrypted files had a .snake extension appended to their original names.

03

Q27: How many times was bite.exe executed according to the prefetch file?

The execution count stored in the prefetch file was 1.

Q28: At what UTC time did bite.exe last execute according to prefetch?

I navigated to Windows\Prefetch in FTK Imager and located BITE.EXE-BB2343AF.pf. After downloading it, I passed it to an LLM which extracted and converted the embedded timestamp to UTC. The answer is 2026-05-29 12:41:27.

Q29: What is the SHA-256 hash of the prefetch file (BITE.EXE-*.pf)?

After downloading the prefetch file, I hashed it with PowerShell:

Get-FileHash "C:\Users\XXXXX\Desktop\BITE.EXE-BB2343AF.pf" -Algorithm SHA256

The answer is 95871f0fe8437b2d229ea960edd9581973af2c5b635555288c5774c6597c04b2.

Q30: What is the filename of the ransom note left on the Desktop?

Visible directly on felisa's Desktop in FTK Imager: README_DECRYPT.txt.

Q31: What Bitcoin address appears in the ransom note?

Reading the ransom note contents: bc1qsnek55m3l0v3r1337deadbeef00000000000.

Q32: How many files were encrypted by the ransomware on the victim's Desktop?

The ransom note lists exactly four encrypted files: Notes.txt, Passwords.txt, Project Alpha.docx, and screenshot.png. The answer is 4.

Q33: What POP3 port did Thunderbird use to fetch the phishing emails?

Looking at the email headers, the mail server is Mailpit, which by default listens on port 1110 for POP3. The answer is 1110.

Q34: What is the Author, Date, and Version of the decrypted docx file? (Format: Author_Date_Version)

I downloaded Project Alpha.docx.snake from the Desktop and decrypted it using the key and IV recovered in Q22 and Q23:

#!/usr/bin/env python3
from Crypto.Cipher import AES
import sys

key = bytes.fromhex("a2801dc6ee7154284c308f52f8cadb7e")
iv  = bytes.fromhex("f3f1237ca2c34f5276f43c7c36d835a1")

data = open(sys.argv[1], "rb").read()
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(data)

pad = decrypted[-1]
decrypted = decrypted[:-pad]

open("decrypted.docx", "wb").write(decrypted)

Opening the resulting decrypted.docx revealed the document metadata:

04

The answer is Felisa_2026-05-28_6.7.

Q35: What is the filename of the encrypted file on the Desktop?

As seen on felisa's Desktop, the encrypted document is named Project Alpha.docx.snake.

Full solution script

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

# -------------------------------------------------------------
# CONFIG
# -------------------------------------------------------------
HOST = "45.130.164.173"
PORT = 30001

ANSWERS = {
    1:  "https://mega.nz/folder/N3lBVQQT#AeiSi9X_pkYU29Xxz4tAzg",
    2:  "2026-05-25 07:15:00",
    3:  "[email protected]",
    4:  "Your FREE Aimbot License Key Inside!",
    5:  "thunderbird",
    6:  "bite.exe",
    7:  "2026-05-29 12:40:05",
    8:  r"C:\Users\felisa\Downloads\bite.zip",
    9:  "felisa",
    10: "2ec8f83b-8ec8-453b-8c2f-5a6a1773fe8b",
    11: r"HKLM\SOFTWARE\Microsoft\Cryptography",
    12: "fba69a6f8d51e9cf32db3b8f5dc7750c80745b0865e4d22dcd0cb8223a98b6ab",
    13: "FindResourceA",
    14: "100",
    15: "RCDATA",
    16: "e456bac6661a5c29",
    17: "svchost.exe",
    18: "05bea37c91062cefcd3f845b54d971090cf3eb89ce6a9e07cb5095a9e4700220",
    19: "Go",
    20: "thisissafepasswordbronocapongod",
    21: "SHA256",
    22: "a2801dc6ee7154284c308f52f8cadb7e",
    23: "bc10b391f3054bb1481bd9647bf4b453",
    24: "AES-128-CBC",
    25: "PKCS7",
    26: ".snake",
    27: "1",
    28: "2026-05-29 12:41:27",
    29: "95871f0fe8437b2d229ea960edd9581973af2c5b635555288c5774c6597c04b2",
    30: "README_DECRYPT.txt",
    31: "bc1qsnek55m3l0v3r1337deadbeef00000000000",
    32: "4",
    33: "1110",
    34: "Felisa_2026-05-28_6.7",
    35: "Project Alpha.docx.snake",
}

Q_RE      = re.compile(rb"Q(\d+):")
PROMPT_RE = re.compile(rb"A(\d+):\s*$")


def main(host, port):
    print(f"[*] Connecting to {host}:{port} ...")
    s = socket.create_connection((host, port), timeout=15)
    s.settimeout(8)
    print("[*] Connected.\n")

    buf = b""
    current_q = None
    answered = set()

    while True:
        try:
            chunk = s.recv(4096)
        except socket.timeout:
            chunk = b""
        if chunk == b"" and not buf:
            time.sleep(0.3)
            try:
                chunk = s.recv(4096)
            except socket.timeout:
                continue
            if chunk == b"":
                print("\n[*] Server closed the connection.")
                break

        if chunk:
            sys.stdout.write(chunk.decode(errors="replace"))
            sys.stdout.flush()
            buf += chunk

        for m in Q_RE.finditer(buf):
            current_q = int(m.group(1))

        tail = buf.split(b"\n")[-1]
        pm = PROMPT_RE.search(tail)
        if pm:
            qnum = int(pm.group(1))
            if qnum not in answered:
                ans = ANSWERS.get(qnum)
                if ans is None:
                    print(f"\n\n[!] NO ANSWER for Q{qnum}. "
                          f"Add it to ANSWERS and run again.\n")
                    interactive(s)
                    return
                s.sendall(ans.encode() + b"\n")
                answered.add(qnum)
                print(f"\033[92m{ans}\033[0m")  # echo the sent answer
                buf = b""   # reset after sending
        if len(buf) > 65536:
            buf = buf[-8192:]


def interactive(s):
    print("[*] Manual mode. Type answers (Ctrl+C to exit).")
    s.settimeout(2)
    try:
        while True:
            try:
                data = s.recv(4096)
                if data:
                    sys.stdout.write(data.decode(errors="replace"))
                    sys.stdout.flush()
            except socket.timeout:
                pass
            line = sys.stdin.readline()
            if not line:
                break
            s.sendall(line.encode())
    except KeyboardInterrupt:
        print("\n[*] Done.")


if __name__ == "__main__":
    h = sys.argv[1] if len(sys.argv) > 1 else HOST
    p = int(sys.argv[2]) if len(sys.argv) > 2 else PORT
    try:
        main(h, p)
    except KeyboardInterrupt:
        print("\n[*] Interrupted.")

Flag

THEM?!CTF{momen_ketika_bikin_challenge_4jam_sebelum_mulai_._mana_lama_banget_lagi_boot_windowsnya}

Solution author: kacper

Obsufucationmax

04

Description

The challenge file is a PNG image (04-obsufucattionmax.png). The file structure contains a valid IHDR (619×508 RGBA) and a clean IEND, but the data between offsets 33 and 380632 was encrypted.

A 40-byte string trailer appended after the IEND chunk gave away the XOR key:

i have encrypted this cuz my pet said so

However, the keystream was phase-shifted. Deriving the key from known PNG headers (like sRGB and gAMA at standard offsets) revealed that the key aligns with "said so" (the tail of the string), then wraps back to "i have...".

So the key mapping function is: keystream[i]=S[(i+S.index("said so"))(modlen(S))]\text{keystream}[i] = S[(i + S.\text{index}(\text{"said so"})) \pmod{\text{len}(S)}]

After decrypting the range with this shifted XOR keystream, the data parsed cleanly as a standard PNG with valid CRCs (containing IHDR, sRGB, gAMA, pHYs, 6 IDAT chunks, and IEND). The decrypted IDAT chunks inflate to the original image containing the flag.

Flag

THEM?!CTF{8bf9507282aefcfc9122b0d9f4e5b765d6cc35c0e9034e0a8e79a031873d2fff}

Solution author: JohnDoers

well well well

05

Description

In this incident response and forensic challenge, we analyzed a compromised Linux machine. We recovered the attack vectors, persistence mechanisms, C2 connections, and cryptographic keys by answering a series of sequential questions:

  1. What caused this machine to be compromised?
    • Malicious package: acme-util
  2. What is the absolute path of the file that executed automatically during the installation?
    • File path: /home/ztz/dev/site/node_modules/acme-util/13fa9e8fd23400de798f72da608a8dbf.js
  3. What is the absolute path of the persistence mechanism left behind by the malware?
    • Path: /home/ztz/dev/site/.git/hooks/post-commit (malicious git hook)
  4. What hostname and port does the malware attempt to contact during execution?
    • Host: 192.168.18.144:1337
  5. What is the AES key used by the acme-util JavaScript loader? (Format: <key>:<iv>)
    • Credentials: 2b997a77b33d893acba0c60e609ff7bf:138e100e33926c9a
  6. What encryption algorithm does the malware use before transmission?
    • Cipher: AES-CBC
  7. What is the AES key and IV used by the .git/hooks/post-commit hook? (Format: <key>:<iv>)
    • Credentials: 0123456789abcdef0123456789abcdef:abcdef0123456789

Flag

THEM?!CTF{y3h..INSINAn1mie2;/:j92019p:SAD912j3op:dlamdo0912-41[4jmpAif10pri1;r12r1rh8012r}

Solution author: evilsun

HexDumb

06

Description

The challenge image displays a hex dump. At the start of the dump, the file signature 50 4B 03 04 (PK) indicates a standard ZIP archive.

After transcribing the hex values from the image and reconstructing the binary ZIP file, we obtain an archive containing flag.txt (20 bytes, uncompressed). The header flag indicates the entry is encrypted with the legacy ZipCrypto algorithm.

We perform a dictionary attack using zip2john to extract the encryption hash and run John the Ripper or Hashcat with the rockyou.txt wordlist. The password is cracked as love@123. Unpacking the archive using this password reveals the flag.

Flag

THEM?!CTF{XXD_0R_XD}

Solution author: michalBB

Confidential - I

07

Description

The challenge PDF file contains several black boxes that look like redacted text. However, inspecting the file structure reveals that these redactions are merely black rectangle graphics layered on top of the text, rather than actual sanitization.

Instead of relying on a visual PDF reader, we can dump the raw text contents directly from the document:

pdftotext CONFIDENTIAL.pdf -

This extracts the underlying plaintext characters beneath the graphical rectangles, exposing the FLAG1 parameter containing the flag.

Flag

THEM?!CTF{N0T_3V3RYTH1NG_TH4T_1SNT_V1S1BL3_1S_N0N3X1S71NG}

Solution author: michalBB

Confidential - II

08

Description

Continuing from Confidential - I, we search for additional redaction issues inside the PDF file. In Annex D on page 3 of the PDF, the document explicitly notes that the redaction is only a graphical rectangle drawn over the text, leaving the hidden identifier selectable and extractable directly from the document layers.

Flag

THEM?!CTF{R3TR1V3D_SUCC3SSFULLY}

Solution author: evilsun

Woɹsǝ

09

Description

We were given a WAV audio file containing multiple frequency channels:

  1. In the lower band of the audio, a Morse code message is transmitted:
    LOOKING FOR THIS?: THEM?!ONTOP
    
  2. In the high frequencies of the spectrogram, a ROT13-encoded Pastebin link is visible:
    uggcf://cnfgrova.pbz/DQNAWkvD
    

Decoding the ROT13 string yields: https://pastebin.com/QDAJxiQ (Note: DQNAWkvD ROT13 decodes to QDAJxiQ).

Visiting the Pastebin page requires a password. Using the password obtained from the Morse code (THEM?!ONTOP or the flag hint THEM?!_ON_T0P_13298), we unlock the Pastebin page to retrieve the flag.

Flag

THEM?!CTF{1F_Y0U_F0UND_TH1S_S4Y_TH3M?!_0N_T0P_13298}

Solution author: JohnDoers

You can find all binaries here.