Introduction
I was recently presented with a packet capture file to perform some forensics on it as a challenge and see if I could find the hidden message. Naturally, it being a packet capture I fired up Wireshark only to be faced with a very bland single colour screen, quite different from the usual network captures most would be used to when using Wireshark (see Figure 1 below). There was none of the usual indication of different protocols broken down by colour. This was going to be a different type of challenge, and one I was going to learn a lot from, I knew I would enjoy it, and I sure did!

Goal of this Post
The packet capture turned out to be from a USB keyboard. In order to process the packet capture I had to avail of a number of resources in order to figure out how. I’ll link out to the most useful resources throughout the article. In processing the packet capture I initially converted the key press events manually step by step, this was a lengthy procedure and if there had been a large amount of key presses captured it would have been futile to try to do by hand. So as I was going along, the Python part of my brain was figuring out how I could automate this process which brings me to the Goal of this Blog post. The main goal of this post is to provide the reader with a Jupyter Notebook that will take the extracted key press data from a USB packet capture and will build up the message typed out on the keyboard.
Caveats to the solution presented:
- The solution presented is specific to a US English keyboard, but my guess is the solution could be adapted for other keyboard languages (@s and #s may be incorrectly presented if your pcap is from a UK keyboard).
- Not all possible key presses are accounted for, you will see from the keys dictionary that I only accounted for the key type up to 0x73, again if other keys need to be accounted for in your solution it would be relatively easy to add them.
- Some of the keys from 0x00 to 0x73 aren’t accounted for either e.g. PAGE_UP and PAGE_DOWN, but again, if your solution required them they could be accommodated.
- Very little validation or error checking and handling (close to none) is provided in my code. This is more of a proof of concept and if used in any sort of production environment would pose a risk of exploitation.
How to Analyse a USB Keyboard Packet Capture
So, I’m not going to detail the steps I took, mainly because I found some great resources and I’m just going to link to those. As with most problems, I began with a simple search online to see how I could approach this packet capture. I first found this Medium post which I read through and compared the details against my packet capture to confirm that I was looking at a legitimate USB keyboard packet capture.
There wasn’t a chance that I was going to have the patience to manually extract each key press event so the next thing I searched for was a tshark command that I could just type into the command line and it would spit out each event. I found such a command on this page about CTF Forensics which had a nice section on USB Forensics in particular. The command was very simple:
tshark -r usb-keyboard-data.pcap -T fields -e usb.capdata
This command spits out a line per keyboard event that are split up into 8 hex encoded bytes per line. Taken from the above resource again, the different bytes represent:
Byte 0: Keyboard modifier bits (SHIFT, ALT, CTRL etc)
Byte 1: reserved
Byte 2-7: Up to six keyboard usage indexes representing the keys that are currently “pressed”. Order is not important, a key is either pressed (present in the buffer) or not pressed.
https://bitvijays.github.io/LFC-Forensics.html#usb-keyboard
The next problem was to figure out what each byte value represented in terms of key presses and the next invaluable resource found was a github gist that list all the USB HID Keyboard Scan Codes, very useful!
After that I was able to take my tshark generated key press events and reproduce the message that had been typed.
The Automation Part
What follows below is the Jupyter Notebook I used to reproduce the message generated by the key presses extracted from the capture (it looks a bit better if viewed over on Github directly). The first section of the notebook is the creation of a dictionary to specify how to interpret the keys pressed.
For my solution the input expected is a text file containing the output generated by the tshark command. The next section reads in this text file and echoes it to the Notebook as a sanity check. I added the keys pressed manually which appear on the right hand side of the printed text file so that someone coming fresh to this Notebook would be able to understand what message was typed and verify the ultimate output easily.
Finally the processing of the key events is conducted to reproduce the ultimate message. At the end two messages are printed out. The first message is the completed interpreted message, in which certain navigation key movements are accounted for e.g. left/right arrow, Home, End, Backspace, Delete. The next message is the raw typed keys, in which the press of left arrow is included as “LEFT_ARROW” for example.
The original capture file sent to me included a much longer message which this code was able to adequately deal with and produced the correct message. Aside from a lack of error handling or input validation in this code, as long as you can trust your input (which is a terrible idea) this solution should work quite well with most English language keyboard captures.
Solution to convert Keyboard pcap file to Typed Message¶
- This solution assumes a USB pcap file was captured for a keyboard connecting to a computer.
- It assumes the pcap file was processed using tshark to output the INTERRUPT IN events
- tshark command:
- tshark -r keyboard_capture.pcap -T fields -e usb.capdata
- Obviously this should then be redirected to a text file to work with
- Another assumption for this solution is that there is only one key press per interrupt event, so only the third column is checked for key data.
- Another assumption is that input is clean, limited or no error checking provided in this version
# KEY BINDING FROM: https://gist.github.com/MightyPork/6da26e382a7ad91b5496ee55fdc73db2
# NOT ALL KEYS ADDED, NOT NEEDED FOR PROBLEM CURRENTLY TRYING TO SOLVE, BUT POSSIBLE TO IMPLEMENT LATER IF NECESSARY
# NOT ALL OF THE KEYS THAT HAVE BEEN ADDED ARE ACCOUNTED FOR IN THE CODE TO INTERPRET THE CAPTURE RESULTS
# keys DICTIONARY INDEX IS RELATED TO VALUE IN THIRD COLUMN OF INPUT EVENT
# EACH KEY HAS A LIST ASSOCIATED WITH IT WHERE POSITION 0 IS UNSHIFTED KEY VALUE AND POSITION 1 IS SHIFTED KEY VALUE
keys = {"00": ["NONE",""], "01": ["KB_ERR_TOO_MANY_KEYS",""], "02": ["POST_FAIL",""], "03": ["KB_ERR_UNDEF",""],
"04": ["a","A"], "05": ["b","B"], "06": ["c","C"], "07": ["d","D"], "08": ["e","E"], "09": ["f","F"],
"0a": ["g","G"], "0b": ["h","H"], "0c": ["i","I"], "0d": ["j","J"], "0e": ["k","K"], "0f": ["l","L"],
"10": ["m","M"], "11": ["n","N"], "12": ["o","O"], "13": ["p","P"], "14": ["q","Q"], "15": ["r","R"],
"16": ["s","S"], "17": ["t","T"], "18": ["u","U"], "19": ["v","V"], "1a": ["w","W"], "1b": ["x","X"],
"1c": ["y","Y"], "1d": ["z","Z"], "1e": ["1","!"], "1f": ["2","@"], "20": ["3","#"], "21": ["4","$"],
"22": ["5","%"], "23": ["6","^"], "24": ["7","&"], "25": ["8","*"], "26": ["9","("], "27": ["0",")"],
"28": ["\n",""], "29": ["Esc",""], "2a": ["BACKSPACE",""], "2b": ["\t",""], "2c": [" ",""], "2d": ["-","_"],
"2e": ["=","+"], "2f": ["[","{"], "30": ["]","}"], "31": ["\\","|"], "32": ["#","~"], "33": [";",":"],
"34": ["'",'"'],# CONFUSION WARNING
"35": ["`","~"], "36": [",","<"], "37": [".",">"], "38": ["/","?"], "39": ["CAPS_LOCK",""], "3a": ["F1",""],
"3b": ["F2",""], "3c": ["F3",""], "3d": ["F4",""], "3e": ["F5",""], "3f": ["F6",""], "40": ["F7",""],
"41": ["F8",""], "42": ["F9",""], "43": ["F10",""], "44": ["F11",""], "45": ["F12",""], "46": ["PRT_SCR",""],
"47": ["SCROLL_LOCK",""], "48": ["PAUSE",""], "49": ["INSERT",""], "4a": ["HOME",""], "4b": ["PAGE_UP",""],
"4c": ["DELETE",""], "4d": ["END",""], "4e": ["PAGE_DOWN",""], "4f": ["RIGHT_ARROW",""], "50": ["LEFT_ARROW",""],
"51": ["DOWN_ARROW",""], "52": ["UP_ARROW",""], "53": ["NUM_LOCK",""], "54": ["/","CLEAR"], "55": ["*",""],
"56": ["-",""], "57": ["+",""], "58": ["\n",""], "59": ["1","END"], "5a": ["2","DOWN_ARROW"],
"5b": ["3","PAGE_DOWN"], "5c": ["4","LEFT_ARROW"], "5d": ["5",""], "5e": ["6","RIGHT_ARROW"], "5f": ["7","HOME"],
"60": ["8","UP_ARROW"], "61": ["9","PAGE_UP"], "62": ["0","INSERT"], "63": [".","DELETE"], "64": ["\\","|"],
"65": ["APPLICATION",""], "66": ["POWER",""], "67": ["=",""], "68": ["F13",""], "69": ["F14",""], "6a": ["F15",""],
"6b": ["F16",""], "6c": ["F17",""], "6d": ["F18",""], "6e": ["F19",""], "6f": ["F20",""], "70": ["F21",""],
"71": ["F22",""], "72": ["F23",""], "73": ["F24",""],
#remainder of keys ignored in this solution
"74": ["",""],"75": ["",""],"76": ["",""],"77": ["",""],"78": ["",""],"79": ["",""],"7a": ["",""],"7b": ["",""],
"7c": ["",""],"7d": ["",""],"7e": ["",""],"7f": ["",""],"80": ["",""],"81": ["",""],"82": ["",""],"83": ["",""],
"84": ["",""],"85": ["",""],"86": ["",""],"87": ["",""],"88": ["",""],"89": ["",""],"8a": ["",""],"8b": ["",""],
"8c": ["",""],"8d": ["",""],"8e": ["",""],"8f": ["",""],"90": ["",""],"91": ["",""],"92": ["",""],"93": ["",""],
"94": ["",""],"95": ["",""],"96": ["",""],"97": ["",""],"98": ["",""],"99": ["",""],"9a": ["",""],"9b": ["",""],
"9c": ["",""],"9d": ["",""],"9e": ["",""],"9f": ["",""],"a0": ["",""],"a1": ["",""],"a2": ["",""],"a3": ["",""],
"a4": ["",""],"a5": ["",""],"a6": ["",""],"a7": ["",""],"a8": ["",""],"a9": ["",""],"aa": ["",""],"ab": ["",""],
"ac": ["",""],"ad": ["",""],"ae": ["",""],"af": ["",""],"b0": ["",""],"b1": ["",""],"b2": ["",""],"b3": ["",""],
"b4": ["",""],"b5": ["",""],"b6": ["",""],"b7": ["",""],"b8": ["",""],"b9": ["",""],"ba": ["",""],"bb": ["",""],
"bc": ["",""],"bd": ["",""],"be": ["",""],"bf": ["",""],"c0": ["",""],"c1": ["",""],"c2": ["",""],"c3": ["",""],
"c4": ["",""],"c5": ["",""],"c6": ["",""],"c7": ["",""],"c8": ["",""],"c9": ["",""],"ca": ["",""],"cb": ["",""],
"cc": ["",""],"cd": ["",""],"ce": ["",""],"cf": ["",""],"d0": ["",""],"d1": ["",""],"d2": ["",""],"d3": ["",""],
"d4": ["",""],"d5": ["",""],"d6": ["",""],"d7": ["",""],"d8": ["",""],"d9": ["",""],"da": ["",""],"db": ["",""],
"dc": ["",""],"dd": ["",""],
}
# IGNORED KEYS FROM 0X74 ONWARDS FOR LATER ADDITION:
'''
#define KEY_OPEN 0x74 // Keyboard Execute
#define KEY_HELP 0x75 // Keyboard Help
#define KEY_PROPS 0x76 // Keyboard Menu
#define KEY_FRONT 0x77 // Keyboard Select
#define KEY_STOP 0x78 // Keyboard Stop
#define KEY_AGAIN 0x79 // Keyboard Again
#define KEY_UNDO 0x7a // Keyboard Undo
#define KEY_CUT 0x7b // Keyboard Cut
#define KEY_COPY 0x7c // Keyboard Copy
#define KEY_PASTE 0x7d // Keyboard Paste
#define KEY_FIND 0x7e // Keyboard Find
#define KEY_MUTE 0x7f // Keyboard Mute
#define KEY_VOLUMEUP 0x80 // Keyboard Volume Up
#define KEY_VOLUMEDOWN 0x81 // Keyboard Volume Down
// 0x82 Keyboard Locking Caps Lock
// 0x83 Keyboard Locking Num Lock
// 0x84 Keyboard Locking Scroll Lock
#define KEY_KPCOMMA 0x85 // Keypad Comma
// 0x86 Keypad Equal Sign
#define KEY_RO 0x87 // Keyboard International1
#define KEY_KATAKANAHIRAGANA 0x88 // Keyboard International2
#define KEY_YEN 0x89 // Keyboard International3
#define KEY_HENKAN 0x8a // Keyboard International4
#define KEY_MUHENKAN 0x8b // Keyboard International5
#define KEY_KPJPCOMMA 0x8c // Keyboard International6
// 0x8d Keyboard International7
// 0x8e Keyboard International8
// 0x8f Keyboard International9
#define KEY_HANGEUL 0x90 // Keyboard LANG1
#define KEY_HANJA 0x91 // Keyboard LANG2
#define KEY_KATAKANA 0x92 // Keyboard LANG3
#define KEY_HIRAGANA 0x93 // Keyboard LANG4
#define KEY_ZENKAKUHANKAKU 0x94 // Keyboard LANG5
// 0x95 Keyboard LANG6
// 0x96 Keyboard LANG7
// 0x97 Keyboard LANG8
// 0x98 Keyboard LANG9
// 0x99 Keyboard Alternate Erase
// 0x9a Keyboard SysReq/Attention
// 0x9b Keyboard Cancel
// 0x9c Keyboard Clear
// 0x9d Keyboard Prior
// 0x9e Keyboard Return
// 0x9f Keyboard Separator
// 0xa0 Keyboard Out
// 0xa1 Keyboard Oper
// 0xa2 Keyboard Clear/Again
// 0xa3 Keyboard CrSel/Props
// 0xa4 Keyboard ExSel
// 0xb0 Keypad 00
// 0xb1 Keypad 000
// 0xb2 Thousands Separator
// 0xb3 Decimal Separator
// 0xb4 Currency Unit
// 0xb5 Currency Sub-unit
#define KEY_KPLEFTPAREN 0xb6 // Keypad (
#define KEY_KPRIGHTPAREN 0xb7 // Keypad )
// 0xb8 Keypad {
// 0xb9 Keypad }
// 0xba Keypad Tab
// 0xbb Keypad Backspace
// 0xbc Keypad A
// 0xbd Keypad B
// 0xbe Keypad C
// 0xbf Keypad D
// 0xc0 Keypad E
// 0xc1 Keypad F
// 0xc2 Keypad XOR
// 0xc3 Keypad ^
// 0xc4 Keypad %
// 0xc5 Keypad <
// 0xc6 Keypad >
// 0xc7 Keypad &
// 0xc8 Keypad &&
// 0xc9 Keypad |
// 0xca Keypad ||
// 0xcb Keypad :
// 0xcc Keypad #
// 0xcd Keypad Space
// 0xce Keypad @
// 0xcf Keypad !
// 0xd0 Keypad Memory Store
// 0xd1 Keypad Memory Recall
// 0xd2 Keypad Memory Clear
// 0xd3 Keypad Memory Add
// 0xd4 Keypad Memory Subtract
// 0xd5 Keypad Memory Multiply
// 0xd6 Keypad Memory Divide
// 0xd7 Keypad +/-
// 0xd8 Keypad Clear
// 0xd9 Keypad Clear Entry
// 0xda Keypad Binary
// 0xdb Keypad Octal
// 0xdc Keypad Decimal
// 0xdd Keypad Hexadecimal
'''
# Print raw file contents to see data being worked with
file_name="kboutputsample.txt"
f=open(file_name, "r")
print(f.read())
f.close()
# File kboutputsample.txt processed in this section, processed typed string shown printed at end
# Assumption made as to the initial state of the keyboard
# Not implemented in this version, added in case of future work
CAPS_LOCK=False
NUM_LOCK=False
SCROLL_LOCK=False
#track typed message as intended
typed_message= []
#track typed message in raw form, e.g. including backspace as 'BACKSPACE'
raw_typed_message = ""
cursor_position = 0
for data_line in open(file_name,"r").readlines():
capdata = data_line.split(":")
if capdata[2] != "00":
raw_typed_message += keys[capdata[2]][1] if (capdata[0] == "02" or capdata[0] == "20") else keys[capdata[2]][0]
# Deal with Cursor Position changes first
# if home cursor to zero position
if keys[capdata[2]][0] == "HOME":
cursor_position = 0
# if end cursor to message length
elif keys[capdata[2]][0] == "END":
cursor_position = len(typed_message)
# if right arrow and cursor less than message length cursor++
elif keys[capdata[2]][0] == "RIGHT_ARROW":
# Cursor position doesn't need to move if it is already at the end of the message when right arrow pressed
if cursor_position < len(typed_message):
cursor_position = cursor_position + 1
# if left arrow and cursor greater than 1 cursor--
elif keys[capdata[2]][0] == "LEFT_ARROW":
# Cursor position doesn't need to move if it is already at the start of the message when left arrow pressed
if cursor_position > 0:
cursor_position = cursor_position - 1
# Other key press events are assumed to change typed_message
else:
# Behaviour of key events depends on cursor position
# if cursor postion is at the end of the message
if cursor_position == len(typed_message):
#Delete Key ignored if cursor is at end of string
#Backspace Key and there is something to backspace
if keys[capdata[2]][0] == "BACKSPACE" and len(typed_message) > 0:
typed_message.pop()
cursor_position = cursor_position -1
#Other Keys
else:
# if shift key is held append shifted key, else append unshifted key
typed_message.append(keys[capdata[2]][1]) if (capdata[0] == "02" or capdata[0] == "20") else typed_message.append(keys[capdata[2]][0])
cursor_position = cursor_position + 1
#Cursor position is not at the end of the message
else:
#Delete character to right of cursor position
if keys[capdata[2]][0] == "DELETE" and len(typed_message) > 0:
typed_message.pop(cursor_position)
#Backspace Key and there is something to backspace
elif keys[capdata[2]][0] == "BACKSPACE" and cursor_position > 0:
typed_message.pop(cursor_position-1)
cursor_position = cursor_position -1
else:
# if shift key is held insert shifted key, else insert unshifted key
typed_message.insert(cursor_position, keys[capdata[2]][1]) if (capdata[0] == "02" or capdata[0] == "20") else typed_message.insert(cursor_position, keys[capdata[2]][0])
cursor_position = cursor_position + 1
print("".join(typed_message))
print('"' + raw_typed_message + '"')
Alternative Solution
If you find my solution above long or messy (and I would understand if you did), allow me to offer you an alternative. Some further research led me to another Python solution that has the added benefit of reading directly from the packet capture itself. Both solutions could obviously be combined if I ever wanted to take this seriously, but it was just a fun challenge so I didn’t. This alternate solution will generate one message that is more akin to the raw typed message from the solution above.
Conclusion
If you enjoyed this Blog post or it helped you in anyway, please leave a comment below. If you would like anything clarified, ask a question below and I’ll do my best to help you out.