#!/usr/bin/env python # # Copyright 2009 Joshua Wright, Michael Ossmann # # This file is part of gr-bluetooth # # gr-bluetooth is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2, or (at your option) # any later version. # # gr-bluetooth is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with gr-bluetooth; see the file COPYING. If not, write to # the Free Software Foundation, Inc., 51 Franklin Street, # Boston, MA 02110-1301, USA. import sys import struct import time from pcapdump.pcapdump import * DLT_EN10MB = 1 DLT_BLUETOOTH_HCI_H4 = 187 ELLISYS_CSV_HDR = "\"Depth\",\"Time\",\"Name\",\"Data\"\x0d\x0a" ELLISYS_HID_INPUT = "HID Input 1" USBHID_MAP = { 0x04 : "a", 0x05 : "b", 0x06 : "c", 0x07 : "d", 0x08 : "e", 0x09 : "f", 0x0A : "g", 0x0B : "h", 0x0C : "i", 0x0D : "j", 0x0E : "k", 0x0F : "l", 0x10 : "m", 0x11 : "n", 0x12 : "o", 0x13 : "p", 0x14 : "q", 0x15 : "r", 0x16 : "s", 0x17 : "t", 0x18 : "u", 0x19 : "v", 0x1A : "w", 0x1B : "x", 0x1C : "y", 0x1D : "z", 0x1E : "1", 0x1F : "2", 0x20 : "3", 0x21 : "4", 0x22 : "5", 0x23 : "6", 0x24 : "7", 0x25 : "8", 0x26 : "9", 0x27 : "0", 0x28 : "[Return]\n", 0x29 : "[Esc]", 0x2A : "[Backspace]", 0x2B : "[Tab]\t", 0x2C : " ", 0x2D : "-", 0x2E : "=", 0x2F : "[", 0x30 : "]", 0x31 : "\\", 0x32 : "#", 0x33 : ";", 0x34 : "'", 0x35 : "[Grave Accent]", 0x36 : ",", 0x37 : ".", 0x38 : "/", 0x39 : "[Caps Lock]", 0x3A : "[F1]", 0x3B : "[F2]", 0x3C : "[F3]", 0x3D : "[F4]", 0x3E : "[F5]", 0x3F : "[F6]", 0x40 : "[F7]", 0x41 : "[F8]", 0x42 : "[F9]", 0x43 : "[F10]", 0x44 : "[F11]", 0x45 : "[F12]", 0x46 : "[PrintScreen]", 0x47 : "[Scroll]", 0x48 : "[Pause]", 0x49 : "[Insert]", 0x4A : "[Home]", 0x4B : "[PageUp]", 0x4C : "[Delete]", 0x4D : "[End]", 0x4E : "[PageDown]", 0x4F : "[RightArrow]", 0x50 : "[LeftArrow]", 0x51 : "[DownArrow]", 0x52 : "[UpArrow]", 0x53 : "[Keypad Num Lock and Clear]", 0x54 : "[Keypad /]", 0x55 : "[Keypad *]", 0x56 : "[Keypad -]", 0x57 : "[Keypad +]", 0x58 : "[Keypad Enter]\n", 0x59 : "[Keypad 1 and End]", 0x5A : "[Keypad 2 and Down Arrow]", 0x5B : "[Keypad 3 and PageDn]", 0x5C : "[Keypad 4 and Left Arrow]", 0x5D : "[Keypad 5]", 0x5E : "[Keypad 6 and Right Arrow]", 0x5F : "[Keypad 7 and Home]", 0x60 : "[Keypad 8 and Up Arrow]", 0x61 : "[Keypad 9 and PageUp]", 0x62 : "[Keypad 0 and Insert]", 0x63 : "[Keypad . and Delete]", 0x64 : "\\", 0x65 : "[WinKey]", 0x66 : "[Power9]", 0x67 : "[Keypad =]", 0x68 : "[F13]", 0x69 : "[F14]", 0x6A : "[F15]", 0x6B : "[F16]", 0x6C : "[F17]", 0x6D : "[F18]", 0x6E : "[F19]", 0x6F : "[F20]", 0x70 : "[F21]", 0x71 : "[F22]", 0x72 : "[F23]", 0x73 : "[F24]", 0x74 : "[Execute]", 0x75 : "[Help]", 0x76 : "[Menu]", 0x77 : "[Select]", 0x78 : "[Stop]", 0x79 : "[Again]", 0x7A : "[Undo]", 0x7B : "[Cut]", 0x7C : "[Copy]", 0x7D : "[Paste]", 0x7E : "[Find]", 0x7F : "[Mute]", 0x80 : "[Volume Up]", 0x81 : "[Volume Down]", 0x82 : "[Locking Caps Lock]", 0x83 : "[Locking Num Lock]", 0x84 : "[Locking Scroll Lock]", 0x85 : "[Keypad Comma]", 0x86 : "[Keypad Equal]", 0x87 : "[International1]", 0x88 : "[International2]", 0x89 : "[International3]", 0x8A : "[International4]", 0x8B : "[International5]", 0x8C : "[International6]", 0x8D : "[International7]", 0x8E : "[International8]", 0x8F : "[International9]", 0x90 : "[LANG1]", 0x91 : "[LANG2]", 0x92 : "[LANG3]", 0x93 : "[LANG4]", 0x94 : "[LANG5]", 0x95 : "[LANG6]", 0x96 : "[LANG7]", 0x97 : "[LANG8]", 0x98 : "[LANG9]", 0x99 : "[Alternate Erase]", 0x9A : "[SysReq/Attention]", 0x9B : "[Cancel]", 0x9C : "[Clear]", 0x9D : "[Prior]", 0x9E : "[Return]\n", 0x9F : "[Separator]", 0xA0 : "[Out]", 0xA1 : "[Oper]", 0xA2 : "[Clear/Again]", 0xA3 : "[CrSel/Props]", 0xA4 : "[ExSel]", 0xB0 : "[Keypad 00]", 0xB1 : "[Keypad 000]", 0xB2 : "[Thousands Separator]", 0xB3 : "[Decimal Separator]", 0xB4 : "[Currency Unit]", 0xB5 : "[Currency Sub-unit]", 0xB6 : "[Keypad (]", 0xB7 : "[Keypad )]", 0xB8 : "[Keypad {]", 0xB9 : "[Keypad }]", 0xBA : "[Keypad Tab]\t", 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]", 0xE0 : "[LeftControl]", 0xE1 : "[LeftShift]", 0xE2 : "[LeftAlt]", 0xE3 : "[LeftWinKey]", 0xE4 : "[RightControl]", 0xE5 : "[RightShift]", 0xE6 : "[RightAlt]", 0xE7 : "[RightWinKey]" } # some keycodes represent different things when Shift is held down USBHID_SHIFT_MAP = { 0x04 : "A", 0x05 : "B", 0x06 : "C", 0x07 : "D", 0x08 : "E", 0x09 : "F", 0x0A : "G", 0x0B : "H", 0x0C : "I", 0x0D : "J", 0x0E : "K", 0x0F : "L", 0x10 : "M", 0x11 : "N", 0x12 : "O", 0x13 : "P", 0x14 : "Q", 0x15 : "R", 0x16 : "S", 0x17 : "T", 0x18 : "U", 0x19 : "V", 0x1A : "W", 0x1B : "X", 0x1C : "Y", 0x1D : "Z", 0x1E : "!", 0x1F : "@", 0x20 : "#", 0x21 : "$", 0x22 : "%", 0x23 : "^", 0x24 : "&", 0x25 : "*", 0x26 : "(", 0x27 : ")", 0x2D : "_", 0x2E : "+", 0x2F : "{", 0x30 : "}", 0x31 : "|", 0x32 : "~", 0x33 : ":", 0x34 : "\"", 0x35 : "~", 0x36 : "<", 0x37 : ">", 0x38 : "?", 0x64 : "|" } # global variable to track currently depressed keys active_keys = [] def hid2ascii(scancode, shift): ''' Convert the specified scancode value to the ASCII equivalent using the USBHID_MAP list. ''' if shift: try: code = USBHID_SHIFT_MAP[scancode] return code except KeyError: pass try: code = USBHID_MAP[scancode] except KeyError: return "[Reserved]" return code def usage(): print >>sys.stderr, "Usage: btaptap [-r pcapfile.pcap | -e ellisysfile.csv] [-c count] [-h]\n" sys.exit(0) def parse_l2cap_keydata(l2cappkt): global active_keys TRANS_HDR_IN_DATA = 0xA1 REPORT_ID_KEYBOARD = 0x01 CTRL = 1 SHIFT = 2 ALT = 4 GUI = 8 # Keyboard keystrokes are only seen in L2CAP packets at least 10 bytes long l2clen = (ord(l2cappkt[1]) << 8) | ord(l2cappkt[0]) if l2clen < 10: return # Keyboard keystrokes are only carried by Channel ID >= 0x40 cid = (ord(l2cappkt[3]) << 8) | ord(l2cappkt[2]) if cid < 0x40: return # Ideally we would check for the particular CID for the HID_INTERRUPT # channel, but we don't handle the negotiation (and may not have even # seen it). # Transaction Header should indicate input data thdr = ord(l2cappkt[4]) if thdr != TRANS_HDR_IN_DATA: return # Report ID should indicate this is a keyboard rid = ord(l2cappkt[5]) if rid != REPORT_ID_KEYBOARD: return # This byte describes modifier key status (one bit per key) mod = ord(l2cappkt[6]) # We don't care whether left or right modifier keys are pressed, so we # combine the status bits. leftmod = mod & 0x0f rightmod = (mod & 0xf0) >> 4 mod = leftmod | rightmod # up to six keys can be reported at once keycodes = [] #for byte in range(8,14): for byte in range(8,11): keystroke = ord(l2cappkt[byte]) if keystroke != 0x00: keycodes.append(keystroke) for keystroke in keycodes: # don't repeat keys that are still held down if active_keys.count(keystroke) == 0: if (mod & CTRL): sys.stdout.write("CTRL^") if (mod & ALT): sys.stdout.write("ALT^") if (mod & GUI): # e.g. Windows key sys.stdout.write("GUI^") sys.stdout.write(hid2ascii(keystroke, mod & SHIFT)) active_keys = keycodes def parse_ellisys_export(exportfile): try: cap = open(exportfile, "r") except (OSError, IOError) as e: print >>sys.stderr, "Unable to open Ellisys capture file." return # Check to make sure the CSV file header matches out expectations hdr=cap.readline() if (hdr != ELLISYS_CSV_HDR): print >>sys.stderr, "Invalid CSV file (does not match Ellisys export format)" return for packetline in cap.xreadlines(): try: (edepth, etime, ename, edata) = packetline.replace('"', '').strip().split(",") except ValueError: continue # We are only interedted in HID Input data if (ename != ELLISYS_HID_INPUT): continue parse_ellisys_keydata(edata) def parse_ellisys_keydata(payload): TRANS_HDR_IN_DATA = "\xA1" DEST_CHANNEL_ID = "\x06\x03" # Convert space-separated hex into string payload = payload.replace(' ','').decode("hex") # The Ellisys CSV file format doesn't give us the L2CAP data, so we fake it here by adding # a 2-byte length field, destination CID, and transaction header packet = chr(len(payload)+1) + "\x00" + DEST_CHANNEL_ID + TRANS_HDR_IN_DATA + payload parse_l2cap_keydata(packet) def parse_bb_keydata(packet): BTBBHDR_TYPE_MASK = 0x78 BTBBHDR_TYPE_SHIFT = 3 BTBBHDR_TYPE_DM1 = 3 BTBBPAYLOADHDR_LLID_MASK = 0x03 BTBBPAYLOADHDR_LLID_SHIFT = 0 BTBBPAYLOADHDR_LEN_MASK = 0xF8 BTBBPAYLOADHDR_LEN_SHIFT = 3 LLID_L2CAP = 2 # Keyboard keystrokes are only seen in frames at least 40 bytes long if len(packet) < 40: return # Keyboard keystrokes are only seen in DM1 frames btbbhdr = packet[20:23] type = (ord(btbbhdr[0]) & BTBBHDR_TYPE_MASK) >> BTBBHDR_TYPE_SHIFT if type != BTBBHDR_TYPE_DM1: return # Keyboard keystrokes are only seen in L2CAP packets 14 bytes long btbbpayloadhdr = ord(packet[23]) llid = btbbpayloadhdr & (BTBBPAYLOADHDR_LLID_MASK) >> BTBBPAYLOADHDR_LLID_SHIFT l2clen = (btbbpayloadhdr & BTBBPAYLOADHDR_LEN_MASK) >> BTBBPAYLOADHDR_LEN_SHIFT #print "Debug btbbpayloadhdr 0x%02x, llid %d, l2clen %d"%(btbbpayloadhdr, llid, l2clen) if llid != LLID_L2CAP or l2clen < 14: return parse_l2cap_keydata(packet[24:38]) def parse_hci_keydata(packet): HCI_TYPE_ACL_DATA = 2 # Keyboard keystrokes are only seen in frames at least 19 bytes long if len(packet) < 19: return # Keyboard keystrokes are only seen in ACL Data frames type = ord(packet[0]) if type != HCI_TYPE_ACL_DATA: return parse_l2cap_keydata(packet[5:]) if __name__ == '__main__': arg_pcapfile = None arg_ellisysfile = None arg_count = -1 packetcount = 0 while len(sys.argv) > 1: op = sys.argv.pop(1) if op == '-r': arg_pcapfile = sys.argv.pop(1) if op == '-c': arg_count = int(sys.argv.pop(1)) if op == '-e': arg_ellisysfile = sys.argv.pop(1) if op == '-h': usage() sys.exit(0) if (arg_ellisysfile == None and arg_pcapfile == None): print >>sys.stderr, "Must specify a libpcap capture or an Ellisys CSV file" usage() sys.exit(0) if arg_pcapfile != None: cap = PcapReader(arg_pcapfile) while arg_count != packetcount: try: (pheader, packet) = cap.pnext() pkttime = pheader[0] packetcount+=1 if cap.datalink() == DLT_EN10MB: parse_bb_keydata(packet) elif cap.datalink() == DLT_BLUETOOTH_HCI_H4: parse_hci_keydata(packet) else: print >>sys.stderr, "Unsupported libpcap data link layer: %d\n" % cap.datalink() except TypeError: # raised when pnext returns Null (end of capture) break cap.close() if arg_ellisysfile != None: parse_ellisys_export(arg_ellisysfile) print