Reversing Yubikey’s Static Password
One of the functions that that Yubikey can provide is the option to “store” a static password on the token which will be “typed” out on the host whenever you press the button. Having already done quite of a lot of work on the USB HID implementation, I was curious to know how Yubico had decided to emulate the keyboard functionality. Ultimately, I was hoping that I’d be able to set all kinds of different modifiers like Ctrl+Alt+Del and Super+R to have a little more fun with it (BadUSB/Rubber Ducky style).
A look at the source code⌗
Luckily for us, the source for the Yubikey personalization tool has been open sourced and is on their Github page. The main file of interest here is this one. It defines what is basically a dictionary called keyMap
in a function called UsScanEdit()
.
#define SHIFT = 0x80;
UsScanEdit::UsScanEdit() {
keyMap = QMap<int, int>();
keyMap[0x00] = 0;
...
keyMap[0x09] = 0x2b; /* \t */
keyMap[0x0a] = 0x28; /* \n */
...
keyMap[0x2c] = 0x36; /* , */
...
keyMap[0x41] = 0x04 | SHIFT; /* A */
...
keyMap[0x61] = 0x04; /* a*/
...
keyMap[0x7f] = 0;
}
So, what’s going on here? Well, the binary exploitation fans among you would probably have spotted that A
is commented next to the 0x41
entry of the dictionary keyMap
. For those who aren’t aware, 0x41
is the ASCII for the uppercase letter A
so we know that this is just a lookup table of keyboard characters. (In fact, it turns out at Yubico are using Latin1
rather that ASCII as can be seen on line 57 here, but it doesn’t really make any difference).
This is why most of keyMap
is just set to 0
- they’re all the ASCII characters that don’t appear on a keyboard!
Keyboard Scan Codes⌗
The question now is: if this is lookup table, what is it looking up? These are actually the keyboard “scan codes”. Taking a look at this beast of PDF from the USB Foundation, all the way down on page 53, we get the table of all the scan codes listed under “Usage ID (Hex)” and their “Usage Name"s (or the names of the keys).
Scan Code | Keyboard Key |
---|---|
00 | Reserved |
01 | ErrorRollOver |
02 | POSTFail |
03 | ErrorUndefined |
04-1D | a/A … z/Z |
1E-27 | 1/!, 2/@, 3/#, 4/$, 5/%, 6/^, 7/&, 8/*, 9/(, 0/) |
28 | Return |
29 | Escape |
2A | Delete |
2B | Tab |
2C | Space |
2D-38 | -/_, =/+, [/{, ]/}, \/|, ;/:, ‘/”, `/~, ,,<, .,>, //? |
39 | CapsLock |
3A-45 | F1-F12 |
… | … |
E0 | Left Ctrl |
E1 | Left Shift |
E2 | Left Alt |
E3 | Left Super |
E4 | Right Control |
E5 | Right Shift |
E6 | Right Alt |
E7 | Right Super |
E8-FFFF | Reserved |
You get the idea. What’s important here is that every possible keypress corresponds to a two byte scan code which is what actually gets sent to the host to be interpreted as input. The Yubico personalization tool is taking our password as input, looking up each character’s scan code and configuring the Yubikey to spit this string of bytes back at the host whenever the button is pressed.
However, there’s still one thing we haven’t explained: modifiers. What happens if I want to send an input that requires multiple keys to be pressed at once, like a capital A (Shift+a) or Ctrl+r? It turns out that it’s got something to do with the #DEFINE SHIFT=0x80;
from the source code extract further back. To understand what’s going on, we need to know a little bit about HID packets.
HID Packets⌗
If we restrict our attention to USB keyboards, then the HID protocol is surprisingly simple. Ignoring the USB layer stuff which initiates the connection by sending across a bunch of identifying information like VID/PIDs, serial numbers, etc (plug in a USB and check the output of dmesg
to get an idea), let’s assume that you’ve plugged a USB keyboard into your computer and the correct drivers have been loaded.
(Note for the curious: Part of the initialization involves sending across a HID descriptor along with all the other USB descriptors (like VID/PID). This HID descriptor lays out how it’s packets should be interpreted by the host. It’s not really important for our present discussion though as it’s essentially just another hard-coded value.)
What happens when you push a key and release it? The microcontroller that handles the bus, sends a HID packet down the wire. Unlike TCP or one of the other common network protocols that you may be more familiar with, there are no chunks or encapsulation going on - each packet simply contains the information about a single complete keyboard instruction. The key here is “complete”, so holding Ctrl
and then tapping a
results in a single packet being send across (again, this is a slight simplification. Today, modern keyboards sometimes do some funky things like sending incomplete reports across like just holding down Ctrl
, but is all to do with what report descriptor is sendt across (see the comment above)).
A HID packet is 8 bytes long, but we actually only care about 2 of those bytes.
00 | 02 | 04 | 06 | 08 | 0A | 0C | 0E |
---|---|---|---|---|---|---|---|
Modifier | Reserved | Scan Code | Reserved | Reserved | Reserved | Reserved | Reserved |
The third byte is pretty obvious - it just contains the scan code of the key that we’ve pressed. But what about the first byte - labelled “modifier”. It’s defined (again by the report descriptor) to be an 8-bit deep bitfield where each bit corresponds to one of the four modifiers; Ctrl
, Shift
, Alt
, Super
.
0/1 | 0/1 | 0/1 | 0/1 |
---|---|---|---|
Ctrl |
Shift |
Alt |
Super |
For example, holding Ctrl
and Alt
would give 0b1010=0x10
, so the modifier byte would simply be 0x10
. It’s as simple as that! So, if we wanted to send the complete instruction Ctrl+Alt+Del
, our final HID packet would look like
10 | 00 | 2A | 00 | 00 | 00 | 00 | 00 |
---|---|---|---|---|---|---|---|
Ctrl+Alt |
Del |
If we were the microcontroller, we just have to “say” \x10\x00\x2A\x00\x00\x00\x00\x00
to the host, and it would react as if someone had pressed Ctrl+Alt+Del
!
What does all this have to do with the Yubikey? Remember the keyMap
dictionary from earlier? It only maps things up to 0x7F
- but our table from the HID specification goes up to 0xE8
? It turns out that Yubico have only implemented support for the first 127 (0x00-0x7F
) scan codes and mapped the second 127 (0x80-0xFF
) to the exact same scan codes as the first.
Whenever a scan code greater than 0x80
is sent, the modifier bit gets set to 0x02
. This simply corresponds to holding down the shift key. It appears to be a bit of a cheeky hack on Yubico’s part to get what should be 4 bytes compressed down to 2 when a character is stored as a static password on the Yubikey. It also means less logic is required by the Yubikey to handle constructing and sending HID packets. It’s probably just a simple switch case in the onboard firmware.
This theory is supported by going back to the source code.
#define SHIFT = 0x80;
UsScanEdit::UsScanEdit() {
keyMap = QMap<int, int>();
keyMap[0x00] = 0;
...
keyMap[0x41] = 0x04 | SHIFT; /* A */
...
keyMap[0x61] = 0x04; /* a*/
...
keyMap[0x7f] = 0;
}
Notice how for keyMap[0x41]
(corresponding to A
), we have the scan code 0x04 | SHIFT
. Well, at the top, SHIFT
is pre-defined to be 0x80
, so this is just 0x84
. i.e. the Shift
ed characters are just mapped to the 0x80-0xFF
region. The true scan code for 0x84
is Scroll Lock
, so we know that the Yubikey must be ORing any scan code higher than 0x80
with 0x80
before setting the modifer key and sending it down the wire. It might look something like this…
int * craftHIDPacket(int scanCode)
{
int packet[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
if ( scanCode >= 0x80 )
{
modifierBit = 0x20;
keyCode = scanCode ^ 0x80;
}
else
{
modifierBit = 0x00;
keyCode = scanCode;
}
packet[0] = modifierBit;
packet[2] = keyCode;
return packet;
}
Excusing my probably dodgy-looking C code, hopefully you get the idea as to what’s going on. Unfortunately, all this means that it looks like there isn’t a way to turn a Yubikey into a fully-fledged HID injection tool without rewriting the on-board firmware (which I’m not aware of a way to do). All in all, I hit a bit of a brick wall, but perhaps this read will be useful for someone who is looking to the USB HID implementation. My (perhaps choppy) implementation in Go can be found on Github here.
Until next time…