Flash CTF - PasswordlessChecks

PasswordlessChecks Writeup

Overview

The handout is a USB capture and a Windows installer. The capture records an Xbox controller being used, and the installer holds an RPG Maker game called NamingSimulator that opens onto a name entry screen and nothing else. Whoever made the capture spelled the flag on that screen with the D-pad and a face button. Decoding the controller reports gives the button presses, and walking those presses across the game's character grid spells the flag back out.

Background

USB devices report their state on an interrupt endpoint at a fixed polling interval. A gamepad sends the same report over and over while nothing changes, so a capture contains far more frames than there were actions, and the interesting part is the transitions rather than the frames themselves.

An Xbox controller does not speak USB HID. Microsoft's wireless protocol, GIP, carries its own report layout, which is why Wireshark shows the payloads as opaque bytes rather than decoding named axes and buttons. The layout is documented by the open source Linux driver for these controllers, xone, in driver/gamepad.c. That is enough to find the face buttons, the D-pad, the triggers and the sticks inside each report.

The other half is the target application. RPG Maker VX draws its name entry screen from a character table defined in the engine's own Ruby scripts, Window_NameInput. The cursor starts at the top left, the D-pad walks it around with wraparound at every edge, and a confirm button appends the character under it. Anyone who knows the table can turn a movement sequence back into text without ever running the game.

Reconnaissance

Opening capture.pcapng in Wireshark shows USB traffic rather than network traffic, captured with USBPcap. Most frames are short and carry no payload at all. Filtering for frames that actually contain data narrows it to a single interrupt IN endpoint, 0x82, carrying 48 bytes each:

usb.endpoint_address == 0x82 && usb.data_len == 48

205 of those are in the capture. The payloads change only in a few bytes, and only in bursts, which is what a controller being nudged around a menu looks like.

The description says where the traffic came from, so the report layout can be read off the xone driver:

https://github.com/medusalix/xone/blob/master/driver/gamepad.c

Lining that up against the bytes, the fields land as follows, counting bits from the least significant bit of each byte:

  • byte 4, bits 4 to 7: A, B, X, Y
  • byte 5, bits 0 to 3: up, down, left, right
  • bytes 6 to 9: the triggers
  • bytes 10 onward: the stick axes

The sticks never move in this capture. Only the D-pad and X are ever pressed, apart from one A press at the start and one at the end.

The installer is the other half. It is a CAB archive despite the .exe extension, so its contents come out without a Windows box:‍

7z l NotAGame.exe

‍
Inside is a stock RPG Maker VX layout: Game.exe, Game.ini and Game.rgss2a. The ini names it:

[Game]
Library=RGSS202E.dll
Scripts=Data\Scripts.rvdata
Title=NamingSimulator

A naming simulator. The game exists only to present a name entry screen.

Exploitation

Because a held button repeats across many frames, the presses are the rising edges. Taking those in order gives a single clean sequence:

A  DD RRR X  UU RRR X  DDDD L X  DDDD LLL X  UUUU X  DDD X  L X  ...  X  A

Sixteen X presses separated by D-pad movement. Sixteen characters.

Running the game and letting the recorded inputs drive it is the direct route, and is what the author did: read the capture, convert each report to the matching arrow key and confirm key, and replay it with a keyboard automation library while the game has focus. writeup/author-replay.py is that script.

It can also be done entirely offline, which avoids needing Windows at all. The cursor starts at the top left of the grid and wraps at every edge, so tracking it is a few lines of arithmetic. The only thing missing is the grid itself, and it is worth being careful here: this game does not use the RPG Maker VX Ace default table. Assuming the default decodes all the letters correctly and then produces garbage for every digit, which is a convincing enough near miss to send you looking in the wrong place.

The real table lives in the game's own scripts, inside the encrypted archive. Game.rgss2a is an RGSSAD version 1 file: a plaintext header, then entries whose name lengths, names, sizes and contents are XORed against a keystream seeded at 0xDEADCAFE and advanced with key = key * 7 + 3 after every word. Decrypting Data/Scripts.rvdata gives a Ruby marshal blob holding every engine script as a zlib stream. Inflating them and looking for Window_NameInput produces the grid:‍

ENGLISH = [ 'A','B','C','D','E',  'a','b','c','d','e',
            'F','G','H','I','J',  'f','g','h','i','j',
            'K','L','M','N','O',  'k','l','m','n','o',
            'P','Q','R','S','T',  'p','q','r','s','t',
            'U','V','W','X','Y',  'u','v','w','x','y',
            'Z',' ',' ',' ',' ',  'z',' ',' ',' ',' ',
            ' ',' ',' ',' ',' ',  ' ',' ',' ',' ',' ',
            '1','2','3','4','5',  ' ',' ',' ',' ',' ',
            '6','7','8','9','0',  ' ',' ',' ',' ','OK']

‍
Ten columns, nine rows. The digits are 12345 and 67890 on the bottom two rows, where the engine default has 01234/!#$%& and 56789/()*+-. That is the discrepancy that breaks a default-table decode.

With the real grid, the first few groups check out immediately. Starting at A, down twice and right three times lands on N; up twice and right three times lands on b; down four and left one lands on u.

Getting the Flag

Walking all sixteen selections across the grid:

python3 writeup/solve.py -f dist/capture.pcapng


[*] 205 gamepad reports on endpoint 0x82
[*] button presses: A D D R R R X U U R R R X D D D D L X D D D D L L L X U U U U X D D D X L X R U U U U U R R L L L U X R R R R R R R D D X L L D X L U U U U R X D D D L L L X U X D X U X D X A
[+] MetaCTF{Nbu8W32GsvbSNSNS}

The same string appears in the game's name box if the inputs are replayed into it instead.

‍

Trust our customers

Interested in joining our team? Let’s connect!