Tags: network quake3 ppc

Rating: 0

_We didn't manage to solve this task during the CTF. I was close enough, so I decided to finish it and share the solution._

We are given pcap traffic of some OpenArena(it is an open-source implementation of quake3 engine) game session. Youtube clip reveals that during this session player draws the flag on the wall.

## OpenArena network protocol
We can get some idea of how this protocol works by looking at the [game sources](https://github.com/OpenArena/legacy/tree/master/engine/openarena-engine-source-0.8.8).
Basically it is some kind of reliable udp with adaptive huffman coding and delta compression, but we don't need to know all the specifics to solve this task.
Also, I found this cool python wrapper on engine's msg.c code to help with huffman decompression -- [q3huff](https://github.com/jkent/q3huff).
All we need to know is that all packets start with a sequence number, and contain a sequence of messages.

## Solution
My first idea was to replay client packets against our server and watch this session. I patched server to match challenge and serverId values from this session. However, when I was replaying client packets I realized that we actually need to replay server packets in order to see what the other player was doing. I got an idea to write server packets in a demo file, but I didn't manage to do that in time.

Next day, I decided to give it a try.
Demo recordings file format is pretty straightforward: it is a sequence of server packets:

[packet.seq|len(packet.data)|packet.data]

The only problem is that we can't just write network packets as is, we need to decode them first(some kind of spoofing protection):
python
decoded = []
index = 0
# modify the key with the last sent and acknowledged command
if string[index] == 0:
index = 0
if string[index] > 127 or string[index] == ord('%'):
key ^= (ord('.') << (i & 1)) & 0xFF
else:
key ^= (string[index] << (i & 1)) & 0xFF

index += 1
# decode the data with this key
decoded.append(data[i] ^ key)

As you can see, we need to maintain the list of server and client commands in order to decode all packets.

Thankfully, in this game session, all svc_serverCommand and clc_clientCommand messages are placed at the start of the packets, so we don't need to write parsers for every message type and just skip irrelevant ones.
Server part(client part looks the same):
python
def svc_parse(seq, msg):
msg.oob = False # enables Huffman decompression
while True:
if cmd != svc_serverCommand:
break
index = msg.read_long() & (64 - 1)
serverCommands[index] = s.encode('utf8') + b'\x00'
print(f'stored svc_serverCommand "{s}" at {index}')


Now we can decode server packets:
python
reliableAcknowledge = msg.read_long() & (64 - 1)
key = (CHALLENGE ^ seq) & 0xFF
data = msg_decode(data, 4, clientCommands[reliableAcknowledge], key)


And client packets:
python
reliableAcknowledge = msg.read_long() & (64 - 1)
key = (CHALLENGE ^ serverId ^ messageAcknowledge) & 0xFF
data = msg_decode(data, 12, serverCommands[reliableAcknowledge], key)


That's it. We can write these packets to the demo file and play it in the game client.
python
demo_file.write(struct.pack('<II', seq, len(data)))
demo_file.write(data)

In the recording we can see that the player writes the flag on the wall: PCTF{PEWPEWPEWWX}

Full solution code:
`python
from scapy.all import *
from collections import defaultdict
import q3huff
import struct

CHALLENGE = -1338626257 & 0xFFFFFFFF

FRAGMENT_SIZE = 0x514

svc_serverCommand = 0x5
clc_clientCommand = 0x4

serverCommands = defaultdict(lambda: b'\x00\x00')
clientCommands = defaultdict(lambda: b'\x00\x00')

decoded = []
index = 0
# modify the key with the last sent and acknowledged command
if string[index] == 0:
index = 0
if string[index] > 127 or string[index] == ord('%'):
key ^= (ord('.') << (i & 1)) & 0xFF
else:
key ^= (string[index] << (i & 1)) & 0xFF

index += 1
# decode the data with this key
decoded.append(data[i] ^ key)

def svc_parse(seq, msg):
msg.oob = False # enables Huffman decompression
while True:
if cmd != svc_serverCommand:
break
index = msg.read_long() & (64 - 1)
serverCommands[index] = s.encode('utf8') + b'\x00'
print(f'stored svc_serverCommand "{s}" at {index}')

def clc_parse(seq, msg):
msg.oob = False # enables Huffman decompression
while True:
if cmd != clc_clientCommand:
break
index = msg.read_long() & (64 - 1)
clientCommands[index] = s.encode('utf8') + b'\x00'
print(f'stored clc_clientCommand "{s}" at {index}')

sv_fragment_buffer = b''
cl_fragment_buffer = b''

return frag_size != FRAGMENT_SIZE, raw_data[4:4+frag_size]

demo_file = open('flag.dm_71', 'wb')
for packet in rdpcap('graffiti.pcap'):
if not (packet.haslayer(UDP) and packet[UDP].sport == packet[UDP].dport == 27961):
continue

is_server = str(packet[IP].src) == '192.168.151.139'

msg.oob = True # disables Huffman decompression

if seq == 0xFFFFFFFF:
# skip connectionless packets
continue

if is_server:
if seq & (1 << 31):
seq ^= (1 << 31)
sv_fragment_buffer += frag
if is_last:
data = sv_fragment_buffer
sv_fragment_buffer = b''
else:
continue