You can download the challenge files and solutions here: MCTF2026-HeapItUp-main.zip
Description (Player Facing)
Subject: Investigation Log 902 We found a rogue numbers station broadcasting from a bunker sealed since the Cold War. The equipment is ancient. It doesn't erase the magnetic tape before recording new messages, it just overwrites them. The last broadcast contained a high-value secret. The operator tried to delete it, but the magnetic imprint remains on the loop. The tape segment is exactly 128 bytes long. If you send a new, empty signal of the exact same length, the old message might bleed through the static.
The Protocol
To interact with the TCP transmitter, send a 12-byte header followed by your Zlib-compressed payload.
[ 12-Byte Header (Big Endian) ] [ Payload ]
+------------+------------+------------+ +-----------------------+
| Opcode | Target | Payload | | |
| (HEX) | Size | Size | | Zlib Compressed |
| 0x00000001 | 0x00000080 | 0x........ | | Static / Padding |
+------------+------------+------------+ +-----------------------+
4 bytes 4 bytes 4 bytes Variable Length
Clues:
- Target Size: The ghost signal is trapped in a 128-byte (
0x80) tape loop. Request this exact size to access it. - Interference: The tape header is damaged. You must send ~16 bytes of "static" (padding) to skip the glitch and hear the message.
- Opcode: The transmitter supports multiple opcodes.
Solution
1. Reconnaissance & Analysis
We are given a network service. The description is actually a disguised technical specification for a Heap Exploitation challenge.
Let's break down the clues hidden in the description:
"It doesn't erase the magnetic tape before recording new messages; it just overwrites them."
This hints at an Uninitialized Memory or Use-After-Free (UAF) vulnerability. The server allocates memory ("recording new message") but doesn't clear it (memset or "erase"). If we allocate memory that was previously used by something important, the old data might still be there.
"The tape segment is exactly 128 bytes long."
The target memory chunk size is 128 bytes. In heap allocators (like malloc), chunks are often grouped by size. To get the "dirty" chunk containing the secret, we must request an allocation of exactly 128 bytes.
"You must send ~16 bytes of 'static' (padding) to skip the glitch..."
When a heap chunk is freed, the memory manager often writes metadata into the first few bytes of the data area. If we print the chunk immediately, this metadata looks like garbage (the "glitch") and might stop string functions from printing the rest (e.g 0x00 bytes). We need to overwrite these first ~16 bytes with our own data (padding) to reach the flag.
The Protocol
The description gives us the exact binary structure of the packet we need to send:
- Header (12 Bytes):
Opcode(4 bytes):0x1(implied by the diagram).Target Size(4 bytes):0x80(128 bytes).Payload Size(4 bytes): The length of our Zlib data.
- Payload:
- Our input data, compressed with Zlib.
2. Developing the Exploit strategy
Based on the analysis, the server logic seems to be:
- Receive Header.
- Allocate a buffer of
Target Size(128 bytes). - Decompress our
Payloadinto this buffer. - Send the buffer back to us.
The Attack Plan:
- We request a 128-byte buffer. The allocator will give us the "dirty" chunk that previously held the flag.
- We send a payload that decompresses to 16 bytes of 'A's.
- The server writes "AAAAAAAAAAAAAAAA" into the start of the buffer, overwriting the "glitch" (heap metadata).
- The rest of the buffer (bytes 16-128) is not overwritten because our payload is too short. It still contains the Flag.
- The server sends the whole buffer back.
3. The Solution Script
Here is the Python script snippet implementing the protocol and the padding strategy.
We request a 128-byte buffer and send a payload that decompresses to 16 bytes of 'A's.
opcode = 1
uncompressed_size = 128
# We send 16 'A's. This will overwrite the metadata (the NULL pointer)
# and allow the server to read past the gap into the remaining flag.
payload_content = b"A" * 16
payload = zlib.compress(payload_content)
payload_size = len(payload)
print(f"Sending Payload (padding 16 bytes) -> Requesting 128 bytes")
header = struct.pack(">III", opcode, uncompressed_size, payload_size)
s.sendall(header + payload)
4. Execution & Flag
Running the script against the challenge instance:
[+] Connecting to 127.0.0.1:8080...
[+] Crafting packet: Opcode=1, Target=128, PayloadLen=24
[+] Response received!
Raw Hex: 414141414141414141414141414141414d4354467b4d5953555045524d49444e49474854464c41477d...
Decoded: AAAAAAAAAAAAAAAAMCTF{FLAG}
[SUCCESS] Flag found!