Xh4H

Home GitHub Twitter
22 September 2019

DragonSectorCTF - Reversing Switch Pro's USB keystrokes - PlayCAP

by Xh4H

We (TMHC) played DragonSectorCTF - Teaser - and ranked 42, pretty happy about it.

Challenge description

Miscellaneous, 203 pts
Difficulty: easy (53 solvers)

Here is a recording of the flag being entered into the app.html HTML5 application.
 - PlayCAP.pcapng
 - app.html

Action We are given a Packet Capture file (.pcap) and a link to a site. Let’s start by accessing the site.

There’s a 10x6 map of characters. Inspecting the source, we see an interesting function handleButtons:

It expects 6 different actions:

state parameter is not used at all there, it just checks that its not empty/undefined/falsy. handleButtons("right", "test") will move the selected character to the right by one cell.

We also see a few comments that will help us in the future:

checkChange is a defined function that checks the state of certain buttons every 50 miliseconds. After it’s declaration we see it’s looking for gamePads connected to the computer, therefore we know that a controller with pads was used to type the flag (read the challenge description again). X button is assigned to “reset” and A to “select”.

Time to open the pcap file.

We are given a network capture containing 5079 packets. After inspecting them we see many packets with the same length, 91.

We apply the following filter: frame.len == 91. All of these packets have the same data structure, and there is one value we care of: Leftover Capture Data. A quick google search reveals a ctf writeup, pretty similar to this challenge.

In order to work with the packet data, we are going to add the Leftover Capture Data as a column in order to export everything. Right click on a packet’s Leftover data and Apply as column.

Now we are ready to export all the data as .csv.

The Leftover Capture Data looks like this:

Small summary

After some googling, we find the following piece of code:

enum {
	SWITCH_BUTTON_USB_MASK_A = 0x00000800,
	SWITCH_BUTTON_USB_MASK_B = 0x00000400,
	SWITCH_BUTTON_USB_MASK_X = 0x00000200,
	SWITCH_BUTTON_USB_MASK_Y = 0x00000100,

	SWITCH_BUTTON_USB_MASK_DPAD_UP = 0x02000000,
	SWITCH_BUTTON_USB_MASK_DPAD_DOWN = 0x01000000,
	SWITCH_BUTTON_USB_MASK_DPAD_LEFT = 0x08000000,
	SWITCH_BUTTON_USB_MASK_DPAD_RIGHT = 0x04000000,

	SWITCH_BUTTON_USB_MASK_PLUS = 0x00020000,
	SWITCH_BUTTON_USB_MASK_MINUS = 0x00010000,
	SWITCH_BUTTON_USB_MASK_HOME = 0x00100000,
	SWITCH_BUTTON_USB_MASK_SHARE = 0x00200000,

	SWITCH_BUTTON_USB_MASK_L = 0x40000000,
	SWITCH_BUTTON_USB_MASK_ZL = 0x80000000,
	SWITCH_BUTTON_USB_MASK_THUMB_L = 0x00080000,

	SWITCH_BUTTON_USB_MASK_R = 0x00004000,
	SWITCH_BUTTON_USB_MASK_ZR = 0x00008000,
	SWITCH_BUTTON_USB_MASK_THUMB_R = 0x00040000,
};

These are the masks used for every single button in the Switch Pro. Next step was looking into the packets to see if we could find these values.

The 7th and 8th bytes tell us which button is being pressed:

The 11th and 12th bytes tell us which button from the DPAD is being pressed:

Time to parse the data

We wrote the following string to parse the dump into actual pressed buttons:

const csv = require("csvtojson");
let chars = [];

let dpad = {
	'01':'D',
	'02':'U',
	'04':'R',
	'08':'L'
}

let buttons = {
	'01': 'Y',
	'02': 'X',
	'04': 'B',
	'08': 'A'
}

let previousButton, previousDpad;


csv()
.fromFile("exported.csv")
.then((jsonObj)=>{
	for (const packet of jsonObj) {
		let switch_packet = packet["Leftover Capture Data"].replace("\"", "");
		let button_type = switch_packet.substr(6, 2); // 7th and 8th byte -> Button type
		let dpad_type = switch_packet.substr(10, 2); // 11th and 12th byte -> dpad type

		if (previousButton != button_type) {
			previousButton = button_type;
			if (buttons[button_type] != undefined) {
				chars.push(buttons[button_type]);
			}
		}

		if (previousDpad != dpad_type) {
			previousDpad = dpad_type;
			if (buttons[dpad_type] != undefined) {
				chars.push(dpad[dpad_type]);
			}
		}	
	}

	require("fs").writeFileSync("parsed.json", JSON.stringify(chars));
})

Important: Whenever you press a button, be it in a keyboard or a controller, you are sending a lot of packets with the same information, X button being pressed. In order to not have duplicated-useless data, we added the check of previousButton and previousDpad so that we don’t add more “X button was pressed” until X was in fact released (key up).

With the output, we use as well the following script in order to simulate the pressed buttons directly into the browser:

let goLeft = () => handleButtons('left', 'test');
let goRight = () => handleButtons('right', 'test');
let goUp = () => handleButtons('up', 'test');
let goDown = () => handleButtons('down', 'test');
let goReset = () => handleButtons('reset', 'test');
let goSelect = () => handleButtons('select', 'test');

let arr = ["Y","Y","Y","Y","Y","Y","Y","Y","Y","Y","Y","X","X","X","R","R","R","A","D","D","D","D","A","U","L","A","R","R","R","R","R","R","R","A","R","R","R","R","R","U","U","R","R","R","R","A","D","D","D","D","A","U","U","U","U","L","L","L","L","L","L","L","A","D","D","L","A","R","R","R","R","R","D","A","L","A","U","U","U","R","A","D","D","R","R","R","L","A","U","L","A","D","D","L","L","L","L","L","L","D","A","U","U","U","U","U","A","R","R","R","R","R","R","A","D","D","A","R","R","D","A","R","R","A","R","D","A","U","U","R","R","R","R","R","A","R","R","R","A","D","D","D","A","X"]

function insertKey(letter) {
	switch (letter.toLowerCase()) {
		case "l":
			goLeft();
			break;
		case "r":
			goRight();
			break;
		case "u":
			goUp();
			break;
		case "d":
			goDown();
			break;
		case "x":
			goReset();
			break;
		case "a":
			goSelect();
			break;
		default:
			console.log("Skipping");
	}
}

for (const letter of arr) {
	insertKey(letter.toLowerCase())
}

Flag: DrgnS{LetsPlayAGamepad}

Thanks for reading :)

tags: wireshark - dragonsector - ctf - switch - pro