CTF HUB/SUNSHINECTF 2026 DOSSIER

SunshineCTF 2026: Forensics Operations Dossier

Full technical writeups for all 5 SunshineCTF forensics challenges: VoIP telephony stream carving, PNG IHDR height tampering, MSKLC typewriter snake trail graph walking, PPTM VBA macro deobfuscation, and dead key finite state machine analysis.

VOIP // RTP AUDIO100 PTS

Welcome Call!: VoIP Telephony Forensics & Audio Backmasking

Inspecting SIP handshakes and G.711 mu-law SDP parameters, extracting raw RTP voice streams via Python and Wireshark, and reversing audio backmasking in Audacity to recover the flag.

Step 1: SIP Handshake & Codec Triage

Analyzing `welcomecall.pcap` reveals a SIP voice call handshake on UDP port 5060. The SDP parameters specify a G.711 mu-law audio stream negotiated between ports 4000 and 4002.

CAPTUREwelcomecall.pcap
PROTOCOLSSIP (5060) / RTP
CODECG.711 PCMU (8kHz)
AUDIOBackmasked Speech
RTP Audio Carving Script (`extract_rtp.py`):
from scapy.all import rdpcap, UDP
import audioop, wave

packets = rdpcap('welcomecall.pcap')
rtp_packets = []

for pkt in packets:
    if UDP in pkt and (pkt[UDP].dport == 4002 or pkt[UDP].sport == 4000):
        payload = bytes(pkt[UDP].payload)
        if len(payload) > 12 and (payload[0] >> 6) == 2:
            seq = int.from_bytes(payload[2:4], 'big')
            cc = payload[0] & 0x0F
            rtp_packets.append((seq, payload[12 + cc * 4:]))

rtp_packets.sort(key=lambda x: x[0])
raw_audio = b''.join([data for seq, data in rtp_packets])

# Convert G.711 mu-law into 16-bit linear PCM WAV
pcm_data = audioop.ulaw2lin(raw_audio, 2)

with wave.open('call.wav', 'wb') as wf:
    wf.setnchannels(1)
    wf.setsampwidth(2)
    wf.setframerate(8000)
    wf.writeframes(pcm_data)

print(f"Extracted {len(rtp_packets)} RTP packets -> call.wav")

Step 3: Audio Backmasking in Audacity

Playing `call.wav` plays an incomprehensible reversed speech pattern. Loading the file into Audacity, selecting all audio, and applying Effect → Reverse immediately produces clear spoken English: “Thank you for playing... The flag is sun open bracket thankyouforplaying close bracket”.

Final Submitted Flag:
sun{thankyouforplaying}
PNG // IHDR REPAIR150 PTS

u cut me off: PNG IHDR Height Tampering & CRC32 Repair

Forensic analysis of a cropped Discord screenshot. Calculating scanline byte counts from decompressed IDAT streams, identifying hidden image height, patching IHDR dimensions in hex, and recalculating CRC32.

01. Challenge Overview & PNG Structure

We received a single image named hereyougo.png depicting a Discord chat. The message prompt at the bottom was abruptly truncated, pointing directly to IHDR height tampering.

02. Scanline Derivation from Decompressed IDAT

Bytes per scanline = 1 + (Width × 4) = 1 + (492 × 4) = 1969 bytes
Total Decompressed Bytes = 823,042 bytes
Actual Scanlines = 823,042 / 1,969 = 418 rows (Header declared only 382 rows!)

03. Hex Modification & CRC32 Patching

FieldOffsetOriginal HexPatched Hex
Height0x14 - 0x1700 00 01 7E (382)00 00 01 A2 (418)
IHDR CRC320x1D - 0x2040 84 29 2825 78 F5 7C
Python 1-Click Solver Script:
import struct, zlib

with open('hereyougo.png', 'rb') as f:
    data = bytearray(f.read())

# Correct height is 418 px (0x01A2) at offset 0x14 (byte 20)
struct.pack_into('>I', data, 20, 418)

# Recalculate CRC32 for IHDR chunk (offset 12 to 29)
new_crc = zlib.crc32(data[12:29]) & 0xFFFFFFFF
struct.pack_into('>I', data, 29, new_crc)

with open('recovered.png', 'wb') as f:
    f.write(data)

print("[+] Successfully repaired IHDR height and CRC!")
Final Submitted Flag:
sun{totallyoriginalchallengeidea}
MSKLC // GRAPH THEORY250 PTS

suntrail: MSKLC Keyboard Layout Snake Trail Walkthrough

