Meghnad's Trap

By Team Cryptonite · 3 min read

About the CTF: Pinaka CTF

Meghnad fought unseen, his commands hidden inside a storm of relays. The captured signals say many things, most of them false. Find the one true order in the invisible war. Submit to:…

Meghnad's Trap

**Pinaka CTF 2026** · Forensics · Team Cryptonite · All 44 Pinaka write-ups


Challenge

> Meghnad fought unseen, his commands hidden inside a storm of relays. The captured signals say many things, most of them false. Find the one true order in the invisible war.

>

> Submit to: https://pinaka-meghnad-validator.onrender.com

We're given two captures, captureuttara.pcap and capturevyoma.pcap.

"Many things, most of them false" told me early that this wasn't about decrypting anything. It was about working out which packets to trust. Having two captures also suggested they were the same network seen from two different places.

Recon

Both files are small (about 30 packets each), so I opened them in Wireshark and also looked at the IP conversations:


tshark -r capture_uttara.pcap -q -z conv,ip
================================================================================
IPv4 Conversations
Filter:<No Filter>
                                               |       <-      | |       ->      | |     Total     |    Relative    |   Duration   |
                                               | Frames  Bytes | | Frames  Bytes | | Frames  Bytes |      Start     |              |
10.7.0.3             <-> 10.7.0.6                   1 84 bytes       14 1,313 bytes      15 1,397 bytes     0.000000000         1.4000
10.7.0.1             <-> 10.7.0.255                 0 0 bytes         5 540 bytes       5 540 bytes     0.300000000         1.0000
10.7.0.4             <-> 10.7.0.6                   0 0 bytes         4 394 bytes       4 394 bytes     0.850000000         0.0600
10.7.0.1             <-> 10.7.0.2                   1 84 bytes        1 84 bytes        2 168 bytes     0.020000000         0.0200
================================================================================

for the other one:


tshark -r capture_vyoma.pcap -q -z conv,ip
================================================================================
IPv4 Conversations
Filter:<No Filter>
                                               |       <-      | |       ->      | |     Total     |    Relative    |   Duration   |
                                               | Frames  Bytes | | Frames  Bytes | | Frames  Bytes |      Start     |              |
10.7.0.3             <-> 10.7.0.6                   1 84 bytes       14 1,313 bytes      15 1,397 bytes     0.000000000         1.4002
10.7.0.7             <-> 10.7.0.6                   1 84 bytes        8 759 bytes       9 843 bytes     0.100012000         0.8801
10.7.0.1             <-> 10.7.0.255                 0 0 bytes         5 540 bytes       5 540 bytes     0.300036000         1.0001
10.7.0.4             <-> 10.7.0.6                   0 0 bytes         4 394 bytes       4 394 bytes     0.850102000         0.0600
================================================================================

Everything is UDP on 10.7.0.0/24, using ports 48000-48006. Wireshark doesn't recognise the protocol, but every payload begins with MG, so it's a custom protocol.

Meghnad's Trap: step screenshot

The last byte of each MAC matches the host: 02:00:00:00:00:02 is 10.7.0.3, :03 is 10.7.0.4, and so on. The node id is the last octet of the IP minus one. This turns out to matter later.

Figuring out the MG header

I lined up a few payloads in hex and compared them. The ones carrying readable text made it easy:


tshark -r capture_uttara.pcap -T fields -e frame.number -e ip.src -e udp.payload

data hex

Here's the layout I worked out:


0-1    "MG"
2      version
3      type (flags)
4      src node id
5      dst node id
6-7    session id
8      epoch
9      clock flag
10-17  timestamp (u64, microseconds)
18-19  message id
20     fragment index
21     fragment count
22-23  sequence number
24-25  payload length
26..   payload, then an 8-byte tag at the end

The types behave like bit flags: 0x01 SYN, 0x02 ACK, 0x03 SYN-ACK/RESYNC, 0x04 DATA, 0x10 beacon, 0x20 FIN. Orders are sent in plaintext, split into two fragments each.

