342 words
2 minutes
ApoorvCTF 2026 - Routine Checks - Forensics Writeup

Category: Forensics Flag: apoorvctf{b1ts_wh1sp3r_1n_th3_l0w3st_b1t}

Challenge Description#

Routine system checks were performed on the city’s communication network after reports of instability.

Operators sent brief messages between nodes to confirm everything was running smoothly.

Most of the exchanges are ordinary status updates, but one message stands out as… different.

Analysis#

This one was a packet-forensics challenge where almost everything looked like normal short status chatter, so the fastest way in was to find the one TCP conversation that was much larger than the rest.

tshark -r challenge.pcap -q -z conv,tcp
... 
127.0.0.1:33610        <-> 127.0.0.1:5001              8      6232 bytes       3      206 bytes        5      6026 bytes
...

That outlier matched the challenge hint perfectly, so I pivoted straight to the suspicious packet and checked its payload length + byte edges.

# parse_frame14_info.py
import subprocess

line = subprocess.check_output([
    'tshark', '-r', 'challenge.pcap',
    '-Y', 'frame.number==14', '-T', 'fields', '-e', 'tcp.len', '-e', 'data'
], text=True).strip()

length, hexdata = line.split('\t')
print(f'tcp.len={length}')
print(f'data_prefix={hexdata[:40]}')
print(f'data_suffix={hexdata[-40:]}')
python parse_frame14_info.py
tcp.len=5688
data_prefix=3fd8ffe000104a46494600010102001c001c0000
data_suffix=14514514514514514514514514514514515fffd9

The payload looked like a JPEG (...d8 ff e0 ... JFIF ... ff d9) except for the first byte being 3f instead of ff, so I wrote a tiny extraction/repair script to rebuild the image exactly from that packet.

# extract_frame14.py
import subprocess

hexdata = subprocess.check_output([
    'tshark', '-r', 'challenge.pcap',
    '-Y', 'frame.number==14', '-T', 'fields', '-e', 'data'
], text=True).strip()

payload = bytes.fromhex(hexdata)
fixed = bytes([0xFF]) + payload[1:]

open('frame14_fix.jpg', 'wb').write(fixed)

print(f'tcp_payload_bytes={len(payload)}')
print(f'prefix_before_fix={payload[:8].hex()}')
print(f'prefix_after_fix={fixed[:8].hex()}')
python extract_frame14.py
tcp_payload_bytes=5688
prefix_before_fix=3fd8ffe000104a46
prefix_after_fix=ffd8ffe000104a46

At that point it turned into a neat stego check, and the empty-password extraction worked immediately.

smile

steghide extract -sf frame14_fix.jpg -p "" -xf realflag.txt -f
wrote extracted data to "realflag.txt".

I still checked the visible QR layer and got baited by a decoy flag in the image, which is exactly the kind of troll this category loves.

zbarimg frame14_fix.jpg
QR-Code:apoorvctf{this_aint_it_brother}
scanned 1 barcode symbols from 1 images in 0.02 seconds

tableflip

The real flag was in the steghide output file, so reading that file gave the final answer.

# print_flag.py
flag = open('realflag.txt', 'r').read().strip()
print(flag)
python print_flag.py
apoorvctf{b1ts_wh1sp3r_1n_th3_l0w3st_b1t}

Solution#

# extract_frame14.py
import subprocess

hexdata = subprocess.check_output([
    'tshark', '-r', 'challenge.pcap',
    '-Y', 'frame.number==14', '-T', 'fields', '-e', 'data'
], text=True).strip()

payload = bytes.fromhex(hexdata)
fixed = bytes([0xFF]) + payload[1:]
open('frame14_fix.jpg', 'wb').write(fixed)
python extract_frame14.py
steghide extract -sf frame14_fix.jpg -p "" -xf realflag.txt -f
python print_flag.py
apoorvctf{b1ts_wh1sp3r_1n_th3_l0w3st_b1t}
ApoorvCTF 2026 - Routine Checks - Forensics Writeup
https://blog.rei.my.id/posts/98/apoorvctf-2026-routine-checks-forensics-writeup/
Author
Reidho Satria
Published at
2026-03-10
License
CC BY-NC-SA 4.0