Reverse engineering a Microsoft Keyboard Layout Creator (.klc) file. Decoding Shift State 0 directional arrows and Shift State 1 hidden hex chars to trace a snake path across staggered QWERTY keyboard keys.

01. Initial Reconnaissance & Format Triage

We were provided with a single file named suntrail.klc. A .klc file is a plain-text configuration file specifying scan codes, virtual keys, and shift states.

LAYOUT

10	Q	0	2198	0073	-1
11	W	0	2192	0077	-1
12	E	0	2198	0065	-1
13	R	0	2192	0073	-1
14	T	0	2198	0075	-1

1e	A	0	2198	0075	-1
1f	S	0	2196	0071	-1
20	D	0	2198	0072	-1
21	F	0	2196	005f	-1
22	G	0	2198	0063	-1
23	H	0	25a0	007d	-1

2c	Z	0	2192	006e	-1
2d	X	0	2196	007b	-1
2e	C	0	2192	0074	-1
2f	V	0	2196	0079	-1
30	B	0	2192	006b	-1
31	N	0	2196	0073	-1

02. Decoding the Two Shift State Columns

  • Shift State 0 (Arrows): 2198 (↘), 2192 (→), 2196 (↖), 25a0 (■ Stop)
  • Shift State 1 (Hidden ASCII): Hex chars representing ASCII text: 0x0073 ('s'), 0x0075 ('u'), 0x006e ('n')...

03. The Core Concept: Mechanical Stagger & Directed Graph