Once I could read the headers, the junk was easy to spot.

  1. Beacons. 10.7.0.1 keeps broadcasting 32-byte blobs that look random to 10.7.0.255. I spent a few minutes thinking they might be encrypted orders, but they're just noise.
  1. A replay after the epoch change. Partway through, the real relay 10.7.0.3 sends RESYNC2 and moves to epoch 2. Right after that, 10.7.0.4 sends 4 packets (frames 16-19 in uttara). They:

tshark -r capture_uttara.pcap -Y 'udp.payload contains bb:f7:2c:03:5e:f1:44:48' -T fields -e frame.number -e eth.src -e ip.src -e udp.payload
12	02:00:00:00:00:02	10.7.0.3	4d4701040205a1a10100000000012a0f19c000020002000600164d41534b2054484520534b592043484152494f545320bbf72c035ef14448
16	02:00:00:00:00:03	10.7.0.4	4d4701040205a1a10200000000012a12ea5000020002000600164d41534b2054484520534b592043484152494f545320bbf72c035ef14448

This is the main bug in the challenge. The tag doesn't cover the epoch or the timestamp, so old packets can be relabelled with a new epoch and timestamp and a receiver will still accept them. The flag says the same thing.

3. A fake session that only Vyoma saw. The Vyoma capture contains a whole extra session, b2b2, from 10.7.0.7, with a very different set of orders:


WITHDRAW ALL DIVISIONS BEYOND THE RIVER BY FIRST LIGHT
SURRENDER THE NORTHERN FORD AND SUE FOR TERMS
DISBAND THE SKY CHARIOTS AND GROUND THE HOST

tshark -r capture_vyoma.pcap -Y 'ip.src == 10.7.0.7'

b2b2 packets

The Uttara capture never sees this session, and its timestamps come from a completely different clock (around 8247s, against around 5000s for everything else). The real orders show up in both captures. This one doesn't, so it's fake.

Solving

I wrote indrajit.py to parse both pcaps and keep only DATA fragments that pass all of these checks:

After that, the script sorts by sequence number, groups the fragments by message id and joins them back together.


python3 indrajit.py capture_uttara.pcap capture_vyoma.pcap 
{"command_transcript": "MOBILIZE THE NAGA DIVISION AT THE NORTHERN FORD\nHOLD THE SOUTHERN GATE UNTIL THE THIRD WATCH\nMASK THE SKY CHARIOTS BEHIND THE STORM WALL\nFEIGN RETREAT AT DUSK AND DRAW THEM TO THE MARSH\nSTRIKE THE CROWN CAMP WHEN THE MOON CLEARS THE RIDGE", "relay_identity": "2"}

MOBILIZE THE NAGA DIVISION AT THE NORTHERN FORD
HOLD THE SOUTHERN GATE UNTIL THE THIRD WATCH
MASK THE SKY CHARIOTS BEHIND THE STORM WALL
FEIGN RETREAT AT DUSK AND DRAW THEM TO THE MARSH
STRIKE THE CROWN CAMP WHEN THE MOON CLEARS THE RIDGE

Every surviving fragment came from node 2 (10.7.0.3), so that's the genuine relay.

Validator

The validator wanted a JSON POST to /verify with two fields: the command transcript (one order per line) and the relay identity.


{"command_transcript": "MOBILIZE THE NAGA ...\n...\nSTRIKE THE CROWN CAMP WHEN THE MOON CLEARS THE RIDGE", "relay_identity": "2"}

PINAKA{epoch_unb0und_the_signal_wears_a_false_timestamp}

Summary

  1. Both pcaps carry a custom UDP protocol, MG. I reversed the header by comparing payloads.
  2. The fake traffic is beacon noise, a spoofer replaying old fragments under a new epoch and timestamp with the same tags, and a fake session that only one capture saw.
  3. Filter on MAC = claimed src, unique tags, and sessions seen in both captures, then rebuild the fragments.
  4. Submit the 5 real orders with relay identity 2 to get the flag.

Read this post on CyberHx →