heikki.juva.lu

Display case for my projects and writeups. I mostly work on InfoSec, hardware hacking and electronics.


Project maintained by Zokol Hosted on GitHub Pages — Theme by mattgraham

USB-keyboard keylogger CTF challenge

This CTF challenge is from Persec 2024 event.

Starting point was a challenge.pcap-file, containing USB-packets recorded between a keyboard and PC. Idea of the challenge was that the user input on the keyboard contains the flag. We need to extract the keypresses and decode them into ascii-characters.

The USB-payloads can be extracted from the file using tshark: tshark -r challenge.pcapng -T fields -e usb.capdata | tr -d :

Finnish keycodes

Due to the fact that the challenge was recorded using Finnish-keyboard, the mapping is different to US-layout:

# Lower-case keypresses
lcasekey_fi = {
    4: "a",
    5: "b",
    6: "c",
    7: "d",
    8: "e",
    9: "f",
    10: "g",
    11: "h",
    12: "i",
    13: "j",
    14: "k",
    15: "l",
    16: "m",
    17: "n",
    18: "o",
    19: "p",
    20: "q",
    21: "r",
    22: "s",
    23: "t",
    24: "u",
    25: "v",
    26: "w",
    27: "x",
    28: "y",
    29: "z",
    30: "1",
    31: "2",
    32: "3",
    33: "4",
    34: "5",
    35: "6",
    36: "7",
    37: "8",
    38: "9",
    39: "0",
    45: "-",
    46: "=",
    47: "å",
    48: "¨",
    49: "'",
    51: "ö",
    52: "ä",
    53: "§",
    54: ",",
    55: ".",
    56: "-"
}

# Upper-case keypresses (shift modifier)
ucasekey_fi = {
    4: "A",
    5: "B",
    6: "C",
    7: "D",
    8: "E",
    9: "F",
    10: "G",
    11: "H",
    12: "I",
    13: "J",
    14: "K",
    15: "L",
    16: "M",
    17: "N",
    18: "O",
    19: "P",
    20: "Q",
    21: "R",
    22: "S",
    23: "T",
    24: "U",
    25: "V",
    26: "W",
    27: "X",
    28: "Y",
    29: "Z",
    30: "!",
    31: "\"",
    32: "#",
    33: "¤",
    34: "%",
    35: "&",
    36: "/",
    37: "(",
    38: ")",
    39: "=",
    45: "_",
    46: "+",
    47: "Å",
    48: "^",
    49: "*",
    51: "Ö",
    52: "Ä",
    53: "½",
    54: ";",
    55: ":",
    56: "_"
}

# Alt-modifier keypresses
altkey_fi = {
    30: "@",
    31: "£",
    32: "$",
    33: "€",
    36: "{",
    39: "}",
    38: "\\",
    45: "?",
    46: "`",
    47: "Å",
    48: "~",
    49: "´",
    51: "Ö",
    52: "Ä",
    53: "§",
    54: "<",
    55: ">",
    56: "/"
}

Parsing USB packets

To make the parsing easier, tshark output is processed by python-program. tshark-output contains empty rows for some reason, so we need to remove those:

# use tshark to extract usb.capdata from pcap
import os
# tshark -r challenge.pcapng -T fields -e usb.capdata | tr -d :
os.system("tshark -r data/challenge.pcapng -T fields -e usb.capdata | tr -d : > usb_payloads.txt")

with open("usb_payloads.txt", "r") as f:
    raw_data = f.readlines()
    
## Remove empty lines
raw_data = [x.strip() for x in raw_data if x.strip()]

Payloads are then parsed, so that we are left with just the significant bytes:

input = []
## Parse payloads
for packet in raw_data:
    tmp = [int(packet[0:2],16)]
    for offset in range(0, 11, 2):
        start = 4 + offset
        end = start + 2
        tmp.append(int(packet[start:end],16))
    input.append(tmp)