[Cycle 1] Q ↘ A ↘ Z → X ↖ S ↖ W → E → s u n { q w
[Cycle 2] E ↘ D ↘ C → V ↖ F ↖ R → T → e r t y _ s
[Cycle 3] T ↘ G ↘ B → N ↖ H → u c k s }
StepKeyDirectionNextCharRunning Flag
1Q↘ (2198)Ass
2A↘ (2198)Zusu
3Z→ (2192)Xnsun
4X↖ (2196)S{sun{
5S↖ (2196)Wqsun{q
6W→ (2192)Ewsun{qw
7E↘ (2198)Desun{qwe
8D↘ (2198)Crsun{qwer
9C→ (2192)Vtsun{qwert
10V↖ (2196)Fysun{qwerty
11F↖ (2196)R_sun{qwerty_
12R→ (2192)Tssun{qwerty_s
13T↘ (2198)Gusun{qwerty_su
14G↘ (2198)Bcsun{qwerty_suc
15B→ (2192)Nksun{qwerty_suck
16N↖ (2196)Hssun{qwerty_sucks
17H■ (25a0)Stop}sun{qwerty_sucks}
Python Automated Graph Walker:
import re

KEY_MAP = {
    (0,0): 'Q', (0,1): 'W', (0,2): 'E', (0,3): 'R', (0,4): 'T',
    (1,0): 'A', (1,1): 'S', (1,2): 'D', (1,3): 'F', (1,4): 'G', (1,5): 'H',
    (2,0): 'Z', (2,1): 'X', (2,2): 'C', (2,3): 'V', (2,4): 'B', (2,5): 'N'
}
COORD_MAP = {v: k for k, v in KEY_MAP.items()}

moves, payloads = {}, {}
with open('suntrail.klc', 'r', encoding='utf-16le', errors='ignore') as f:
    for line in f:
        m = re.match(r'^[0-9a-fA-F]+\s+([A-Z])\s+0\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)', line)
        if m:
            key, arrow, char_hex = m.groups()
            moves[key] = arrow.lower()
            payloads[key] = chr(int(char_hex, 16))

curr = 'Q'
flag = []
while curr in moves:
    flag.append(payloads[curr])
    arrow = moves[curr]
    if arrow == '25a0': break
    r, c = COORD_MAP[curr]
    if arrow == '2198': curr = KEY_MAP[(r + 1, c)]
    elif arrow == '2192': curr = KEY_MAP[(r, c + 1)]
    elif arrow == '2196': curr = KEY_MAP[(r - 1, c)]

print("[+] Recovered Flag:", "".join(flag))
Final Submitted Flag:
sun{qwerty_sucks}
PPTM // OLEVBA MACRO425 PTS

NAS coal: PPTM Macro Forensics & PowerShell Base64 Deobfuscation

Forensic analysis of a malicious PowerPoint Macro-Enabled Presentation (.pptm). Extracting VBA code via olevba, analyzing MediaCache.bas, and decoding UTF-16LE Base64 PowerShell payload to recover the flag.

01. Challenge Overview

We are provided with a Microsoft PowerPoint Macro-Enabled Presentation named gem_collection.pptm containing compiled VBA macros inside ppt/vbaProject.bin.

MediaCache.bas: Extracted VBA Macro:
Option Explicit

Public Sub RefreshCache()
    Dim encoded As String
    Dim commandLine As String
    encoded = "JABjAGEAbQBwAGEAaQBnAG4AIAA9ACAAJwBzAHUAbgB7AHkAdQBwAF8AaQBzAHMAYQBfAGcAZQBtAH0AJwANAAoAJABzAG8AdQByAGMAZQAgAD0AIAAnAGgAdAB0AHAAcwA6AC8ALwBnAGUAbQAtAGMAYQBjAGgAZQAuAGUAeABhAG0AcABsAGUALgBpAG4AdgBhAGwAaQBkAC8AYwBvAGEAbAAuAGIAaQBuACcADQAKACQAZABlAHMAdABpAG4AYQB0AGkAbwBuACAAPQAgACcAYwBvAGEAbAAuAGIAaQBuACcADQAKAFsAcABzAGMAdQBzAHQAbwBtAG8AYgBqAGUAYwB0AF0AQAB7AE8AcABlAHIAYQB0AGkAbwBuAD0AJwBkAG8AdwBuAGwAbwBhAGQAJwA7ACAAQwBhAG0AcABhAGkAZwBuAD0AJABjAGEAbQBwAGEAaQBnAG4AOwAgAFMAbwB1AHIAYwBlAD0AJABzAG8AdQByAGMAZQA7ACAARABlAHMAdABpAG4AYQB0AGkAbwBuAD0AJABkAGUAcwB0AGkAbwBuAGQAaQBvAG4AfQANAAoA"
    commandLine = "powershell.exe -NoProfile -EncodedCommand " & encoded
    Debug.Print commandLine
End Sub

03. Decoding PowerShell EncodedCommand

$campaign = 'sun{yup_issa_gem}'
$source = 'https://gem-cache.example.invalid/coal.bin'
$destination = 'coal.bin'
[pscustomobject]@{Operation='download'; Campaign=$campaign; Source=$source; Destination=$destination}
Final Submitted Flag:
sun{yup_issa_gem}
DEADKEY // FINITE AUTOMATA300 PTS

my eyes burn: MSKLC Dead Key Finite State Machine

Reverse engineering a Microsoft Keyboard Layout Creator (.klc) file. Analyzing dead key chaining and state transitions from scancode 29 to Unicode U+2600 (Black Sun With Rays) to extract the flag.

01. Challenge Overview

We are given a single file named boardwriter.klc. The prompt indicates that typing a specific keystroke sequence produces a “sun” glyph.

02. Dead Key Chaining Mechanics

Scancode 29 is mapped to 0060@ (entering Dead Key state). The dead key transition outputs another dead key with @, forming a finite state machine that terminates at U+2600 (Black Sun With Rays).

STEPCURRENT DEAD KEYINPUT HEXINPUT CHARNEXT STATE / OUTPUT
100600x0073s02d0@
202d00x0075u02ed@
302ed0x006en02b4@
402b40x007b{02ef@
502ef0x0070p02ba@
602ba0x0072r02c9@
702c90x0061a02d3@
802d30x0069i02e9@
902e90x0073s02bd@
1002bd0x0065e02cd@
1102cd0x0074t02e4@
1202e40x0068h02d8@
1302d80x0065e02ee@
1402ee0x0073s02d4@
1502d40x0075u02e1@
1602e10x006en02b0@
1702b00x007d}2600 (Black Sun With Rays)
Automated Python Script (`solve_deadkeys.py`):
import re

with open('boardwriter.klc', 'r', encoding='utf-16le', errors='ignore') as f:
    content = f.read()

deadkey_blocks = re.findall(r'DEADKEY\s+([0-9a-fA-F]+)\s*\n([0-9a-fA-F]+)\s+([0-9a-fA-F]+@?)', content)

transitions = {}
for state, in_char_hex, next_state in deadkey_blocks:
    transitions[state.lower()] = (int(in_char_hex, 16), next_state.lower())

current_state = '0060'
flag_chars = []

while True:
    if current_state not in transitions: break
    in_code, next_target = transitions[current_state]
    flag_chars.append(chr(in_code))
    if next_target == '2600':
        print(f"[+] Reached Sun Symbol (U+2600)! Flag: {''.join(flag_chars)}")
        break
    current_state = next_target.rstrip('@')
Final Submitted Flag:
sun{praisethesun}