├── fitbit-smartrf.psd ├── LICENSE ├── README.md ├── tibtle2pcap.py └── pcapdump.py /fitbit-smartrf.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joswr1ght/tibtle2pcap/HEAD/fitbit-smartrf.psd -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Joshua Wright 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | tibtle2pcap 2 | =========== 3 | 4 | Convert TI SmartRF Bluetooth Low Energy Packet Captures to Libpcap Format 5 | 6 | HEAD 7 | 8 | Read a Bluetooth Low Energy packet capture savefile generated by 9 | the TI Packet Sniffer utility (.psd file, but not PhotoShop), and convert it 10 | to a libpcap packet capture file. The libpcap packet capture file is formatted 11 | to use the PPI DLT, with DLT_USER set so the BTLE Wireshark plugin can be used 12 | to decode the BTLE traffic. 13 | 14 | You can download the SmartRF Packet Sniffer software here: 15 | http://www.ti.com/tool/packet-sniffer 16 | 17 | The CC2540 USB Evaluation Kit USB dongle that captures Bluetooth LE 18 | traffic (and injects) with default firmware is available from digikey.com 19 | and many other sites for $50 with the part number CC2540EMK-USB. 20 | 21 | 22 | USAGE 23 | 24 | ``` 25 | C:\>python tibtle2pcap.py fitbit-smartrf.psd out.pcap 26 | ``` 27 | 28 | The capture file "fitbit-smartrf.psd" is included with this software as an example. 29 | 30 | 31 | BUGS 32 | 33 | + Timestamp information is not present in the output libpcap file. 34 | + The PPI header does not include important characteristics such as channel, 35 | RSSI, etc. This information is available, but the PPI header format doesn't 36 | accommodate for link layer information outside of 802.11. The libpcap-workers 37 | list (with Mike Ryan's assistance) is working on rectifying this. I'll update 38 | at some point in the future. 39 | 40 | 41 | THANKS 42 | 43 | Many thanks to Mike Ryan for blazing the path forward to open up Bluetooth LE 44 | sniffing and traffic analysis. 45 | 46 | The PcapDump class is borrowed from the KillerBee project. I wrote it 47 | initially, then Ryan Speers and Ricky Melgares made it better. I stole it here 48 | so as not to have to deal with dependencies. 49 | 50 | 51 | Joshua Wright, 2014-03-03 52 | jwright@hasborg.com 53 | -------------------------------------------------------------------------------- /tibtle2pcap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # tibtle2pcap: Read a Bluetooth Low Energy packet capture savefile generated by 3 | # the TI Packet Sniffer utility (.psd file, but not PhotoShop), and convert it 4 | # to a libpcap packet capture file. The libpcap packet capture file is formatted 5 | # to use the PPI DLT, with DLT_USER set so the BTLE Wireshark plugin can be used 6 | # to decode the BTLE traffic. 7 | # 8 | # You can download the SmartRF Packet Sniffer software here: 9 | # http://www.ti.com/tool/packet-sniffer 10 | # The CC2540 USB Evaluation Kit USB dongle that captures Bluetooth LE 11 | # traffic (and injects) with default firmware is available from digikey.com 12 | # and many other sites for $50 with the part number CC2540EMK-USB. 13 | # 14 | # Many thanks to Mike Ryan for blazing the path forward to open up Bluetooth LE 15 | # sniffing and traffic analysis. 16 | # Joshua Wright, 2014-03-03 17 | 18 | import struct 19 | import sys 20 | import pcapdump 21 | TIRECLEN=271 22 | DLT_PPI=192 23 | 24 | def chan2mhz(chan): 25 | chanmap = { 26 | 37:2402, 0:2404, 1:2406, 2:2408, 3:2410, 4:2412, 5:2414, 6:2416, 7:2418, 27 | 8:2420, 9:2422, 10:2424, 38:2426, 11:2428, 12:2430, 13:2432, 14:2434, 15:2436, 28 | 16:2438, 17:2440, 18:2442, 19:2444, 20:2446, 21:2448, 22:2450, 23:2452, 24:2454, 29 | 25:2456, 26:2458, 27:2460, 28:2462, 29:2464, 30:2466, 31:2468, 32:2470, 33:2472, 30 | 34:2474, 35:2476, 36:2478, 39:2480 } 31 | try: 32 | return chanmap[chan] 33 | except IndexError: 34 | return 0 35 | 36 | if len(sys.argv) < 3: 37 | print "tibtle2pcap.py [TI psd file] [pcapfile]" 38 | sys.exit(1) 39 | 40 | capfile = open(sys.argv[1], "rb") 41 | capturedata = capfile.read() 42 | capfile.close() 43 | 44 | pd = pcapdump.PcapDumper(DLT_PPI, sys.argv[2]) 45 | 46 | # Chunk packet content into generator 47 | packets=(capturedata[i:i+TIRECLEN] for i in xrange(0, len(capturedata), TIRECLEN)) 48 | 49 | for packet in packets: 50 | (pinfo, pnum, pts, plen) = struct.unpack(' 0) 62 | 63 | #print "Packet",pnum,"RSSI",rssi,"Channel",channel,"FCSOK",fcsok 64 | #hexdump.hexdump(payload) 65 | # This hideoous string is a PPI header with DLT_USER specified so we can use 66 | # the btle plugin with Wireshark. 67 | pd.pcap_dump("\x00\x00\x08\x00\x93\x00\x00\x00" + payload) 68 | print 69 | -------------------------------------------------------------------------------- /pcapdump.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import time 3 | 4 | PCAPH_MAGIC_NUM = 0xa1b2c3d4 5 | PCAPH_VER_MAJOR = 2 6 | PCAPH_VER_MINOR = 4 7 | PCAPH_THISZONE = 0 8 | PCAPH_SIGFIGS = 0 9 | PCAPH_SNAPLEN = 65535 10 | DLT_PPI = 192 11 | DOT11COMMON_TAG = 000002 12 | GPS_TAG = 30002 13 | 14 | class PcapDumper: 15 | def __init__(self, datalink, savefile, ppi = False): 16 | ''' 17 | Creates a libpcap file using the specified datalink type. 18 | @type datalink: Integer 19 | @param datalink: Datalink type, one of DLT_* defined in pcap-bpf.h 20 | @type savefile: String 21 | @param savefile: Output libpcap filename to open 22 | @rtype: None 23 | ''' 24 | self.ppi = ppi 25 | self.__fh = open(savefile, mode='wb') 26 | self.datalink = datalink 27 | self.__fh.write(''.join([ 28 | struct.pack("I", PCAPH_MAGIC_NUM), 29 | struct.pack("H", PCAPH_VER_MAJOR), 30 | struct.pack("H", PCAPH_VER_MINOR), 31 | struct.pack("I", PCAPH_THISZONE), 32 | struct.pack("I", PCAPH_SIGFIGS), 33 | struct.pack("I", PCAPH_SNAPLEN), 34 | struct.pack("I", DLT_PPI if self.ppi else self.datalink) 35 | ])) 36 | 37 | def pcap_dump(self, packet, ts_sec=None, ts_usec=None, orig_len=None, 38 | freq_mhz = None, ant_dbm = None, location = None, dlt = None): 39 | ''' 40 | Appends a new packet to the libpcap file. Optionally specify ts_sec 41 | and tv_usec for timestamp information, otherwise the current time is 42 | used. Specify orig_len if your snaplen is smaller than the entire 43 | packet contents. 44 | @type ts_sec: Integer 45 | @param ts_sec: Timestamp, number of seconds since Unix epoch. Default 46 | is the current timestamp. 47 | @type ts_usec: Integer 48 | @param ts_usec: Timestamp microseconds. Defaults to current timestamp. 49 | @type orig_len: Integer 50 | @param orig_len: Length of the original packet, used if the packet you 51 | are writing is smaller than the original packet. Defaults to the 52 | specified packet's length. 53 | @type location: Tuple 54 | @param location: 3-tuple of (longitude, latitude, altitude). 55 | @type packet: String 56 | @param packet: Packet contents 57 | @rtype: None 58 | ''' 59 | 60 | # Build CACE PPI headers if requested 61 | if self.ppi is True: 62 | pph_len = 8 #ppi_header 63 | 64 | #CACE PPI Field 802.11-Common 65 | pph_len += 24 #802.11-common header and data 66 | rf_freq_mhz = 0x0000 67 | if freq_mhz is not None: rf_freq_mhz = freq_mhz 68 | rf_ant_dbm = 0 69 | if ant_dbm is not None: rf_ant_dbm = ant_dbm 70 | caceppi_f80211common = ''.join([ 71 | struct.pack(" -180.00000005 and lat < 180.00000005: 90 | lat_i = int(round((lat + 180.0) * 1e7)) 91 | else: 92 | raise Exception("Latitude value is out of expected range: %.8f" % lat) 93 | if lon > -180.00000005 and lon < 180.00000005: 94 | lon_i = int(round((lon + 180.0) * 1e7)) 95 | else: 96 | raise Exception("Longitude value is out of expected range: %.8f" % lon) 97 | if alt > -180000.00005 and alt < 180000.00005: 98 | alt_i = int(round((alt + 180000.0) * 1e4)) 99 | else: 100 | raise Exception("Altitude value is out of expected range: %.8f" % lon) 101 | # Build Geolocation PPI Header 102 | caceppi_fgeolocation = ''.join([ 103 | struct.pack("