Keypress is then converted to ASCII. Packet stream is parsed into modifier and list of simultaneus keypresses (0-6 keys)

The parsed packets are then processes as “rounds”, or series of keypresses between lifting all keys. Inside these rounds, all keypresses are collected into ordered set, and printed out in the end of the round.

This may sound complicated, but the given packet stream includes few things to make parsing more complicated;

NO_MODIFIER = 0
SHIFT = 1
ALT = 2

key_lifted = True                               # Keep track of ending one "round" of simultaneous keypresses
keys_pressed = {}                               # Dictionary to store keys pressed in one "round", to store ordered set of pressed keys
for keypress in input:                          # Iterate all packets
    if all(x == 0x00 for x in keypress[1:]):    # KEY LIFTED
        key_lifted = True
        active_mod = -1
        for key in keys_pressed:                # Iterate all simultaneously pressed keys
            if active_mod == -1:                
                active_mod = keys_pressed[key]  # Store active modifier in this round
            elif active_mod != keys_pressed[key]:   # Modifier released before key lifted, drop this packet!
                break
            print(key, end="")
        keys_pressed = {}
        continue                                # END OF ROUND
    for key in keypress[1:]:                    # Iterate all keys in simultaneous key presses
        if key == 0x00:                         # Not valid keypress
            continue                            # Next keypress
        if keypress[0] == 0x00:                 # NO MODIFIER
            keys_pressed[lcasekey_fi[key]] = NO_MODIFIER
        elif (keypress[0] == 0x02
              or keypress[0] == 0x20):          # SHIFT
            keys_pressed[ucasekey_fi[key]] = SHIFT
        elif keypress[0] == 0x40:               # ALT
            keys_pressed[altkey_fi[key]] = ALT
    key_lifted = False                          # END OF PACKET

Complete program:

# use tshark to extract usb.capdata from pcap
import os
# tshark -r challenge.pcapng -T fields -e usb.capdata | tr -d :
os.system("tshark -r data/challenge.pcapng -T fields -e usb.capdata | tr -d : > usb_payloads.txt")

with open("usb_payloads.txt", "r") as f:
    raw_data = f.readlines()
    
## Remove empty lines
raw_data = [x.strip() for x in raw_data if x.strip()]

input = []
## Parse payloads
for packet in raw_data:
    tmp = [int(packet[0:2],16)]
    for offset in range(0, 11, 2):
        start = 4 + offset
        end = start + 2
        tmp.append(int(packet[start:end],16))
    input.append(tmp)

NO_MODIFIER = 0
SHIFT = 1
ALT = 2

key_lifted = True                               # Keep track of ending one "round" of simultaneous keypresses
keys_pressed = {}                               # Dictionary to store keys pressed in one "round", to store ordered set of pressed keys
for keypress in input:                          # Iterate all packets
    if all(x == 0x00 for x in keypress[1:]):    # KEY LIFTED
        key_lifted = True
        active_mod = -1
        for key in keys_pressed:                # Iterate all simultaneously pressed keys
            if active_mod == -1:                
                active_mod = keys_pressed[key]  # Store active modifier in this round
            elif active_mod != keys_pressed[key]:   # Modifier released before key lifted, drop this packet!
                break
            print(key, end="")
        keys_pressed = {}
        continue                                # END OF ROUND
    for key in keypress[1:]:                    # Iterate all keys in simultaneous key presses
        if key == 0x00:                         # Not valid keypress
            continue                            # Next keypress
        if keypress[0] == 0x00:                 # NO MODIFIER
            keys_pressed[lcasekey_fi[key]] = NO_MODIFIER
        elif (keypress[0] == 0x02
              or keypress[0] == 0x20):          # SHIFT
            keys_pressed[ucasekey_fi[key]] = SHIFT
        elif keypress[0] == 0x40:               # ALT
            keys_pressed[altkey_fi[key]] = ALT
    key_lifted = False                          # END OF PACKET