Roo CTF 2025: Paddington (tuxops)
title: “Paddington” date: “2025-10-25” author: “TechnoDot (technodot)” description: ""
mrnullone, TechnoDot
Given the capture file chall.pcap, find the flag. chall.pcap was modified to hide the flag.
Opening the file, we see that almost every packet is the Modbus protocol. Putting not modbus into the filter bar yields a few TCP packets, which seem unimportant. Looking through the packets, we notice that in the ascii contents section, there are some jumbled ascii characters as well. There seems to be no rhyme nor reason to these characters, and it can reasonably be inferred that we’ll have to do deeper digging. There’s also a lot of malformed packets, but I can’t seem to make anything of them. Sorting through packets, one value that seems to change consistently is the Register Value.
![][image1]
This is intriguing. To compare differences between register values, I right clicked on the register value line and selected Apply as column. Now I could sort by register value, and see packets that contain register values.Wondering what the register values could correlate to, I noticed that they had the same formatting as ascii codes. I looked at the most common register value, 32, which correlates to space in ascii. This checks out. I now turn to my trusty cli tool tshark to extract all packets that contain register values and put them in order.
tshark -r chall.pcap -Y "mbtcp.len==5" -T fields -e modbus.regval_uint16 > out.txt
This in turn put all of the ascii codes into out.txt, which I then converted to text using a small python one liner.
python3 -c "for line in open('out.txt','r'): print(chr(int(line)),end='')"
![][image2]
That’s weird. It appears to be scrambled but readable all at the same time. I wonder if there’s multiple streams that are combining together to cause a jumbled string, sort of a mesh.
Jumping back to wireshark, I checked TCP streams by going to Statistics > Conversations > IPv4. From this, I can see that there’s three separate streams, which could be interfering with one another. Thus, I add a filter specifying the TCP stream to the tshark command.
tshark -r chall.pcap -Y "tcp.stream==0 and mbtcp.len==5" -T fields -e modbus.regval_uint16 > out.txt
![][image3]
Nice, now we have legible text! Let’s keep trying with the rest of the TCP streams.
![][image4]![][image5]
Hmm. It’s almost as if the flag has been cut off, almost like the text that's supposed to be there is Malformed. Almost like those Malformed Packets we saw earlier…
TechnoDot here! We need to repair all of those malformed packets. Sorting by the Info column, we can see all of the malformed packets in one place. Looking for similarities, we see that they are all from 238.0.0.6 to 238.0.0.5. Filtering by ip.src == 238.0.0.6 and ip.dst == 238.0.0.5 and sorting by number, in the register values, we can read flag{ before the remaining packets are malformed, confirming our suspicions. Now, the primary objective is to repair the malformed packets. In the intact packets, at position 0x37 we see byte 0xe8. However, in the malformed packets, at the same position, we see ASCII string \xe8.
0000 00 00 00 00 00 00 00 00 00 00 00 00 08 00 45 00 ..............E. 0010 00 33 00 01 00 00 40 06 9e b8 ee 00 00 06 ee 00 .3....@......... 0020 00 05 01 f6 08 a8 01 f8 ae 07 18 f7 e0 5b 50 18 .............[P. 0030 20 00 1a d4 00 00 03 e8 00 00 00 05 64 03 02 00 ...........d... 0040 7b { 0000 00 00 00 00 00 00 00 00 00 00 00 00 08 00 45 00 ..............E. 0010 00 33 00 01 00 00 40 06 9e b8 ee 00 00 06 ee 00 .3....@......... 0020 00 05 01 f6 08 a8 01 f8 ae 12 18 f7 e0 67 50 18 .............gP. 0030 20 00 48 bd 00 00 03 5c 78 65 38 00 00 00 05 64 .H....\xe8....d 0040 03 02 00 55 ...U |
|---|
We also see that the length of all malformed packets are 3 bytes greater than those of intact packets. Therefore, we can infer that replacing \xe8 with actual byte 0xe8 will repair the packet. Giving all of the information to Claude Sonnet 4.5, we ask it to pretty please write a Python script with scapy to make the necessary modifications:
#!/usr/bin/env python3 from scapy.all import rdpcap, wrpcap, IP, Raw def modify_pcap(input_file, output_file): print(f"Reading packets from {input_file}...") packets = rdpcap(input_file) modified_count = 0 replacement_count = 0 print(f"Total packets: {len(packets)}") pattern = b'\x5c\x78\x65\x38' # 5c 78 65 38 replacement = b'\xe8' # e8 # Create a new list to hold modified packets modified_packets = [] for i, packet in enumerate(packets): # Check if packet has IP layer if IP in packet: ip_layer = packet[IP] # Check source and destination if ip_layer.src == "238.0.0.6" and ip_layer.dst == "238.0.0.5": print(f"\nPacket {i}: Found matching packet") print(f" Source: {ip_layer.src}, Dest: {ip_layer.dst}, Length: {len(packet)}") # Convert packet to bytes packet_bytes = bytes(packet) # Count occurrences before replacement occurrences = packet_bytes.count(pattern) if occurrences > 0: print(f" Found {occurrences} occurrence(s) of pattern 5c 78 65 38") # Replace the pattern in the raw bytes modified_bytes = packet_bytes.replace(pattern, replacement) # We need to find where the pattern was and modify the packet layers # The safest approach is to work at the Raw layer level # Let's check if there's a Raw layer if Raw in packet: # Get the raw payload raw_layer = packet[Raw] raw_bytes = bytes(raw_layer) if pattern in raw_bytes: # Replace in the raw layer new_raw = raw_bytes.replace(pattern, replacement) # Build a new packet with the same structure but modified raw data # Copy all layers except Raw new_packet = packet.copy() # Remove the Raw layer if Raw in new_packet: new_packet[Raw].load = new_raw modified_packets.append(new_packet) modified_count += 1 replacement_count += occurrences print(f" Replaced pattern in Raw layer. New length: {len(new_packet)}") else: # Pattern not in Raw layer, search in full packet # This is trickier - we'll reconstruct from modified bytes # Use Ether layer if present, otherwise IP from scapy.all import Ether if Ether in packet: new_packet = Ether(modified_bytes) else: new_packet = IP(modified_bytes) modified_packets.append(new_packet) modified_count += 1 replacement_count += occurrences print(f" Replaced pattern. New length: {len(new_packet)}") else: # No Raw layer - reconstruct from modified bytes from scapy.all import Ether if Ether in packet: new_packet = Ether(modified_bytes) else: new_packet = IP(modified_bytes) modified_packets.append(new_packet) modified_count += 1 replacement_count += occurrences print(f" Replaced pattern. New length: {len(new_packet)}") else: # No pattern found, keep original packet modified_packets.append(packet) else: # Doesn't match our filter, keep original packet modified_packets.append(packet) else: # No IP layer, keep original packet modified_packets.append(packet) print(f"\n{'='*60}") print(f"Summary:") print(f" Packets modified: {modified_count}") print(f" Total replacements made: {replacement_count}") print(f"\nWriting modified packets to {output_file}...") # Write modified packets to output file wrpcap(output_file, modified_packets) print(f"Done! Modified pcap saved to {output_file}") if __name__ == "__main__": input_file = "chall.pcap" output_file = "chall_modified.pcap" modify_pcap(input_file, output_file) |
|---|
Running it, we get the output:
PS C:\Users\technodot\workspace\vscode\ctfmcp> uv run chall.py Reading packets from chall.pcap... Total packets: 228 Packet 3: Found matching packet Source: 238.0.0.6, Dest: 238.0.0.5, Length: 65 Packet 9: Found matching packet Source: 238.0.0.6, Dest: 238.0.0.5, Length: 65 ### etc... ============================================================ Summary: Packets modified: 28 Total replacements made: 28 Writing modified packets to chall_modified.pcap... Done! Modified pcap saved to chall_modified.pcap PS C:\Users\technodot\workspace\vscode\ctfmcp> |
|---|
We repaired the pcap! Back to mrnullone:
Now armed with our repaired pcap file, loading it into wireshark yields more packets that contain register values than before. Now trying our conversion script on the new file yields us the flag.
![][image6]
flag{UND3r_TH3_M40G1C4L_M0DBU55555
Swapping flag out for roo, and adding the end brace, and we have our answer! Try it!
...
*crashout noises*
![][image7]
Opening the repaired file in Wireshark and applying the stream filter, we see packet 118 was not malformed, but chilling in the middle of the repaired packets. We see the register value corresponds to ascii 0. In the middle of the flag, we notice that magical is not spelled MAOGICAL. Deleting the zero, we get the flag:
roo{UND3r_TH3_M4G1C4L_M0DBU55555}