Introduction
This challenge was part of the Deloitte Hackazon Hacky Holidays “Unlock the City” 2022 CTF (yeah, what a name!). Labeled under the #ppc
category, which apparently stands for “professional programming challenge”, it was the final challenge under the “District 1” segment of the CTF and categorized under the Hard difficulty.
This was the first CTF problem which didn’t just challenge my ability to critically think and problem solve - it also challenged my motor control and hand-eye coordination. Why? Because I solved it by hand! I believe this challenge was meant to be solved using 100% programming, but I wanted to challenge myself. This was the process.
Port Authority
The harbour is in total chaos, the ships are no longer on course. The AI has disabled the brakes of all the ships and corrupted our control systems. The ships about to crash into each other, can you build a new AI that will rescue the ships and deliver the cargo?
Note
This is an instance-based challenge. No website URL will be provided!
Initial Inspection & Interaction
We’re initially provided with a link that takes us to a nice-looking webgame called the “Port Traffic Control Interface”:
Although we can’t directly interact with the game using keyboard controls, there’s a manual on the top-right which details the task:
According to this, we can start playing the game and controlling the ships that appear through a WebSocket connection, which is an API that enables two-way communication between a user’s browser and a server. This documentation describes the protocol alongside how to open/close and send/receive using JavaScript.
Heavily referencing the aforementioned documentation, I started off by installing the WebSocket package with npm i ws
, and then creating a solve.js
with the following code:
// Make sure you install WebSocket with "npm i ws"!const WebSocket = require('ws')// Regex so that I can freely paste the URL when the instance is changedconst url = 'https://[REDACTED].challenge.hackazon.org/'// Opens WebSocket connectionconst socket = new WebSocket(`wss://${url.replace(/^https?:\/\//, '')}ws`)
// Runs on socket open, equivalent to .addEventListener()socket.onopen = function () { console.log('[+] Connected!') // Converts object to string socket.send( JSON.stringify({ type: 'START_GAME', level: 1, }), )}
// Runs when output from server is receivedsocket.onmessage = function (event) { // Output is received in event console.log(`[-] ${event.data}`)}
Look what happens when we establish a connection - the game starts running, and we start receiving per-tick input from the server in our console:
$ node test.js[+] Connected![-] {"type":"GAME_START","level":{"id":1,"board":{"width":1886,"height":1188,"obstructions":[{"type":"HARBOUR_BORDER","area":[{"x":577,"y":0},{"x":627,"y":215.7142857142857}]},{"type":"HARBOUR_BORDER","area":[{"x":875,"y":0},{"x":925,"y":215.7142857142857}]},{"type":"BORDER_ROCK","area":[{"x":0,"y":0},{"x":577,"y":51}]},{"type":"BORDER_ROCK","area":[{"x":925,"y":0},{"x":1886,"y":51}]}],"harbour":[{"x":700,"y":0},{"x":850,"y":107.85714285714285}]},"mechanics":{"borderCollision":false,"sequentialDocking":true},"ships":[null]}}[-] {"type":"TICK","ships":[{"type":"SHIP_6","area":[{"x":472,"y":795},{"x":532,"y":1063.75}],"direction":"UP","speed":3,"id":0,"isDocked":false}]}[-] {"type":"TICK","ships":[{"type":"SHIP_6","area":[{"x":472,"y":795},{"x":532,"y":1063.75}],"direction":"UP","speed":3,"id":0,"isDocked":false}]}[-] {"type":"TICK","ships":[{"type":"SHIP_6","area":[{"x":472,"y":792},{"x":532,"y":1060.75}],"direction":"UP","speed":3,"id":0,"isDocked":false}]}[-] {"type":"TICK","ships":[{"type":"SHIP_6","area":[{"x":472,"y":789},{"x":532,"y":1057.75}],"direction":"UP","speed":3,"id":0,"isDocked":false}]}...
Let’s see what happens when we send the SHIP_STEER
command to the server after five seconds. We can do that with the setTimeout()
method in our socket.onopen
listener:
socket.onopen = function() { console.log("[+] Connected!"); // Converts object to string socket.send(JSON.stringify({ "type": "START_GAME", "level": 1 })); // Sends steer command after one second setTimeout(() => { socket.send(JSON.stringify({ "type": "SHIP_STEER", "shipId": 0 })); }, 5000);};
From the provided GIF, we can see that the ship will turn clockwise on its central point when told to steer!
With this, we have a goal: get the ship into the port by sending JSON instructions to the WebSocket server. However, it’s definitely a good idea to create some quality-of-life features first, such as:
- A way to convert our JSON data into an object we can reference
- A class which can construct objects for each ship
- An HTML/JS “controller”, which can be used to steer the ships with UI and to start new levels
Firstly, cleaning up the output involves parsing what we receive from the server, which we can do with the JSON.parse()
method. We’ll assign it into a variable named obj
(and also delete our steer-testing code):
// Sends steer command after one second setTimeout(() => { socket.send(JSON.stringify({ "type": "SHIP_STEER", "shipId": 0 })); }, 5000);};
// Runs when output from server is receivedsocket.onmessage = function(event) { // Output is received in event console.log(`[-] ${event.data}`); // Converts server output into object let obj = JSON.parse(event.data);};
Each tick, obj
will change to an object structured this way:
{ "type": "TICK", "ships": [ { "type": "SHIP_6", "area": [ { "x": 472, "y": 795 }, { "x": 532, "y": 1063.75 } ], "direction": "UP", "speed": 3, "id": 0, "isDocked": false } ]}
Check out the obj.type
key - there’ll be multiple types of these (including but not limited to "LOSS"
, "GAME_START"
). We’ll make it so that if obj.type
is "TICK"
, it will create a new Class
instance for each object in the obj.ships
array:
class Ship { // Initializes class object instance constructor(id, topLeft, bottomRight, direction) { this.id = id; this.topLeft = topLeft; this.bottomRight = bottomRight; this.direction = direction; } // Getter + abusing template literals get printState() { return `ID: ${this.id} | (${Math.floor(this.topLeft.x)},${Math.floor(this.topLeft.y)}) (${Math.floor(this.bottomRight.x)},${Math.floor(this.bottomRight.y)}) | DIR: ${this.direction}`; }}
// Runs when output from server is receivedsocket.onmessage = function(event) { // Converts server output into object let obj = JSON.parse(event.data); if(obj.type == "TICK") { let ships = []; // For each ship in obj.ships, push class object into ships array for(const i of obj.ships) { ships.push(new Ship(i.id, i.area[0], i.area[1], i.direction)); } // Call the string literal getter for(const i of ships) { console.log(i.printState); } }};
With this new Class, we can get both our own ships
array and really clean logging from the server:
$ node test.js[+] Connected!ID: 0 | (211, 256) (271, 524) | DIR: UPID: 0 | (211, 256) (271, 524) | DIR: UPID: 0 | (211, 252) (271, 520) | DIR: UPID: 0 | (211, 248) (271, 516) | DIR: UP...
Let’s finally get to solving the challenge. I’ll use subposts to break down each level.