├── .gitignore ├── LICENSE ├── README.md └── btsnoop ├── __init__.py ├── android ├── __init__.py ├── executor.py ├── phone.py └── snoopphone.py ├── bt ├── __init__.py ├── att.py ├── hci.py ├── hci_acl.py ├── hci_cmd.py ├── hci_evt.py ├── hci_sco.py ├── hci_uart.py ├── l2cap.py └── smp.py └── btsnoop ├── __init__.py └── btsnoop.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Original work Copyright (c) 2013 robotika.cz 4 | Modified work Copyright (c) 2015 Tomas Nilsson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | btsnoop 2 | ======= 3 | 4 | Parsing module for BtSnoop packet capture files and encapsulated Bluetooth packets 5 | 6 | Documentation 7 | ------------- 8 | 9 | Specifications 10 | - BtSnoop format 11 | - http://tools.ietf.org/html/rfc1761 12 | - http://www.fte.com/webhelp/NFC/Content/Technical_Information/BT_Snoop_File_Format.htm 13 | - Bluetooth specification 14 | - https://www.bluetooth.org/en-us/specification/adopted-specifications 15 | 16 | Module overview 17 | --------------- 18 | 19 | The `btsnoop` module contains three submodules; `android`, `bt` and `btsnoop`. 20 | 21 | The `android` submodule contains functionality for connecting to, and fetching data from, an Android device. It requires an installation of the Android `adb` tool available in `PATH`. 22 | 23 | The `btsnoop` submodule contains functionality for parsing a btsnoop file. 24 | 25 | The `bt` submodule contains functionality for parsing the Bluetooth data parsed from the btsnoop file. 26 | 27 | Usage 28 | ----- 29 | 30 | ### android 31 | 32 | Getting the btsnoop log from an android device 33 | 34 | ```python 35 | >>> import os 36 | >>> from btsnoop.android.snoopphone import SnoopPhone 37 | >>> 38 | >>> phone = SnoopPhone() 39 | >>> filename = phone.pull_btsnoop() 40 | >>> 41 | >>> print filename 42 | /tmp/tmp7t971D/btsnoop_hci.log 43 | ``` 44 | 45 | You can also specify the output file 46 | 47 | ```python 48 | >>> import os 49 | >>> from btsnoop.android.snoopphone import SnoopPhone 50 | >>> 51 | >>> phone = SnoopPhone() 52 | >>> home = os.path.expanduser("~") 53 | >>> dst = os.path.join(home, 'tmp', 'mysnoop.log') 54 | >>> filename = phone.pull_btsnoop(dst) 55 | >>> 56 | >>> print filename 57 | /home/joekickass/tmp/mysnoop.log 58 | ``` 59 | 60 | ### btsnoop 61 | 62 | Parsing a btsnoop capture file 63 | 64 | ```python 65 | >>> import os 66 | >>> import btsnoop.btsnoop.btsnoop as bts 67 | >>> 68 | >>> home = os.path.expanduser("~") 69 | >>> filename = os.path.join(home, 'tmp', 'mysnoop.log') 70 | >>> 71 | >>> records = bts.parse(filename) 72 | >>> 73 | >>> print len(records) 74 | 24246 75 | >>> print records[0] 76 | (1, 4, 2, datetime.datetime(2015, 4, 2, 6, 29, 25, 914577), '\x01\x03\x0c\x00') 77 | >>> print records[24245] 78 | (24246, 8, 3, datetime.datetime(2015, 4, 2, 9, 9, 57, 655656), '\x04\x13\x05\x01@\x00\x01\x00') 79 | ``` 80 | 81 | Some of the information in a record can be printed as human readable strings 82 | 83 | ```python 84 | >>> import btsnoop.btsnoop.btsnoop as bts 85 | ... 86 | >>> print len(records) 87 | 24246 88 | >>> print records[0] 89 | (1, 4, 2, datetime.datetime(2015, 4, 2, 6, 29, 25, 914577), '\x01\x03\x0c\x00') 90 | >>> record = records[0] 91 | >>> seq_nbr = record[0] 92 | >>> pkt_len = record[1] 93 | >>> flags = bts.flags_to_str(record[2]) 94 | >>> timestamp = record[3] 95 | >>> data = record[4] 96 | >>> print seq_nbr 97 | 1 98 | >>> print pkt_len 99 | 4 100 | >>> print flags 101 | ('host', 'controller', 'command') 102 | >>> print timestamp 103 | 2015-04-02 06:29:25.914577 104 | >>> print data 105 | '\x01\x03\x0c\x00' 106 | ``` 107 | 108 | ### bt 109 | 110 | This is the fun stuff. The data contained in a btsnoop record can be parsed using the `bt` submodule. 111 | 112 | Parse HCI UART type. This is the first byte of the payload. It tells us what type of HCI packet that is contained in the record. 113 | 114 | ```python 115 | >>> import btsnoop.bt.hci_uart as hci_uart 116 | >>> import btsnoop.bt.hci as hci 117 | >>> 118 | >>> rec_data = '\x01\x03\x0c\x00' 119 | >>> 120 | >>> hci_type, data = hci_uart.parse(rec_data) 121 | >>> 122 | >>> print hci_type 123 | 1 124 | >>> print data 125 | '\x03\x0c\x00' 126 | >>> print hci_uart.type_to_str(hci_type) 127 | HCI_CMD 128 | ``` 129 | 130 | Parse a HCI command packet. We need to specify HCI type as described in the HCI UART example. 131 | 132 | ```python 133 | >>> import btsnoop.bt.hci as hci 134 | >>> import btsnoop.bt.hci_cmd as hci_cmd 135 | >>> 136 | >>> hci_type = 1 137 | >>> hci_data = '\x03\x0c\x00' 138 | >>> 139 | >>> opcode, length, data = hci.parse(hci_type, hci_data) 140 | >>> 141 | >>> print opcode 142 | 3075 143 | >>> print length 144 | 0 145 | >>> print data 146 | 147 | >>> print hci_cmd.cmd_to_str(opcode) 148 | COMND Reset 149 | ``` 150 | 151 | Parse a HCI event packet. We need to specify HCI type as described in the HCI UART example. 152 | 153 | ```python 154 | >>> import btsnoop.bt.hci as hci 155 | >>> import btsnoop.bt.hci_evt as hci_evt 156 | >>> 157 | >>> hci_type = 4 158 | >>> hci_data = '\x13\x05\x01@\x00\x01\x00' 159 | >>> 160 | >>> ret = hci.parse(hci_type, hci_data) 161 | >>> print len(ret) 162 | 3 163 | >>> 164 | >>> evtcode, length, data = ret 165 | >>> print evtcode 166 | 19 167 | >>> print length 168 | 5 169 | >>> print data 170 | '\x01@\x00\x01\x00' 171 | >>> print hci_evt.evt_to_str(evtcode) 172 | EVENT Number_Of_Completed_Packets 173 | ``` 174 | 175 | Parse a HCI ACL packet. We need to specify HCI type as described in the HCI UART example. 176 | 177 | ```python 178 | >>> import btsnoop.bt.hci as hci 179 | >>> import btsnoop.bt.hci_acl as hci_acl 180 | >>> 181 | >>> hci_type = 2 182 | >>> hci_data = '@ \x07\x00\x03\x00\x04\x00\x0b@\x04' 183 | >>> 184 | >>> ret = hci.parse(hci_type, hci_data) 185 | >>> print len(ret) 186 | 5 187 | >>> 188 | >>> handle, pb, bc, length, data = ret 189 | >>> print handle 190 | 64 191 | >>> print pb 192 | 2 193 | >>> print data 194 | '\x00\x03\x00\x04\x00\x0b@\x04' 195 | >>> print hci_acl.pb_to_str(pb) 196 | ACL_PB START_AUTO_L2CAP_PDU 197 | ``` 198 | 199 | ### More complex samples 200 | 201 | ```python 202 | import sys 203 | import binascii 204 | import string 205 | from prettytable import PrettyTable 206 | 207 | import btsnoop.btsnoop.btsnoop as btsnoop 208 | import btsnoop.bt.hci_uart as hci_uart 209 | import btsnoop.bt.hci_cmd as hci_cmd 210 | import btsnoop.bt.hci_evt as hci_evt 211 | import btsnoop.bt.hci_acl as hci_acl 212 | import btsnoop.bt.l2cap as l2cap 213 | import btsnoop.bt.att as att 214 | import btsnoop.bt.smp as smp 215 | 216 | def get_rows(records): 217 | 218 | rows = [] 219 | for record in records: 220 | 221 | seq_nbr = record[0] 222 | time = record[3].strftime("%b-%d %H:%M:%S.%f") 223 | 224 | hci_pkt_type, hci_pkt_data = hci_uart.parse(record[4]) 225 | hci = hci_uart.type_to_str(hci_pkt_type) 226 | 227 | if hci_pkt_type == hci_uart.HCI_CMD: 228 | 229 | opcode, length, data = hci_cmd.parse(hci_pkt_data) 230 | cmd_evt_l2cap = hci_cmd.cmd_to_str(opcode) 231 | 232 | elif hci_pkt_type == hci_uart.HCI_EVT: 233 | 234 | hci_data = hci_evt.parse(hci_pkt_data) 235 | evtcode, data = hci_data[0], hci_data[-1] 236 | cmd_evt_l2cap = hci_evt.evt_to_str(evtcode) 237 | 238 | elif hci_pkt_type == hci_uart.ACL_DATA: 239 | 240 | hci_data = hci_acl.parse(hci_pkt_data) 241 | l2cap_length, l2cap_cid, l2cap_data = l2cap.parse(hci_data[2], hci_data[4]) 242 | 243 | if l2cap_cid == l2cap.L2CAP_CID_ATT: 244 | 245 | att_opcode, att_data = att.parse(l2cap_data) 246 | cmd_evt_l2cap = att.opcode_to_str(att_opcode) 247 | data = att_data 248 | 249 | elif l2cap_cid == l2cap.L2CAP_CID_SMP: 250 | 251 | smp_code, smp_data = smp.parse(l2cap_data) 252 | cmd_evt_l2cap = smp.code_to_str(smp_code) 253 | data = smp_data 254 | 255 | elif l2cap_cid == l2cap.L2CAP_CID_SCH or l2cap_cid == l2cap.L2CAP_CID_LE_SCH: 256 | 257 | sch_code, sch_id, sch_length, sch_data = l2cap.parse_sch(l2cap_data) 258 | cmd_evt_l2cap = l2cap.sch_code_to_str(sch_code) 259 | data = sch_data 260 | 261 | data = binascii.hexlify(data) 262 | data = len(data) > 30 and data[:30] + "..." or data 263 | 264 | rows.append([seq_nbr, time, hci, cmd_evt_l2cap, data]) 265 | 266 | return rows 267 | 268 | 269 | def main(filename): 270 | """ 271 | Parse a btsnoop log and print relevant data in a table 272 | 273 | Note: Using an old version of PrettyTable. 274 | """ 275 | 276 | table = PrettyTable(['No.', 'Time', 'HCI', 'CMD/EVT/L2CAP', 'Data']) 277 | table.aligns[3] = 'l' 278 | table.aligns[4] = 'l' 279 | 280 | records = btsnoop.parse(filename) 281 | rows = get_rows(records) 282 | [table.add_row(r) for r in rows] 283 | 284 | print table 285 | 286 | 287 | if __name__ == "__main__": 288 | if len(sys.argv) == 2: 289 | main(sys.argv[1]) 290 | else: 291 | sys.exit(-1) 292 | ``` 293 | -------------------------------------------------------------------------------- /btsnoop/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # btsnoop module 3 | # 4 | # TODO: Move stuff here to their corresponding modules 5 | # 6 | import binascii 7 | import btsnoop.btsnoop as bts 8 | import bt.hci_cmd as hci_cmd 9 | import bt.hci_uart as hci_uart 10 | 11 | from android.snoopphone import SnoopPhone 12 | 13 | 14 | def get_ltk(path=None): 15 | """ 16 | Get the Long Term Key 17 | """ 18 | records = get_records(path=path) 19 | cmds = get_cmds(records) 20 | start_enc_cmds = filter(lambda (opcode, length, data): opcode == 0x2019, cmds) 21 | ltks = map(lambda (opcode, length, data): binascii.hexlify(data)[-32:], start_enc_cmds) 22 | last_ltk = len(ltks) != 0 and ltks[-1] or "" 23 | return "".join(map(str.__add__, last_ltk[1::2] ,last_ltk[0::2])) 24 | 25 | 26 | def get_rand_addr(path=None): 27 | """ 28 | Get the Host Private Random Address 29 | """ 30 | records = get_records(path=path) 31 | cmds = get_cmds(records) 32 | set_rand_addr = filter(lambda (opcode, length, data): opcode == 0x2005, cmds) 33 | addrs = map(lambda (opcode, length, data): binascii.hexlify(data)[-12:], set_rand_addr) 34 | last_addr = len(addrs) != 0 and addrs[-1] or "" 35 | return "".join(map(str.__add__, last_addr[1::2], last_addr[0::2])) 36 | 37 | 38 | def get_records(path=None): 39 | if not path: 40 | path = _pull_log() 41 | return bts.parse(path) 42 | 43 | 44 | def get_cmds(records): 45 | hci_uarts = map(lambda record: hci_uart.parse(record[4]), records) 46 | hci_cmds = filter(lambda (hci_type, hci_data): hci_type == hci_uart.HCI_CMD, hci_uarts) 47 | return map(lambda (hci_type, hci_data): hci_cmd.parse(hci_data), hci_cmds) 48 | 49 | 50 | def _pull_log(): 51 | """ 52 | Pull the btsnoop log from a connected phone 53 | """ 54 | phone = SnoopPhone() 55 | return phone.pull_btsnoop() -------------------------------------------------------------------------------- /btsnoop/android/__init__.py: -------------------------------------------------------------------------------- 1 | from . import executor 2 | from . import phone 3 | from . import snoopphone -------------------------------------------------------------------------------- /btsnoop/android/executor.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | 4 | class Executor(object): 5 | """ 6 | Wrapper for subprocess 7 | """ 8 | 9 | def __init__(self, command): 10 | self.cmd = command 11 | 12 | def execute(self): 13 | """ 14 | Executes the given command 15 | 16 | Returns a tuple of (exit code, stdout+stderr) 17 | """ 18 | ret = '' 19 | exit_code = 0 20 | try: 21 | ret = subprocess.check_output(self.cmd, stderr=subprocess.STDOUT, shell=True) 22 | ret = ret.rstrip() 23 | except subprocess.CalledProcessError as error: 24 | exit_code = error.returncode 25 | ret = error.output.rstrip() 26 | return exit_code, ret 27 | -------------------------------------------------------------------------------- /btsnoop/android/phone.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from executor import Executor 3 | 4 | class Phone(object): 5 | 6 | def __init__(self, serial=None): 7 | self.serial = serial 8 | 9 | def shell(self, cmd): 10 | cmd = "adb shell " + cmd 11 | ret, out = Executor(cmd).execute() 12 | if ret != 0: 13 | raise ValueError("Could not execute adb shell " + cmd) 14 | return out 15 | 16 | def pull(self, src, dst): 17 | cmd = "adb pull " + src + " " + dst 18 | return Executor(cmd).execute() 19 | 20 | def push(self, src, dst): 21 | cmd = "adb push " + src + " " + dst 22 | return Executor(cmd).execute() 23 | 24 | def ls(self, path): 25 | out = self.shell("ls " + path) 26 | return out.splitlines() 27 | 28 | def start_app(self, pkg_name): 29 | cmd = 'monkey -p ' + pkg_name + ' -c android.intent.category.LAUNCHER 1' 30 | self.shell(cmd) -------------------------------------------------------------------------------- /btsnoop/android/snoopphone.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import ConfigParser 4 | 5 | from phone import Phone 6 | 7 | 8 | BTSTACK_CONFIG_FILE = 'bt_stack.conf' 9 | # Needs to always be in UNIX format, hence no os.path.join 10 | BTSTACK_CONFIG_PATH = '/etc/bluetooth/' + BTSTACK_CONFIG_FILE 11 | 12 | BTSNOOP_FALLBACK_FILE = 'btsnoop_hci.log' 13 | # Needs to always be in UNIX format, hence no os.path.join 14 | BTSNOOP_FALLBACK_PATH = '/sdcard/' + BTSNOOP_FALLBACK_FILE 15 | 16 | 17 | class SnoopPhone(Phone): 18 | 19 | def __init__(self, serial=None): 20 | super(SnoopPhone, self).__init__(serial=serial) 21 | self._tmp_dir = tempfile.mkdtemp() 22 | 23 | def pull_btsnoop(self, dst=None): 24 | 25 | btsnoop_path, btsnoop_file = self._locate_btsnoop() 26 | 27 | if not dst: 28 | dst = os.path.join(self._tmp_dir, btsnoop_file) 29 | 30 | ret = super(SnoopPhone, self).pull(btsnoop_path, dst) 31 | if ret[0] == 0: 32 | print ret[1] + " " + dst 33 | return dst 34 | else: 35 | return None 36 | 37 | def _locate_btsnoop(self): 38 | tmp_config_path = self._pull_btconfig() 39 | config = self._parse_btconfig(tmp_config_path) 40 | try: 41 | btsnoop_path = config['btsnoopfilename'] 42 | return btsnoop_path, os.path.basename(btsnoop_path) 43 | except: 44 | return BTSNOOP_FALLBACK_PATH, BTSNOOP_FALLBACK_FILE 45 | 46 | def _parse_btconfig(self, path): 47 | if not os.path.exists(path): 48 | raise ValueError("Failed to read bt_stack.conf") 49 | 50 | # Needed because ConfigParser won't work without at least one header 51 | class DummyHeaderFile(object): 52 | def __init__(self, fp): 53 | self.fp = fp 54 | self.dummy = '[dummy]\n' 55 | 56 | def readline(self): 57 | if self.dummy: 58 | try: 59 | return self.dummy 60 | finally: 61 | self.dummy = None 62 | else: 63 | return self.fp.readline() 64 | 65 | # Parse key/values 66 | parser = ConfigParser.SafeConfigParser() 67 | with open(path, 'r') as f: 68 | parser.readfp(DummyHeaderFile(f)) 69 | return dict(parser.items('dummy')) 70 | 71 | def _pull_btconfig(self): 72 | dst = os.path.join(self._tmp_dir, BTSTACK_CONFIG_FILE) 73 | retcode, out = super(SnoopPhone, self).pull(BTSTACK_CONFIG_PATH, dst) 74 | if retcode == 0: 75 | return dst 76 | else: 77 | raise ValueError("Failed to pull bt_stack.conf") -------------------------------------------------------------------------------- /btsnoop/bt/__init__.py: -------------------------------------------------------------------------------- 1 | from . import hci 2 | from . import hci_uart 3 | from . import hci_cmd 4 | from . import hci_evt 5 | from . import hci_sco 6 | from . import hci_acl 7 | from . import l2cap 8 | from . import att 9 | from . import smp -------------------------------------------------------------------------------- /btsnoop/bt/att.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parse ATT packets 3 | """ 4 | import struct 5 | 6 | 7 | """ 8 | ATT PDUs 9 | 10 | References can be found here: 11 | * https://www.bluetooth.org/en-us/specification/adopted-specifications - Core specification 4.1 12 | ** [vol 3] Part F (Section 3.4.8) - Attribute Opcode Summary 13 | """ 14 | ATT_PDUS = { 15 | 0x01 : "ATT Error_Response", 16 | 0x02 : "ATT Exchange_MTU_Request", 17 | 0x03 : "ATT Exchange_MTU_Response", 18 | 0x04 : "ATT Find_Information_Request", 19 | 0x05 : "ATT Find_Information_Response", 20 | 0x06 : "ATT Find_By_Type_Value_Request", 21 | 0x07 : "ATT Find_By_Type_Value_Response", 22 | 0x08 : "ATT Read_By_Type_Request", 23 | 0x09 : "ATT Read_By_Type_Response", 24 | 0x0A : "ATT Read_Request", 25 | 0x0B : "ATT Read_Response", 26 | 0x0C : "ATT Read_Blob_Request", 27 | 0x0D : "ATT Read_Blob_Response", 28 | 0x0E : "ATT Read_Multiple_Request", 29 | 0x0F : "ATT Read_Multiple_Response", 30 | 0x10 : "ATT Read_By_Group_Type_Request", 31 | 0x11 : "ATT Read_By_Group_Type_Response", 32 | 0x12 : "ATT Write_Request", 33 | 0x13 : "ATT Write_Response", 34 | 0x52 : "ATT Write_Command", 35 | 0xD2 : "ATT Signed_Write_Command", 36 | 0x16 : "ATT Prepare_Write_Request", 37 | 0x17 : "ATT Prepare_Write_Response", 38 | 0x18 : "ATT Execute_Write_Request", 39 | 0x19 : "ATT Execute_Write_Response", 40 | 0x1B : "ATT Handle_Value_Notification", 41 | 0x1D : "ATT Handle_Value_Indication", 42 | 0x1E : "ATT Handle_Value_Confirmation" 43 | } 44 | 45 | 46 | def parse(data): 47 | """ 48 | Attribute opcode is the first octet of the PDU 49 | 50 | 0 1 2 3 4 5 6 7 51 | ----------------- 52 | | att opcode | 53 | ----------------- 54 | | a |b|c| 55 | ----------------- 56 | a - method 57 | b - command flag 58 | c - authentication signature flag 59 | 60 | References can be found here: 61 | * https://www.bluetooth.org/en-us/specification/adopted-specifications - Core specification 4.1 62 | ** [vol 3] Part F (Section 3.3) - Attribute PDU 63 | 64 | Return a tuple (opcode, data) 65 | """ 66 | opcode = struct.unpack(" 5 | """ 6 | import datetime 7 | import sys 8 | import struct 9 | 10 | 11 | """ 12 | Record flags conform to: 13 | - bit 0 0 = sent, 1 = received 14 | - bit 1 0 = data, 1 = command/event 15 | - bit 2-31 reserved 16 | 17 | Direction is relative to host / DTE. i.e. for Bluetooth controllers, 18 | Send is Host->Controller, Receive is Controller->Host 19 | """ 20 | BTSNOOP_FLAGS = { 21 | 0 : ("host", "controller", "data"), 22 | 1 : ("controller", "host", "data"), 23 | 2 : ("host", "controller", "command"), 24 | 3 : ("controller", "host", "event") 25 | } 26 | 27 | 28 | def parse(filename): 29 | """ 30 | Parse a Btsnoop packet capture file. 31 | 32 | Btsnoop packet capture file is structured as: 33 | 34 | ----------------------- 35 | | header | 36 | ----------------------- 37 | | packet record nbr 1 | 38 | ----------------------- 39 | | packet record nbr 2 | 40 | ----------------------- 41 | | ... | 42 | ----------------------- 43 | | packet record nbr n | 44 | ----------------------- 45 | 46 | References can be found here: 47 | * http://tools.ietf.org/html/rfc1761 48 | * http://www.fte.com/webhelp/NFC/Content/Technical_Information/BT_Snoop_File_Format.htm 49 | 50 | Return a list of records, each holding a tuple of: 51 | * sequence nbr 52 | * record length (in bytes) 53 | * flags 54 | * timestamp 55 | * data 56 | """ 57 | with open(filename, "rb") as f: 58 | 59 | # Validate file header 60 | (identification, version, type) = _read_file_header(f) 61 | _validate_file_header(identification, version, type) 62 | 63 | # Not using the following data: 64 | # record[1] - original length 65 | # record[4] - cumulative drops 66 | return map(lambda record: 67 | (record[0], record[2], record[3], _parse_time(record[5]), record[6]), 68 | _read_packet_records(f)) 69 | 70 | 71 | def _read_file_header(f): 72 | """ 73 | Header should conform to the following format 74 | 75 | ---------------------------------------- 76 | | identification pattern| 77 | | 8 bytes | 78 | ---------------------------------------- 79 | | version number | 80 | | 4 bytes | 81 | ---------------------------------------- 82 | | data link type = HCI UART (H4) | 83 | | 4 bytes | 84 | ---------------------------------------- 85 | 86 | All integer values are stored in "big-endian" order, with the high-order bits first. 87 | """ 88 | ident = f.read(8) 89 | version, data_link_type = struct.unpack( ">II", f.read(4 + 4) ) 90 | return (ident, version, data_link_type) 91 | 92 | 93 | def _validate_file_header(identification, version, data_link_type): 94 | """ 95 | The identification pattern should be: 96 | 'btsnoop\0' 97 | 98 | The version number should be: 99 | 1 100 | 101 | The data link type can be: 102 | - Reserved 0 - 1000 103 | - Un-encapsulated HCI (H1) 1001 104 | - HCI UART (H4) 1002 105 | - HCI BSCP 1003 106 | - HCI Serial (H5) 1004 107 | - Unassigned 1005 - 4294967295 108 | 109 | For SWAP, data link type should be: 110 | HCI UART (H4) 1002 111 | """ 112 | assert identification == "btsnoop\0" 113 | assert version == 1 114 | assert data_link_type == 1002 115 | print "Btsnoop capture file version {0}, type {1}".format(version, data_link_type) 116 | 117 | 118 | def _read_packet_records(f): 119 | """ 120 | A record should confirm to the following format 121 | 122 | -------------------------- 123 | | original length | 124 | | 4 bytes 125 | -------------------------- 126 | | included length | 127 | | 4 bytes 128 | -------------------------- 129 | | packet flags | 130 | | 4 bytes 131 | -------------------------- 132 | | cumulative drops | 133 | | 4 bytes 134 | -------------------------- 135 | | timestamp microseconds | 136 | | 8 bytes 137 | -------------------------- 138 | | packet data | 139 | -------------------------- 140 | 141 | All integer values are stored in "big-endian" order, with the high-order bits first. 142 | """ 143 | seq_nbr = 1 144 | while True: 145 | pkt_hdr = f.read(4 + 4 + 4 + 4 + 8) 146 | if not pkt_hdr or len(pkt_hdr) != 24: 147 | # EOF 148 | break 149 | 150 | orig_len, inc_len, flags, drops, time64 = struct.unpack( ">IIIIq", pkt_hdr) 151 | assert orig_len == inc_len 152 | 153 | data = f.read(inc_len) 154 | assert len(data) == inc_len 155 | 156 | yield ( seq_nbr, orig_len, inc_len, flags, drops, time64, data ) 157 | seq_nbr += 1 158 | 159 | 160 | def _parse_time(time): 161 | """ 162 | Record time is a 64-bit signed integer representing the time of packet arrival, 163 | in microseconds since midnight, January 1st, 0 AD nominal Gregorian. 164 | 165 | In order to avoid leap-day ambiguity in calculations, note that an equivalent 166 | epoch may be used of midnight, January 1st 2000 AD, which is represented in 167 | this field as 0x00E03AB44A676000. 168 | """ 169 | time_betw_0_and_2000_ad = int("0x00E03AB44A676000", 16) 170 | time_since_2000_epoch = datetime.timedelta(microseconds=time) - datetime.timedelta(microseconds=time_betw_0_and_2000_ad) 171 | return datetime.datetime(2000, 1, 1) + time_since_2000_epoch 172 | 173 | 174 | def flags_to_str(flags): 175 | """ 176 | Returns a tuple of (src, dst, type) 177 | """ 178 | assert flags in [0,1,2,3] 179 | return BTSNOOP_FLAGS[flags] 180 | 181 | 182 | def print_hdr(): 183 | """ 184 | Print the script header 185 | """ 186 | print "" 187 | print "##############################" 188 | print "# #" 189 | print "# btsnoop parser v0.1 #" 190 | print "# #" 191 | print "##############################" 192 | print "" 193 | 194 | 195 | def main(filename): 196 | records = parse(filename) 197 | print records 198 | return 0 199 | 200 | 201 | if __name__ == "__main__": 202 | if len(sys.argv) < 2: 203 | print __doc__ 204 | sys.exit(1) 205 | 206 | print_hdr() 207 | sys.exit(main(sys.argv[1])) --------------------------------------------------------------------------------