Day 17 Won’t You Guide My Drone Tonight?
Reverse engineer the protocol by AI:
Protocol Analysis
- Server: ctf.csd.lol:6969
- Protocol uses "KMPS" magic header (0x53504d4b)
- Message types: 1=handshake, 2=get state, 3=make move, 4=get flag/status
Game Mechanics
- Current node: 32-bit integer
C - Target node:
T(from type 4 response: "Reach 0x... to get the flag") - Neighbors/move vectors: list of 32-bit integers
[m1, m2, ..., mn] - Move operation: from node
u, move vectormtakes you to nodeu ^ m - To move: send
(m, u ^ m)in type 3 message - Server validates:
mmust be in neighbor list, and second value must equalu ^ m - Error message: "Not a neighbor of the current node" if validation fails
Problem Interpretation
Graph navigation problem:
- Nodes are 32-bit integers
- From node
u, can move tou ^ mfor eachmin neighbor list ofu - Need to find path from start
Cto targetT - Only see neighbors of current node
- Limited time per connection ("short amount of time before kicked off")
Challenges
- Graph structure unknown
- Move vectors change with position (different nodes have different allowed moves)
- Server appears to rate-limit connections
- Need to solve quickly within time limit
Key Insights
- Move vectors are XOR differences, not destination nodes
- Server maintains some state (likely per IP/session)
- Target is fixed location, changes displayed based on current position
Recommended Attack Strategy
- Implement graph exploration with reconnection
- Cache visited nodes and moves
- Use BFS to find path to target
- Execute path quickly within time limit
- Handle server rate-limiting with delays
Unresolved Questions
- Does server reset state on new connection?
- Is graph consistent or random?
- Time limit duration?
- Can multiple type3 messages be sent in one connection?
Code Structure Needed
- Graph explorer with BFS
- State persistence across connections
- Rate limit handling
- Fast path execution
According to the analysis, use random walk that skips already walked notes until we reach the target:
#!/usr/bin/env python3
import socket
import struct
import re
import time
from collections import deque
class GraphExplorer:
def __init__(self):
self.sock = None
self.current = None
self.target = None
def connect(self):
if self.sock:
self.sock.close()
self.sock = socket.socket()
self.sock.settimeout(3)
self.sock.connect(("ctf.csd.lol", 6969))
def send_msg(self, msg_type, data=b""):
print("Send", msg_type, data)
length = 8 + len(data)
header = struct.pack("<IBBH", 0x53504D4B, 1, msg_type, length)
self.sock.sendall(header + data)
def recv_exact(self, n):
data = b""
while len(data) < n:
chunk = self.sock.recv(n - len(data))
if not chunk:
raise ConnectionError("Connection closed")
data += chunk
print("Recv", data)
return data
def get_state(self):
"""Get current node and neighbors"""
# Type 2
self.send_msg(2)
resp = self.recv_exact(16)
C = struct.unpack("<I", resp[8:12])[0]
n = struct.unpack("<I", resp[12:16])[0]
neighbors = []
if n > 0:
data = self.recv_exact(4 * n)
for i in range(0, 4 * n, 4):
val = struct.unpack("<I", data[i : i + 4])[0]
neighbors.append(val)
self.current = C
return C, neighbors
def get_target(self):
"""Get target node"""
# Type 4
self.send_msg(4)
resp = self.recv_exact(72)
msg_text = resp[8:].decode("ascii", errors="ignore").rstrip("\x00")
match = re.search(r"0x([0-9a-f]+)", msg_text, re.IGNORECASE)
if match:
T = int(match.group(1), 16)
self.target = T
return T
return None
def move_to(self, neighbor):
"""Move to neighbor node"""
if not self.sock:
return False
v23 = neighbor
v24 = self.current ^ neighbor
data = struct.pack("<II", v23, v24)
self.send_msg(3, data)
try:
resp = self.recv_exact(12)
# Check if move succeeded
# Response should be 12 bytes: header + echoed v23
echoed = struct.unpack("<I", resp[8:12])[0]
if echoed == v23:
self.current = v24 # Update current
return True
else:
print(f"Unexpected response: {resp.hex()}")
return False
except:
print("Move failed or connection closed")
return False
def simple_solve(self):
"""Try to solve by exploring"""
self.connect()
self.send_msg(1)
self.recv_exact(16)
C, neighbors = self.get_state()
T = self.get_target()
history = []
while True:
history.append(C)
print(f"Start: 0x{C:08x}, Target: 0x{T:08x}")
print(f"Neighbors ({len(neighbors)}): {[hex(n) for n in neighbors]}")
# Check if target is direct neighbor
if T in neighbors:
print(f"Target is direct neighbor!")
if self.move_to(T):
print(f"Moved to target!")
# Get flag
self.send_msg(4)
resp = self.recv_exact(72)
flag_msg = resp[8:].decode("ascii", errors="ignore").rstrip("\x00")
print(f"Flag: {flag_msg}")
if flag_msg.startswith("csd{"):
return flag_msg
return None
# Find a new neighbor
while neighbors[-1] in history:
neighbors.pop()
n = neighbors[-1]
print(f"\nTrying neighbor: 0x{n:08x}")
if self.move_to(n):
C, neighbors = self.get_state()
continue
return None
def main():
explorer = GraphExplorer()
try:
flag = explorer.simple_solve()
if flag:
print(f"\nSUCCESS! Flag: {flag}")
return flag
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()