From ece0909fb102cddf829eb0ac9a2f18b91c6dc5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Mon, 3 Aug 2020 09:08:04 +0100 Subject: Extend the Libre2 encryption reversing tool. --- .../abbott/encrypted_setup_extractor.py | 179 +++++++++++++++++++++ reversing_tools/abbott/extract_freestyle.py | 42 +++++ 2 files changed, 221 insertions(+) create mode 100644 reversing_tools/abbott/encrypted_setup_extractor.py diff --git a/reversing_tools/abbott/encrypted_setup_extractor.py b/reversing_tools/abbott/encrypted_setup_extractor.py new file mode 100644 index 0000000..cc57f0f --- /dev/null +++ b/reversing_tools/abbott/encrypted_setup_extractor.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +# +# SPDX-FileCopyrightText: © 2019 The usbmon-tools Authors +# SPDX-FileCopyrightText: © 2020 The glucometerutils Authors +# +# SPDX-License-Identifier: Apache-2.0 + +import argparse +import logging +import sys + +import construct +import usbmon +import usbmon.pcapng + +_SERIAL_NUMBER_RESPONSE_TYPE = 0x06 +_ENCRYPTION_SETUP_REQ_TYPE = 0x14 +_ENCRYPTION_SETUP_RESP_TYPE = 0x33 + + +_START_AUTHORIZE_CMD = 0x11 +_CHALLENGE_CMD = 0x16 +_CHALLENGE_RESPONSE_CMD = 0x17 + + +_ABBOTT_VENDOR_ID = 0x1A61 +_LIBRE2_PRODUCT_ID = 0x3950 + +_SERIAL_NO = construct.Struct( + message_type=construct.Const(_SERIAL_NUMBER_RESPONSE_TYPE, construct.Byte), + length=construct.Const(14, construct.Byte), + serial_number=construct.PaddedString(13, "ascii"), + termination=construct.Const(0, construct.Byte), +) + +_CHALLENGE = construct.Struct( + message_type=construct.Const(_ENCRYPTION_SETUP_RESP_TYPE, construct.Byte), + length=construct.Const(16, construct.Byte), + subcmd=construct.Const(_CHALLENGE_CMD, construct.Byte), + challenge=construct.Bytes(8), + iv=construct.Bytes(7), +) + +_CHALLENGE_RESPONSE = construct.Struct( + message_type=construct.Const(_ENCRYPTION_SETUP_REQ_TYPE, construct.Byte), + length=construct.Const(26, construct.Byte), + subcmd=construct.Const(_CHALLENGE_RESPONSE_CMD, construct.Byte), + challenge_response_encrypted=construct.Bytes(16), + const=construct.Const(1, construct.Byte), + mac=construct.Bytes(8), +) + + +def main(): + if sys.version_info < (3, 7): + raise Exception("Unsupported Python version, please use at least Python 3.7.") + + parser = argparse.ArgumentParser() + + parser.add_argument( + "--device_address", + action="store", + type=str, + help=( + "Device address (busnum.devnum) of the device to extract capture" + " of. If none provided, device descriptors will be relied on." + ), + ) + + parser.add_argument( + "--vlog", + action="store", + required=False, + type=int, + help=( + "Python logging level. See the levels at" + " https://docs.python.org/3/library/logging.html#logging-levels" + ), + ) + + parser.add_argument( + "pcap_files", + action="store", + type=argparse.FileType(mode="rb"), + help="Path to the pcapng file with the USB capture.", + nargs="+", + ) + + args = parser.parse_args() + + logging.basicConfig(level=args.vlog) + + for pcap_file in args.pcap_files: + session = usbmon.pcapng.parse_stream(pcap_file, retag_urbs=False) + + if not args.device_address: + for descriptor in session.device_descriptors.values(): + if ( + descriptor.vendor_id == _ABBOTT_VENDOR_ID + and descriptor.product_id == _LIBRE2_PRODUCT_ID + ): + if ( + args.device_address + and args.device_address != descriptor.address + ): + raise Exception( + "Multiple Libre2 devices present in capture, please" + " provide a --device_address flag." + ) + device_address = descriptor.address + else: + device_address = descriptor.address + + descriptor = session.device_descriptors.get(device_address, None) + if descriptor: + assert descriptor.vendor_id == _ABBOTT_VENDOR_ID + assert descriptor.product_id == _LIBRE2_PRODUCT_ID + + serial_number = "UNKNOWN" + challenge = "UNKNOWN" + iv = "UNKNOWN" + encrypted_challenge = "UNKNOWN" + mac = "UNKNOWN" + + for first, second in session.in_pairs(): + # Ignore stray callbacks/errors. + if not first.type == usbmon.constants.PacketType.SUBMISSION: + continue + + if not first.address.startswith(f"{device_address}."): + # No need to check second, they will be linked. + continue + + if first.xfer_type == usbmon.constants.XferType.INTERRUPT: + pass + elif ( + first.xfer_type == usbmon.constants.XferType.CONTROL + and not first.setup_packet + or first.setup_packet.type == usbmon.setup.Type.CLASS + ): + pass + else: + continue + + if first.direction == usbmon.constants.Direction.OUT: + packet = first + else: + packet = second + + if not packet.payload: + continue + + assert len(packet.payload) >= 2 + + message_type = packet.payload[0] + + if message_type == _SERIAL_NUMBER_RESPONSE_TYPE: + obj = _SERIAL_NO.parse(packet.payload) + serial_number = obj.serial_number + elif ( + message_type == _ENCRYPTION_SETUP_RESP_TYPE + and packet.payload[2] == _CHALLENGE_CMD + ): + obj = _CHALLENGE.parse(packet.payload) + challenge = obj.challenge.hex() + iv = obj.iv.hex() + elif ( + message_type == _ENCRYPTION_SETUP_REQ_TYPE + and packet.payload[2] == _CHALLENGE_RESPONSE_CMD + ): + obj = _CHALLENGE_RESPONSE.parse(packet.payload) + encrypted_challenge = obj.challenge_response_encrypted.hex() + mac = obj.mac.hex() + + print(f"{serial_number},{challenge},{iv},{encrypted_challenge},{mac}") + + +if __name__ == "__main__": + main() diff --git a/reversing_tools/abbott/extract_freestyle.py b/reversing_tools/abbott/extract_freestyle.py index 006b393..52eeaf2 100755 --- a/reversing_tools/abbott/extract_freestyle.py +++ b/reversing_tools/abbott/extract_freestyle.py @@ -33,6 +33,13 @@ _UNENCRYPTED_TYPES = ( _KEEPALIVE_TYPE, ) +_ENCRYPTION_SETUP_TYPES = (0x14, 0x33) + +_START_AUTHORIZE_CMD = 0x11 +_CHALLENGE_CMD = 0x16 +_CHALLENGE_RESPONSE_CMD = 0x17 +_CHALLENGE_ACCEPTED_CMD = 0x18 + _ABBOTT_VENDOR_ID = 0x1A61 _LIBRE2_PRODUCT_ID = 0x3950 @@ -69,6 +76,15 @@ def main(): ), ) + parser.add_argument( + "--verbose-encryption-setup", + action="store_true", + help=( + "Whether to parse encryption setup commands and printing their component" + " together with the raw messsage." + ), + ) + parser.add_argument( "--vlog", action="store", @@ -173,6 +189,32 @@ def main(): message_type = f"x{message_type:02x}" message = parsed.encrypted_message + elif args.verbose_encryption_setup and message_type in _ENCRYPTION_SETUP_TYPES: + message_length = packet.payload[1] + message_end_idx = 2 + message_length + message = packet.payload[2:message_end_idx] + + if message[0] == _START_AUTHORIZE_CMD: + message_metadata.append("START_AUTHORIZE") + elif message[0] == _CHALLENGE_CMD: + message_metadata.append("CHALLENGE") + challenge = message[1:9] + iv = message[9:16] + message_metadata.append(f"CHALLENGE={challenge.hex()}") + message_metadata.append(f"IV={iv.hex()}") + elif message[0] == _CHALLENGE_RESPONSE_CMD: + message_metadata.append("CHALLENGE_RESPONSE") + encrypted_challenge = message[1:17] + challenge_mac = message[18:26] + message_metadata.append( + f"ENCRYPTED_CHALLENGE={encrypted_challenge.hex()}" + ) + message_metadata.append(f"MAC={challenge_mac.hex()}") + elif message[0] == _CHALLENGE_ACCEPTED_CMD: + message_metadata.append("CHALLENGE_ACCEPTED") + + message_metadata.append(f"RAW_LENGTH={message_length}") + message_type = f" {message_type:02x}" else: message_length = packet.payload[1] message_metadata.append(f"LENGTH={message_length}") -- cgit v1.2.3