From 9d0eec53dc09703b64fb0d1391628f020d92bca9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 04:49:07 +0000 Subject: [PATCH] Complete capture parsing, protocol experts and test suite - Detect pcapng / classic pcap from file contents; parse multiple pcapng sections and interfaces, simple packet blocks, and add Linux usbmon (189/220) alongside Darwin and USBPcap. - Treat captures as untrusted: bounds check every structure, replace asserts with warnings, stop cleanly on corrupt framing, and stop unknown link types from hanging the parser. - Pair submissions and completions so OUT data comes from the submit and IN data from the completion; handle USBPcap control transfers without a data stage (SET_CONFIGURATION previously never applied). - Track interface alternate settings and SET_INTERFACE, key configurations by value, keep BOS descriptors, decode strings to UTF-8, and start a new device session on re-enumeration. - USBMUX: fix the payload offset passed to streams, reassemble mux packets across USB transfers, honour v1/v2 headers and TCP data offset, track FIN/RST and pre-capture connections. - MuxStream: decode length-prefixed XML and binary plists in both directions, detect TLS and raw streams, name services from lockdownd StartService replies. - DFU expert reassembles DNLOAD images, follows GETSTATUS state and flags anomalies; Recovery expert decodes iBoot commands, uploads and console output. - CLI: --verbose, --plists, --extract, --warnings. - Build a static library; add 34 tests over synthetic captures in every format including fuzzing, an ASan/UBSan option and CI on Linux, sanitizers and macOS. Fix the unbalanced #pragma pack in pcapng.h. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_017MAjMcmK7j74hjnqWwUgCi --- .github/workflows/ci.yml | 35 +- .gitignore | 3 +- CMakeLists.txt | 50 ++- README.md | 282 +++++++----- ext/pcapng.h | 84 +++- ext/usb.h | 4 +- ext/usbmux.h | 3 + src/demuxusb.hpp | 66 ++- src/file/InputFile.cpp | 350 ++++++++++++++- src/file/InputFile.h | 130 +++--- src/file/PcapInputFile.cpp | 49 +++ src/file/PcapInputFile.h | 22 + src/file/PcapNGInputFile.cpp | 219 ++++------ src/file/PcapNGInputFile.h | 6 +- src/main.cpp | 146 ++++++- src/protocol/DFUInterfaceExpert.cpp | 150 ++++++- src/protocol/DFUInterfaceExpert.h | 42 ++ src/protocol/InterfaceExpert.h | 30 +- src/protocol/MuxStream.cpp | 201 ++++++--- src/protocol/MuxStream.h | 95 ++++- src/protocol/RecoveryInterfaceExpert.cpp | 130 +++++- src/protocol/RecoveryInterfaceExpert.h | 36 +- src/protocol/USBMUXInterfaceExpert.cpp | 253 ++++++++--- src/protocol/USBMUXInterfaceExpert.h | 36 +- src/usb/Device.cpp | 381 +++++++++++------ src/usb/Device.h | 79 +++- src/usb/USBDescriptor.cpp | 5 - src/usb/USBDescriptor.h | 15 - test/CaptureBuilder.h | 521 +++++++++++++++++++++++ test/test_DFU.cpp | 100 +++++ test/test_Device.cpp | 119 ++++++ test/test_PcapInputFile.cpp | 37 ++ test/test_PcapNGInputFile.cpp | 147 ++++++- test/test_Recovery.cpp | 85 ++++ test/test_Robustness.cpp | 93 ++++ test/test_USBMUX.cpp | 188 ++++++++ 36 files changed, 3505 insertions(+), 687 deletions(-) create mode 100644 src/file/PcapInputFile.cpp create mode 100644 src/file/PcapInputFile.h delete mode 100644 src/usb/USBDescriptor.cpp delete mode 100644 src/usb/USBDescriptor.h create mode 100644 test/CaptureBuilder.h create mode 100644 test/test_DFU.cpp create mode 100644 test/test_Device.cpp create mode 100644 test/test_PcapInputFile.cpp create mode 100644 test/test_Recovery.cpp create mode 100644 test/test_Robustness.cpp create mode 100644 test/test_USBMUX.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4862c4a..2d24c61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,24 +1,43 @@ name: ci.yaml on: - - push + push: + pull_request: jobs: ci: - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: ubuntu + os: ubuntu-latest + cmake_flags: -DCMAKE_BUILD_TYPE=Release + - name: ubuntu-sanitizers + os: ubuntu-latest + cmake_flags: -DCMAKE_BUILD_TYPE=Debug -DDEMUXUSB_SANITIZE=ON + - name: macos + os: macos-latest + cmake_flags: -DCMAKE_BUILD_TYPE=Release + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - - name: Install dependencies + - name: Install dependencies (Ubuntu) + if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y cmake pkg-config libplist-dev libboost-container-dev libplist++-dev libboost-filesystem-dev libboost-iostreams-dev libboost-test-dev + sudo apt-get install -y cmake pkg-config libplist-dev libboost-container-dev libboost-filesystem-dev libboost-iostreams-dev libboost-test-dev + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: brew install pkg-config boost libplist - name: Configure CMake - run: cmake -B build -DCMAKE_BUILD_TYPE=Release + run: cmake -B build ${{ matrix.cmake_flags }} - name: Build - run: cmake --build build + run: cmake --build build --parallel - name: Run tests run: ctest --test-dir build --output-on-failure - \ No newline at end of file diff --git a/.gitignore b/.gitignore index 492ff5c..f524ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ cmake-build-debug/ -build/ \ No newline at end of file +build/ +build-*/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 928908a..d465365 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,31 +1,49 @@ cmake_minimum_required(VERSION 3.17) -project(demuxusb) +project(demuxusb CXX) enable_testing() find_package(PkgConfig REQUIRED) set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +option(DEMUXUSB_SANITIZE "Build with address and undefined behaviour sanitizers" OFF) +if (DEMUXUSB_SANITIZE) + add_compile_options(-fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer) + add_link_options(-fsanitize=address,undefined) +endif () pkg_check_modules(PLIST REQUIRED IMPORTED_TARGET libplist-2.0) find_package(Boost REQUIRED COMPONENTS container filesystem iostreams unit_test_framework) -add_executable(demuxusb src/main.cpp src/file/InputFile.cpp src/file/InputFile.h - src/usb/Device.cpp src/usb/Device.h src/protocol/MuxStream.cpp src/protocol/MuxStream.h src/file/PcapNGInputFile.cpp - src/file/PcapNGInputFile.h src/protocol/InterfaceExpert.cpp src/protocol/InterfaceExpert.h src/protocol/DFUInterfaceExpert.cpp - src/protocol/DFUInterfaceExpert.h src/protocol/RecoveryInterfaceExpert.cpp src/protocol/RecoveryInterfaceExpert.h - src/protocol/USBMUXInterfaceExpert.cpp src/protocol/USBMUXInterfaceExpert.h src/usb/USBDescriptor.cpp src/usb/USBDescriptor.h) - -target_include_directories(demuxusb PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/ext) -target_precompile_headers(demuxusb PRIVATE src/demuxusb.hpp) -target_link_libraries(demuxusb PUBLIC PkgConfig::PLIST Boost::container - Boost::iostreams Boost::filesystem) +add_library(demuxusb_lib STATIC + src/file/InputFile.cpp src/file/InputFile.h + src/file/PcapNGInputFile.cpp src/file/PcapNGInputFile.h + src/file/PcapInputFile.cpp src/file/PcapInputFile.h + src/usb/Device.cpp src/usb/Device.h + src/protocol/MuxStream.cpp src/protocol/MuxStream.h + src/protocol/InterfaceExpert.cpp src/protocol/InterfaceExpert.h + src/protocol/DFUInterfaceExpert.cpp src/protocol/DFUInterfaceExpert.h + src/protocol/RecoveryInterfaceExpert.cpp src/protocol/RecoveryInterfaceExpert.h + src/protocol/USBMUXInterfaceExpert.cpp src/protocol/USBMUXInterfaceExpert.h) +set_target_properties(demuxusb_lib PROPERTIES OUTPUT_NAME demuxusb) + +target_include_directories(demuxusb_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/ext) +target_compile_options(demuxusb_lib PRIVATE -Wall) +target_link_libraries(demuxusb_lib PUBLIC PkgConfig::PLIST Boost::container Boost::iostreams Boost::filesystem) +if (PLIST_VERSION VERSION_GREATER_EQUAL 2.3) + target_compile_definitions(demuxusb_lib PRIVATE HAVE_PLIST_MEM_FREE) +endif () + +add_executable(demuxusb src/main.cpp) +target_link_libraries(demuxusb PRIVATE demuxusb_lib) # Testing framework -add_executable(demuxusb_test_suite test/test_main.cpp test/test_PcapNGInputFile.cpp) -target_precompile_headers(demuxusb_test_suite REUSE_FROM demuxusb) -target_link_libraries(demuxusb_test_suite PUBLIC Boost::unit_test_framework) -target_include_directories(demuxusb_test_suite PRIVATE ${Boost_INCLUDE_DIRS}) -add_test(NAME tests COMMAND demuxusb_test_suite) \ No newline at end of file +add_executable(demuxusb_test_suite test/test_main.cpp test/CaptureBuilder.h test/test_PcapNGInputFile.cpp + test/test_PcapInputFile.cpp test/test_Device.cpp test/test_USBMUX.cpp test/test_DFU.cpp + test/test_Recovery.cpp test/test_Robustness.cpp) +target_link_libraries(demuxusb_test_suite PRIVATE demuxusb_lib Boost::unit_test_framework) +add_test(NAME tests COMMAND demuxusb_test_suite) diff --git a/README.md b/README.md index f186f31..661a7c5 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,17 @@ Beyond reverse engineering, DeMuxUSB provides digital forensics capabilities to ## Features -- **PcapNG Capture Processing:** Memory-mapped parsing of PCAPNG captures from macOS Darwin USB (`LINKTYPE_USB_DARWIN`) and Windows/Linux USBPcap (`LINKTYPE_USBPCAP`). -- **USB Device State Reconstruction:** Tracks device descriptors, configuration trees, interfaces, endpoints, string descriptors, and Binary Device Object Store (BOS) descriptors. -- **In-Flight Transaction Assembly:** Correlates USB control setup packets (`usb_setup_t`) with completion stages across all endpoints. -- **Protocol Demultiplexing:** Automatically routes endpoint traffic to specialized **Interface Experts** for DFU, Recovery, and USBMUX protocols. -- **USBMUX & Plist Stream Extraction:** Tracks bidirectional TCP streams multiplexed over USB, decodes packet streams, and deserializes Apple property lists (`plist_t` via `libplist`). -- **Forensic & Traffic Statistics:** Comprehensive accounting of packets, bytes, control transfers, and payload transactions per device and interface. +- **Capture Processing:** Memory-mapped parsing of PCAPNG (multiple sections and interfaces, Enhanced and Simple Packet Blocks) and classic libpcap files, with the format detected from the file contents. +- **Hostile Input Tolerance:** Captures are treated as untrusted. Every structure is bounds checked; corrupt packets are skipped and reported as warnings, and the analysis keeps whatever was parsed before structural damage. The test suite fuzzes every supported format under AddressSanitizer and UndefinedBehaviorSanitizer. +- **USB Device State Reconstruction:** Tracks device descriptors, configuration trees, interfaces and their alternate settings, endpoints, string descriptors (UTF-16 decoded to UTF-8), and Binary Device Object Store (BOS) descriptors, following `SET_CONFIGURATION` and `SET_INTERFACE`. +- **Re-enumeration Tracking:** When an address re-enumerates as a different device (DFU → Recovery → Normal mode during a restore) a new device session is started, so each mode is reported separately. +- **Transaction Assembly:** Correlates submissions with completions for control, bulk and interrupt transfers, taking OUT data from the submission and IN data from the completion as each capture format requires. +- **Protocol Demultiplexing:** Routes endpoint traffic to specialized **Interface Experts** for DFU, Recovery, and USBMUX protocols. +- **USBMUX & Plist Stream Extraction:** Reassembles usbmux packets split across (or packed into) USB transfers, tracks the TCP connections they tunnel, deserializes length-prefixed XML and binary property lists (`libplist`), detects TLS upgrades, and names service connections from lockdownd `StartService` replies. +- **DFU Analysis:** Reassembles `DFU_DNLOAD` images, follows the DFU state machine from `GETSTATUS`, and flags protocol anomalies (oversized downloads, aborted partial downloads, unexpected requests). +- **Recovery Mode Analysis:** Decodes iBoot commands and responses, file uploads, and console output as spoken by `libirecovery`. +- **Artifact Extraction:** Writes DFU images, recovery mode uploads, the iBoot console and plist transcripts to disk. +- **Forensic & Traffic Statistics:** Accounting of packets, bytes, control transfers, and payload transactions per device and interface. --- @@ -44,18 +49,21 @@ Beyond reverse engineering, DeMuxUSB provides digital forensics capabilities to | Mode / Protocol | USB Vendor / Product ID | Description | | :--- | :--- | :--- | -| **DFU Mode** | `0x05ac` / `0x1227` (DFU), `0x1222` (WTF) | Device Firmware Upgrade mode interface; parses DFU functional descriptors and control transfer commands. | -| **Recovery Mode** | `0x05ac` / `0x1280` - `0x1283` | iBoot recovery mode handling bulk and control communications during system restore. | -| **USBMUX (Normal Mode)** | Class `0xFF`, SubClass `0xFE`, Protocol `0x02` | Apple USB Multiplexing protocol encapsulating TCP connections and plist-based service transactions. | +| **DFU Mode** | `0x05ac` / `0x1227` (DFU), `0x1222` (WTF) | Device Firmware Upgrade mode; parses DFU functional descriptors and DFU class requests, reassembles downloaded images. Any vendor's DFU class interface (`0xFE`/`0x01`) is also decoded. | +| **Recovery Mode** | `0x05ac` / `0x1280` - `0x1283` | iBoot recovery mode: vendor control commands, `0x41` announced bulk uploads, bulk IN console. | +| **USBMUX (Normal Mode)** | Class `0xFF`, SubClass `0xFE`, Protocol `0x02` | Apple USB Multiplexing protocol (v1 and v2 headers) encapsulating TCP connections and plist-based service transactions. | --- ## Input Capture Formats -Currently supported front-end input handlers: +Captures may be PCAPNG or classic pcap (either byte order, micro or nanosecond timestamps). Supported link types: -- **macOS Darwin PcapNG (`LINKTYPE_USB_DARWIN` / 266):** Captured via tools like Wireshark on macOS capturing on the `XHC20` USB monitor interface. Supports both Darwin 1.0 and 1.1 USB headers. -- **USBPcap PcapNG (`LINKTYPE_USBPCAP` / 249):** Captured via USBPcap on Windows / Linux systems. +- **macOS Darwin (`LINKTYPE_USB_DARWIN` / 266):** Captured with Wireshark on macOS on the `XHC20` (or similar) USB monitor interface. Darwin 1.0 and 1.1 USB headers. +- **USBPcap (`LINKTYPE_USBPCAP` / 249):** Captured with USBPcap on Windows. +- **Linux usbmon (`LINKTYPE_USB_LINUX` / 189 and `LINKTYPE_USB_LINUX_MMAPPED` / 220):** Captured with Wireshark or `tcpdump -i usbmonN` on Linux. + +Big-endian PCAPNG sections are rejected with an error. Packets of any other link type are skipped with a warning. --- @@ -63,10 +71,10 @@ Currently supported front-end input handlers: To build DeMuxUSB, ensure you have: -- **C++ Compiler:** C++20 compliant compiler (AppleClang 12+, GCC 10+, or Clang 11+) +- **C++ Compiler:** C++20 compliant compiler (AppleClang 14+, GCC 10+, or Clang 13+) - **CMake:** Version 3.17 or higher - **PkgConfig:** For dependency discovery -- **libplist:** Version 2.0+ (`libplist-2.0` and C++ bindings `libplist++`) +- **libplist:** Version 2.0+ (`libplist-2.0`) - **Boost Libraries:** - `Boost.Container` - `Boost.Filesystem` @@ -98,16 +106,25 @@ sudo apt-get install -y build-essential cmake pkg-config \ # Configure the build directory cmake -B build -S . -# Compile the executable and test suite +# Compile the library, executable and test suite cmake --build build ``` +The build produces `libdemuxusb.a` (target `demuxusb_lib`), the `demuxusb` command line tool and the `demuxusb_test_suite`. + ### Run Tests ```bash ctest --test-dir build --output-on-failure ``` +The tests build synthetic captures in every supported format (see `test/CaptureBuilder.h`), so no capture files are needed. To run them under AddressSanitizer and UndefinedBehaviorSanitizer: + +```bash +cmake -B build-asan -DCMAKE_BUILD_TYPE=Debug -DDEMUXUSB_SANITIZE=ON +cmake --build build-asan && ctest --test-dir build-asan --output-on-failure +``` + --- ## Command Line Usage @@ -115,39 +132,69 @@ ctest --test-dir build --output-on-failure ### Syntax ```bash -./build/demuxusb +./build/demuxusb [options] ``` ### Options & Arguments -- ``: Path to the input packet capture file in PCAP or PCAPNG format. +- ``: Path to the input packet capture in PCAP or PCAPNG format. The format is detected from the file contents. +- `-v`, `--verbose`: Show decoded protocol details: recovery commands and responses, a summary of every plist message, DFU request counts and anomalies. +- `-p`, `--plists`: Print every decoded USBMUX property list as XML. +- `-x`, `--extract `: Write DFU images, recovery mode uploads, the iBoot console and per-stream plist transcripts to `/device--/`. +- `-w`, `--warnings`: List the packets that could not be decoded (by default only their count is printed). +- `-h`, `--help`: Show usage. + +The exit status is non-zero when the file cannot be opened or is not a capture. If a capture is damaged part way through, what was parsed before the damage is still reported. ### Example ```bash -./build/demuxusb captures/restore_session.pcapng +./build/demuxusb -v -x extracted captures/restore_session.pcapng ``` ### Sample Output ```text -Device ID: 100000000000001 +Device ID: 1410000000000005 + VID/PID: 05ac:12a8 (Normal mode) Manufacturer: Apple Inc. Product: iPhone - Statistics: 24890 packets and 18392010 bytes. (control 142 packets, 9088 bytes). + Serial: 00008101-001234567890001E + Statistics: 7 packets and 793 bytes. (control 10 packets, 198 bytes). + Interface Experts: + USBMUXInterfaceExpert (configuration=1, interface=1, version=2): + - c000f27e00000001 : 49152 -> 62078 [lockdownd] (2 transactions, in 276 bytes, out 284 bytes, plist) + - c001c3cb00000001 : 49153 -> 50123 [com.apple.afc] (0 transactions, in 0 bytes, out 13 bytes, raw/unknown) + + stream c000f27e00000001 (49152 -> 62078): + > Request=StartService, Service=com.apple.afc + < Service=com.apple.afc, Port=50123 + Extracted extracted/device-0-1410000000000005/usbmux-c000f27e00000001-49152-62078.plist.txt (622 bytes) + + +Device ID: 1420000000000007 + VID/PID: 05ac:1281 (Recovery mode) + Manufacturer: Apple Inc. + Product: Apple Mobile Device (Recovery Mode) + Serial: CPID:8101 ECID:0012 + Statistics: 2 packets and 22 bytes. (control 11 packets, 222 bytes). Interface Experts: - USBMUXInterfaceExpert (configuration=1, interface=1): - - 100010001 : 49152 -> 62078 (34 transactions, in 4102 bytes, out 8204 bytes) - - 100010002 : 49153 -> 62078 (120 transactions, in 102400 bytes, out 51200 bytes) + RecoveryInterfaceExpert (config=1, int=0) - 1 commands, 1 uploads (12 bytes), 10 console bytes; 2 control packets (8 bytes) and 2 bulk packets (22 bytes) + > saveenv + ^ upload of 12 bytes + Extracted extracted/device-1-1420000000000007/recovery-upload-0.bin (12 bytes) + Extracted extracted/device-1-1420000000000007/recovery-console.txt (10 bytes) ``` +Stream encodings are reported per direction as `host/device` when they differ: `plist` (length-prefixed property lists), `tls` (the session was upgraded to TLS), `raw` (some other framing, e.g. AFC) or `unknown` (no payload seen yet). + --- ## C++ API Reference -DeMuxUSB is designed with a modular architecture split into three layers: the **File/Input layer**, the **USB Device state machine**, and the **Protocol Interface Experts**. +DeMuxUSB is designed with a modular architecture split into three layers: the **File/Input layer**, the **USB Device state machine**, and the **Protocol Interface Experts**. Link against the `demuxusb_lib` CMake target. -All classes and helper functions reside in the `demuxusb` namespace (defined across headers in `src/`). +The input and device layers reside in the `demuxusb` namespace (defined across headers in `src/`); the protocol experts are in the global namespace. ### Namespace & Core Definitions @@ -155,6 +202,9 @@ Include header: `#include "demuxusb.hpp"` - **`byte_array`**: `std::pair` representing memory buffers and payload slices. - **`usb_endpoint_id`**: `uint8_t` representing endpoint addresses. +- **`demuxusb::ParseError`**: `std::runtime_error` thrown when a capture is structurally unreadable (not a capture, bad magic, big-endian PCAPNG, corrupt first block). +- **`demuxusb::readStruct(data, available, out)`**: Bounds-checked, alignment-safe copy of a structure out of capture memory. +- **`demuxusb::utf16leToUtf8(data, length)`**: Decodes USB string descriptor text. - **Endpoint Macros:** - `USB_ENDPOINT_IN(ep)`: Evaluates whether endpoint direction is IN (`ep & 0x80U`). - `USB_ENDPOINT_OUT(ep)`: Evaluates whether endpoint direction is OUT (`!(ep & 0x80U)`). @@ -168,31 +218,35 @@ Include header: `#include "demuxusb.hpp"` ### Input Layer -Header: `#include "file/InputFile.h"` and `#include "file/PcapNGInputFile.h"` +Header: `#include "file/InputFile.h"` (and `file/PcapNGInputFile.h`, `file/PcapInputFile.h`) #### `demuxusb::InputFile` -Abstract base class representing an input capture stream mapped into memory. +Abstract base class representing an input capture mapped into memory. -- `explicit InputFile(fs::path path)`: Opens and memory-maps the input file using `boost::iostreams::mapped_file_source`. -- `virtual void parse() = 0`: Pure virtual method implemented by subclasses to parse packet streams. -- `std::vector getDevices()`: Returns pointers to all reconstructed `Device` objects discovered in the capture. +- `static std::unique_ptr open(const fs::path &path)`: Opens a capture, choosing `PcapNGInputFile` or `PcapInputFile` from the file's magic number. Throws `ParseError` for anything else. +- `explicit InputFile(fs::path path)`: Opens and memory-maps the input file using `boost::iostreams::mapped_file_source`. Throws `ParseError` if it cannot be opened. +- `virtual void parse() = 0`: Parses the packet stream. +- `std::vector getDevices()`: Returns every reconstructed `Device` session, in the order devices first appeared. +- `bool processPacket(uint32_t linktype, const std::byte *data, size_t length)`: Decodes one captured USB packet of the given link type (Darwin, USBPcap, usbmon or mmapped usbmon). Usable directly to feed packets from another source. +- `const std::vector &getWarnings()` / `uint64_t getWarningCount()`: Packets that could not be decoded (the first 100 are kept). +- `uint64_t getPacketCount()`: Packets processed. #### `demuxusb::PcapNGInputFile` -Inherits from `InputFile`. Parses PCAPNG blocks: +Reads little endian PCAPNG: any number of sections and interfaces (each interface with its own link type), Enhanced and Simple Packet Blocks. Other block types are skipped. -- `explicit PcapNGInputFile(fs::path input)`: Constructs a PCAPNG file parser. -- `void parse() override`: Iterates through PCAPNG Section Headers, Interface Description Blocks (`TIME_INTERFACE_DESCRIPTOR_BLOCK`), and Enhanced Packet Blocks (`TYPE_ENHANCED_PACKET_BLOCK`). -- `bool process_dawrin_packet(std::byte *location)`: Parses Darwin USB 1.0 / 1.1 headers (`darwin_usb_header_1_1_t`), tracks control/bulk I/O submit and completion events, and forwards transfers to `DeviceHandle`. -- `bool process_usbpcap_packet(std::byte *location)`: Parses USBPcap transfer headers and control buffer stages. +#### `demuxusb::PcapInputFile` +Reads classic libpcap files in either byte order with micro or nanosecond timestamps. #### `demuxusb::DeviceHandle` -Internal state helper that tracks active, in-flight USB control setups (`usb_setup_t`) and bulk transactions matching submissions with completions. +Tracks in-flight transfers on one device address, pairing submissions with completions, and holds the device sessions seen at that address. -- `void beginControl(uint8_t endpoint, uint64_t id, usb_setup_t *setup)` -- `usb_setup_t endControl(uint8_t endpoint, uint64_t id)` -- `void beginBulk(uint8_t endpoint, uint64_t id)` -- `void endBulk(uint8_t endpoint, uint64_t id)` -- `Device *device()`: Returns the underlying `Device` instance. +- `void beginControl(uint8_t endpoint, uint64_t id, const usb_setup_t &setup, byte_array data = {})` +- `void appendControl(uint8_t endpoint, uint64_t id, byte_array data)` +- `void endControl(uint8_t endpoint, uint64_t id, byte_array data)` +- `void beginBulk(uint8_t endpoint, uint64_t id, byte_array data = {})` +- `void endBulk(uint8_t endpoint, uint64_t id, byte_array data)` +- `void abort(uint8_t endpoint, uint64_t id)` +- `Device *device()`: The current device session. `std::vector devices()` returns all of them. --- @@ -201,30 +255,32 @@ Internal state helper that tracks active, in-flight USB control setups (`usb_set Header: `#include "usb/Device.h"` #### `demuxusb::Device` -Represents an individual physical or virtual USB device. Tracks enumeration state, descriptor data, transfer statistics, and dispatches transfers to interface experts. +Represents one enumeration of a USB device. Tracks descriptor data, the active configuration and alternate settings, transfer statistics, and dispatches transfers to interface experts. -- `explicit Device(uint64_t id)`: Constructs a device with a 64-bit unique bus/location identifier. +- `explicit Device(uint64_t id)`: Constructs a device with a 64-bit bus/location identifier. - `uint64_t getIdentifier() const`: Returns the device identifier. +- `bool hasDeviceDescriptor() const`, `const usb_device_descriptor &getDeviceDescriptor() const`, `uint16_t getVendorId() const`, `uint16_t getProductId() const` - `bool isApple() const`: Returns `true` if `idVendor == APPLE_VID`. -- `bool isAppleDFU() const`: Returns `true` if device matches Apple DFU mode PID (`0x1227`). +- `bool isAppleDFU() const`: Returns `true` for the Apple DFU (`0x1227`) and WTF (`0x1222`) PIDs. - `bool isAppleRecovery() const`: Returns `true` if device matches any Apple Recovery mode PID (`0x1280`–`0x1283`). -- `std::wstring getProduct()`: Decodes the product string descriptor. -- `std::wstring getManufacturer()`: Decodes the manufacturer string descriptor. -- `std::wstring getSerial()`: Decodes the device serial number string descriptor. -- `std::wstring getString(uint8_t index)`: Retrieves a decoded UTF-16 string descriptor by index. -- `uint64_t getPacketCount() const`: Total packet count processed for the device. -- `uint64_t getByteCount() const`: Total byte count processed for the device. -- `uint64_t getControlPacketCount() const`: Total control packet count. -- `uint64_t getControlByteCount() const`: Total control byte count. -- `std::vector> getExperts()`: Returns all active interface experts attached to configured interfaces. -- `void processControlPacketIn(uint8_t endpoint, usb_setup_t setup, byte_array data)` -- `void processControlPacketOut(uint8_t endpoint, usb_setup_t setup, byte_array data)` -- `void processBulkPacketIn(uint8_t endpoint, byte_array data)` -- `void processBulkPacketOut(uint8_t endpoint, byte_array data)` +- `std::string getMode() const`: `"DFU"`, `"WTF"`, `"Recovery"`, `"Normal"` or `"Unknown"`. +- `std::string getProduct()`, `getManufacturer()`, `getSerial()`, `getString(uint8_t index)`: UTF-8 decoded string descriptors, or `""`. +- `uint64_t getPacketCount() const`, `getByteCount() const`, `getControlPacketCount() const`, `getControlByteCount() const` +- `uint8_t getCurrentConfiguration() const`, `uint8_t getAlternateSetting(uint8_t interface) const` +- `const std::map &getConfigurations() const`: Configurations keyed by `bConfigurationValue`. +- `const std::vector &getBOSDescriptors() const` +- `std::vector> getExperts()`: Every distinct expert attached to any interface. +- `const std::vector &getAnomalies() const`: Malformed or mismatched descriptors. +- `void processControlPacketIn/Out(uint8_t endpoint, usb_setup_t setup, byte_array data)` +- `void processBulkPacketIn/Out(uint8_t endpoint, byte_array data)` #### `demuxusb::usb_configuration` -Descriptor structure holding parsed USB configurations: -- `static void parse(usb_configuration &config, std::byte *data, size_t size)`: Parses raw descriptor blobs into interface and endpoint structures, attaching experts automatically. +- `usb_config_descriptor descriptor`, `usb_dfu_functional_descriptor dfu_descriptor` (with `has_dfu_descriptor`) +- `std::vector interfaces`: One entry per interface alternate setting, each with its `usb_interface_descriptor`, endpoints and expert (shared by all alternates of an interface). +- `static bool parse(usb_configuration &config, const std::byte *data, size_t size)`: Parses a configuration descriptor set; returns `false` if it is malformed. +- `usb_interface *find(uint8_t interface, uint8_t alternate)` + +Bulk traffic is routed by endpoint address within the active configuration and alternate setting. When `SET_CONFIGURATION` or `SET_INTERFACE` happened before the capture started, the single configuration and the first alternate declaring the endpoint are used. --- @@ -233,32 +289,41 @@ Descriptor structure holding parsed USB configurations: Header: `#include "protocol/InterfaceExpert.h"` #### `InterfaceExpert` -Base class for endpoint traffic inspectors. +Base class for interface traffic inspectors. - `InterfaceExpert(uint8_t configuration, uint8_t interface)` -- `virtual void processControlIn(byte_array data)` -- `virtual void processControlOut(byte_array data)` -- `virtual void processBulkIn(byte_array data)` -- `virtual void processBulkOut(byte_array data)` +- `virtual void processControlIn(const usb_setup_t &setup, byte_array data)` / `processControlOut(...)`: Class and vendor requests addressed to the interface (or to the device, for the first interface with an expert). +- `virtual void processBulkIn(byte_array data)` / `processBulkOut(byte_array data)` - `virtual bool isEmpty()`: Returns `true` if no traffic has been recorded on this interface. -- `virtual std::string toString()`: Formats interface configuration, packet counts, and byte counters into a human-readable summary. +- `virtual std::string toString()`: One line summary. +- `virtual std::string details()`: Verbose decode (used by `--verbose`). +- `virtual std::vector getArtifacts()`: Named files (`name`, `data`) the expert can extract (used by `--extract`). #### `DFUInterfaceExpert` Header: `#include "protocol/DFUInterfaceExpert.h"` -Specialized expert for Apple DFU mode interfaces. Records control transfers and DFU functional state changes. + +- `std::vector> getImages()`: Images terminated by a zero length `DFU_DNLOAD`, then any partial image. +- `int getLastState() const`, `int getLastStatus() const`, `static std::string stateName(int state)` +- `const std::vector &getAnomalies() const`: Downloads larger than `wTransferSize`, `DFU_ABORT` after a partial download, unexpected requests, data length / `wLength` mismatches. #### `RecoveryInterfaceExpert` Header: `#include "protocol/RecoveryInterfaceExpert.h"` -Specialized expert for Apple Recovery mode interfaces. Captures recovery mode payloads and command sequences. + +One expert spans all interfaces of a recovery mode device so commands and uploads stay in order. + +- `const std::vector &getPayloads() const`: `Command`, `Response` and `Upload` payloads in capture order. +- `std::vector getCommands()` +- `const std::string &getConsole() const`: iBoot console output read over bulk IN. #### `USBMUXInterfaceExpert` Header: `#include "protocol/USBMUXInterfaceExpert.h"` Specialized expert for Apple USBMUX2 (`bInterfaceClass = 0xFF`, `bInterfaceSubClass = 0xFE`, `bInterfaceProtocol = 0x02`). -- Parses `mux_header` (`USBMUX_MAGIC_IN = 0xFACEFACE`, `USBMUX_MAGIC_OUT = 0xFEEDFACE`). -- Inspects TCP headers (`tcphdr`) encapsulated inside USBMUX frames. -- Detects `SYN` packets to establish stream sessions (`MuxStream`). -- Demultiplexes incoming and outgoing data bytes into active `MuxStream` instances. +- Reassembles usbmux packets per direction by their header length, accepting v1 (8 byte) and v2 (`mux_header`, magic `0xFEEDFACE` / `0xFACEFACE`) headers, and resynchronises after lost framing. +- Records the negotiated protocol version (`getProtocolVersion()`). +- Opens a `MuxStream` for each host `SYN`, closes it on `FIN` / `RST`, and creates streams implicitly for connections opened before the capture. +- `const std::map> &getStreams() const` +- `uint64_t getResyncCount() const` --- @@ -270,14 +335,20 @@ Header: `#include "protocol/MuxStream.h"` Represents an individual TCP stream multiplexed over USBMUX between a host port and a device port. - `MuxStream(uint16_t host, uint16_t device)` -- `uint16_t getHostPort()`: Returns the client host port number. -- `uint16_t getDevicePort()`: Returns the destination device port number. -- `uint64_t getBytesIn()`: Total inbound bytes received on this stream. -- `uint64_t getBytesOut()`: Total outbound bytes sent on this stream. -- `size_t getTransactionCount()`: Returns total count of inbound and outbound transactions. -- `bool shouldMaterializePlist()`: Checks if port traffic should be deserialized as Apple XML/Binary Property Lists. -- `void receiveBytes(size_t length, std::byte *start)`: Feeds inbound data stream bytes for reassembly and parsing. -- `void sendBytes(size_t length, std::byte *start)`: Feeds outbound data stream bytes for reassembly and parsing. +- `uint16_t getHostPort()`, `uint16_t getDevicePort()` +- `uint64_t getBytesIn()`, `uint64_t getBytesOut()`: Payload bytes device → host and host → device. +- `size_t getTransactionCount()`: Number of decoded plist messages. +- `StreamEncoding getEncodingIn()`, `getEncodingOut()`: `Unknown`, `Plist`, `Raw` or `TLS`. +- `bool shouldMaterializePlist()`: `true` while the host → device direction may still carry plists. +- `const std::vector &getMessages() const`: Decoded messages in order. +- `const std::string &getServiceName()`: `lockdownd`, or the service lockdownd reported for the device port. +- `bool isClosed()` +- `void receiveBytes(size_t length, std::byte *start)` / `void sendBytes(size_t length, std::byte *start)`: Feed device → host and host → device bytes. + +#### `MuxMessage` +- `bool outbound`, `uint32_t size`, `unique_plist plist` +- `std::string xml() const`, `std::string summary() const` +- `std::string stringValue(const char *key) const`, `int64_t uintValue(const char *key) const` --- @@ -287,11 +358,9 @@ The following example demonstrates how to use the DeMuxUSB C++ library programma ```cpp #include -#include #include -#include "file/PcapNGInputFile.h" -#include "usb/Device.h" -#include "protocol/InterfaceExpert.h" +#include "file/InputFile.h" +#include "protocol/USBMUXInterfaceExpert.h" int main(int argc, char *argv[]) { if (argc < 2) { @@ -299,32 +368,31 @@ int main(int argc, char *argv[]) { return 1; } - boost::filesystem::path capture_path(argv[1]); - - // Create and parse the PCAPNG file - auto input_file = std::make_unique(capture_path); - input_file->parse(); - - // Iterate over all discovered USB devices - for (demuxusb::Device *device : input_file->getDevices()) { - std::wcout << L"Found Device ID: 0x" << std::hex << device->getIdentifier() << std::endl; - std::wcout << L" Manufacturer: " << device->getManufacturer() << std::endl; - std::wcout << L" Product: " << device->getProduct() << std::endl; - std::wcout << L" Serial: " << device->getSerial() << std::endl; - - if (device->isAppleDFU()) { - std::cout << " -> Device is in Apple DFU Mode" << std::endl; - } else if (device->isAppleRecovery()) { - std::cout << " -> Device is in Apple Recovery Mode" << std::endl; - } + std::unique_ptr input; + try { + input = demuxusb::InputFile::open(boost::filesystem::path(argv[1])); + input->parse(); + } catch (const demuxusb::ParseError &error) { + std::cerr << error.what() << std::endl; + return 1; + } - std::cout << " Total Packets: " << std::dec << device->getPacketCount() - << " (" << device->getByteCount() << " bytes)" << std::endl; + for (demuxusb::Device *device : input->getDevices()) { + std::cout << "Found Device ID: 0x" << std::hex << device->getIdentifier() << std::dec + << " (" << device->getMode() << " mode)" << std::endl; + std::cout << " Product: " << device->getProduct() << std::endl; + std::cout << " Serial: " << device->getSerial() << std::endl; - // Inspect interface experts for (const auto &expert : device->getExperts()) { - if (!expert->isEmpty()) { - std::cout << " " << expert->toString() << std::endl; + if (expert->isEmpty()) { continue; } + std::cout << " " << expert->toString() << std::endl; + + if (auto usbmux = std::dynamic_pointer_cast(expert)) { + for (const auto &[id, stream] : usbmux->getStreams()) { + for (const auto &message : stream->getMessages()) { + std::cout << " " << (message.outbound ? "> " : "< ") << message.summary() << std::endl; + } + } } } } @@ -337,4 +405,4 @@ int main(int argc, char *argv[]) { ## License -This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for complete details. \ No newline at end of file +This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for complete details. diff --git a/ext/pcapng.h b/ext/pcapng.h index 454f658..90ca9ed 100644 --- a/ext/pcapng.h +++ b/ext/pcapng.h @@ -1,13 +1,23 @@ - #ifndef _PCAPNG_H #define _PCAPNG_H -#include +#include +/* PCAPNG block types */ #define TYPE_SECTION_HEADER 0x0A0D0D0A #define TIME_INTERFACE_DESCRIPTOR_BLOCK 0x00000001 +#define TYPE_SIMPLE_PACKET_BLOCK 0x00000003 #define TYPE_ENHANCED_PACKET_BLOCK 0x06 +#define PCAPNG_BYTE_ORDER_MAGIC 0x1A2B3C4D +#define PCAPNG_BYTE_ORDER_MAGIC_SWAPPED 0x4D3C2B1A + +/* Classic libpcap file magic numbers (as read in host byte order) */ +#define PCAP_MAGIC_MICROSECONDS 0xA1B2C3D4 +#define PCAP_MAGIC_NANOSECONDS 0xA1B23C4D +#define PCAP_MAGIC_MICROSECONDS_SWAPPED 0xD4C3B2A1 +#define PCAP_MAGIC_NANOSECONDS_SWAPPED 0x4D3CB2A1 + #define USB_CONTROL_STAGE_SETUP 0x00 #define USB_CONTROL_STAGE_DATA 0x01 #define USB_CONTROL_STAGE_STATUS 0x02 @@ -19,12 +29,17 @@ #define USB_EP_INTERRUPT 0x03 -#define LINKTYPE_USB_DARWIN 266 +#define LINKTYPE_USB_LINUX 189 +#define LINKTYPE_USB_LINUX_MMAPPED 220 #define LINKTYPE_USBPCAP 249 +#define LINKTYPE_USB_DARWIN 266 #define DARWIN_IO_SUBMIT 0 #define DARWIN_IO_COMPLETE 1 +#define DARWIN_USB_HEADER_VERSION_1_0 0x0100 +#define DARWIN_USB_HEADER_VERSION_1_1 0x0101 + /* USB speeds */ #define DARWIN_SPEED_LOW 0 @@ -75,6 +90,14 @@ typedef struct { uint32_t length; } block_t; +typedef struct { + uint32_t byte_order_magic; + uint16_t major_version; + uint16_t minor_version; + int64_t section_length; + /* ... Options ... */ +} pcapng_shb_t; + typedef struct { uint32_t interface_id; uint32_t timestamp_high; @@ -86,6 +109,28 @@ typedef struct { /* ... Options ... */ } pcapng_epb_t; +typedef struct { + uint32_t packet_len; + /* ... Packet Data ... */ +} pcapng_spb_t; + +typedef struct { + uint32_t magic; + uint16_t version_major; + uint16_t version_minor; + int32_t thiszone; + uint32_t sigfigs; + uint32_t snaplen; + uint32_t linktype; +} pcap_file_header_t; + +typedef struct { + uint32_t ts_sec; + uint32_t ts_usec; + uint32_t incl_len; + uint32_t orig_len; +} pcap_record_header_t; + typedef struct { uint8_t bmRequestType; uint8_t bRequest; @@ -94,6 +139,16 @@ typedef struct { uint16_t wLength; } usb_setup_t; +/* Linux usbmon header (LINKTYPE_USB_LINUX), LINKTYPE_USB_LINUX_MMAPPED appends pcap_usb_header_mmapped_t */ +#define USBMON_EVENT_SUBMIT 'S' +#define USBMON_EVENT_COMPLETE 'C' +#define USBMON_EVENT_ERROR 'E' + +#define USBMON_TRANSFER_ISOCHRONOUS 0 +#define USBMON_TRANSFER_INTERRUPT 1 +#define USBMON_TRANSFER_CONTROL 2 +#define USBMON_TRANSFER_BULK 3 + typedef struct { uint64_t id; uint8_t event_type; @@ -111,7 +166,17 @@ typedef struct { usb_setup_t setup; } pcap_usb_header_t; -#pragma pack(1) +typedef struct { + pcap_usb_header_t header; + int32_t interval; + int32_t start_frame; + uint32_t xfer_flags; + uint32_t ndesc; +} pcap_usb_header_mmapped_t; + +#define USBMON_ISO_DESCRIPTOR_SIZE 16 + +#pragma pack(push, 1) typedef struct { uint16_t headerLen; /* This header length */ @@ -129,6 +194,8 @@ typedef struct uint32_t dataLength;/* Data length */ } usbpcap_packet_header_t; +#define USBPCAP_INFO_PDO_TO_FDO 0x01 + #define USBPCAP_TRANSFER_ISOCHRONOUS 0 #define USBPCAP_TRANSFER_INTERRUPT 1 #define USBPCAP_TRANSFER_CONTROL 2 @@ -139,12 +206,12 @@ typedef struct #define USBPCAP_CONTROL_STAGE_STATUS 2 #define USBPCAP_CONTROL_STAGE_COMPLETE 3 -#pragma pack(1) typedef struct { usbpcap_packet_header_t header; uint8_t stage; } usbpcap_buffer_control_header_t; +#pragma pack(pop) typedef struct { uint16_t linktype; // Link layer type (e.g., Ethernet, WLAN) @@ -153,4 +220,9 @@ typedef struct { /* Options follow here (variable length, padded to 32 bits) */ } pcap_interface_descriptor_t; -#endif \ No newline at end of file +static_assert(sizeof(darwin_usb_header_1_0_t) == 32, "Darwin 1.0 header must be 32 bytes"); +static_assert(sizeof(pcap_usb_header_t) == 48, "usbmon header must be 48 bytes"); +static_assert(sizeof(pcap_usb_header_mmapped_t) == 64, "usbmon mmapped header must be 64 bytes"); +static_assert(sizeof(usbpcap_packet_header_t) == 27, "USBPcap header must be 27 bytes"); + +#endif diff --git a/ext/usb.h b/ext/usb.h index 9287fa2..93dc5f4 100644 --- a/ext/usb.h +++ b/ext/usb.h @@ -33,9 +33,7 @@ #define __USB_H -#include - -#include "usb/Device.h" +#include /*-------------------------------------------------------------------------*/ /* CONTROL REQUEST SUPPORT */ /* diff --git a/ext/usbmux.h b/ext/usbmux.h index d5daf37..58836f3 100644 --- a/ext/usbmux.h +++ b/ext/usbmux.h @@ -28,7 +28,10 @@ #define USBMUX_MAGIC_IN 0xFACEFACE #define USBMUX_MAGIC_OUT 0xFEEDFACE +#define USB_TCP_FLAG_FIN 0x01U #define USB_TCP_FLAG_SYN 0x02U +#define USB_TCP_FLAG_RST 0x04U +#define USB_TCP_FLAG_ACK 0x10U enum mux_protocol { MUX_PROTO_VERSION = 0, diff --git a/src/demuxusb.hpp b/src/demuxusb.hpp index 39b841a..a28c040 100644 --- a/src/demuxusb.hpp +++ b/src/demuxusb.hpp @@ -10,7 +10,12 @@ #include #include #include +#include +#include +#include #include +#include +#include #include // Boost @@ -20,7 +25,7 @@ #include -#include +#include namespace fs = boost::filesystem; namespace io = boost::iostreams; @@ -48,4 +53,61 @@ enum apple_usb_device_id { IRECV_K_DFU_MODE = 0x1227 }; -#endif \ No newline at end of file +namespace demuxusb { + // Raised when a capture is structurally unreadable (bad magic, unsupported byte order, corrupt block framing). + // Individual malformed packets are reported and skipped instead. + class ParseError : public std::runtime_error { + public: + using std::runtime_error::runtime_error; + }; + + // Copies a trivially copyable structure out of capture memory. Capture data is untrusted and unaligned, so all + // structure access goes through here rather than reinterpret_cast. + template + bool readStruct(const std::byte *data, size_t available, T &out) { + static_assert(std::is_trivially_copyable_v); + if (data == nullptr || available < sizeof(T)) { return false; } + std::memcpy(&out, data, sizeof(T)); + return true; + } + + // Decodes UTF-16LE (as used by USB string descriptors) into UTF-8. + inline std::string utf16leToUtf8(const std::byte *data, size_t length) { + std::string result; + for (size_t i = 0; i + 1 < length; i += 2) { + uint32_t unit = static_cast(data[i]) | (static_cast(data[i + 1]) << 8U); + uint32_t codepoint = unit; + + if (unit >= 0xD800 && unit <= 0xDBFF && i + 3 < length) { + uint32_t low = static_cast(data[i + 2]) | (static_cast(data[i + 3]) << 8U); + if (low >= 0xDC00 && low <= 0xDFFF) { + codepoint = 0x10000 + ((unit - 0xD800) << 10U) + (low - 0xDC00); + i += 2; + } else { + codepoint = 0xFFFD; + } + } else if (unit >= 0xD800 && unit <= 0xDFFF) { + codepoint = 0xFFFD; + } + + if (codepoint < 0x80) { + result.push_back(static_cast(codepoint)); + } else if (codepoint < 0x800) { + result.push_back(static_cast(0xC0 | (codepoint >> 6U))); + result.push_back(static_cast(0x80 | (codepoint & 0x3FU))); + } else if (codepoint < 0x10000) { + result.push_back(static_cast(0xE0 | (codepoint >> 12U))); + result.push_back(static_cast(0x80 | ((codepoint >> 6U) & 0x3FU))); + result.push_back(static_cast(0x80 | (codepoint & 0x3FU))); + } else { + result.push_back(static_cast(0xF0 | (codepoint >> 18U))); + result.push_back(static_cast(0x80 | ((codepoint >> 12U) & 0x3FU))); + result.push_back(static_cast(0x80 | ((codepoint >> 6U) & 0x3FU))); + result.push_back(static_cast(0x80 | (codepoint & 0x3FU))); + } + } + return result; + } +} + +#endif diff --git a/src/file/InputFile.cpp b/src/file/InputFile.cpp index 2c65d65..a0ea35f 100644 --- a/src/file/InputFile.cpp +++ b/src/file/InputFile.cpp @@ -3,22 +3,362 @@ // #include "InputFile.h" +#include "PcapInputFile.h" +#include "PcapNGInputFile.h" + +#include namespace demuxusb { - InputFile::InputFile(fs::path path) { - this->m_path = std::move(path); + static byte_array mutableBytes(const std::byte *data, size_t length) { + // Experts only read packet data; the mapping itself is read only. + return byte_array{const_cast(data), length}; + } + + static bool isControlDescriptorRead(const usb_setup_t &setup) { + return setup.bmRequestType == (USB_DIR_IN | USB_TYPE_STANDARD | USB_RECIP_DEVICE) && + setup.bRequest == USB_REQ_GET_DESCRIPTOR && (setup.wValue >> 8U) == USB_DT_DEVICE; + } - this->m_mappedInputFile.open(this->m_path); + void DeviceHandle::beginControl(uint8_t endpoint, uint64_t id, const usb_setup_t &setup, byte_array data) { + auto &pending = this->m_inflight[{USB_ENDPOINT_ID(endpoint), id}]; + pending = Pending{true, setup, {}}; + if (data.first != nullptr && data.second > 0) { + pending.data.assign(data.first, data.first + data.second); + } + } + + void DeviceHandle::appendControl(uint8_t endpoint, uint64_t id, byte_array data) { + auto found = this->m_inflight.find({USB_ENDPOINT_ID(endpoint), id}); + if (found == this->m_inflight.end() || data.second == 0) { return; } + found->second.data.insert(found->second.data.end(), data.first, data.first + data.second); + } + + void DeviceHandle::endControl(uint8_t endpoint, uint64_t id, byte_array data) { + auto found = this->m_inflight.find({USB_ENDPOINT_ID(endpoint), id}); + if (found == this->m_inflight.end() || !found->second.control) { + // Completion of a transfer submitted before the capture started; without the setup it can't be decoded + this->m_unmatched++; + return; + } + + auto pending = std::move(found->second); + this->m_inflight.erase(found); + + byte_array payload = data; + if (pending.setup.bmRequestType & USB_DIR_IN) { + if (payload.second == 0 && !pending.data.empty()) { + payload = {pending.data.data(), pending.data.size()}; + } + } else if (!pending.data.empty()) { + payload = {pending.data.data(), pending.data.size()}; + } + + // Never hand on more than was requested + payload.second = std::min(payload.second, pending.setup.wLength); + + this->dispatchControl(USB_ENDPOINT_ID(endpoint), pending.setup, payload); + } + + void DeviceHandle::dispatchControl(uint8_t endpoint, const usb_setup_t &setup, byte_array data) { + if (isControlDescriptorRead(setup) && data.second >= USB_DT_DEVICE_SIZE && this->device()->hasDeviceDescriptor()) { + const auto ¤t = this->device()->getDeviceDescriptor(); + if (std::memcmp(¤t, data.first, USB_DT_DEVICE_SIZE) != 0) { + this->m_sessions.push_back(std::make_unique(this->m_id)); + } + } + + if (setup.bmRequestType & USB_DIR_IN) { + this->device()->processControlPacketIn(endpoint, setup, data); + } else { + this->device()->processControlPacketOut(endpoint, setup, data); + } + } + + void DeviceHandle::beginBulk(uint8_t endpoint, uint64_t id, byte_array data) { + auto &pending = this->m_inflight[{endpoint, id}]; + pending = Pending{false, {}, {}}; + if (data.first != nullptr && data.second > 0 && USB_ENDPOINT_OUT(endpoint)) { + pending.data.assign(data.first, data.first + data.second); + } + } + + void DeviceHandle::endBulk(uint8_t endpoint, uint64_t id, byte_array data) { + auto found = this->m_inflight.find({endpoint, id}); + Pending pending{}; + if (found != this->m_inflight.end()) { + pending = std::move(found->second); + this->m_inflight.erase(found); + } else { + this->m_unmatched++; + } + + if (USB_ENDPOINT_IN(endpoint)) { + this->device()->processBulkPacketIn(USB_ENDPOINT_ID(endpoint), data); + } else { + auto payload = (data.second == 0 && !pending.data.empty()) + ? byte_array{pending.data.data(), pending.data.size()} : data; + this->device()->processBulkPacketOut(USB_ENDPOINT_ID(endpoint), payload); + } + } + + void DeviceHandle::abort(uint8_t endpoint, uint64_t id) { + this->m_inflight.erase({endpoint, id}); + this->m_inflight.erase({USB_ENDPOINT_ID(endpoint), id}); + } + + std::vector DeviceHandle::devices() { + std::vector result; + for (const auto &session: this->m_sessions) { + result.push_back(session.get()); + } + return result; + } + + InputFile::InputFile(fs::path path) : m_path{std::move(path)} { + boost::system::error_code error; + auto size = fs::file_size(this->m_path, error); + if (error) { + throw ParseError("Unable to open " + this->m_path.string() + ": " + error.message()); + } + + if (size > 0) { + try { + this->m_mappedInputFile.open(this->m_path); + } catch (const std::exception &exception) { + throw ParseError("Unable to map " + this->m_path.string() + ": " + exception.what()); + } + this->m_data = reinterpret_cast(this->m_mappedInputFile.data()); + this->m_size = this->m_mappedInputFile.size(); + } + } + + std::unique_ptr InputFile::open(const fs::path &path) { + uint32_t magic = 0; + { + std::ifstream stream(path.string(), std::ios::binary); + if (!stream) { throw ParseError("Unable to open " + path.string()); } + stream.read(reinterpret_cast(&magic), sizeof(magic)); + if (stream.gcount() != sizeof(magic)) { throw ParseError(path.string() + " is too short to be a capture"); } + } + + switch (magic) { + case TYPE_SECTION_HEADER: + return std::make_unique(path); + case PCAP_MAGIC_MICROSECONDS: + case PCAP_MAGIC_NANOSECONDS: + case PCAP_MAGIC_MICROSECONDS_SWAPPED: + case PCAP_MAGIC_NANOSECONDS_SWAPPED: + return std::make_unique(path); + default: + throw ParseError(path.string() + " is not a pcap or pcapng capture"); + } } std::vector InputFile::getDevices() { auto result = std::vector{}; - for (const auto &device: this->m_devices) { - result.push_back(device.second->device()); + for (const auto &id: this->m_deviceOrder) { + for (auto *device: this->m_devices[id]->devices()) { + result.push_back(device); + } } return result; } + + DeviceHandle &InputFile::handleFor(uint64_t id) { + auto &device = this->m_devices[id]; + + if (device == nullptr) { + device = std::make_shared(id); + this->m_deviceOrder.push_back(id); + } + + return *device; + } + + void InputFile::warn(const std::string &warning) { + this->m_warningCount++; + if (this->m_warnings.size() < MAX_WARNINGS) { + this->m_warnings.push_back(warning); + } + } + + bool InputFile::processPacket(uint32_t linktype, const std::byte *data, size_t length) { + this->m_packetCount++; + + switch (linktype) { + case LINKTYPE_USB_DARWIN: + return this->process_darwin_packet(data, length); + case LINKTYPE_USBPCAP: + return this->process_usbpcap_packet(data, length); + case LINKTYPE_USB_LINUX: + return this->process_usbmon_packet(data, length, false); + case LINKTYPE_USB_LINUX_MMAPPED: + return this->process_usbmon_packet(data, length, true); + default: + if (!this->m_unsupportedLinktypes.contains(linktype)) { + this->m_unsupportedLinktypes.insert(linktype); + this->warn((boost::format("Skipping packets with unsupported link type %1%") % linktype).str()); + } + return false; + } + } + + bool InputFile::process_darwin_packet(const std::byte *data, size_t length) { + darwin_usb_header_1_0_t usbHeader{}; + if (!readStruct(data, length, usbHeader)) { + this->warn("Darwin packet shorter than its header"); + return false; + } + + if (usbHeader.version != DARWIN_USB_HEADER_VERSION_1_0 && usbHeader.version != DARWIN_USB_HEADER_VERSION_1_1) { + this->warn((boost::format("Unsupported Darwin USB header version 0x%1$04x") % usbHeader.version).str()); + return false; + } + if (usbHeader.headerLength < sizeof(darwin_usb_header_1_0_t) || usbHeader.headerLength > length) { + this->warn("Darwin packet header length is out of bounds"); + return false; + } + + auto deviceAddress = (static_cast(usbHeader.locationId) << 32U) | usbHeader.deviceId; + auto *payload = data + usbHeader.headerLength; + auto payloadLength = std::min(usbHeader.length, length - usbHeader.headerLength); + auto &device = this->handleFor(deviceAddress); + const auto endpoint = (usb_endpoint_id) usbHeader.endpointId; + + switch (usbHeader.endpointType) { + case USB_EP_CONTROL: + if (usbHeader.type == DARWIN_IO_SUBMIT) { + usb_setup_t setup{}; + if (!readStruct(payload, payloadLength, setup)) { + this->warn("Darwin control submission without a setup packet"); + return false; + } + device.beginControl(endpoint, usbHeader.uniqueId, setup, + mutableBytes(payload + sizeof(setup), payloadLength - sizeof(setup))); + } else if (usbHeader.type == DARWIN_IO_COMPLETE) { + device.endControl(endpoint, usbHeader.uniqueId, mutableBytes(payload, payloadLength)); + } + break; + case USB_EP_BULK: + case USB_EP_INTERRUPT: + if (usbHeader.type == DARWIN_IO_SUBMIT) { + device.beginBulk(endpoint, usbHeader.uniqueId, mutableBytes(payload, payloadLength)); + } else if (usbHeader.type == DARWIN_IO_COMPLETE) { + device.endBulk(endpoint, usbHeader.uniqueId, mutableBytes(payload, payloadLength)); + } + break; + default: + break; + } + return true; + } + + bool InputFile::process_usbpcap_packet(const std::byte *data, size_t length) { + usbpcap_packet_header_t usbHeader{}; + if (!readStruct(data, length, usbHeader)) { + this->warn("USBPcap packet shorter than its header"); + return false; + } + if (usbHeader.headerLen < sizeof(usbpcap_packet_header_t) || usbHeader.headerLen > length) { + this->warn("USBPcap packet header length is out of bounds"); + return false; + } + + auto deviceAddress = (static_cast(usbHeader.bus) << 16U) | usbHeader.device; + auto *payload = data + usbHeader.headerLen; + auto payloadLength = std::min(usbHeader.dataLength, length - usbHeader.headerLen); + auto &device = this->handleFor(deviceAddress); + const auto endpoint = (usb_endpoint_id) usbHeader.endpoint; + + switch (usbHeader.transfer) { + case USBPCAP_TRANSFER_CONTROL: { + usbpcap_buffer_control_header_t controlHeader{}; + if (usbHeader.headerLen < sizeof(controlHeader) || !readStruct(data, length, controlHeader)) { + this->warn("USBPcap control packet without a stage"); + return false; + } + + switch (controlHeader.stage) { + case USBPCAP_CONTROL_STAGE_SETUP: { + usb_setup_t setup{}; + if (!readStruct(payload, payloadLength, setup)) { + this->warn("USBPcap setup stage without a setup packet"); + return false; + } + device.beginControl(endpoint, usbHeader.irpId, setup, + mutableBytes(payload + sizeof(setup), payloadLength - sizeof(setup))); + } + break; + case USBPCAP_CONTROL_STAGE_DATA: + device.appendControl(endpoint, usbHeader.irpId, mutableBytes(payload, payloadLength)); + break; + case USBPCAP_CONTROL_STAGE_COMPLETE: + device.endControl(endpoint, usbHeader.irpId, mutableBytes(payload, payloadLength)); + break; + default: + break; + } + } + break; + case USBPCAP_TRANSFER_BULK: + case USBPCAP_TRANSFER_INTERRUPT: + if (usbHeader.info & USBPCAP_INFO_PDO_TO_FDO) { + device.endBulk(endpoint, usbHeader.irpId, mutableBytes(payload, payloadLength)); + } else { + device.beginBulk(endpoint, usbHeader.irpId, mutableBytes(payload, payloadLength)); + } + break; + default: + break; + } + return true; + } + + bool InputFile::process_usbmon_packet(const std::byte *data, size_t length, bool mmapped) { + pcap_usb_header_t usbHeader{}; + size_t headerLength = mmapped ? sizeof(pcap_usb_header_mmapped_t) : sizeof(pcap_usb_header_t); + if (length < headerLength || !readStruct(data, length, usbHeader)) { + this->warn("usbmon packet shorter than its header"); + return false; + } + + auto deviceAddress = (static_cast(usbHeader.bus_id) << 8U) | usbHeader.device_address; + auto *payload = data + headerLength; + auto payloadLength = std::min(usbHeader.data_len, length - headerLength); + auto &device = this->handleFor(deviceAddress); + const auto endpoint = (usb_endpoint_id) usbHeader.endpoint_number; + + if (usbHeader.event_type == USBMON_EVENT_ERROR) { + device.abort(endpoint, usbHeader.id); + return true; + } + + switch (usbHeader.transfer_type) { + case USBMON_TRANSFER_CONTROL: + if (usbHeader.event_type == USBMON_EVENT_SUBMIT) { + if (usbHeader.setup_flag != 0) { + this->warn("usbmon control submission without a setup packet"); + return false; + } + device.beginControl(endpoint, usbHeader.id, usbHeader.setup, mutableBytes(payload, payloadLength)); + } else if (usbHeader.event_type == USBMON_EVENT_COMPLETE) { + device.endControl(endpoint, usbHeader.id, mutableBytes(payload, payloadLength)); + } + break; + case USBMON_TRANSFER_BULK: + case USBMON_TRANSFER_INTERRUPT: + if (usbHeader.event_type == USBMON_EVENT_SUBMIT) { + device.beginBulk(endpoint, usbHeader.id, mutableBytes(payload, payloadLength)); + } else if (usbHeader.event_type == USBMON_EVENT_COMPLETE) { + device.endBulk(endpoint, usbHeader.id, mutableBytes(payload, payloadLength)); + } + break; + default: + break; + } + return true; + } } diff --git a/src/file/InputFile.h b/src/file/InputFile.h index d7cf635..a9dc676 100644 --- a/src/file/InputFile.h +++ b/src/file/InputFile.h @@ -11,73 +11,56 @@ namespace demuxusb { + // Correlates the submission and completion events of USB transfers on one device address and hands completed + // transfers to the Device. Capture formats differ in which event carries the payload (OUT data travels with the + // submission, IN data with the completion), so both are retained and the right one chosen at completion. class DeviceHandle { public: - explicit DeviceHandle(uint64_t id) { - this->m_device = std::make_unique(id); + explicit DeviceHandle(uint64_t id) : m_id{id} { + this->m_sessions.push_back(std::make_unique(id)); } - void beginControl(uint8_t endpoint, uint64_t id, usb_setup_t *setup) { - if (this->m_inflight[endpoint].contains(id)) { - std::cerr << "Already had inflight operation on " << std::dec << (uint32_t) endpoint << " with id " - << std::hex << id << std::endl; - return; - } - this->m_inflight[endpoint].insert(id); - this->m_controlSetup[endpoint][id] = *setup; - } + // Starts a control transfer. data is any OUT payload that followed the setup packet. + void beginControl(uint8_t endpoint, uint64_t id, const usb_setup_t &setup, byte_array data = {nullptr, 0}); - usb_setup_t endControl(uint8_t endpoint, uint64_t id) { - if (!this->m_inflight[endpoint].contains(id)) { - std::cerr << "Control completion on endpoint " << std::dec << (uint32_t) endpoint - << " could not find submit for " << std::hex << id << std::endl; - return usb_setup_t{}; - } - - if (!this->m_controlSetup[endpoint].contains(id)) { - std::cerr << "Control completion on endpoint " << std::dec << (uint32_t) endpoint << " with id " << id - << " missing setup info" << std::endl; - return usb_setup_t{}; - } - - auto result = this->m_controlSetup[endpoint][id]; - this->m_controlSetup[endpoint].erase(id); - this->m_inflight[endpoint].erase(id); - return result; - } + // Adds payload from an intermediate stage (USBPcap emits a separate DATA stage). + void appendControl(uint8_t endpoint, uint64_t id, byte_array data); - void beginBulk(uint8_t endpoint, uint64_t id) { + // Completes a control transfer and dispatches it. data is the payload of the completion event. + void endControl(uint8_t endpoint, uint64_t id, byte_array data); - if (this->m_inflight[endpoint].contains(id)) { - std::cerr << "Bulk already had inflight operation on " << std::dec << (uint32_t) endpoint << " with id " - << std::hex << id << std::endl; - return; - } - this->m_inflight[endpoint].insert(id); - } + void beginBulk(uint8_t endpoint, uint64_t id, byte_array data = {nullptr, 0}); - void endBulk(uint8_t endpoint, uint64_t id) { - if (!this->m_inflight[endpoint].contains(id)) { - std::cerr << "Bulk completion on endpoint " << std::dec << (uint32_t) endpoint << " with id " - << std::hex << id << " does not have matching submit." << std::endl; - return; - } - this->m_inflight[endpoint].erase(id); - } + void endBulk(uint8_t endpoint, uint64_t id, byte_array data); + + // Drops an in-flight transfer that completed with an error. + void abort(uint8_t endpoint, uint64_t id); + + // The current device session on this address + Device *device() { return this->m_sessions.back().get(); } - Device *device() { return this->m_device.get(); } + // Every session seen on this address. A new session begins when the address re-enumerates as a different + // device, e.g. DFU -> recovery -> normal mode during a restore. + std::vector devices(); + + [[nodiscard]] uint64_t getUnmatchedCount() const { return this->m_unmatched; } private: - std::unique_ptr m_device; - std::array, UINT8_MAX> m_inflight{}; - std::array, UINT8_MAX> m_controlSetup{}; - }; + struct Pending { + bool control{}; + usb_setup_t setup{}; + std::vector data{}; + }; + + typedef std::pair transfer_key; - typedef struct { - uint8_t endpoint; - uint32_t length; + void dispatchControl(uint8_t endpoint, const usb_setup_t &setup, byte_array data); - } usb_frame_info_t; + uint64_t m_id; + uint64_t m_unmatched{}; + std::map m_inflight{}; + std::vector> m_sessions{}; + }; class InputFile { public: @@ -85,14 +68,53 @@ namespace demuxusb { virtual ~InputFile() = default; + // Opens a capture, choosing the reader from the file's magic number. Throws ParseError if the format is not + // recognised. + static std::unique_ptr open(const fs::path &path); + virtual void parse() = 0; + // Devices in the order they first appeared in the capture std::vector getDevices(); + // Problems with individual packets (truncation, unknown link types, unmatched completions...). Only the first + // MAX_WARNINGS are retained; getWarningCount() has the total. + [[nodiscard]] const std::vector &getWarnings() const { return this->m_warnings; } + + [[nodiscard]] uint64_t getWarningCount() const { return this->m_warningCount; } + + [[nodiscard]] uint64_t getPacketCount() const { return this->m_packetCount; } + + // Dispatches one captured USB packet of the given link type. Returns false if it could not be decoded. + bool processPacket(uint32_t linktype, const std::byte *data, size_t length); + + static const size_t MAX_WARNINGS = 100; + protected: + bool process_darwin_packet(const std::byte *data, size_t length); + + bool process_usbpcap_packet(const std::byte *data, size_t length); + + bool process_usbmon_packet(const std::byte *data, size_t length, bool mmapped); + + DeviceHandle &handleFor(uint64_t id); + + void warn(const std::string &warning); + + [[nodiscard]] const std::byte *data() const { return this->m_data; } + + [[nodiscard]] size_t size() const { return this->m_size; } + fs::path m_path; io::mapped_file_source m_mappedInputFile; + const std::byte *m_data{}; + size_t m_size{}; + uint64_t m_packetCount{}; + uint64_t m_warningCount{}; + std::vector m_warnings{}; + std::set m_unsupportedLinktypes{}; std::map> m_devices; + std::vector m_deviceOrder{}; }; } diff --git a/src/file/PcapInputFile.cpp b/src/file/PcapInputFile.cpp new file mode 100644 index 0000000..2c4f607 --- /dev/null +++ b/src/file/PcapInputFile.cpp @@ -0,0 +1,49 @@ +#include "PcapInputFile.h" + + +namespace demuxusb { + void PcapInputFile::parse() { + pcap_file_header_t header{}; + if (!readStruct(this->data(), this->size(), header)) { + throw ParseError("Truncated pcap file header"); + } + + bool swapped; + switch (header.magic) { + case PCAP_MAGIC_MICROSECONDS: + case PCAP_MAGIC_NANOSECONDS: + swapped = false; + break; + case PCAP_MAGIC_MICROSECONDS_SWAPPED: + case PCAP_MAGIC_NANOSECONDS_SWAPPED: + swapped = true; + break; + default: + throw ParseError("Not a pcap file: bad magic"); + } + + auto value = [swapped](uint32_t field) { return swapped ? boost::endian::endian_reverse(field) : field; }; + + // The upper bits of the link type field carry FCS information + auto linktype = value(header.linktype) & 0x0FFFFFFFU; + size_t offset = sizeof(pcap_file_header_t); + + while (offset < this->size()) { + auto remaining = this->size() - offset; + pcap_record_header_t record{}; + if (!readStruct(this->data() + offset, remaining, record)) { + this->warn("Capture ends with a truncated record header"); + break; + } + + auto included = value(record.incl_len); + if (included > remaining - sizeof(pcap_record_header_t)) { + this->warn((boost::format("Record at offset %1% overruns the file; stopping") % offset).str()); + break; + } + + this->processPacket(linktype, this->data() + offset + sizeof(pcap_record_header_t), included); + offset += sizeof(pcap_record_header_t) + included; + } + } +} diff --git a/src/file/PcapInputFile.h b/src/file/PcapInputFile.h new file mode 100644 index 0000000..97d1ab6 --- /dev/null +++ b/src/file/PcapInputFile.h @@ -0,0 +1,22 @@ +#ifndef demuxusb_PCAPINPUTFILE_H +#define demuxusb_PCAPINPUTFILE_H + +#include "../demuxusb.hpp" +#include "InputFile.h" + +#include + +namespace demuxusb { + + // Reads classic libpcap captures (micro or nanosecond timestamps, either byte order). + class PcapInputFile : public InputFile { + public: + explicit PcapInputFile(fs::path input) : InputFile(std::move(input)) {} + + ~PcapInputFile() override = default; + + void parse() override; + }; +} + +#endif //demuxusb_PCAPINPUTFILE_H diff --git a/src/file/PcapNGInputFile.cpp b/src/file/PcapNGInputFile.cpp index 75a54fb..982be60 100644 --- a/src/file/PcapNGInputFile.cpp +++ b/src/file/PcapNGInputFile.cpp @@ -6,161 +6,116 @@ namespace demuxusb { - bool PcapNGInputFile::process_dawrin_packet(std::byte *location) { - auto *usbHeader = reinterpret_cast(location + sizeof(block_t) + sizeof(pcapng_epb_t)); - assert(usbHeader->version == 0x101); - assert(usbHeader->headerLength == sizeof(darwin_usb_header_1_1_t)); + // Block type + length at the start and the repeated length at the end + static const size_t BLOCK_OVERHEAD = sizeof(block_t) + sizeof(uint32_t); - auto deviceAddress = (static_cast(usbHeader->locationId) << 32U) | usbHeader->deviceId; + struct pcapng_interface { + uint16_t linktype; + uint32_t snaplen; + }; - auto *dataAddress = (location + sizeof(block_t) + sizeof(pcapng_epb_t) + usbHeader->headerLength); - const auto data = byte_array{dataAddress, usbHeader->length}; - const auto endpointId = (usb_endpoint_id) usbHeader->endpointId; - - auto &device = this->m_devices[deviceAddress]; + void PcapNGInputFile::parse() { + const auto *location = this->data(); + size_t offset = 0; + std::vector interfaces; - if (device == nullptr) { - device = std::make_shared(deviceAddress); - this->m_devices[deviceAddress] = device; - } + while (offset < this->size()) { + auto remaining = this->size() - offset; + auto *blockStart = location + offset; - switch (usbHeader->endpointType) { - case USB_EP_CONTROL: - if (usbHeader->type == DARWIN_IO_SUBMIT) { - auto *setup = reinterpret_cast(data.first); - device->beginControl(USB_ENDPOINT_ID(usbHeader->endpointId), usbHeader->uniqueId, setup); - } else if (usbHeader->type == DARWIN_IO_COMPLETE) { - auto setup = device->endControl(USB_ENDPOINT_ID(usbHeader->endpointId), - usbHeader->uniqueId); - - if (USB_ENDPOINT_IN(usbHeader->endpointId)) { - device->device()->processControlPacketIn(USB_ENDPOINT_ID(usbHeader->endpointId), setup, - data); - } else { - device->device()->processControlPacketOut(USB_ENDPOINT_ID(usbHeader->endpointId), setup, - data); - } - } + block_t block{}; + if (remaining < BLOCK_OVERHEAD || !readStruct(blockStart, remaining, block)) { + this->warn("Capture ends with a truncated block"); break; - case USB_EP_BULK: - if (usbHeader->type == DARWIN_IO_SUBMIT) { - device->beginBulk(usbHeader->endpointId, usbHeader->uniqueId); - } else { - device->endBulk(usbHeader->endpointId, usbHeader->uniqueId); - - if (USB_ENDPOINT_IN(usbHeader->endpointId)) { - device->device()->processBulkPacketIn(USB_ENDPOINT_ID(usbHeader->endpointId), data); - } else { - device->device()->processBulkPacketOut(USB_ENDPOINT_ID(usbHeader->endpointId), data); - } - } - break; - default: ; - } - return true; - } - - bool PcapNGInputFile::process_usbpcap_packet(std::byte *location) { - - auto *usbHeader = reinterpret_cast(location + sizeof(block_t) + sizeof(pcapng_epb_t)); - if (usbHeader->transfer == USBPCAP_TRANSFER_CONTROL) { - assert(usbHeader->headerLen == sizeof(usbpcap_buffer_control_header_t)); - } else { - assert(usbHeader->headerLen == sizeof(usbpcap_packet_header_t)); - } - - auto deviceAddress = usbHeader->device; - - auto *dataAddress = (location + sizeof(block_t) + sizeof(pcapng_epb_t) + usbHeader->headerLen); - const auto data = byte_array{dataAddress, usbHeader->dataLength}; - const auto endpointId = (usb_endpoint_id) usbHeader->endpoint; + } - auto &device = this->m_devices[deviceAddress]; + if (offset == 0 && block.type != TYPE_SECTION_HEADER) { + throw ParseError("Not a PCAPNG file: missing section header block"); + } - if (device == nullptr) { - device = std::make_shared(deviceAddress); - this->m_devices[deviceAddress] = device; - } - auto *controlHeader = reinterpret_cast(location + sizeof(block_t) + sizeof(pcapng_epb_t)); - - switch (usbHeader->transfer) { - case USBPCAP_TRANSFER_CONTROL: - - if (controlHeader->stage == USBPCAP_CONTROL_STAGE_SETUP) { - auto *setup = reinterpret_cast(data.first); - device->beginControl(USB_ENDPOINT_ID(usbHeader->endpoint), usbHeader->irpId, setup); - } else if (controlHeader->stage == USBPCAP_CONTROL_STAGE_DATA) { - auto setup = device->endControl(USB_ENDPOINT_ID(usbHeader->endpoint), - usbHeader->irpId); - - if (USB_ENDPOINT_IN(usbHeader->endpoint)) { - device->device()->processControlPacketIn(USB_ENDPOINT_ID(usbHeader->endpoint), setup, - data); - } else { - device->device()->processControlPacketOut(USB_ENDPOINT_ID(usbHeader->endpoint), setup, - data); - } + if (block.type == TYPE_SECTION_HEADER) { + pcapng_shb_t sectionHeader{}; + if (!readStruct(blockStart + sizeof(block_t), remaining - sizeof(block_t), sectionHeader)) { + throw ParseError("Truncated PCAPNG section header block"); } - break; - case USBPCAP_TRANSFER_BULK: - device->beginBulk(usbHeader->endpoint, usbHeader->irpId); - device->endBulk(usbHeader->endpoint, usbHeader->irpId); - - auto endpoint_id = USB_ENDPOINT_ID(usbHeader->endpoint); - if (USB_ENDPOINT_IN(usbHeader->endpoint)) { - device->device()->processBulkPacketIn(endpoint_id, data); - } else { - device->device()->processBulkPacketOut(endpoint_id, data); + if (sectionHeader.byte_order_magic == PCAPNG_BYTE_ORDER_MAGIC_SWAPPED) { + throw ParseError("Big endian PCAPNG captures are not supported"); } - } - return true; - - } - - void PcapNGInputFile::parse() { - - auto *location = (std::byte *) this->m_mappedInputFile.data(); - const auto *endLocation = location + this->m_mappedInputFile.size(); - - uint16_t interface_type; + if (sectionHeader.byte_order_magic != PCAPNG_BYTE_ORDER_MAGIC) { + throw ParseError("Corrupt PCAPNG section header byte order magic"); + } + // Interface ids are scoped to their section + interfaces.clear(); + } - while (location < endLocation) { - auto *block = reinterpret_cast(location); - if (block->type == TIME_INTERFACE_DESCRIPTOR_BLOCK) { - auto *interfaceDescriptor = reinterpret_cast(location + sizeof(block_t)); + if (block.length < BLOCK_OVERHEAD || block.length % 4 != 0 || block.length > remaining) { + if (offset == 0) { throw ParseError("Corrupt PCAPNG section header block length"); } + this->warn((boost::format("Corrupt or truncated block at offset %1%; stopping") % offset).str()); + break; + } - interface_type = interfaceDescriptor->linktype; - location += block->length; - continue; + uint32_t trailingLength = 0; + readStruct(blockStart + block.length - sizeof(uint32_t), sizeof(uint32_t), trailingLength); + if (trailingLength != block.length) { + if (offset == 0) { throw ParseError("Corrupt PCAPNG section header block"); } + this->warn((boost::format("Block at offset %1% has mismatched trailing length; stopping") % offset).str()); + break; } - if (block->type == TYPE_ENHANCED_PACKET_BLOCK) { - auto *epb = reinterpret_cast(location + sizeof(block_t)); - assert(epb->interface_id == 0); + auto *body = blockStart + sizeof(block_t); + auto bodyLength = block.length - BLOCK_OVERHEAD; - switch (interface_type) { - case LINKTYPE_USB_DARWIN: - process_dawrin_packet(location); + switch (block.type) { + case TIME_INTERFACE_DESCRIPTOR_BLOCK: { + pcap_interface_descriptor_t interfaceDescriptor{}; + if (!readStruct(body, bodyLength, interfaceDescriptor)) { + this->warn("Truncated interface description block"); break; + } + interfaces.push_back({interfaceDescriptor.linktype, interfaceDescriptor.snaplen}); + break; + } - case LINKTYPE_USBPCAP: - process_usbpcap_packet(location); + case TYPE_ENHANCED_PACKET_BLOCK: { + pcapng_epb_t epb{}; + if (!readStruct(body, bodyLength, epb)) { + this->warn("Truncated enhanced packet block"); + break; + } + if (epb.interface_id >= interfaces.size()) { + this->warn((boost::format("Packet references undeclared interface %1%") % + epb.interface_id).str()); + break; + } + if (epb.captured_len > bodyLength - sizeof(pcapng_epb_t)) { + this->warn("Enhanced packet block data overruns the block"); break; - default: - continue; + } + this->processPacket(interfaces[epb.interface_id].linktype, body + sizeof(pcapng_epb_t), + epb.captured_len); + break; } - } + case TYPE_SIMPLE_PACKET_BLOCK: { + pcapng_spb_t spb{}; + if (!readStruct(body, bodyLength, spb) || interfaces.empty()) { + this->warn("Simple packet block without an interface"); + break; + } + size_t captured = std::min(spb.packet_len, bodyLength - sizeof(pcapng_spb_t)); + if (interfaces[0].snaplen != 0) { captured = std::min(captured, interfaces[0].snaplen); } + this->processPacket(interfaces[0].linktype, body + sizeof(pcapng_spb_t), captured); + break; + } - auto *trailingSize = reinterpret_cast(location + block->length - sizeof(uint32_t)); - if (block->length != *trailingSize) { - std::cerr << "Block length != trailingLength" << std::endl; - exit(-1); + default: + // Name resolution, statistics, custom blocks... + break; } - location += block->length; + offset += block.length; } } PcapNGInputFile::~PcapNGInputFile() = default; -} \ No newline at end of file +} diff --git a/src/file/PcapNGInputFile.h b/src/file/PcapNGInputFile.h index 4770ee2..5cc0b07 100644 --- a/src/file/PcapNGInputFile.h +++ b/src/file/PcapNGInputFile.h @@ -12,16 +12,14 @@ namespace demuxusb { + // Reads little endian PCAPNG captures: any number of sections, each with any number of interfaces, carrying + // Enhanced or Simple Packet Blocks. class PcapNGInputFile : public InputFile { public: explicit PcapNGInputFile(fs::path input) : InputFile(std::move(input)) {} ~PcapNGInputFile() override; - bool process_dawrin_packet(std::byte *location); - - bool process_usbpcap_packet(std::byte *location); - void parse() override; }; } diff --git a/src/main.cpp b/src/main.cpp index 6c7ad31..d802e8b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,44 +1,146 @@ #include +#include #include #include #include "file/InputFile.h" -#include "file/PcapNGInputFile.h" +#include "protocol/USBMUXInterfaceExpert.h" using namespace demuxusb; -int main(int argc, const char *argv[]) { - if (argc < 2) { - std::cerr << "Usage: " << argv[0] << " " << std::endl; - return 1; +static void usage(const char *program) { + std::cerr << "Usage: " << program << " [options] \n" + << "\n" + << "Options:\n" + << " -v, --verbose Show decoded protocol details (recovery commands, plist summaries,\n" + << " DFU requests and anomalies)\n" + << " -p, --plists Print every decoded USBMUX property list as XML\n" + << " -x, --extract Write DFU images, recovery uploads and plist transcripts to \n" + << " -w, --warnings Print packets that could not be decoded\n" + << " -h, --help Show this help\n"; +} + +static void extract(const fs::path &directory, size_t index, Device *device) { + auto deviceDirectory = directory / (boost::format("device-%1%-%2$x") % index % device->getIdentifier()).str(); + + for (const auto &expert: device->getExperts()) { + for (const auto &artifact: expert->getArtifacts()) { + fs::create_directories(deviceDirectory); + auto path = deviceDirectory / artifact.name; + std::ofstream output(path.string(), std::ios::binary); + output.write(reinterpret_cast(artifact.data.data()), + static_cast(artifact.data.size())); + std::cout << " Extracted " << path.string() << " (" << artifact.data.size() << " bytes)" << std::endl; + } } +} - fs::path input_path(argv[1]); +int main(int argc, const char *argv[]) { + bool verbose = false, plists = false, warnings = false; + std::string extractDirectory; + std::string inputPath; - std::unique_ptr input; + for (int i = 1; i < argc; i++) { + std::string argument = argv[i]; + if (argument == "-v" || argument == "--verbose") { + verbose = true; + } else if (argument == "-p" || argument == "--plists") { + plists = true; + } else if (argument == "-w" || argument == "--warnings") { + warnings = true; + } else if (argument == "-x" || argument == "--extract") { + if (i + 1 >= argc) { + usage(argv[0]); + return 1; + } + extractDirectory = argv[++i]; + } else if (argument == "-h" || argument == "--help") { + usage(argv[0]); + return 0; + } else if (!argument.empty() && argument[0] == '-') { + std::cerr << "Unknown option " << argument << std::endl; + usage(argv[0]); + return 1; + } else if (inputPath.empty()) { + inputPath = argument; + } else { + usage(argv[0]); + return 1; + } + } - if (input_path.extension() == ".pcapng" || input_path.extension() == ".pcap") { - input = std::make_unique(input_path); - } else { - std::cerr << "Input file type is not supported." << std::endl; - exit(-1); + if (inputPath.empty()) { + usage(argv[0]); + return 1; } - input->parse(); + std::unique_ptr input; + try { + input = InputFile::open(fs::path(inputPath)); + input->parse(); + } catch (const ParseError &error) { + std::cerr << "Error: " << error.what() << std::endl; + if (input == nullptr) { return 1; } + std::cerr << "Reporting what was parsed before the error." << std::endl; + } + size_t index = 0; for (const auto &device: input->getDevices()) { - std::wcout << L"\n\nDevice ID: " << std::hex << device->getIdentifier() << std::endl; - std::wcout << L" Manufacturer: " << device->getManufacturer() << std::endl; - std::wcout << L" Product: " << device->getProduct() << std::endl; - std::wcout << L" Statistics: " << std::dec << device->getPacketCount() << L" packets and " << - device->getByteCount() << L" bytes. (control " << device->getControlPacketCount() << L" packets, " << - device->getControlByteCount() << L" bytes)." << std::endl; + std::cout << "\n\nDevice ID: " << std::hex << device->getIdentifier() << std::dec << std::endl; + if (device->hasDeviceDescriptor()) { + std::cout << " VID/PID: " << boost::format("%1$04x:%2$04x") % device->getVendorId() % + device->getProductId() + << " (" << device->getMode() << " mode)" << std::endl; + } + std::cout << " Manufacturer: " << device->getManufacturer() << std::endl; + std::cout << " Product: " << device->getProduct() << std::endl; + std::cout << " Serial: " << device->getSerial() << std::endl; + std::cout << " Statistics: " << device->getPacketCount() << " packets and " << + device->getByteCount() << " bytes. (control " << device->getControlPacketCount() << " packets, " << + device->getControlByteCount() << " bytes)." << std::endl; + if (verbose) { + for (const auto &anomaly: device->getAnomalies()) { + std::cout << " ! " << anomaly << std::endl; + } + } - std::wcout << L" Interface Experts:\n"; + std::cout << " Interface Experts:\n"; for (const auto &expert: device->getExperts()) { - if (!expert->isEmpty()) { - std::cout << " " << expert->toString() << std::endl; + if (expert->isEmpty()) { continue; } + + std::cout << " " << expert->toString() << std::endl; + if (verbose) { + std::cout << expert->details(); + } + + auto *usbmux = dynamic_cast(expert.get()); + if (plists && usbmux != nullptr) { + for (const auto &[id, stream]: usbmux->getStreams()) { + for (const auto &message: stream->getMessages()) { + std::cout << boost::format(" --- stream %1$x %2% ---\n") % id % + (message.outbound ? "host -> device" : "device -> host"); + std::cout << message.xml(); + } + } + } + } + + if (!extractDirectory.empty()) { + extract(fs::path(extractDirectory), index, device); + } + index++; + } + + if (input->getWarningCount() > 0) { + std::cerr << "\n" << input->getWarningCount() << " packets could not be fully decoded" + << (warnings ? ":" : " (use --warnings to list them)") << std::endl; + if (warnings) { + for (const auto &warning: input->getWarnings()) { + std::cerr << " " << warning << std::endl; + } + if (input->getWarningCount() > input->getWarnings().size()) { + std::cerr << " ..." << std::endl; } } } diff --git a/src/protocol/DFUInterfaceExpert.cpp b/src/protocol/DFUInterfaceExpert.cpp index 7274c60..28457cf 100644 --- a/src/protocol/DFUInterfaceExpert.cpp +++ b/src/protocol/DFUInterfaceExpert.cpp @@ -4,9 +4,155 @@ #include "DFUInterfaceExpert.h" +static const char *requestName(uint8_t request) { + switch (request) { + case DFU_DETACH: return "DETACH"; + case DFU_DNLOAD: return "DNLOAD"; + case DFU_UPLOAD: return "UPLOAD"; + case DFU_GETSTATUS: return "GETSTATUS"; + case DFU_CLRSTATUS: return "CLRSTATUS"; + case DFU_GETSTATE: return "GETSTATE"; + case DFU_ABORT: return "ABORT"; + default: return "UNKNOWN"; + } +} + +std::string DFUInterfaceExpert::stateName(int state) { + switch (state) { + case 0: return "appIDLE"; + case 1: return "appDETACH"; + case 2: return "dfuIDLE"; + case 3: return "dfuDNLOAD-SYNC"; + case 4: return "dfuDNBUSY"; + case 5: return "dfuDNLOAD-IDLE"; + case 6: return "dfuMANIFEST-SYNC"; + case 7: return "dfuMANIFEST"; + case 8: return "dfuMANIFEST-WAIT-RESET"; + case 9: return "dfuUPLOAD-IDLE"; + case 10: return "dfuERROR"; + case -1: return "unknown"; + default: return (boost::format("state(%1%)") % state).str(); + } +} + +void DFUInterfaceExpert::anomaly(const std::string &description) { + this->m_anomalies.push_back(description); +} + +void DFUInterfaceExpert::processControlOut(const usb_setup_t &setup, byte_array data) { + InterfaceExpert::processControlOut(setup, data); + + if ((setup.bmRequestType & USB_TYPE_MASK) != USB_TYPE_CLASS) { return; } + this->m_requestCounts[setup.bRequest]++; + + if (data.second != setup.wLength) { + this->anomaly((boost::format("%1% carried %2% bytes but wLength was %3%") % requestName(setup.bRequest) % + data.second % setup.wLength).str()); + } + + switch (setup.bRequest) { + case DFU_DNLOAD: + if (setup.wLength == 0) { + // A zero length download moves the device to manifestation, completing the image + if (!this->m_currentImage.empty()) { + this->m_images.push_back(std::move(this->m_currentImage)); + this->m_currentImage = {}; + } + break; + } + + if (this->m_transferSize != 0 && setup.wLength > this->m_transferSize) { + this->anomaly((boost::format("DNLOAD of %1% bytes exceeds wTransferSize %2%") % setup.wLength % + this->m_transferSize).str()); + } + + this->m_currentImage.insert(this->m_currentImage.end(), data.first, data.first + data.second); + break; + + case DFU_ABORT: + if (!this->m_currentImage.empty()) { + this->anomaly((boost::format("ABORT after a partial download of %1% bytes") % + this->m_currentImage.size()).str()); + this->m_images.push_back(std::move(this->m_currentImage)); + this->m_currentImage = {}; + } + break; + + case DFU_DETACH: + case DFU_CLRSTATUS: + break; + + default: + this->anomaly((boost::format("Unexpected class OUT request 0x%1$02x") % (uint32_t) setup.bRequest).str()); + } +} + +void DFUInterfaceExpert::processControlIn(const usb_setup_t &setup, byte_array data) { + InterfaceExpert::processControlIn(setup, data); + + if ((setup.bmRequestType & USB_TYPE_MASK) != USB_TYPE_CLASS) { return; } + this->m_requestCounts[setup.bRequest]++; + + switch (setup.bRequest) { + case DFU_GETSTATUS: + // bStatus, bwPollTimeout[3], bState, iString + if (data.second >= 5) { + this->m_lastStatus = static_cast(data.first[0]); + this->m_lastState = static_cast(data.first[4]); + } + break; + case DFU_GETSTATE: + if (data.second >= 1) { + this->m_lastState = static_cast(data.first[0]); + } + break; + case DFU_UPLOAD: + this->m_uploadBytes += data.second; + break; + default: + this->anomaly((boost::format("Unexpected class IN request 0x%1$02x") % (uint32_t) setup.bRequest).str()); + } +} + +std::vector> DFUInterfaceExpert::getImages() { + auto result = this->m_images; + if (!this->m_currentImage.empty()) { + result.push_back(this->m_currentImage); + } + return result; +} + +std::vector DFUInterfaceExpert::getArtifacts() { + std::vector result; + auto images = this->getImages(); + for (size_t i = 0; i < images.size(); i++) { + auto partial = (i == this->m_images.size()) ? "-partial" : ""; + result.push_back({(boost::format("dfu-image-%1%%2%.bin") % i % partial).str(), std::move(images[i])}); + } + return result; +} + std::string DFUInterfaceExpert::toString() { - auto format = boost::format("DFUInterfaceExpert (config=%1%, int=%2%) - %3% packets (%4% bytes)"); + uint64_t imageBytes = 0; + auto images = this->getImages(); + for (const auto &image: images) { imageBytes += image.size(); } + + auto format = boost::format( + "DFUInterfaceExpert (config=%1%, int=%2%) - %3% control packets (%4% bytes), %5% images downloaded " + "(%6% bytes), %7% bytes uploaded, last state %8%, %9% anomalies"); return (format % (uint32_t) this->m_configuration % (uint32_t) this->m_interface % this->m_controlPackets % - this->m_controlBytes).str(); + this->m_controlBytes % images.size() % imageBytes % this->m_uploadBytes % stateName(this->m_lastState) % + this->m_anomalies.size()).str(); +} + +std::string DFUInterfaceExpert::details() { + std::string result; + for (const auto &[request, count]: this->m_requestCounts) { + result += (boost::format(" %1%: %2%\n") % requestName(request) % count).str(); + } + for (const auto &anomaly: this->m_anomalies) { + result += " ! " + anomaly + "\n"; + } + return result; } diff --git a/src/protocol/DFUInterfaceExpert.h b/src/protocol/DFUInterfaceExpert.h index bcbb849..94e5623 100644 --- a/src/protocol/DFUInterfaceExpert.h +++ b/src/protocol/DFUInterfaceExpert.h @@ -8,14 +8,56 @@ #include "../demuxusb.hpp" #include "InterfaceExpert.h" +enum dfu_request { + DFU_DETACH = 0, + DFU_DNLOAD = 1, + DFU_UPLOAD = 2, + DFU_GETSTATUS = 3, + DFU_CLRSTATUS = 4, + DFU_GETSTATE = 5, + DFU_ABORT = 6 +}; + +// Follows the USB DFU 1.1 class protocol: reassembles DNLOAD transfers into images, tracks the device state machine +// from GETSTATUS / GETSTATE responses and records protocol anomalies worth a forensic look. class DFUInterfaceExpert : public InterfaceExpert { public: DFUInterfaceExpert(uint8_t configuration, uint8_t interface) : InterfaceExpert(configuration, interface) {}; + void processControlOut(const usb_setup_t &setup, byte_array data) override; + + void processControlIn(const usb_setup_t &setup, byte_array data) override; + std::string toString() override; + std::string details() override; + + std::vector getArtifacts() override; + + void setTransferSize(uint16_t transferSize) { this->m_transferSize = transferSize; } + + // Completed DNLOAD images (terminated by a zero length DNLOAD), followed by any partial image still in flight. + std::vector> getImages(); + + [[nodiscard]] const std::vector &getAnomalies() const { return this->m_anomalies; } + + [[nodiscard]] int getLastState() const { return this->m_lastState; } + + [[nodiscard]] int getLastStatus() const { return this->m_lastStatus; } + + static std::string stateName(int state); + protected: + void anomaly(const std::string &description); + uint16_t m_transferSize{}; + std::vector> m_images{}; + std::vector m_currentImage{}; + uint64_t m_uploadBytes{}; + std::map m_requestCounts{}; + std::vector m_anomalies{}; + int m_lastState{-1}; + int m_lastStatus{-1}; }; diff --git a/src/protocol/InterfaceExpert.h b/src/protocol/InterfaceExpert.h index b9e7e0e..81c82e8 100644 --- a/src/protocol/InterfaceExpert.h +++ b/src/protocol/InterfaceExpert.h @@ -6,6 +6,14 @@ #define demuxusb_INTERFACEEXPERT_H #include "../demuxusb.hpp" +#include "pcapng.h" +#include "usb.h" + +// A file-like product of protocol analysis (a DFU image, a recovery mode upload, ...) that can be written to disk. +struct ExtractedArtifact { + std::string name; + std::vector data; +}; class InterfaceExpert { public: @@ -14,12 +22,12 @@ class InterfaceExpert { virtual ~InterfaceExpert() = default; - virtual void processControlOut(byte_array data) { + virtual void processControlOut(const usb_setup_t &setup, byte_array data) { this->m_controlPackets++; this->m_controlBytes += data.second; }; - virtual void processControlIn(byte_array data) { + virtual void processControlIn(const usb_setup_t &setup, byte_array data) { this->m_controlPackets++; this->m_controlBytes += data.second; }; @@ -36,8 +44,26 @@ class InterfaceExpert { virtual bool isEmpty() { return (this->m_controlPackets == 0) && (this->m_packets == 0); } + // One line (or short block) summary of the interface. virtual std::string toString(); + // Verbose decode of everything the expert understood (commands, property lists, ...). Empty when there is none. + virtual std::string details() { return {}; } + + virtual std::vector getArtifacts() { return {}; } + + [[nodiscard]] uint8_t getConfiguration() const { return this->m_configuration; } + + [[nodiscard]] uint8_t getInterface() const { return this->m_interface; } + + [[nodiscard]] uint32_t getControlPacketCount() const { return this->m_controlPackets; } + + [[nodiscard]] uint32_t getPacketCount() const { return this->m_packets; } + + [[nodiscard]] uint64_t getControlByteCount() const { return this->m_controlBytes; } + + [[nodiscard]] uint64_t getByteCount() const { return this->m_bytes; } + protected: uint8_t m_configuration; uint8_t m_interface; diff --git a/src/protocol/MuxStream.cpp b/src/protocol/MuxStream.cpp index d9df44a..ad47305 100644 --- a/src/protocol/MuxStream.cpp +++ b/src/protocol/MuxStream.cpp @@ -4,93 +4,160 @@ #include "MuxStream.h" -void MuxStream::receiveBytes(size_t length, std::byte *start) { - this->m_bytesIn += length; - - if (this->shouldMaterializePlist()) { +// Messages larger than this are not plausible service messages; treat the stream as not length framed. +static const uint32_t MAX_PLIST_MESSAGE = 64U * 1024U * 1024U; + +static const char XML_PLIST_PREFIX[] = "m_bytesOut += length; - - if (this->shouldMaterializePlist() && length > sizeof(uint32_t)) { - plist_service_item *item = nullptr; - // This is often the first request - if (this->m_inItems.empty()) { - uint32_t size = *((uint32_t *) start); - item = new plist_service_item{size, 0, static_cast(malloc(size))}; - start += sizeof(uint32_t); - length -= sizeof(uint32_t); - - this->m_inItems.push_back(std::make_unique(*item)); - } else { - item = this->m_inItems[this->m_inItems.size() - 1].get(); +static bool startsWith(const std::byte *data, size_t length, const char *prefix, size_t prefixLength) { + return length >= prefixLength && std::memcmp(data, prefix, prefixLength) == 0; +} - if (item->size == item->received) { - uint32_t size = *((uint32_t *) start); - item = new plist_service_item{size, 0, static_cast(malloc(size))}; - start += sizeof(uint32_t); - length -= sizeof(uint32_t); +static bool looksLikeTLS(const std::vector &buffer) { + // TLS record: handshake (0x16) or application data (0x17), version 3.x + return buffer.size() >= 3 && (buffer[0] == std::byte{0x16} || buffer[0] == std::byte{0x17}) && + buffer[1] == std::byte{0x03} && static_cast(buffer[2]) <= 0x04; +} - this->m_inItems.push_back(std::make_unique(*item)); - } - } +static uint32_t readBigEndian32(const std::byte *data) { + uint32_t value; + std::memcpy(&value, data, sizeof(value)); + return boost::endian::big_to_native(value); +} - auto handled_bytes = this->handleBytes(*item, length, start); - if (handled_bytes != length) { - auto recursive_length = length - handled_bytes; - auto *recursive_data = start + handled_bytes; +std::string MuxMessage::xml() const { + if (this->plist == nullptr) { return {}; } + char *xml = nullptr; + uint32_t length = 0; + plist_to_xml(this->plist.get(), &xml, &length); + if (xml == nullptr) { return {}; } + std::string result(xml, length); + freePlistMemory(xml); + return result; +} - this->receiveBytes(recursive_length, recursive_data); - } - } +std::string MuxMessage::stringValue(const char *key) const { + if (this->plist == nullptr || plist_get_node_type(this->plist.get()) != PLIST_DICT) { return {}; } + auto item = plist_dict_get_item(this->plist.get(), key); + if (item == nullptr || plist_get_node_type(item) != PLIST_STRING) { return {}; } + uint64_t length = 0; + const char *value = plist_get_string_ptr(item, &length); + return value == nullptr ? std::string{} : std::string(value, length); } -MuxStream::~MuxStream() { - for (auto &in_item: this->m_inItems) { - if (in_item->data != nullptr) { - free(in_item->data); - in_item->data = nullptr; - } - if (in_item->plist != nullptr) { - plist_free(in_item->plist); - in_item->plist = nullptr; +int64_t MuxMessage::uintValue(const char *key) const { + if (this->plist == nullptr || plist_get_node_type(this->plist.get()) != PLIST_DICT) { return -1; } + auto item = plist_dict_get_item(this->plist.get(), key); + if (item == nullptr || plist_get_node_type(item) != PLIST_UINT) { return -1; } + uint64_t value = 0; + plist_get_uint_val(item, &value); + return static_cast(value); +} + +std::string MuxMessage::summary() const { + if (this->plist == nullptr) { return ""; } + if (plist_get_node_type(this->plist.get()) != PLIST_DICT) { return ""; } + + std::string result; + for (const char *key: {"Request", "Type", "Service", "Key", "Domain", "Error", "Result", "MessageType", + "Command"}) { + auto value = this->stringValue(key); + if (!value.empty()) { + if (!result.empty()) { result += ", "; } + result += std::string(key) + "=" + value; } } - for (auto &out_item: this->m_outItems) { - if (out_item->data != nullptr) { - free(out_item->data); - out_item->data = nullptr; - } - if (out_item->plist != nullptr) { - plist_free(out_item->plist); - out_item->plist = nullptr; - } + + auto port = this->uintValue("Port"); + if (port >= 0) { + if (!result.empty()) { result += ", "; } + result += "Port=" + std::to_string(port); } + + return result.empty() ? "" : result; } -void MuxStream::materialize(plist_service_item &item) { - plist_format_t format; - plist_from_memory(reinterpret_cast(item.data), item.size, &item.plist, &format); +void MuxStream::receiveBytes(size_t length, std::byte *start) { + this->consume(this->m_in, length, start); +} - if (PLIST_IS_DICT(item.plist)) { - plist_dict_iter it{}; - char *key = nullptr; - plist_t value; - plist_dict_new_iter(item.plist, &it); +void MuxStream::sendBytes(size_t length, std::byte *start) { + this->consume(this->m_out, length, start); +} +void MuxStream::consume(Direction &direction, size_t length, std::byte *start) { + direction.bytes += length; + if (length == 0) { return; } - } + if (direction.encoding == StreamEncoding::Raw || direction.encoding == StreamEncoding::TLS) { return; } + + direction.buffer.insert(direction.buffer.end(), start, start + length); + this->drain(direction); } -size_t MuxStream::handleBytes(plist_service_item &item, size_t length, std::byte *data) { - auto remaining = item.size - item.received; - auto available = std::min((size_t) remaining, (size_t) length); - std::memcpy(item.data + item.received, data, available); +void MuxStream::drain(Direction &direction) { + while (direction.buffer.size() >= sizeof(uint32_t)) { + if (looksLikeTLS(direction.buffer)) { + direction.encoding = StreamEncoding::TLS; + direction.buffer.clear(); + return; + } + + auto size = readBigEndian32(direction.buffer.data()); + if (size == 0 || size > MAX_PLIST_MESSAGE) { + direction.encoding = StreamEncoding::Raw; + direction.buffer.clear(); + return; + } + + auto *payload = direction.buffer.data() + sizeof(uint32_t); + auto available = direction.buffer.size() - sizeof(uint32_t); + + if (direction.encoding == StreamEncoding::Unknown) { + // Decide on framing once enough of the first message is present to see the plist signature + auto needed = std::min(size, sizeof(BINARY_PLIST_PREFIX) - 1); + if (available < needed) { return; } + + if (startsWith(payload, available, XML_PLIST_PREFIX, sizeof(XML_PLIST_PREFIX) - 1) || + startsWith(payload, available, BINARY_PLIST_PREFIX, sizeof(BINARY_PLIST_PREFIX) - 1)) { + direction.encoding = StreamEncoding::Plist; + } else { + direction.encoding = StreamEncoding::Raw; + direction.buffer.clear(); + return; + } + } - if (item.received == item.size) { this->materialize(item); } + if (available < size) { return; } - return available; + plist_t plist = nullptr; + if (startsWith(payload, size, BINARY_PLIST_PREFIX, sizeof(BINARY_PLIST_PREFIX) - 1)) { + plist_from_bin(reinterpret_cast(payload), size, &plist); + } else { + plist_from_xml(reinterpret_cast(payload), size, &plist); + } + + this->m_messages.push_back(MuxMessage{direction.outbound, size, unique_plist{plist}}); + direction.buffer.erase(direction.buffer.begin(), + direction.buffer.begin() + static_cast(sizeof(uint32_t) + size)); + } } diff --git a/src/protocol/MuxStream.h b/src/protocol/MuxStream.h index d2388eb..a451596 100644 --- a/src/protocol/MuxStream.h +++ b/src/protocol/MuxStream.h @@ -7,48 +7,105 @@ #include "../demuxusb.hpp" -struct plist_service_item { - uint32_t size{}; - uint32_t received{}; - std::byte *data{}; - plist_t plist{}; +#define LOCKDOWN_PORT 62078 + +struct PlistDeleter { + void operator()(void *plist) const { plist_free(static_cast(plist)); } +}; + +typedef std::unique_ptr unique_plist; + +// How the payload of one direction of a stream is framed. Lockdown and most lockdown services exchange +// property lists prefixed with a big endian 32 bit length; once a session is upgraded the bytes become TLS. +enum class StreamEncoding { + Unknown, + Plist, + Raw, + TLS +}; + +const char *encodingName(StreamEncoding encoding); + +inline std::ostream &operator<<(std::ostream &stream, StreamEncoding encoding) { + return stream << encodingName(encoding); +} + +// One deserialized property list message. outbound is host -> device. +struct MuxMessage { + bool outbound; + uint32_t size; + unique_plist plist; + + [[nodiscard]] std::string xml() const; + + // Returns the string value of a top level dictionary key, or an empty string. + [[nodiscard]] std::string stringValue(const char *key) const; + + // Returns the unsigned integer value of a top level dictionary key, or -1. + [[nodiscard]] int64_t uintValue(const char *key) const; + + // A short human readable description (Request / Type / Service / Error keys). + [[nodiscard]] std::string summary() const; }; +// A TCP connection tunnelled over USBMUX between a host port and a device port. class MuxStream { public: MuxStream(uint16_t host, uint16_t device) : m_host{host}, m_device{device} {}; - ~MuxStream(); - uint16_t getHostPort() { return this->m_host; } uint16_t getDevicePort() { return this->m_device; } - uint64_t getBytesIn() { return this->m_bytesIn; } + uint64_t getBytesIn() { return this->m_in.bytes; } + + uint64_t getBytesOut() { return this->m_out.bytes; } - uint64_t getBytesOut() { return this->m_bytesOut; } + size_t getTransactionCount() { return this->m_messages.size(); } - size_t getTransactionCount() { return this->m_inItems.size() + this->m_outItems.size(); } + bool shouldMaterializePlist() { + return this->m_out.encoding == StreamEncoding::Unknown || this->m_out.encoding == StreamEncoding::Plist; + } - bool shouldMaterializePlist() { return this->getDevicePort() != 12345; } + StreamEncoding getEncodingIn() { return this->m_in.encoding; } + StreamEncoding getEncodingOut() { return this->m_out.encoding; } + + // Device -> host bytes void receiveBytes(size_t length, std::byte *start); + // Host -> device bytes void sendBytes(size_t length, std::byte *start); + [[nodiscard]] const std::vector &getMessages() const { return this->m_messages; } + + const std::string &getServiceName() { return this->m_serviceName; } + + void setServiceName(std::string name) { this->m_serviceName = std::move(name); } + + bool isClosed() { return this->m_closed; } + + void close() { this->m_closed = true; } + protected: - void materialize(plist_service_item &item); + struct Direction { + bool outbound; + StreamEncoding encoding{StreamEncoding::Unknown}; + uint64_t bytes{}; + std::vector buffer{}; + }; - size_t handleBytes(plist_service_item &item, size_t length, std::byte *data); + void consume(Direction &direction, size_t length, std::byte *start); + + void drain(Direction &direction); uint16_t m_host; uint16_t m_device; - uint64_t m_bytesIn{}; - uint64_t m_bytesOut{}; - bool m_isPlistService{true}; - - std::vector> m_inItems{}; - std::vector> m_outItems{}; + bool m_closed{false}; + std::string m_serviceName{}; + Direction m_in{false}; + Direction m_out{true}; + std::vector m_messages{}; }; diff --git a/src/protocol/RecoveryInterfaceExpert.cpp b/src/protocol/RecoveryInterfaceExpert.cpp index e2a8792..677a705 100644 --- a/src/protocol/RecoveryInterfaceExpert.cpp +++ b/src/protocol/RecoveryInterfaceExpert.cpp @@ -4,10 +4,136 @@ #include "RecoveryInterfaceExpert.h" +static std::string cString(byte_array data) { + auto *start = reinterpret_cast(data.first); + auto length = strnlen(start, data.second); + return {start, length}; +} + +static std::string printable(const std::string &value) { + std::string result; + for (auto c: value) { + if (c == '\n') { + result += "\\n"; + } else if (static_cast(c) < 0x20 || c == 0x7F) { + result += (boost::format("\\x%1$02x") % (uint32_t) static_cast(c)).str(); + } else { + result.push_back(c); + } + } + return result; +} + +void RecoveryInterfaceExpert::processControlOut(const usb_setup_t &setup, byte_array data) { + InterfaceExpert::processControlOut(setup, data); + + if (setup.bmRequestType == 0x40 && data.second > 0) { + this->m_payloads.push_back(RecoveryPayload{RecoveryPayloadKind::Command, setup.bRequest, cString(data)}); + } else if (setup.bmRequestType == 0x41 && setup.bRequest == 0) { + // Start of a file transfer; the image follows on bulk OUT + this->m_payloads.push_back(RecoveryPayload{RecoveryPayloadKind::Upload, setup.bRequest}); + this->m_uploadOpen = true; + } +} + +void RecoveryInterfaceExpert::processControlIn(const usb_setup_t &setup, byte_array data) { + InterfaceExpert::processControlIn(setup, data); + + if (setup.bmRequestType == 0xC0 && data.second > 0) { + this->m_payloads.push_back(RecoveryPayload{RecoveryPayloadKind::Response, setup.bRequest, cString(data)}); + } +} + +RecoveryPayload &RecoveryInterfaceExpert::currentUpload() { + if (!this->m_uploadOpen) { + // Capture began mid transfer or the client skipped the announcement + this->m_payloads.push_back(RecoveryPayload{RecoveryPayloadKind::Upload}); + this->m_uploadOpen = true; + } + + for (auto it = this->m_payloads.rbegin(); it != this->m_payloads.rend(); ++it) { + if (it->kind == RecoveryPayloadKind::Upload) { return *it; } + } + + // Unreachable: an open upload always has a payload + this->m_payloads.push_back(RecoveryPayload{RecoveryPayloadKind::Upload}); + return this->m_payloads.back(); +} + +void RecoveryInterfaceExpert::processBulkOut(byte_array data) { + InterfaceExpert::processBulkOut(data); + if (data.second == 0) { return; } + + auto &upload = this->currentUpload(); + upload.data.insert(upload.data.end(), data.first, data.first + data.second); +} + +void RecoveryInterfaceExpert::processBulkIn(byte_array data) { + InterfaceExpert::processBulkIn(data); + this->m_console.append(reinterpret_cast(data.first), data.second); +} + bool RecoveryInterfaceExpert::isEmpty() { - return this->m_payloads.empty(); + return this->m_payloads.empty() && InterfaceExpert::isEmpty(); +} + +std::vector RecoveryInterfaceExpert::getCommands() { + std::vector result; + for (const auto &payload: this->m_payloads) { + if (payload.kind == RecoveryPayloadKind::Command) { result.push_back(payload.text); } + } + return result; +} + +std::vector RecoveryInterfaceExpert::getArtifacts() { + std::vector result; + size_t index = 0; + for (const auto &payload: this->m_payloads) { + if (payload.kind == RecoveryPayloadKind::Upload && !payload.data.empty()) { + result.push_back({(boost::format("recovery-upload-%1%.bin") % index++).str(), payload.data}); + } + } + if (!this->m_console.empty()) { + auto *begin = reinterpret_cast(this->m_console.data()); + result.push_back({"recovery-console.txt", std::vector(begin, begin + this->m_console.size())}); + } + return result; } std::string RecoveryInterfaceExpert::toString() { - return InterfaceExpert::toString(); + size_t commands = 0, uploads = 0; + uint64_t uploadBytes = 0; + for (const auto &payload: this->m_payloads) { + if (payload.kind == RecoveryPayloadKind::Command) { commands++; } + if (payload.kind == RecoveryPayloadKind::Upload) { + uploads++; + uploadBytes += payload.data.size(); + } + } + + auto format = boost::format( + "RecoveryInterfaceExpert (config=%1%, int=%2%) - %3% commands, %4% uploads (%5% bytes), " + "%6% console bytes; %7% control packets (%8% bytes) and %9% bulk packets (%10% bytes)"); + + return (format % (uint32_t) this->m_configuration % (uint32_t) this->m_interface % commands % uploads % + uploadBytes % this->m_console.size() % this->m_controlPackets % this->m_controlBytes % this->m_packets % + this->m_bytes).str(); +} + +std::string RecoveryInterfaceExpert::details() { + std::string result; + for (const auto &payload: this->m_payloads) { + switch (payload.kind) { + case RecoveryPayloadKind::Command: + result += " > " + printable(payload.text) + "\n"; + break; + case RecoveryPayloadKind::Response: + result += " < " + printable(payload.text) + "\n"; + break; + case RecoveryPayloadKind::Upload: + result += (boost::format(" ^ upload of %1% bytes\n") % payload.data.size()).str(); + break; + } + } + return result; } diff --git a/src/protocol/RecoveryInterfaceExpert.h b/src/protocol/RecoveryInterfaceExpert.h index 029141a..6006364 100644 --- a/src/protocol/RecoveryInterfaceExpert.h +++ b/src/protocol/RecoveryInterfaceExpert.h @@ -8,20 +8,54 @@ #include "../demuxusb.hpp" #include "InterfaceExpert.h" -struct RecoveryPayload { +enum class RecoveryPayloadKind { + Command, // Host -> iBoot command string (vendor OUT request, e.g. "setenv auto-boot true") + Response, // iBoot -> host control response (e.g. the value returned for "getenv") + Upload // Host -> iBoot file upload over the bulk OUT endpoint (iBSS, iBEC, ramdisk, ...) +}; +struct RecoveryPayload { + RecoveryPayloadKind kind; + uint8_t request{}; + std::string text{}; + std::vector data{}; }; +// Decodes the iBoot recovery mode protocol as spoken by libirecovery: commands are NUL terminated strings sent as +// vendor control transfers, files are announced by a 0x41 control transfer and then streamed over bulk OUT, and the +// iBoot console is read back over bulk IN. class RecoveryInterfaceExpert : public InterfaceExpert { public: RecoveryInterfaceExpert(uint8_t configuration, uint8_t interface) : InterfaceExpert(configuration, interface) {}; + void processControlOut(const usb_setup_t &setup, byte_array data) override; + + void processControlIn(const usb_setup_t &setup, byte_array data) override; + + void processBulkOut(byte_array data) override; + + void processBulkIn(byte_array data) override; + std::string toString() override; + std::string details() override; + + std::vector getArtifacts() override; + bool isEmpty() override; + [[nodiscard]] const std::vector &getPayloads() const { return this->m_payloads; } + + std::vector getCommands(); + + [[nodiscard]] const std::string &getConsole() const { return this->m_console; } + protected: + RecoveryPayload ¤tUpload(); + std::vector m_payloads{}; + bool m_uploadOpen{false}; + std::string m_console{}; }; diff --git a/src/protocol/USBMUXInterfaceExpert.cpp b/src/protocol/USBMUXInterfaceExpert.cpp index dca70c5..09b8bd5 100644 --- a/src/protocol/USBMUXInterfaceExpert.cpp +++ b/src/protocol/USBMUXInterfaceExpert.cpp @@ -4,107 +4,218 @@ #include "USBMUXInterfaceExpert.h" +// usbmux v1 header: protocol and length. v2 adds magic and sequence numbers (struct mux_header). +static const size_t MUX_HEADER_V1_SIZE = 8; +// Larger than any usbmux packet usbmuxd will produce; anything bigger means we lost framing. +static const uint32_t MAX_MUX_PACKET = 1024U * 1024U; + +static uint32_t readBigEndian32(const std::byte *data) { + uint32_t value; + std::memcpy(&value, data, sizeof(value)); + return boost::endian::big_to_native(value); +} + std::string USBMUXInterfaceExpert::toString() { - auto format = boost::format("USBMUXInterfaceExpert (configuration=%1%, interface=%2%):\n"); - auto stream_format = boost::format("\t-\t%1$x\t: %2% -> %3% (%4% transactions, in %5%, out %6% bytes)\n"); + auto format = boost::format("USBMUXInterfaceExpert (configuration=%1%, interface=%2%, version=%3%):\n"); + auto stream_format = boost::format( + "\t-\t%1$x\t: %2% -> %3%%4% (%5% transactions, in %6% bytes, out %7% bytes, %8%)\n"); + + std::string result = (format % (uint32_t) this->m_configuration % (uint32_t) this->m_interface % + this->m_version).str(); + + for (auto &[id, stream]: this->m_streams) { + auto service = stream->getServiceName().empty() ? std::string{} : " [" + stream->getServiceName() + "]"; + auto encoding = stream->getEncodingOut() == stream->getEncodingIn() + ? std::string(encodingName(stream->getEncodingOut())) + : std::string(encodingName(stream->getEncodingOut())) + "/" + + encodingName(stream->getEncodingIn()); + + result.append((stream_format % id % stream->getHostPort() % stream->getDevicePort() % service % + stream->getTransactionCount() % stream->getBytesIn() % stream->getBytesOut() % + encoding).str()); + } - std::string result = (format % (uint32_t) this->m_configuration % (uint32_t) this->m_interface).str(); + return result; +} - for (auto &stream: this->m_streams) { - result.append((stream_format % stream.first % stream.second->getHostPort() % - stream.second->getDevicePort() % stream.second->getTransactionCount() % - stream.second->getBytesIn() % - stream.second->getBytesOut()).str()); +std::string USBMUXInterfaceExpert::details() { + std::string result; + for (auto &[id, stream]: this->m_streams) { + if (stream->getMessages().empty()) { continue; } + result += (boost::format(" stream %1$x (%2% -> %3%):\n") % id % stream->getHostPort() % + stream->getDevicePort()).str(); + for (const auto &message: stream->getMessages()) { + result += std::string(" ") + (message.outbound ? "> " : "< ") + message.summary() + "\n"; + } } + return result; +} + +std::vector USBMUXInterfaceExpert::getArtifacts() { + std::vector result; + for (auto &[id, stream]: this->m_streams) { + if (stream->getMessages().empty()) { continue; } + + std::string document; + for (const auto &message: stream->getMessages()) { + document += message.outbound ? "\n" : "\n"; + document += message.xml(); + } + auto name = (boost::format("usbmux-%1$016x-%2%-%3%.plist.txt") % id % stream->getHostPort() % + stream->getDevicePort()).str(); + auto *begin = reinterpret_cast(document.data()); + result.push_back({name, std::vector(begin, begin + document.size())}); + } return result; } uint64_t USBMUXInterfaceExpert::beginStream(uint16_t host, uint16_t device) { uint32_t stream_port = STREAM_PORT(host, device); - if (!this->m_streamIndex.contains(stream_port)) { - this->m_streamIndex[stream_port] = 0; + auto index = ++this->m_streamIndex[stream_port]; + auto stream_id = STREAM_ID(host, device, index); + + auto stream = std::make_shared(host, device); + if (device == LOCKDOWN_PORT) { + stream->setServiceName("lockdownd"); + } else if (this->m_serviceNames.contains(device)) { + stream->setServiceName(this->m_serviceNames[device]); } + this->m_streams[stream_id] = stream; - auto index = this->m_streamIndex[stream_port]; - index++; - this->m_streamIndex[stream_port] = index; - - return STREAM_ID(host, device, index); + return stream_id; } -void USBMUXInterfaceExpert::processBulkOut(byte_array data) { - // Check that we at least have the basic header - if (data.second >= sizeof(mux_header)) { - auto *header = (mux_header *) data.first; - - - switch (boost::endian::big_to_native(header->protocol)) { - case MUX_PROTO_TCP: { - assert(boost::endian::big_to_native(header->magic) == USBMUX_MAGIC_OUT); - assert(data.second >= (sizeof(mux_header) + sizeof(tcphdr))); - auto *tcp_header = (tcphdr *) (data.first + sizeof(mux_header)); - - auto source = boost::endian::big_to_native(tcp_header->source); - auto destination = boost::endian::big_to_native(tcp_header->dest); - auto flags = boost::endian::big_to_native(tcp_header->flags); - - // Magic evaluation for SYN on out (therefore connect) - if (flags & USB_TCP_FLAG_SYN) { - auto stream_id = this->beginStream(source, destination); - this->m_streams[stream_id] = std::make_shared(source, destination); - } else { - auto stream_id = this->getStreamId(source, destination); - auto stream = this->m_streams[stream_id]; - - assert(stream != nullptr); - auto byte_length = data.second - (sizeof(mux_header) + sizeof(tcphdr)); - - stream->sendBytes(byte_length, data.first + byte_length); - } - } - break; - default: - break; - } - +uint64_t USBMUXInterfaceExpert::getStreamId(uint16_t host, uint16_t device) { + uint32_t stream_port = STREAM_PORT(host, device); + auto found = this->m_streamIndex.find(stream_port); + if (found == this->m_streamIndex.end()) { + // Traffic for a connection opened before the capture started + return this->beginStream(host, device); } + + return STREAM_ID(host, device, found->second); +} + +std::shared_ptr USBMUXInterfaceExpert::streamFor(uint16_t host, uint16_t device) { + return this->m_streams[this->getStreamId(host, device)]; +} + +void USBMUXInterfaceExpert::processBulkOut(byte_array data) { + InterfaceExpert::processBulkOut(data); + this->feed(this->m_out, data); } void USBMUXInterfaceExpert::processBulkIn(byte_array data) { - // Check that we at least have the basic header - if (data.second >= sizeof(mux_header)) { - auto *header = (mux_header *) data.first; + InterfaceExpert::processBulkIn(data); + this->feed(this->m_in, data); +} - switch (boost::endian::big_to_native(header->protocol)) { - case MUX_PROTO_TCP: { - assert(boost::endian::big_to_native(header->magic) == USBMUX_MAGIC_IN); - assert(data.second >= (sizeof(mux_header) + sizeof(tcphdr))); - auto *tcp_header = (tcphdr *) (data.first + sizeof(mux_header)); +void USBMUXInterfaceExpert::feed(Direction &direction, byte_array data) { + if (data.second == 0) { return; } + direction.buffer.insert(direction.buffer.end(), data.first, data.first + data.second); - auto source = boost::endian::big_to_native(tcp_header->source); - auto destination = boost::endian::big_to_native(tcp_header->dest); - auto flags = boost::endian::big_to_native(tcp_header->flags); + auto &buffer = direction.buffer; + while (buffer.size() >= MUX_HEADER_V1_SIZE) { + auto protocol = readBigEndian32(buffer.data()); + auto length = readBigEndian32(buffer.data() + sizeof(uint32_t)); - auto stream_id = this->getStreamId(destination, source); - auto stream = this->m_streams[stream_id]; + if (length < MUX_HEADER_V1_SIZE || length > MAX_MUX_PACKET || protocol > MUX_PROTO_TCP) { + // Lost framing (capture started mid packet, or dropped transfers). Discard and wait for a fresh packet. + this->m_resyncs++; + buffer.clear(); + return; + } - assert(stream != nullptr); - auto byte_length = data.second - (sizeof(mux_header) + sizeof(tcphdr)); + // A v2 header carries the magic; version packets are always sent with the v1 header. + auto peek = std::min(length, sizeof(mux_header)); + if (buffer.size() < peek) { return; } - stream->receiveBytes(byte_length, data.first + byte_length); + size_t header_length = MUX_HEADER_V1_SIZE; + if (length >= sizeof(mux_header)) { + auto magic = readBigEndian32(buffer.data() + offsetof(mux_header, magic)); + if (magic == USBMUX_MAGIC_IN || magic == USBMUX_MAGIC_OUT) { + header_length = sizeof(mux_header); } - break; } + if (buffer.size() < length) { return; } + + this->processPacket(direction.outbound, protocol, buffer.data() + header_length, length - header_length); + buffer.erase(buffer.begin(), buffer.begin() + length); } } -uint64_t USBMUXInterfaceExpert::getStreamId(uint16_t host, uint16_t device) { - uint32_t stream_port = STREAM_PORT(host, device); +void USBMUXInterfaceExpert::processPacket(bool outbound, uint32_t protocol, std::byte *payload, size_t length) { + switch (protocol) { + case MUX_PROTO_VERSION: + if (length >= sizeof(uint32_t)) { + this->m_version = readBigEndian32(payload); + } + break; + case MUX_PROTO_TCP: + this->processTCP(outbound, payload, length); + break; + default: + break; + } +} - auto index = this->m_streamIndex[stream_port]; +void USBMUXInterfaceExpert::processTCP(bool outbound, std::byte *payload, size_t length) { + tcphdr tcp_header{}; + if (!demuxusb::readStruct(payload, length, tcp_header)) { return; } + + auto source = boost::endian::big_to_native(tcp_header.source); + auto destination = boost::endian::big_to_native(tcp_header.dest); + auto offset_and_flags = boost::endian::big_to_native(tcp_header.flags); + auto flags = offset_and_flags & 0x3FU; + size_t data_offset = (offset_and_flags >> 12U) * 4U; + if (data_offset < sizeof(tcphdr) || data_offset > length) { data_offset = sizeof(tcphdr); } + + auto *data = payload + data_offset; + auto data_length = length - data_offset; + + if (outbound) { + // Host is the source; a bare SYN from the host opens a new connection + std::shared_ptr stream; + if ((flags & USB_TCP_FLAG_SYN) && !(flags & USB_TCP_FLAG_ACK)) { + stream = this->m_streams[this->beginStream(source, destination)]; + } else { + stream = this->streamFor(source, destination); + } + + if (data_length > 0) { + auto first_new = stream->getMessages().size(); + stream->sendBytes(data_length, data); + this->learnServices(stream, first_new); + } + if (flags & (USB_TCP_FLAG_RST | USB_TCP_FLAG_FIN)) { stream->close(); } + } else { + auto stream = this->streamFor(destination, source); + + if (data_length > 0) { + auto first_new = stream->getMessages().size(); + stream->receiveBytes(data_length, data); + this->learnServices(stream, first_new); + } + if (flags & (USB_TCP_FLAG_RST | USB_TCP_FLAG_FIN)) { stream->close(); } + } +} + +void USBMUXInterfaceExpert::learnServices(const std::shared_ptr &stream, size_t firstNewMessage) { + if (stream->getDevicePort() != LOCKDOWN_PORT) { return; } + + // lockdownd answers StartService with the service name and the device port to connect to + const auto &messages = stream->getMessages(); + for (auto i = firstNewMessage; i < messages.size(); i++) { + const auto &message = messages[i]; + if (message.outbound) { continue; } - return STREAM_ID(host, device, index); + auto service = message.stringValue("Service"); + auto port = message.uintValue("Port"); + if (!service.empty() && port > 0 && port <= UINT16_MAX) { + this->m_serviceNames[static_cast(port)] = service; + } + } } diff --git a/src/protocol/USBMUXInterfaceExpert.h b/src/protocol/USBMUXInterfaceExpert.h index c436226..6a85f6b 100644 --- a/src/protocol/USBMUXInterfaceExpert.h +++ b/src/protocol/USBMUXInterfaceExpert.h @@ -9,31 +9,63 @@ #include "usbmux.h" #include "InterfaceExpert.h" #include "MuxStream.h" -#include "../ext/usbmux.h" #define STREAM_PORT(host, device) (((uint32_t)host << 16U) | (uint32_t)device) #define STREAM_ID(host, device, index) ((((uint64_t)STREAM_PORT(host, device)) << 32U) | (uint64_t)index) +// Demultiplexes the usbmux protocol carried on the Apple USBMUX bulk endpoints into the TCP streams it tunnels. +// usbmux packets may be split across, or packed into, USB transfers so each direction is reassembled by the +// length in the usbmux header before being parsed. class USBMUXInterfaceExpert : public InterfaceExpert { public: USBMUXInterfaceExpert(uint8_t configuration, uint8_t interface) : InterfaceExpert(configuration, interface) {}; std::string toString() override; - bool isEmpty() override { return this->m_streams.empty(); } + std::string details() override; + + std::vector getArtifacts() override; + + bool isEmpty() override { return this->m_streams.empty() && InterfaceExpert::isEmpty(); } void processBulkOut(byte_array data) override; void processBulkIn(byte_array data) override; + [[nodiscard]] const std::map> &getStreams() const { return this->m_streams; } + + [[nodiscard]] uint32_t getProtocolVersion() const { return this->m_version; } + + [[nodiscard]] uint64_t getResyncCount() const { return this->m_resyncs; } + protected: + struct Direction { + bool outbound; + std::vector buffer{}; + }; + uint64_t beginStream(uint16_t host, uint16_t device); uint64_t getStreamId(uint16_t host, uint16_t device); + std::shared_ptr streamFor(uint16_t host, uint16_t device); + + void feed(Direction &direction, byte_array data); + + void processPacket(bool outbound, uint32_t protocol, std::byte *payload, size_t length); + + void processTCP(bool outbound, std::byte *payload, size_t length); + + void learnServices(const std::shared_ptr &stream, size_t firstNewMessage); + private: + Direction m_in{false}; + Direction m_out{true}; + uint32_t m_version{}; + uint64_t m_resyncs{}; std::map m_streamIndex{}; std::map> m_streams{}; + std::map m_serviceNames{}; }; diff --git a/src/usb/Device.cpp b/src/usb/Device.cpp index 95d8127..075658f 100644 --- a/src/usb/Device.cpp +++ b/src/usb/Device.cpp @@ -8,7 +8,20 @@ #include "../protocol/RecoveryInterfaceExpert.h" #include "../protocol/USBMUXInterfaceExpert.h" +#define USB_CLASS_DFU USB_CLASS_APP_SPEC + namespace demuxusb { + std::string Device::getMode() const { + if (!this->m_hasDeviceDescriptor) { return "Unknown"; } + if (this->isApple()) { + if (this->m_deviceDescriptor.idProduct == IRECV_K_DFU_MODE) { return "DFU"; } + if (this->m_deviceDescriptor.idProduct == IRECV_K_WTF_MODE) { return "WTF"; } + if (this->isAppleRecovery()) { return "Recovery"; } + return "Normal"; + } + return "Unknown"; + } + void Device::processBulkPacketIn(uint8_t endpoint, byte_array data) { endpoint |= 0x80U; this->m_packetCount++; @@ -22,6 +35,7 @@ namespace demuxusb { } void Device::processBulkPacketOut(uint8_t endpoint, byte_array data) { + endpoint = USB_ENDPOINT_ID(endpoint); this->m_packetCount++; this->m_byteCount += data.second; @@ -36,106 +50,165 @@ namespace demuxusb { this->m_controlPacketCount++; this->m_controlByteCount += data.second; - if (USB_ENDPOINT_ID(endpoint) == 0) { - if (setup.bmRequestType == 0x80 && setup.bRequest == 0x06) { - // We are a descriptor read - auto descriptorType = setup.wValue >> 8U; - auto descriptorIndex = setup.wValue & 0xFFU; - auto *header = (usb_descriptor_header *) data.first; - - - switch (descriptorType) { - case USB_DT_DEVICE: - assert(data.second == sizeof(usb_device_descriptor)); - memcpy(&this->m_deviceDescriptor, data.first, data.second); - assert(this->m_deviceDescriptor.bDescriptorType == USB_DT_DEVICE); - this->m_vendorId = this->m_deviceDescriptor.idVendor; - this->m_productId = this->m_deviceDescriptor.idProduct; - this->m_configurations = std::vector( - this->m_deviceDescriptor.bNumConfigurations); - break; - - case USB_DT_CONFIG: { - // We only want to handle the full descriptor when returned - auto *configDescriptor = (usb_config_descriptor *) data.first; - auto configurationIndex = configDescriptor->bConfigurationValue - 1; - if (configDescriptor->wTotalLength != data.second) { break; } - auto config = usb_configuration{}; - usb_configuration::parse(config, data.first, data.second); - - // Attach experts - for (auto &interface: config.interfaces) { - if (this->isAppleDFU()) { - interface.expert = std::make_unique( - configDescriptor->bConfigurationValue, interface.interface.bInterfaceNumber); - } else if (this->isAppleRecovery()) { - interface.expert = std::make_unique( - configDescriptor->bConfigurationValue, interface.interface.bInterfaceNumber); - } else if (this->isApple() && - interface.interface.bInterfaceClass == USB_CLASS_APPLICATION_SPECIFIC && - interface.interface.bInterfaceSubClass == APPLE_SUBCLASS_USBMUX && - interface.interface.bInterfaceProtocol == APPLE_PROTOCOL_USBMUX2) { - interface.expert = std::make_unique( - configDescriptor->bConfigurationValue, interface.interface.bInterfaceNumber); - } - } - - this->m_configurations[configurationIndex] = config; - } - break; + if (USB_ENDPOINT_ID(endpoint) != 0) { return; } - case USB_DT_STRING: { - auto length = *data.first; - auto type = *(data.first + 1); - assert(static_cast(type) == USB_DT_STRING); - if (static_cast(length) != data.second) { break; } + if ((setup.bmRequestType & USB_TYPE_MASK) == USB_TYPE_STANDARD) { + if (setup.bRequest == USB_REQ_GET_DESCRIPTOR && (setup.bmRequestType & USB_RECIP_MASK) == USB_RECIP_DEVICE) { + this->processDescriptor(setup, data); + } + return; + } - auto *stringAddress = (char16_t *) (data.first + 2); - auto stringLength = (data.second - 2) / 2; + auto expert = this->getExpertForControl(setup); + if (expert != nullptr) { + expert->processControlIn(setup, data); + } + } - auto stringValue = std::u16string(stringAddress, stringLength); + void Device::processControlPacketOut(uint8_t endpoint, usb_setup_t setup, byte_array data) { + this->m_controlPacketCount++; + this->m_controlByteCount += data.second; - this->m_strings[descriptorIndex] = std::wstring{std::begin(stringValue), std::end(stringValue)}; - } - break; - - case USB_DT_BOS: { - assert(header->bLength >= sizeof(usb_bos_descriptor)); - auto *bos_descriptor = (usb_bos_descriptor *) data.first; - if (header->bLength != bos_descriptor->wTotalLength) { break; } - struct bos_descriptor bos{}; - memcpy(&bos.descriptor, data.first, sizeof(usb_bos_descriptor)); - std::byte *bos_data = data.first + sizeof(usb_bos_descriptor); - auto bos_data_size = bos_descriptor->wTotalLength - sizeof(usb_bos_descriptor); - bos.data.resize(bos_data_size); - memcpy(bos.data.data(), bos_data, bos_data_size); - break; - } + if (USB_ENDPOINT_ID(endpoint) != 0) { return; } - default: - std::cerr << "Unknown descriptor type " << std::hex << descriptorType << std::endl; - } + if ((setup.bmRequestType & USB_TYPE_MASK) == USB_TYPE_STANDARD) { + auto recipient = setup.bmRequestType & USB_RECIP_MASK; + if (recipient == USB_RECIP_DEVICE && setup.bRequest == USB_REQ_SET_CONFIGURATION) { + this->m_currentConfiguration = setup.wValue & 0xFFU; + this->m_alternates.clear(); + } else if (recipient == USB_RECIP_INTERFACE && setup.bRequest == USB_REQ_SET_INTERFACE) { + this->m_alternates[setup.wIndex & 0xFFU] = setup.wValue & 0xFFU; } + return; } + auto expert = this->getExpertForControl(setup); + if (expert != nullptr) { + expert->processControlOut(setup, data); + } } - void Device::processControlPacketOut(uint8_t endpoint, usb_setup_t setup, byte_array data) { - this->m_controlPacketCount++; - this->m_controlByteCount += data.second; + void Device::processDescriptor(usb_setup_t setup, byte_array data) { + auto descriptorType = setup.wValue >> 8U; + auto descriptorIndex = setup.wValue & 0xFFU; + + usb_descriptor_header header{}; + if (!readStruct(data.first, data.second, header)) { return; } + + if (header.bDescriptorType != descriptorType) { + this->m_anomalies.push_back((boost::format("GET_DESCRIPTOR(0x%1$02x) returned descriptor type 0x%2$02x") % + descriptorType % (uint32_t) header.bDescriptorType).str()); + return; + } + + switch (descriptorType) { + case USB_DT_DEVICE: + // Hosts commonly read the first 8 bytes for bMaxPacketSize0 before the full descriptor + if (data.second < USB_DT_DEVICE_SIZE) { break; } + readStruct(data.first, data.second, this->m_deviceDescriptor); + this->m_hasDeviceDescriptor = true; + break; + + case USB_DT_CONFIG: { + usb_config_descriptor configDescriptor{}; + if (!readStruct(data.first, data.second, configDescriptor)) { break; } + // Only handle the full descriptor set, not the initial 9 byte read for wTotalLength + if (data.second < configDescriptor.wTotalLength || configDescriptor.wTotalLength <= USB_DT_CONFIG_SIZE) { + break; + } + + auto config = usb_configuration{}; + if (!usb_configuration::parse(config, data.first, configDescriptor.wTotalLength)) { + this->m_anomalies.push_back((boost::format("Malformed configuration descriptor %1%") % + (uint32_t) configDescriptor.bConfigurationValue).str()); + } + + auto value = config.descriptor.bConfigurationValue; + if (this->m_configurations.contains(value)) { + // Re-reading a configuration keeps the experts (and what they have learned) already attached + break; + } + + this->attachExperts(config); + this->m_configurations[value] = std::move(config); + } + break; + + case USB_DT_STRING: { + // String zero is the supported LANGID list, not a string + if (descriptorIndex == 0) { break; } + if (header.bLength < 2 || data.second < header.bLength) { break; } + + this->m_strings[descriptorIndex] = utf16leToUtf8(data.first + 2, header.bLength - 2); + } + break; + + case USB_DT_BOS: { + usb_bos_descriptor bos_descriptor{}; + if (!readStruct(data.first, data.second, bos_descriptor)) { break; } + // As with configurations the host first reads the header alone + if (bos_descriptor.wTotalLength < USB_DT_BOS_SIZE || data.second < bos_descriptor.wTotalLength) { + break; + } + if (bos_descriptor.wTotalLength == USB_DT_BOS_SIZE && bos_descriptor.bNumDeviceCaps != 0) { break; } + + struct bos_descriptor bos{bos_descriptor, {}}; + bos.data.assign(data.first + USB_DT_BOS_SIZE, data.first + bos_descriptor.wTotalLength); + this->m_bosDescriptors.push_back(std::move(bos)); + break; + } + + default: + break; + } + } + + void Device::attachExperts(usb_configuration &config) { + auto value = config.descriptor.bConfigurationValue; + + // DFU and recovery devices speak a single protocol across all interfaces; share one expert + std::shared_ptr deviceExpert; + if (this->isAppleDFU()) { + auto dfu = std::make_shared(value, 0); + if (config.has_dfu_descriptor) { dfu->setTransferSize(config.dfu_descriptor.wTransferSize); } + deviceExpert = dfu; + } else if (this->isAppleRecovery()) { + deviceExpert = std::make_shared(value, 0); + } - if (USB_ENDPOINT_ID(endpoint) == 0) { - if (setup.bmRequestType == 0x00 && setup.bRequest == 0x09) { - this->m_currentConfiguration = setup.wValue; + std::map> byInterface; - } else if (setup.bmRequestType == 0x21) { - assert(setup.wLength == data.second); + for (auto &interface: config.interfaces) { + const auto &descriptor = interface.interface; + auto number = descriptor.bInterfaceNumber; - const auto &config = this->m_configurations[this->m_currentConfiguration - 1]; - auto expert = config.interfaces[0].expert; + if (deviceExpert != nullptr) { + interface.expert = deviceExpert; + continue; + } - if (expert != nullptr) { - expert->processControlOut(data); + if (byInterface.contains(number)) { + interface.expert = byInterface[number]; + continue; + } + + std::shared_ptr expert; + if (this->isApple() && + descriptor.bInterfaceClass == USB_CLASS_APPLICATION_SPECIFIC && + descriptor.bInterfaceSubClass == APPLE_SUBCLASS_USBMUX && + descriptor.bInterfaceProtocol == APPLE_PROTOCOL_USBMUX2) { + expert = std::make_shared(value, number); + } else if (descriptor.bInterfaceClass == USB_CLASS_DFU && descriptor.bInterfaceSubClass == USB_SUBCLASS_DFU) { + auto dfu = std::make_shared(value, number); + if (config.has_dfu_descriptor) { dfu->setTransferSize(config.dfu_descriptor.wTransferSize); } + expert = dfu; + } + + if (expert != nullptr) { + byInterface[number] = expert; + // Earlier alternate settings of this interface share the expert too + for (auto &other: config.interfaces) { + if (other.interface.bInterfaceNumber == number) { other.expert = expert; } } } } @@ -143,10 +216,12 @@ namespace demuxusb { std::vector> Device::getExperts() { auto result = std::vector>(); + std::set seen; - for (const auto &configuration: this->m_configurations) { + for (const auto &[value, configuration]: this->m_configurations) { for (const auto &interface: configuration.interfaces) { - if (interface.expert != nullptr) { + if (interface.expert != nullptr && !seen.contains(interface.expert.get())) { + seen.insert(interface.expert.get()); result.push_back(interface.expert); } } @@ -155,63 +230,127 @@ namespace demuxusb { return result; } - std::shared_ptr Device::getExpertForEndpoint(uint8_t endpoint) { - auto configuration_id = this->m_currentConfiguration - 1; - if (configuration_id >= this->m_configurations.size()) { return nullptr; } - const auto &config = this->m_configurations[configuration_id]; + usb_configuration *Device::activeConfiguration() { + if (this->m_configurations.empty()) { return nullptr; } + + auto found = this->m_configurations.find(this->m_currentConfiguration); + if (found != this->m_configurations.end()) { return &found->second; } + // SET_CONFIGURATION may predate the capture. With a single configuration there is no ambiguity. + if (this->m_currentConfiguration == 0 && this->m_configurations.size() == 1) { + return &this->m_configurations.begin()->second; + } + + return nullptr; + } + + std::shared_ptr Device::getExpertForEndpoint(uint8_t endpoint) { + auto *config = this->activeConfiguration(); + if (config == nullptr) { return nullptr; } - for (const auto &interface: config.interfaces) { + std::shared_ptr fallback; + for (const auto &interface: config->interfaces) { for (const auto &interface_endpoint: interface.endpoints) { - if (interface_endpoint.bEndpointAddress == endpoint) { + if (interface_endpoint.bEndpointAddress != endpoint) { continue; } + + if (interface.interface.bAlternateSetting == + this->getAlternateSetting(interface.interface.bInterfaceNumber)) { return interface.expert; } + // SET_INTERFACE may predate the capture; remember the first alternate declaring the endpoint + if (fallback == nullptr) { fallback = interface.expert; } } } + return fallback; + } + + std::shared_ptr Device::getExpertForControl(const usb_setup_t &setup) { + auto *config = this->activeConfiguration(); + if (config == nullptr) { + // Class and vendor requests can precede SET_CONFIGURATION (DFU / recovery devices in particular) + if (this->m_configurations.empty()) { return nullptr; } + config = &this->m_configurations.begin()->second; + } + + if ((setup.bmRequestType & USB_RECIP_MASK) == USB_RECIP_INTERFACE) { + auto number = setup.wIndex & 0xFFU; + for (const auto &interface: config->interfaces) { + if (interface.interface.bInterfaceNumber == number) { return interface.expert; } + } + return nullptr; + } + + // Device recipient vendor / class requests belong to the first interface with an expert + for (const auto &interface: config->interfaces) { + if (interface.expert != nullptr) { return interface.expert; } + } return nullptr; } - void usb_configuration::parse(usb_configuration &config, std::byte *data, size_t size) { - usb_interface interface{}; - auto *end = data + size; + usb_interface *usb_configuration::find(uint8_t interface, uint8_t alternate) { + for (auto &candidate: this->interfaces) { + if (candidate.interface.bInterfaceNumber == interface && candidate.interface.bAlternateSetting == alternate) { + return &candidate; + } + } + return nullptr; + } + + bool usb_configuration::parse(usb_configuration &config, const std::byte *data, size_t size) { + const auto *end = data + size; + usb_interface *current = nullptr; while (data < end) { - auto *header = (usb_descriptor_header *) data; + usb_descriptor_header header{}; + auto remaining = static_cast(end - data); + if (!readStruct(data, remaining, header) || header.bLength < sizeof(usb_descriptor_header) || + header.bLength > remaining) { + return false; + } - switch (header->bDescriptorType) { + switch (header.bDescriptorType) { case USB_DT_CONFIG: - assert(header->bLength == sizeof(usb_config_descriptor)); - memcpy(&config.descriptor, data, sizeof(usb_config_descriptor)); - config.interfaces.resize(config.descriptor.bNumInterfaces); + if (header.bLength < USB_DT_CONFIG_SIZE) { return false; } + readStruct(data, remaining, config.descriptor); break; - case USB_DT_DFU_FUNCTIONAL: - assert(header->bLength == sizeof(usb_dfu_functional_descriptor)); - memcpy(&config.dfu_descriptor, data, sizeof(usb_dfu_functional_descriptor)); - break; - // TODO: Alternates - case USB_DT_INTERFACE: - assert(header->bLength == sizeof(usb_interface_descriptor)); - interface = usb_interface{}; - memcpy(&interface.interface, data, sizeof(usb_interface_descriptor)); - config.interfaces[interface.interface.bInterfaceNumber] = interface; + case USB_DT_INTERFACE: { + if (header.bLength < USB_DT_INTERFACE_SIZE) { return false; } + usb_interface interface{}; + readStruct(data, remaining, interface.interface); + config.interfaces.push_back(std::move(interface)); + current = &config.interfaces.back(); break; + } + case USB_DT_ENDPOINT: { - assert((header->bLength == USB_DT_ENDPOINT_SIZE) || - (header->bLength == sizeof(usb_endpoint_descriptor))); + if (header.bLength < USB_DT_ENDPOINT_SIZE) { return false; } + if (current == nullptr) { break; } usb_endpoint_descriptor endpoint{}; - memcpy(&endpoint, header, header->bLength); - interface.endpoints.push_back(endpoint); - config.interfaces[interface.interface.bInterfaceNumber] = interface; + std::memcpy(&endpoint, data, std::min(header.bLength, sizeof(endpoint))); + current->endpoints.push_back(endpoint); break; } + + case USB_DT_DFU_FUNCTIONAL: + // 0x21 is also the HID descriptor type; only treat it as DFU inside a DFU interface + if (current != nullptr && current->interface.bInterfaceClass == USB_CLASS_DFU && + current->interface.bInterfaceSubClass == USB_SUBCLASS_DFU) { + std::memcpy(&config.dfu_descriptor, data, + std::min(header.bLength, sizeof(config.dfu_descriptor))); + config.has_dfu_descriptor = true; + } + break; + default: - std::cerr << "Unknown configuration descriptor type " << std::hex << (int) header->bDescriptorType - << std::endl; + // Interface association, class specific and vendor descriptors are not needed for routing + break; } - data += header->bLength; + data += header.bLength; } + + return true; } -} \ No newline at end of file +} diff --git a/src/usb/Device.h b/src/usb/Device.h index 309b969..39f04cb 100644 --- a/src/usb/Device.h +++ b/src/usb/Device.h @@ -18,6 +18,8 @@ namespace demuxusb { std::vector data; }; + // One alternate setting of an interface, with the endpoints it declares. All alternate settings of an interface + // share a single expert. struct usb_interface { usb_interface_descriptor interface; std::vector endpoints; @@ -25,11 +27,17 @@ namespace demuxusb { }; struct usb_configuration { - usb_config_descriptor descriptor; - usb_dfu_functional_descriptor dfu_descriptor; + usb_config_descriptor descriptor{}; + usb_dfu_functional_descriptor dfu_descriptor{}; + bool has_dfu_descriptor{false}; + // Every interface alternate setting, in descriptor order std::vector interfaces{}; - static void parse(usb_configuration &config, std::byte *data, size_t size); + // Parses a complete configuration descriptor set. Returns false (leaving what could be parsed) when the + // descriptors are malformed. + static bool parse(usb_configuration &config, const std::byte *data, size_t size); + + usb_interface *find(uint8_t interface, uint8_t alternate); }; // The device class is a state machine that reconstructs the devices properties by composing the control endpoint @@ -37,15 +45,24 @@ namespace demuxusb { // metadata from the control endpoint allow for endpoint experts to raise the traffic from there. class Device { public: - explicit Device(uint64_t id) : m_device{id}, m_vendorId{}, m_productId{} {} + explicit Device(uint64_t id) : m_device{id} {} [[nodiscard]] uint64_t getIdentifier() const { return this->m_device; } + [[nodiscard]] bool hasDeviceDescriptor() const { return this->m_hasDeviceDescriptor; } + + [[nodiscard]] const usb_device_descriptor &getDeviceDescriptor() const { return this->m_deviceDescriptor; } + + [[nodiscard]] uint16_t getVendorId() const { return this->m_deviceDescriptor.idVendor; } + + [[nodiscard]] uint16_t getProductId() const { return this->m_deviceDescriptor.idProduct; } + [[nodiscard]] bool isApple() const { return this->m_deviceDescriptor.idVendor == APPLE_VID; } [[nodiscard]] bool isAppleDFU() const { return this->m_deviceDescriptor.idVendor == APPLE_VID && - this->m_deviceDescriptor.idProduct == IRECV_K_DFU_MODE; + (this->m_deviceDescriptor.idProduct == IRECV_K_DFU_MODE || + this->m_deviceDescriptor.idProduct == IRECV_K_WTF_MODE); } [[nodiscard]] bool isAppleRecovery() const { @@ -58,19 +75,23 @@ namespace demuxusb { return false; } - std::wstring getString(uint8_t index) { + // "DFU", "WTF", "Recovery", "Normal" (any other Apple device) or "Unknown" + [[nodiscard]] std::string getMode() const; + + // UTF-8 decoded string descriptor, or "" + std::string getString(uint8_t index) { if (this->m_strings.contains(index)) { return this->m_strings[index]; } - return L""; + return ""; } - std::wstring getProduct() { return this->getString(this->m_deviceDescriptor.iProduct); } + std::string getProduct() { return this->getString(this->m_deviceDescriptor.iProduct); } - std::wstring getManufacturer() { return this->getString(this->m_deviceDescriptor.iManufacturer); } + std::string getManufacturer() { return this->getString(this->m_deviceDescriptor.iManufacturer); } - std::wstring getSerial() { return this->getString(this->m_deviceDescriptor.iSerialNumber); } + std::string getSerial() { return this->getString(this->m_deviceDescriptor.iSerialNumber); } void processControlPacketIn(uint8_t endpoint, usb_setup_t setup, byte_array data); @@ -88,29 +109,51 @@ namespace demuxusb { [[nodiscard]] uint64_t getControlByteCount() const { return this->m_controlByteCount; } + [[nodiscard]] uint8_t getCurrentConfiguration() const { return this->m_currentConfiguration; } + + [[nodiscard]] uint8_t getAlternateSetting(uint8_t interface) const { + auto found = this->m_alternates.find(interface); + return found == this->m_alternates.end() ? 0 : found->second; + } + + [[nodiscard]] const std::map &getConfigurations() const { + return this->m_configurations; + } + + [[nodiscard]] const std::vector &getBOSDescriptors() const { return this->m_bosDescriptors; } + + // Every distinct expert attached to any interface of any configuration std::vector> getExperts(); + // Descriptor or protocol irregularities observed on the default control pipe + [[nodiscard]] const std::vector &getAnomalies() const { return this->m_anomalies; } + protected: + void processDescriptor(usb_setup_t setup, byte_array data); + + void attachExperts(usb_configuration &config); + + usb_configuration *activeConfiguration(); + std::shared_ptr getExpertForEndpoint(uint8_t endpoint); + std::shared_ptr getExpertForControl(const usb_setup_t &setup); + uint64_t m_device; - uint16_t m_vendorId; - uint16_t m_productId; uint64_t m_packetCount{}; uint64_t m_byteCount{}; uint64_t m_controlPacketCount{}; uint64_t m_controlByteCount{}; + bool m_hasDeviceDescriptor{false}; uint8_t m_currentConfiguration{}; usb_device_descriptor m_deviceDescriptor{}; - std::vector m_configurations{}; - std::map m_strings; + std::map m_configurations{}; + std::map m_alternates{}; + std::map m_strings; std::vector m_bosDescriptors{}; - - usb_setup_t m_controlSetup{}; - - std::map> m_interfaces{}; + std::vector m_anomalies{}; }; } diff --git a/src/usb/USBDescriptor.cpp b/src/usb/USBDescriptor.cpp deleted file mode 100644 index 744b953..0000000 --- a/src/usb/USBDescriptor.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// -// Created by Rick Mark on 11/16/20. -// - -#include "USBDescriptor.h" diff --git a/src/usb/USBDescriptor.h b/src/usb/USBDescriptor.h deleted file mode 100644 index 7f20254..0000000 --- a/src/usb/USBDescriptor.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Created by Rick Mark on 11/16/20. -// - -#ifndef demuxusb_USBDESCRIPTOR_H -#define demuxusb_USBDESCRIPTOR_H - -#include "../demuxusb.hpp" - -class USBDescriptor { - -}; - - -#endif //demuxusb_USBDESCRIPTOR_H diff --git a/test/CaptureBuilder.h b/test/CaptureBuilder.h new file mode 100644 index 0000000..63b9459 --- /dev/null +++ b/test/CaptureBuilder.h @@ -0,0 +1,521 @@ +// Builders for synthetic USB captures used by the test suite. A Transcript records USB transfers independently of +// the capture format; encoders turn it into the per-packet layout of each supported link type and the container +// helpers wrap those packets in pcapng or pcap files. + +#ifndef demuxusb_TEST_CAPTUREBUILDER_H +#define demuxusb_TEST_CAPTUREBUILDER_H + +#include "demuxusb.hpp" +#include "pcapng.h" +#include "usb.h" +#include "usbmux.h" +#include "file/InputFile.h" + +#include +#include + +namespace fixture { + typedef std::vector Bytes; + + inline Bytes bytes(std::initializer_list values) { + Bytes result; + for (auto value: values) { result.push_back(static_cast(value)); } + return result; + } + + inline Bytes bytes(const std::string &value) { + auto *begin = reinterpret_cast(value.data()); + return {begin, begin + value.size()}; + } + + inline void append(Bytes &target, const Bytes &source) { + target.insert(target.end(), source.begin(), source.end()); + } + + template + void appendStruct(Bytes &target, const T &value) { + auto *begin = reinterpret_cast(&value); + target.insert(target.end(), begin, begin + sizeof(T)); + } + + inline void appendBE32(Bytes &target, uint32_t value) { + appendStruct(target, boost::endian::native_to_big(value)); + } + + inline void appendBE16(Bytes &target, uint16_t value) { + appendStruct(target, boost::endian::native_to_big(value)); + } + + inline void appendLE16(Bytes &target, uint16_t value) { + appendStruct(target, boost::endian::native_to_little(value)); + } + + inline usb_setup_t setup(uint8_t requestType, uint8_t request, uint16_t value, uint16_t index, uint16_t length) { + return usb_setup_t{requestType, request, value, index, length}; + } + + struct Transfer { + bool control; + uint8_t endpoint; + usb_setup_t setup; + Bytes data; // OUT payload or IN response + }; + + class Transcript { + public: + // A control transfer on endpoint 0. data is the OUT payload, or the IN response. + Transcript &control(const usb_setup_t &setup, const Bytes &data = {}) { + this->transfers.push_back({true, static_cast(setup.bmRequestType & USB_DIR_IN), setup, data}); + return *this; + } + + Transcript &bulkOut(uint8_t endpoint, const Bytes &data) { + this->transfers.push_back({false, static_cast(endpoint & 0x7FU), {}, data}); + return *this; + } + + Transcript &bulkIn(uint8_t endpoint, const Bytes &data) { + this->transfers.push_back({false, static_cast(endpoint | 0x80U), {}, data}); + return *this; + } + + Transcript &getDescriptor(uint8_t type, uint8_t index, const Bytes &descriptor, uint16_t length = 0xFFFF) { + auto response = descriptor; + if (response.size() > length) { response.resize(length); } + return this->control(setup(0x80, USB_REQ_GET_DESCRIPTOR, (type << 8U) | index, type == USB_DT_STRING ? 0x0409 : 0, + length), response); + } + + Transcript &setConfiguration(uint8_t value) { + return this->control(setup(0x00, USB_REQ_SET_CONFIGURATION, value, 0, 0)); + } + + Transcript &setInterface(uint8_t interface, uint8_t alternate) { + return this->control(setup(0x01, USB_REQ_SET_INTERFACE, alternate, interface, 0)); + } + + std::vector transfers; + }; + + // ---- Descriptors ---- + + inline Bytes deviceDescriptor(uint16_t vendor, uint16_t product, uint16_t bcdDevice = 0x0100, + uint8_t configurations = 1) { + usb_device_descriptor descriptor{USB_DT_DEVICE_SIZE, USB_DT_DEVICE, 0x0200, 0, 0, 0, 64, vendor, product, + bcdDevice, 1, 2, 3, configurations}; + Bytes result; + appendStruct(result, descriptor); + return result; + } + + inline Bytes stringDescriptor(const std::string &value) { + Bytes result{static_cast(2 + value.size() * 2), static_cast(USB_DT_STRING)}; + for (auto c: value) { appendLE16(result, static_cast(c)); } + return result; + } + + struct Endpoint { + uint8_t address; + uint8_t attributes; // 2 = bulk, 3 = interrupt + }; + + struct Interface { + uint8_t number; + uint8_t alternate; + uint8_t cls; + uint8_t subclass; + uint8_t protocol; + std::vector endpoints; + Bytes extra{}; // class specific descriptors following the interface descriptor + }; + + inline Bytes configDescriptor(uint8_t value, const std::vector &interfaces) { + Bytes body; + std::set numbers; + for (const auto &interface: interfaces) { + numbers.insert(interface.number); + usb_interface_descriptor descriptor{USB_DT_INTERFACE_SIZE, USB_DT_INTERFACE, interface.number, + interface.alternate, + static_cast(interface.endpoints.size()), interface.cls, + interface.subclass, interface.protocol, 0}; + appendStruct(body, descriptor); + append(body, interface.extra); + for (const auto &endpoint: interface.endpoints) { + body.push_back(std::byte{USB_DT_ENDPOINT_SIZE}); + body.push_back(std::byte{USB_DT_ENDPOINT}); + body.push_back(static_cast(endpoint.address)); + body.push_back(static_cast(endpoint.attributes)); + appendLE16(body, 512); + body.push_back(std::byte{0}); + } + } + + usb_config_descriptor config{USB_DT_CONFIG_SIZE, USB_DT_CONFIG, + static_cast(USB_DT_CONFIG_SIZE + body.size()), + static_cast(numbers.size()), value, 0, 0x80, 250}; + Bytes result; + appendStruct(result, config); + append(result, body); + return result; + } + + // Enumerates a device the way a host does: partial device descriptor, address, full descriptors, strings. + inline Transcript &enumerate(Transcript &transcript, const Bytes &device, const std::vector &configurations, + const std::vector &strings, uint8_t configuration = 1) { + transcript.getDescriptor(USB_DT_DEVICE, 0, device, 8); + transcript.getDescriptor(USB_DT_DEVICE, 0, device, USB_DT_DEVICE_SIZE); + for (size_t i = 0; i < configurations.size(); i++) { + transcript.getDescriptor(USB_DT_CONFIG, i, configurations[i], USB_DT_CONFIG_SIZE); + transcript.getDescriptor(USB_DT_CONFIG, i, configurations[i], configurations[i].size()); + } + transcript.getDescriptor(USB_DT_STRING, 0, bytes({4, USB_DT_STRING, 0x09, 0x04}), 255); + for (size_t i = 0; i < strings.size(); i++) { + transcript.getDescriptor(USB_DT_STRING, i + 1, stringDescriptor(strings[i]), 255); + } + if (configuration != 0) { transcript.setConfiguration(configuration); } + return transcript; + } + + // An iPhone in normal mode: configuration 1 with PTP, configuration 4 with USBMUX on interface 2 whose bulk + // endpoints are only present in alternate setting 1 when withAlternate is set. + inline Bytes iphoneUsbmuxConfiguration(uint8_t value, bool withAlternate = false) { + std::vector interfaces{ + {0, 0, 0x06, 0x01, 0x01, {{0x81, 2}, {0x02, 2}, {0x83, 3}}}, + }; + if (withAlternate) { + interfaces.push_back({1, 0, USB_CLASS_APPLICATION_SPECIFIC, APPLE_SUBCLASS_USBMUX, APPLE_PROTOCOL_USBMUX2, {}}); + interfaces.push_back({1, 1, USB_CLASS_APPLICATION_SPECIFIC, APPLE_SUBCLASS_USBMUX, APPLE_PROTOCOL_USBMUX2, + {{0x85, 2}, {0x04, 2}}}); + } else { + interfaces.push_back({1, 0, USB_CLASS_APPLICATION_SPECIFIC, APPLE_SUBCLASS_USBMUX, APPLE_PROTOCOL_USBMUX2, + {{0x85, 2}, {0x04, 2}}}); + } + return configDescriptor(value, interfaces); + } + + inline Bytes dfuFunctionalDescriptor(uint16_t transferSize) { + Bytes result{std::byte{9}, std::byte{USB_DT_DFU_FUNCTIONAL}, std::byte{0x0B}}; + appendLE16(result, 0x0A); + appendLE16(result, transferSize); + appendLE16(result, 0x0110); + return result; + } + + // ---- usbmux ---- + + inline Bytes muxPacket(uint32_t protocol, const Bytes &payload, bool outbound, bool v2 = true, + uint16_t txSeq = 0, uint16_t rxSeq = 0) { + Bytes result; + auto headerLength = v2 ? sizeof(mux_header) : 8; + appendBE32(result, protocol); + appendBE32(result, static_cast(headerLength + payload.size())); + if (v2) { + appendBE32(result, outbound ? USBMUX_MAGIC_OUT : USBMUX_MAGIC_IN); + appendBE16(result, txSeq); + appendBE16(result, rxSeq); + } + append(result, payload); + return result; + } + + inline Bytes muxVersion(uint32_t major, bool outbound) { + Bytes payload; + appendBE32(payload, major); + appendBE32(payload, 0); + appendBE32(payload, 0); + return muxPacket(MUX_PROTO_VERSION, payload, outbound, false); + } + + inline Bytes tcpSegment(uint16_t source, uint16_t destination, uint16_t flags, const Bytes &data = {}) { + Bytes result; + appendBE16(result, source); + appendBE16(result, destination); + appendBE32(result, 0); + appendBE32(result, 0); + appendBE16(result, static_cast((5U << 12U) | flags)); + appendBE16(result, 0x0200); + appendBE16(result, 0); + appendBE16(result, 0); + append(result, data); + return result; + } + + inline Bytes xmlPlist(const std::string &dictionaryBody) { + return bytes("\n" + "\n" + "" + dictionaryBody + "\n"); + } + + inline Bytes lengthPrefixed(const Bytes &message) { + Bytes result; + appendBE32(result, static_cast(message.size())); + append(result, message); + return result; + } + + // ---- Packet encoders ---- + + enum class Format { + Darwin, + Darwin_1_1, + USBPcap, + Usbmon, + UsbmonMmapped + }; + + inline uint32_t linktype(Format format) { + switch (format) { + case Format::Darwin: + case Format::Darwin_1_1: return LINKTYPE_USB_DARWIN; + case Format::USBPcap: return LINKTYPE_USBPCAP; + case Format::Usbmon: return LINKTYPE_USB_LINUX; + case Format::UsbmonMmapped: return LINKTYPE_USB_LINUX_MMAPPED; + } + return 0; + } + + inline Bytes darwinPacket(bool v11, uint8_t type, uint64_t id, uint32_t location, uint8_t address, + uint8_t endpoint, uint8_t endpointType, const Bytes &payload) { + Bytes result; + darwin_usb_header_1_1_t header{}; + header.version = v11 ? DARWIN_USB_HEADER_VERSION_1_1 : DARWIN_USB_HEADER_VERSION_1_0; + header.headerLength = v11 ? sizeof(darwin_usb_header_1_1_t) : sizeof(darwin_usb_header_1_0_t); + header.type = type; + header.length = payload.size(); + header.uniqueId = id; + header.locationId = location; + header.speed = DARWIN_SPEED_HIGH; + header.deviceId = address; + header.endpointId = endpoint; + header.endpointType = endpointType; + auto *begin = reinterpret_cast(&header); + result.insert(result.end(), begin, begin + header.headerLength); + append(result, payload); + return result; + } + + inline Bytes usbpcapPacket(uint64_t irp, uint8_t info, uint16_t device, uint8_t endpoint, uint8_t transfer, + int stage, const Bytes &payload) { + usbpcap_buffer_control_header_t header{}; + header.header.headerLen = stage >= 0 ? sizeof(usbpcap_buffer_control_header_t) : sizeof(usbpcap_packet_header_t); + header.header.irpId = irp; + header.header.info = info; + header.header.bus = 1; + header.header.device = device; + header.header.endpoint = endpoint; + header.header.transfer = transfer; + header.header.dataLength = payload.size(); + header.stage = stage >= 0 ? stage : 0; + Bytes result; + auto *begin = reinterpret_cast(&header); + result.insert(result.end(), begin, begin + header.header.headerLen); + append(result, payload); + return result; + } + + inline Bytes usbmonPacket(bool mmapped, uint8_t event, uint64_t id, uint8_t transfer, uint8_t endpoint, + uint8_t address, const usb_setup_t *setup, const Bytes &payload) { + pcap_usb_header_mmapped_t header{}; + header.header.id = id; + header.header.event_type = event; + header.header.transfer_type = transfer; + header.header.endpoint_number = endpoint; + header.header.device_address = address; + header.header.bus_id = 1; + header.header.setup_flag = setup != nullptr ? 0 : '-'; + header.header.data_flag = payload.empty() ? '<' : 0; + header.header.urb_len = payload.size(); + header.header.data_len = payload.size(); + if (setup != nullptr) { header.header.setup = *setup; } + Bytes result; + auto *begin = reinterpret_cast(&header); + result.insert(result.end(), begin, begin + (mmapped ? sizeof(header) : sizeof(header.header))); + append(result, payload); + return result; + } + + // Encodes a transcript for one device. address is the device address (Darwin also uses location). + inline std::vector encode(Format format, const Transcript &transcript, uint8_t address = 5, + uint32_t location = 0x14100000, uint64_t firstId = 0x1000) { + std::vector packets; + auto id = firstId; + for (const auto &transfer: transcript.transfers) { + id++; + bool in = transfer.endpoint & USB_DIR_IN; + Bytes outData = in ? Bytes{} : transfer.data; + Bytes inData = in ? transfer.data : Bytes{}; + + switch (format) { + case Format::Darwin: + case Format::Darwin_1_1: { + bool v11 = format == Format::Darwin_1_1; + if (transfer.control) { + Bytes submit; + appendStruct(submit, transfer.setup); + append(submit, outData); + packets.push_back(darwinPacket(v11, DARWIN_IO_SUBMIT, id, location, address, transfer.endpoint, + USB_EP_CONTROL, submit)); + packets.push_back(darwinPacket(v11, DARWIN_IO_COMPLETE, id, location, address, + transfer.endpoint, USB_EP_CONTROL, inData)); + } else { + packets.push_back(darwinPacket(v11, DARWIN_IO_SUBMIT, id, location, address, transfer.endpoint, + USB_EP_BULK, outData)); + packets.push_back(darwinPacket(v11, DARWIN_IO_COMPLETE, id, location, address, + transfer.endpoint, USB_EP_BULK, inData)); + } + break; + } + case Format::USBPcap: + if (transfer.control) { + Bytes submit; + appendStruct(submit, transfer.setup); + append(submit, outData); + packets.push_back(usbpcapPacket(id, 0, address, transfer.endpoint, USBPCAP_TRANSFER_CONTROL, + USBPCAP_CONTROL_STAGE_SETUP, submit)); + packets.push_back(usbpcapPacket(id, USBPCAP_INFO_PDO_TO_FDO, address, transfer.endpoint, + USBPCAP_TRANSFER_CONTROL, USBPCAP_CONTROL_STAGE_COMPLETE, + inData)); + } else { + packets.push_back(usbpcapPacket(id, 0, address, transfer.endpoint, USBPCAP_TRANSFER_BULK, -1, + outData)); + packets.push_back(usbpcapPacket(id, USBPCAP_INFO_PDO_TO_FDO, address, transfer.endpoint, + USBPCAP_TRANSFER_BULK, -1, inData)); + } + break; + case Format::Usbmon: + case Format::UsbmonMmapped: { + bool mmapped = format == Format::UsbmonMmapped; + auto type = transfer.control ? USBMON_TRANSFER_CONTROL : USBMON_TRANSFER_BULK; + packets.push_back(usbmonPacket(mmapped, USBMON_EVENT_SUBMIT, id, type, transfer.endpoint, address, + transfer.control ? &transfer.setup : nullptr, outData)); + packets.push_back(usbmonPacket(mmapped, USBMON_EVENT_COMPLETE, id, type, transfer.endpoint, + address, nullptr, inData)); + break; + } + } + } + return packets; + } + + // ---- Containers ---- + + inline void appendBlock(Bytes &file, uint32_t type, const Bytes &body) { + auto padded = body; + while (padded.size() % 4 != 0) { padded.push_back(std::byte{0}); } + uint32_t length = static_cast(padded.size() + 12); + appendStruct(file, type); + appendStruct(file, length); + append(file, padded); + appendStruct(file, length); + } + + inline Bytes sectionHeaderBody() { + Bytes body; + appendStruct(body, static_cast(PCAPNG_BYTE_ORDER_MAGIC)); + appendStruct(body, static_cast(1)); + appendStruct(body, static_cast(0)); + appendStruct(body, static_cast(-1)); + return body; + } + + inline Bytes interfaceBody(uint32_t linktype) { + Bytes body; + appendStruct(body, static_cast(linktype)); + appendStruct(body, static_cast(0)); + appendStruct(body, static_cast(0x40000)); + return body; + } + + inline Bytes enhancedPacketBody(uint32_t interface, const Bytes &packet) { + Bytes body; + pcapng_epb_t epb{interface, 0, 0, static_cast(packet.size()), static_cast(packet.size())}; + appendStruct(body, epb); + append(body, packet); + return body; + } + + inline Bytes pcapng(uint32_t linktype, const std::vector &packets) { + Bytes file; + appendBlock(file, TYPE_SECTION_HEADER, sectionHeaderBody()); + appendBlock(file, TIME_INTERFACE_DESCRIPTOR_BLOCK, interfaceBody(linktype)); + for (const auto &packet: packets) { + appendBlock(file, TYPE_ENHANCED_PACKET_BLOCK, enhancedPacketBody(0, packet)); + } + return file; + } + + inline Bytes pcap(uint32_t linktype, const std::vector &packets, bool swapped = false, + bool nanoseconds = false) { + auto value = [swapped](uint32_t field) { return swapped ? boost::endian::endian_reverse(field) : field; }; + auto value16 = [swapped](uint16_t field) { return swapped ? boost::endian::endian_reverse(field) : field; }; + + Bytes file; + pcap_file_header_t header{value(nanoseconds ? PCAP_MAGIC_NANOSECONDS : PCAP_MAGIC_MICROSECONDS), value16(2), + value16(4), 0, 0, value(0x40000), value(linktype)}; + appendStruct(file, header); + for (const auto &packet: packets) { + pcap_record_header_t record{0, 0, value(packet.size()), value(packet.size())}; + appendStruct(file, record); + append(file, packet); + } + return file; + } + + inline Bytes capture(Format format, const Transcript &transcript) { + return pcapng(linktype(format), encode(format, transcript)); + } + + // A file in the system temp directory, removed when the object is destroyed. + class TempCapture { + public: + explicit TempCapture(const Bytes &contents, const std::string &extension = ".pcapng") { + this->path = fs::temp_directory_path() / fs::unique_path("demuxusb-test-%%%%-%%%%-%%%%" + extension); + std::ofstream output(this->path.string(), std::ios::binary); + output.write(reinterpret_cast(contents.data()), static_cast(contents.size())); + } + + ~TempCapture() { + boost::system::error_code error; + fs::remove(this->path, error); + } + + fs::path path; + }; + + // ---- Parsing helpers ---- + + struct Parsed { + std::unique_ptr file; + std::unique_ptr input; + + std::vector devices() { return this->input->getDevices(); } + }; + + inline Parsed parse(const Bytes &contents, const std::string &extension = ".pcapng") { + Parsed result; + result.file = std::make_unique(contents, extension); + result.input = demuxusb::InputFile::open(result.file->path); + result.input->parse(); + return result; + } + + template + std::shared_ptr expertOf(demuxusb::Device *device) { + for (const auto &expert: device->getExperts()) { + if (auto typed = std::dynamic_pointer_cast(expert)) { return typed; } + } + return nullptr; + } + + // Enumerates an iPhone exposing USBMUX on configuration 1, interface 1 (endpoints 0x85 IN / 0x04 OUT). + inline Transcript iphone(bool withAlternate = false) { + Transcript transcript; + enumerate(transcript, deviceDescriptor(APPLE_VID, 0x12A8), {iphoneUsbmuxConfiguration(1, withAlternate)}, + {"Apple Inc.", "iPhone", "00008101-001234567890001E"}); + if (withAlternate) { transcript.setInterface(1, 1); } + return transcript; + } +} + +#endif //demuxusb_TEST_CAPTUREBUILDER_H diff --git a/test/test_DFU.cpp b/test/test_DFU.cpp new file mode 100644 index 0000000..bee4f6e --- /dev/null +++ b/test/test_DFU.cpp @@ -0,0 +1,100 @@ +#include +#include "CaptureBuilder.h" +#include "protocol/DFUInterfaceExpert.h" + +using namespace fixture; + +static const uint16_t TRANSFER_SIZE = 0x800; + +static Transcript dfuDevice() { + Transcript transcript; + enumerate(transcript, deviceDescriptor(APPLE_VID, IRECV_K_DFU_MODE), + {configDescriptor(1, {{0, 0, USB_CLASS_APP_SPEC, USB_SUBCLASS_DFU, 0, {}, + dfuFunctionalDescriptor(TRANSFER_SIZE)}})}, + {"Apple Inc.", "Apple Mobile Device (DFU Mode)", + "CPID:8101 CPRV:10 CPFM:03 SCEP:01 BDID:0C ECID:001234567890001E IBFL:3C SRTG:[iBoot-4479.0.0.100.4]"}); + return transcript; +} + +static Bytes getStatusResponse(uint8_t status, uint8_t state) { + return bytes({status, 0, 0, 0, state, 0}); +} + +static Bytes pattern(size_t size, uint8_t seed) { + Bytes result(size); + for (size_t i = 0; i < size; i++) { result[i] = static_cast((i * 7 + seed) & 0xFF); } + return result; +} + +static void download(Transcript &transcript, const Bytes &image) { + for (size_t offset = 0, block = 0; offset < image.size(); offset += TRANSFER_SIZE, block++) { + auto end = std::min(image.size(), offset + TRANSFER_SIZE); + Bytes chunk(image.begin() + offset, image.begin() + end); + transcript.control(setup(0x21, DFU_DNLOAD, block, 0, chunk.size()), chunk); + transcript.control(setup(0xA1, DFU_GETSTATUS, 0, 0, 6), getStatusResponse(0, 5)); + } + transcript.control(setup(0x21, DFU_DNLOAD, 0, 0, 0)); + transcript.control(setup(0xA1, DFU_GETSTATUS, 0, 0, 6), getStatusResponse(0, 6)); + transcript.control(setup(0xA1, DFU_GETSTATUS, 0, 0, 6), getStatusResponse(0, 7)); + transcript.control(setup(0xA1, DFU_GETSTATUS, 0, 0, 6), getStatusResponse(0, 8)); +} + +BOOST_AUTO_TEST_SUITE(dfu) + + BOOST_AUTO_TEST_CASE(test_images_are_reassembled) { + auto image = pattern(TRANSFER_SIZE * 3 + 123, 1); + auto transcript = dfuDevice(); + download(transcript, image); + + auto parsed = parse(capture(Format::Usbmon, transcript)); + auto *device = parsed.devices()[0]; + BOOST_TEST(device->getMode() == "DFU"); + BOOST_TEST(device->getSerial().find("ECID:001234567890001E") != std::string::npos); + + auto dfu = expertOf(device); + BOOST_REQUIRE(dfu != nullptr); + auto images = dfu->getImages(); + BOOST_REQUIRE_EQUAL(images.size(), 1u); + BOOST_TEST((images[0] == image)); + BOOST_TEST(dfu->getLastState() == 8); + BOOST_TEST(DFUInterfaceExpert::stateName(dfu->getLastState()) == "dfuMANIFEST-WAIT-RESET"); + BOOST_TEST(dfu->getAnomalies().empty()); + + auto artifacts = dfu->getArtifacts(); + BOOST_REQUIRE_EQUAL(artifacts.size(), 1u); + BOOST_TEST(artifacts[0].name == "dfu-image-0.bin"); + } + + BOOST_AUTO_TEST_CASE(test_partial_download_is_kept) { + auto transcript = dfuDevice(); + auto chunk = pattern(TRANSFER_SIZE, 2); + transcript.control(setup(0x21, DFU_DNLOAD, 0, 0, chunk.size()), chunk); + + auto parsed = parse(capture(Format::Darwin, transcript)); + auto dfu = expertOf(parsed.devices()[0]); + BOOST_REQUIRE(dfu != nullptr); + BOOST_TEST(dfu->getImages().size() == 1u); + BOOST_TEST(dfu->getArtifacts()[0].name == "dfu-image-0-partial.bin"); + } + + BOOST_AUTO_TEST_CASE(test_protocol_anomalies_are_recorded) { + auto transcript = dfuDevice(); + auto oversized = pattern(TRANSFER_SIZE * 2, 3); + transcript.control(setup(0x21, DFU_DNLOAD, 0, 0, oversized.size()), oversized); + transcript.control(setup(0x21, DFU_ABORT, 0, 0, 0)); + transcript.control(setup(0x21, 0x42, 0, 0, 0)); + transcript.control(setup(0xA1, DFU_GETSTATUS, 0, 0, 6), getStatusResponse(0x0E, 10)); + + auto parsed = parse(capture(Format::USBPcap, transcript)); + auto dfu = expertOf(parsed.devices()[0]); + BOOST_REQUIRE(dfu != nullptr); + const auto &anomalies = dfu->getAnomalies(); + BOOST_REQUIRE_EQUAL(anomalies.size(), 3u); + BOOST_TEST(anomalies[0] == "DNLOAD of 4096 bytes exceeds wTransferSize 2048"); + BOOST_TEST(anomalies[1] == "ABORT after a partial download of 4096 bytes"); + BOOST_TEST(anomalies[2] == "Unexpected class OUT request 0x42"); + BOOST_TEST(dfu->getLastStatus() == 0x0E); + BOOST_TEST(DFUInterfaceExpert::stateName(dfu->getLastState()) == "dfuERROR"); + } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/test_Device.cpp b/test/test_Device.cpp new file mode 100644 index 0000000..2e85598 --- /dev/null +++ b/test/test_Device.cpp @@ -0,0 +1,119 @@ +#include +#include "CaptureBuilder.h" +#include "protocol/USBMUXInterfaceExpert.h" +#include "protocol/DFUInterfaceExpert.h" +#include "protocol/RecoveryInterfaceExpert.h" + +using namespace fixture; + +static Bytes lockdownSyn(uint16_t host) { + return muxPacket(MUX_PROTO_TCP, tcpSegment(host, 62078, USB_TCP_FLAG_SYN), true); +} + +BOOST_AUTO_TEST_SUITE(device) + + BOOST_AUTO_TEST_CASE(test_alternate_settings_route_endpoints) { + auto transcript = iphone(true); + transcript.bulkOut(0x04, lockdownSyn(49152)); + + auto parsed = parse(capture(Format::Darwin, transcript)); + auto *device = parsed.devices()[0]; + BOOST_TEST(device->getAlternateSetting(1) == 1); + + const auto &config = device->getConfigurations().at(1); + BOOST_TEST(config.interfaces.size() == 3u); + + // Both alternate settings of interface 1 share one expert + BOOST_TEST(device->getExperts().size() == 1u); + auto usbmux = expertOf(device); + BOOST_REQUIRE(usbmux != nullptr); + BOOST_TEST(usbmux->getInterface() == 1); + BOOST_TEST(usbmux->getStreams().size() == 1u); + } + + BOOST_AUTO_TEST_CASE(test_traffic_before_configuration_is_known) { + // Capture started after enumeration of a single configuration device: no SET_CONFIGURATION seen + Transcript transcript; + enumerate(transcript, deviceDescriptor(APPLE_VID, 0x12A8), {iphoneUsbmuxConfiguration(1)}, {"Apple Inc."}, 0); + transcript.bulkOut(0x04, lockdownSyn(49152)); + + auto parsed = parse(capture(Format::USBPcap, transcript)); + auto usbmux = expertOf(parsed.devices()[0]); + BOOST_REQUIRE(usbmux != nullptr); + BOOST_TEST(usbmux->getStreams().size() == 1u); + } + + BOOST_AUTO_TEST_CASE(test_strings_decode_utf16) { + Transcript transcript; + auto product = bytes({12, USB_DT_STRING, 'i', 0, 0xe9, 0x00 /* é */, 0x3c, 0xd8, 0x4e, 0xdf /* 🍎 */, '!', 0}); + transcript.getDescriptor(USB_DT_DEVICE, 0, deviceDescriptor(APPLE_VID, 0x12A8), USB_DT_DEVICE_SIZE); + transcript.getDescriptor(USB_DT_STRING, 2, product, 255); + // A partial read of the string header alone must not clobber the full value + transcript.getDescriptor(USB_DT_STRING, 2, product, 2); + + auto parsed = parse(capture(Format::Usbmon, transcript)); + BOOST_TEST(parsed.devices()[0]->getProduct() == "i\xc3\xa9\xf0\x9f\x8d\x8e!"); + BOOST_TEST(parsed.devices()[0]->getManufacturer() == ""); + } + + BOOST_AUTO_TEST_CASE(test_reenumeration_starts_a_new_device_session) { + Transcript transcript; + enumerate(transcript, deviceDescriptor(APPLE_VID, IRECV_K_DFU_MODE), + {configDescriptor(1, {{0, 0, USB_CLASS_APP_SPEC, USB_SUBCLASS_DFU, 0, {}}})}, {"Apple Inc.", "Apple Mobile Device (DFU Mode)"}); + // Same address re-reads its own descriptor: still the same session + transcript.getDescriptor(USB_DT_DEVICE, 0, deviceDescriptor(APPLE_VID, IRECV_K_DFU_MODE), USB_DT_DEVICE_SIZE); + enumerate(transcript, deviceDescriptor(APPLE_VID, IRECV_K_RECOVERY_MODE_2), + {configDescriptor(1, {{0, 0, USB_CLASS_VENDOR_SPEC, 0x01, 0, {}}})}, + {"Apple Inc.", "Apple Mobile Device (Recovery Mode)"}); + + auto parsed = parse(capture(Format::Darwin, transcript)); + auto devices = parsed.devices(); + BOOST_REQUIRE_EQUAL(devices.size(), 2u); + BOOST_TEST(devices[0]->getIdentifier() == devices[1]->getIdentifier()); + BOOST_TEST(devices[0]->getMode() == "DFU"); + BOOST_TEST(devices[0]->getProduct() == "Apple Mobile Device (DFU Mode)"); + BOOST_TEST(expertOf(devices[0]) != nullptr); + BOOST_TEST(devices[1]->getMode() == "Recovery"); + BOOST_TEST(devices[1]->getProduct() == "Apple Mobile Device (Recovery Mode)"); + BOOST_TEST(expertOf(devices[1]) != nullptr); + } + + BOOST_AUTO_TEST_CASE(test_bos_descriptor_is_kept) { + auto bos = bytes({5, USB_DT_BOS, 12, 0, 1, 7, USB_DT_DEVICE_CAPABILITY, 2, 0x06, 0, 0, 0}); + Transcript transcript; + transcript.getDescriptor(USB_DT_DEVICE, 0, deviceDescriptor(APPLE_VID, 0x12A8), USB_DT_DEVICE_SIZE); + transcript.getDescriptor(USB_DT_BOS, 0, bos, 5); + transcript.getDescriptor(USB_DT_BOS, 0, bos, 12); + + auto parsed = parse(capture(Format::Darwin, transcript)); + const auto &descriptors = parsed.devices()[0]->getBOSDescriptors(); + BOOST_REQUIRE_EQUAL(descriptors.size(), 1u); + BOOST_TEST(descriptors[0].descriptor.bNumDeviceCaps == 1); + BOOST_TEST(descriptors[0].data.size() == 7u); + } + + BOOST_AUTO_TEST_CASE(test_malformed_configuration_is_flagged) { + auto config = iphoneUsbmuxConfiguration(1); + // Corrupt the second interface descriptor's length so it runs past the end + config[USB_DT_CONFIG_SIZE + USB_DT_INTERFACE_SIZE + 3 * USB_DT_ENDPOINT_SIZE] = std::byte{200}; + + Transcript transcript; + enumerate(transcript, deviceDescriptor(APPLE_VID, 0x12A8), {config}, {}); + + auto parsed = parse(capture(Format::Darwin, transcript)); + auto *device = parsed.devices()[0]; + BOOST_REQUIRE_EQUAL(device->getAnomalies().size(), 1u); + BOOST_TEST(device->getConfigurations().at(1).interfaces.size() == 1u); + } + + BOOST_AUTO_TEST_CASE(test_mismatched_descriptor_type_is_flagged) { + Transcript transcript; + transcript.control(setup(0x80, USB_REQ_GET_DESCRIPTOR, USB_DT_DEVICE << 8U, 0, 18), + stringDescriptor("not a device descriptor")); + + auto parsed = parse(capture(Format::Darwin, transcript)); + BOOST_TEST(parsed.devices()[0]->getAnomalies().size() == 1u); + BOOST_TEST(!parsed.devices()[0]->hasDeviceDescriptor()); + } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/test_PcapInputFile.cpp b/test/test_PcapInputFile.cpp new file mode 100644 index 0000000..f5c772a --- /dev/null +++ b/test/test_PcapInputFile.cpp @@ -0,0 +1,37 @@ +#include +#include "CaptureBuilder.h" + +using namespace fixture; + +BOOST_AUTO_TEST_SUITE(pcap_input_file) + + BOOST_AUTO_TEST_CASE(test_classic_pcap_variants) { + auto transcript = iphone(); + for (bool swapped: {false, true}) { + for (bool nanoseconds: {false, true}) { + BOOST_TEST_CONTEXT("swapped " << swapped << " nanoseconds " << nanoseconds) { + auto parsed = parse(pcap(LINKTYPE_USB_LINUX_MMAPPED, encode(Format::UsbmonMmapped, transcript), + swapped, nanoseconds), ".pcap"); + BOOST_TEST(parsed.input->getWarningCount() == 0u); + BOOST_REQUIRE_EQUAL(parsed.devices().size(), 1u); + BOOST_TEST(parsed.devices()[0]->getProduct() == "iPhone"); + BOOST_TEST(parsed.devices()[0]->getIdentifier() == 0x105u); + } + } + } + } + + BOOST_AUTO_TEST_CASE(test_format_is_detected_from_content_not_extension) { + auto parsed = parse(pcap(LINKTYPE_USB_LINUX, encode(Format::Usbmon, iphone())), ".pcapng"); + BOOST_TEST(parsed.devices()[0]->getManufacturer() == "Apple Inc."); + } + + BOOST_AUTO_TEST_CASE(test_truncated_record) { + auto file = pcap(LINKTYPE_USB_LINUX, encode(Format::Usbmon, iphone())); + file.resize(file.size() - 3); + auto parsed = parse(file, ".pcap"); + BOOST_TEST(parsed.input->getWarningCount() == 1u); + BOOST_TEST(parsed.devices()[0]->getProduct() == "iPhone"); + } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/test_PcapNGInputFile.cpp b/test/test_PcapNGInputFile.cpp index 010f549..6b3906d 100644 --- a/test/test_PcapNGInputFile.cpp +++ b/test/test_PcapNGInputFile.cpp @@ -3,11 +3,154 @@ // #include +#include "CaptureBuilder.h" +#include "protocol/USBMUXInterfaceExpert.h" + +using namespace fixture; + +static Transcript lockdownSession() { + auto transcript = iphone(); + transcript.bulkOut(0x04, muxVersion(2, true)); + transcript.bulkIn(0x85, muxVersion(2, false)); + transcript.bulkOut(0x04, muxPacket(MUX_PROTO_TCP, tcpSegment(49152, 62078, USB_TCP_FLAG_SYN), true)); + transcript.bulkIn(0x85, muxPacket(MUX_PROTO_TCP, tcpSegment(62078, 49152, USB_TCP_FLAG_SYN | USB_TCP_FLAG_ACK), false)); + transcript.bulkOut(0x04, muxPacket(MUX_PROTO_TCP, tcpSegment(49152, 62078, USB_TCP_FLAG_ACK, lengthPrefixed( + xmlPlist("LabeltestRequestQueryType"))), true)); + transcript.bulkIn(0x85, muxPacket(MUX_PROTO_TCP, tcpSegment(62078, 49152, USB_TCP_FLAG_ACK, lengthPrefixed( + xmlPlist("RequestQueryTypeType" + "com.apple.mobile.lockdown"))), false)); + return transcript; +} BOOST_AUTO_TEST_SUITE(pcapng_input_file) - BOOST_AUTO_TEST_CASE(test_load_fixture_file) { + BOOST_AUTO_TEST_CASE(test_every_link_type_decodes_the_same_session) { + for (auto format: {Format::Darwin, Format::Darwin_1_1, Format::USBPcap, Format::Usbmon, + Format::UsbmonMmapped}) { + BOOST_TEST_CONTEXT("format " << static_cast(format)) { + auto parsed = parse(capture(format, lockdownSession())); + BOOST_TEST(parsed.input->getWarningCount() == 0u); + + auto devices = parsed.devices(); + BOOST_REQUIRE_EQUAL(devices.size(), 1u); + auto *device = devices[0]; + + BOOST_TEST(device->getVendorId() == APPLE_VID); + BOOST_TEST(device->getProductId() == 0x12A8); + BOOST_TEST(device->getMode() == "Normal"); + BOOST_TEST(device->getManufacturer() == "Apple Inc."); + BOOST_TEST(device->getProduct() == "iPhone"); + BOOST_TEST(device->getSerial() == "00008101-001234567890001E"); + BOOST_TEST(device->getCurrentConfiguration() == 1); + + auto usbmux = expertOf(device); + BOOST_REQUIRE(usbmux != nullptr); + BOOST_TEST(usbmux->getProtocolVersion() == 2u); + BOOST_REQUIRE_EQUAL(usbmux->getStreams().size(), 1u); + + auto stream = usbmux->getStreams().begin()->second; + BOOST_TEST(stream->getHostPort() == 49152); + BOOST_TEST(stream->getDevicePort() == 62078); + BOOST_TEST(stream->getServiceName() == "lockdownd"); + BOOST_REQUIRE_EQUAL(stream->getMessages().size(), 2u); + BOOST_TEST(stream->getMessages()[0].outbound); + BOOST_TEST(stream->getMessages()[0].stringValue("Request") == "QueryType"); + BOOST_TEST(!stream->getMessages()[1].outbound); + BOOST_TEST(stream->getMessages()[1].stringValue("Type") == "com.apple.mobile.lockdown"); + } + } + } + + BOOST_AUTO_TEST_CASE(test_unsupported_interface_is_skipped_without_hanging) { + Bytes file; + appendBlock(file, TYPE_SECTION_HEADER, sectionHeaderBody()); + appendBlock(file, TIME_INTERFACE_DESCRIPTOR_BLOCK, interfaceBody(1 /* Ethernet */)); + appendBlock(file, TIME_INTERFACE_DESCRIPTOR_BLOCK, interfaceBody(LINKTYPE_USB_DARWIN)); + appendBlock(file, TYPE_ENHANCED_PACKET_BLOCK, enhancedPacketBody(0, bytes({1, 2, 3, 4, 5, 6}))); + for (const auto &packet: encode(Format::Darwin, lockdownSession())) { + appendBlock(file, TYPE_ENHANCED_PACKET_BLOCK, enhancedPacketBody(1, packet)); + } + appendBlock(file, TYPE_ENHANCED_PACKET_BLOCK, enhancedPacketBody(0, bytes({7, 8}))); + + auto parsed = parse(file); + BOOST_TEST(parsed.input->getWarningCount() == 1u); + BOOST_REQUIRE_EQUAL(parsed.devices().size(), 1u); + BOOST_TEST(parsed.devices()[0]->getProduct() == "iPhone"); + } + + BOOST_AUTO_TEST_CASE(test_multiple_sections_reset_interfaces) { + auto packets = encode(Format::Darwin, lockdownSession()); + auto half = packets.size() / 2; + + Bytes file; + appendBlock(file, TYPE_SECTION_HEADER, sectionHeaderBody()); + appendBlock(file, TIME_INTERFACE_DESCRIPTOR_BLOCK, interfaceBody(LINKTYPE_USB_DARWIN)); + for (size_t i = 0; i < half; i++) { appendBlock(file, TYPE_ENHANCED_PACKET_BLOCK, enhancedPacketBody(0, packets[i])); } + appendBlock(file, TYPE_SECTION_HEADER, sectionHeaderBody()); + appendBlock(file, TIME_INTERFACE_DESCRIPTOR_BLOCK, interfaceBody(LINKTYPE_USB_DARWIN)); + for (size_t i = half; i < packets.size(); i++) { + appendBlock(file, TYPE_ENHANCED_PACKET_BLOCK, enhancedPacketBody(0, packets[i])); + } + + auto parsed = parse(file); + BOOST_TEST(parsed.input->getWarningCount() == 0u); + auto usbmux = expertOf(parsed.devices()[0]); + BOOST_REQUIRE(usbmux != nullptr); + BOOST_TEST(usbmux->getStreams().begin()->second->getMessages().size() == 2u); + } + + BOOST_AUTO_TEST_CASE(test_simple_packet_blocks) { + Bytes file; + appendBlock(file, TYPE_SECTION_HEADER, sectionHeaderBody()); + appendBlock(file, TIME_INTERFACE_DESCRIPTOR_BLOCK, interfaceBody(LINKTYPE_USB_LINUX)); + for (const auto &packet: encode(Format::Usbmon, lockdownSession())) { + Bytes body; + appendStruct(body, static_cast(packet.size())); + append(body, packet); + appendBlock(file, TYPE_SIMPLE_PACKET_BLOCK, body); + } + + auto parsed = parse(file); + BOOST_TEST(parsed.input->getWarningCount() == 0u); + BOOST_TEST(parsed.devices()[0]->getProduct() == "iPhone"); + } + + BOOST_AUTO_TEST_CASE(test_packet_for_undeclared_interface_is_reported) { + Bytes file; + appendBlock(file, TYPE_SECTION_HEADER, sectionHeaderBody()); + appendBlock(file, TYPE_ENHANCED_PACKET_BLOCK, enhancedPacketBody(3, bytes({1, 2, 3}))); + auto parsed = parse(file); + BOOST_TEST(parsed.input->getWarningCount() == 1u); + BOOST_TEST(parsed.devices().empty()); + } + + BOOST_AUTO_TEST_CASE(test_big_endian_section_is_rejected) { + Bytes file; + Bytes body; + appendStruct(body, static_cast(PCAPNG_BYTE_ORDER_MAGIC_SWAPPED)); + appendStruct(body, static_cast(0)); + appendStruct(body, static_cast(-1)); + appendBlock(file, TYPE_SECTION_HEADER, body); + BOOST_CHECK_THROW(parse(file), demuxusb::ParseError); + } + + BOOST_AUTO_TEST_CASE(test_non_capture_is_rejected) { + BOOST_CHECK_THROW(parse(bytes("definitely not a capture file")), demuxusb::ParseError); + BOOST_CHECK_THROW(parse(Bytes{}), demuxusb::ParseError); + } + + BOOST_AUTO_TEST_CASE(test_truncated_capture_keeps_what_was_parsed) { + auto file = capture(Format::Darwin, lockdownSession()); + file.resize(file.size() - 10); + auto parsed = parse(file); + BOOST_TEST(parsed.input->getWarningCount() == 1u); + BOOST_REQUIRE_EQUAL(parsed.devices().size(), 1u); + BOOST_TEST(parsed.devices()[0]->getProduct() == "iPhone"); + auto usbmux = expertOf(parsed.devices()[0]); + BOOST_REQUIRE(usbmux != nullptr); + // The last (device -> host) message was in the truncated block + BOOST_TEST(usbmux->getStreams().begin()->second->getMessages().size() == 1u); } -BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/test_Recovery.cpp b/test/test_Recovery.cpp new file mode 100644 index 0000000..a1fe072 --- /dev/null +++ b/test/test_Recovery.cpp @@ -0,0 +1,85 @@ +#include +#include "CaptureBuilder.h" +#include "protocol/RecoveryInterfaceExpert.h" + +using namespace fixture; + +// iBoot exposes interface 0 for control and, in alternate setting 1 of interface 1, the bulk pipes libirecovery uses +static Transcript recoveryDevice() { + Transcript transcript; + enumerate(transcript, deviceDescriptor(APPLE_VID, IRECV_K_RECOVERY_MODE_2), + {configDescriptor(1, {{0, 0, USB_CLASS_VENDOR_SPEC, 0x01, 0, {}}, + {1, 0, USB_CLASS_VENDOR_SPEC, 0x01, 0, {}}, + {1, 1, USB_CLASS_VENDOR_SPEC, 0x01, 0, {{0x04, 2}, {0x81, 2}}}})}, + {"Apple Inc.", "Apple Mobile Device (Recovery Mode)"}); + transcript.setInterface(0, 0); + transcript.setInterface(1, 1); + return transcript; +} + +static Bytes command(const std::string &value) { + auto result = bytes(value); + result.push_back(std::byte{0}); + return result; +} + +BOOST_AUTO_TEST_SUITE(recovery) + + BOOST_AUTO_TEST_CASE(test_commands_uploads_and_console) { + Bytes ibec(70000); + for (size_t i = 0; i < ibec.size(); i++) { ibec[i] = static_cast(i % 251); } + + auto transcript = recoveryDevice(); + transcript.control(setup(0x40, 0, 0, 0, 22), command("setenv auto-boot true")); + transcript.control(setup(0x40, 0, 0, 0, 9), command("saveenv")); + transcript.control(setup(0x40, 0, 0, 0, 19), command("getenv build-style")); + transcript.control(setup(0xC0, 0, 0, 0, 255), command("RELEASE")); + transcript.control(setup(0x41, 0, 0, 0, 0)); + for (size_t offset = 0; offset < ibec.size(); offset += 0x8000) { + auto end = std::min(ibec.size(), offset + 0x8000); + transcript.bulkOut(0x04, Bytes(ibec.begin() + offset, ibec.begin() + end)); + } + transcript.control(setup(0x40, 0, 0, 0, 3), command("go")); + transcript.bulkIn(0x81, bytes("] go\n")); + transcript.bulkIn(0x81, bytes("jumping into image\n")); + + auto parsed = parse(capture(Format::Darwin, transcript)); + auto *device = parsed.devices()[0]; + BOOST_TEST(device->getMode() == "Recovery"); + + // One expert spans both interfaces so commands and uploads stay in order + BOOST_REQUIRE_EQUAL(device->getExperts().size(), 1u); + auto recovery = expertOf(device); + BOOST_REQUIRE(recovery != nullptr); + + auto commands = recovery->getCommands(); + BOOST_REQUIRE_EQUAL(commands.size(), 4u); + BOOST_TEST(commands[0] == "setenv auto-boot true"); + BOOST_TEST(commands[3] == "go"); + + const auto &payloads = recovery->getPayloads(); + BOOST_TEST((payloads[3].kind == RecoveryPayloadKind::Response && payloads[3].text == "RELEASE")); + BOOST_TEST((payloads[4].kind == RecoveryPayloadKind::Upload && payloads[4].data == ibec)); + BOOST_TEST(recovery->getConsole() == "] go\njumping into image\n"); + + auto artifacts = recovery->getArtifacts(); + BOOST_REQUIRE_EQUAL(artifacts.size(), 2u); + BOOST_TEST(artifacts[0].name == "recovery-upload-0.bin"); + BOOST_TEST(artifacts[1].name == "recovery-console.txt"); + + BOOST_TEST(recovery->details().find("> setenv auto-boot true") != std::string::npos); + BOOST_TEST(recovery->toString().find("4 commands, 1 uploads (70000 bytes)") != std::string::npos); + } + + BOOST_AUTO_TEST_CASE(test_upload_without_announcement) { + auto transcript = recoveryDevice(); + transcript.bulkOut(0x04, bytes("IMG4")); + + auto parsed = parse(capture(Format::Usbmon, transcript)); + auto recovery = expertOf(parsed.devices()[0]); + BOOST_REQUIRE(recovery != nullptr); + BOOST_REQUIRE_EQUAL(recovery->getPayloads().size(), 1u); + BOOST_TEST((recovery->getPayloads()[0].data == bytes("IMG4"))); + } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/test_Robustness.cpp b/test/test_Robustness.cpp new file mode 100644 index 0000000..84e7e5f --- /dev/null +++ b/test/test_Robustness.cpp @@ -0,0 +1,93 @@ +// Captures are untrusted input: a corrupt or hostile file must never crash the analyser. These tests mutate valid +// captures of every supported format and only require that parsing terminates (with or without a ParseError). +// Build with -DDEMUXUSB_SANITIZE=ON to have out of bounds accesses fail the test. + +#include +#include "CaptureBuilder.h" +#include "protocol/USBMUXInterfaceExpert.h" + +using namespace fixture; + +static Transcript busySession() { + auto transcript = iphone(true); + transcript.bulkOut(0x04, muxVersion(2, true)); + transcript.bulkOut(0x04, muxPacket(MUX_PROTO_TCP, tcpSegment(49152, 62078, USB_TCP_FLAG_SYN), true)); + transcript.bulkOut(0x04, muxPacket(MUX_PROTO_TCP, tcpSegment(49152, 62078, USB_TCP_FLAG_ACK, lengthPrefixed( + xmlPlist("RequestQueryType"))), true)); + transcript.bulkIn(0x85, muxPacket(MUX_PROTO_TCP, tcpSegment(62078, 49152, USB_TCP_FLAG_ACK, lengthPrefixed( + xmlPlist("Port50000Servicex"))), false)); + transcript.control(setup(0x21, 1, 0, 1, 4), bytes({1, 2, 3, 4})); + transcript.control(setup(0x40, 0, 0, 0, 3), bytes({'g', 'o', 0})); + return transcript; +} + +static void parseQuietly(const Bytes &contents, const std::string &extension) { + try { + auto parsed = parse(contents, extension); + for (auto *device: parsed.devices()) { + for (const auto &expert: device->getExperts()) { + (void) expert->toString(); + (void) expert->details(); + (void) expert->getArtifacts(); + } + } + } catch (const demuxusb::ParseError &) { + // Rejecting a file is fine; crashing is not + } +} + +BOOST_AUTO_TEST_SUITE(robustness) + + BOOST_AUTO_TEST_CASE(test_mutated_captures_do_not_crash) { + std::mt19937 random(0x5eed); + + std::vector> seeds; + for (auto format: {Format::Darwin, Format::Darwin_1_1, Format::USBPcap, Format::Usbmon}) { + seeds.emplace_back(capture(format, busySession()), ".pcapng"); + } + seeds.emplace_back(pcap(LINKTYPE_USB_LINUX_MMAPPED, encode(Format::UsbmonMmapped, busySession())), ".pcap"); + + for (const auto &[seed, extension]: seeds) { + for (int iteration = 0; iteration < 300; iteration++) { + auto mutated = seed; + auto mutations = 1 + random() % 16; + for (unsigned m = 0; m < mutations; m++) { + auto position = random() % mutated.size(); + switch (random() % 4) { + case 0: + mutated[position] = static_cast(random()); + break; + case 1: + mutated[position] = std::byte{0xFF}; + break; + case 2: + mutated[position] = std::byte{0x00}; + break; + default: + mutated[position] ^= static_cast(1U << (random() % 8)); + } + } + if (random() % 4 == 0) { mutated.resize(random() % mutated.size() + 1); } + + parseQuietly(mutated, extension); + } + } + BOOST_TEST(true); + } + + BOOST_AUTO_TEST_CASE(test_random_files_do_not_crash) { + std::mt19937 random(0xbad); + for (int iteration = 0; iteration < 200; iteration++) { + Bytes file(random() % 512); + for (auto &value: file) { value = static_cast(random()); } + // Give the junk a valid magic so it reaches the block parsers + if (file.size() >= 4) { + uint32_t magic = iteration % 2 ? TYPE_SECTION_HEADER : PCAP_MAGIC_MICROSECONDS; + std::memcpy(file.data(), &magic, sizeof(magic)); + } + parseQuietly(file, ".pcapng"); + } + BOOST_TEST(true); + } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/test_USBMUX.cpp b/test/test_USBMUX.cpp new file mode 100644 index 0000000..a2be102 --- /dev/null +++ b/test/test_USBMUX.cpp @@ -0,0 +1,188 @@ +#include +#include "CaptureBuilder.h" +#include "protocol/USBMUXInterfaceExpert.h" + +using namespace fixture; + +static const uint16_t HOST_PORT = 49152; + +static Bytes out(uint16_t host, uint16_t device, uint16_t flags, const Bytes &data = {}) { + return muxPacket(MUX_PROTO_TCP, tcpSegment(host, device, flags, data), true); +} + +static Bytes in(uint16_t host, uint16_t device, uint16_t flags, const Bytes &data = {}) { + return muxPacket(MUX_PROTO_TCP, tcpSegment(device, host, flags, data), false); +} + +static std::shared_ptr usbmuxOf(Parsed &parsed) { + BOOST_REQUIRE_EQUAL(parsed.devices().size(), 1u); + auto usbmux = expertOf(parsed.devices()[0]); + BOOST_REQUIRE(usbmux != nullptr); + return usbmux; +} + +static std::shared_ptr onlyStream(const std::shared_ptr &usbmux) { + BOOST_REQUIRE_EQUAL(usbmux->getStreams().size(), 1u); + return usbmux->getStreams().begin()->second; +} + +static Bytes request(const std::string &name) { + return lengthPrefixed(xmlPlist("Request" + name + "")); +} + +BOOST_AUTO_TEST_SUITE(usbmux) + + BOOST_AUTO_TEST_CASE(test_mux_packet_split_across_transfers) { + auto packet = out(HOST_PORT, 62078, USB_TCP_FLAG_ACK, request("GetValue")); + auto transcript = iphone(); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_SYN)); + transcript.bulkOut(0x04, Bytes(packet.begin(), packet.begin() + 5)); + transcript.bulkOut(0x04, Bytes(packet.begin() + 5, packet.begin() + 40)); + transcript.bulkOut(0x04, Bytes(packet.begin() + 40, packet.end())); + + auto parsed = parse(capture(Format::Darwin, transcript)); + auto stream = onlyStream(usbmuxOf(parsed)); + BOOST_REQUIRE_EQUAL(stream->getMessages().size(), 1u); + BOOST_TEST(stream->getMessages()[0].stringValue("Request") == "GetValue"); + BOOST_TEST(stream->getBytesOut() == request("GetValue").size()); + } + + BOOST_AUTO_TEST_CASE(test_several_mux_packets_in_one_transfer_and_split_plist) { + auto message = request("StartSession"); + Bytes coalesced; + append(coalesced, in(HOST_PORT, 62078, USB_TCP_FLAG_SYN | USB_TCP_FLAG_ACK)); + // Length prefix in its own segment, then the plist in two more + append(coalesced, in(HOST_PORT, 62078, USB_TCP_FLAG_ACK, Bytes(message.begin(), message.begin() + 4))); + append(coalesced, in(HOST_PORT, 62078, USB_TCP_FLAG_ACK, Bytes(message.begin() + 4, message.begin() + 30))); + append(coalesced, in(HOST_PORT, 62078, USB_TCP_FLAG_ACK, Bytes(message.begin() + 30, message.end()))); + + auto transcript = iphone(); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_SYN)); + transcript.bulkIn(0x85, coalesced); + + auto parsed = parse(capture(Format::USBPcap, transcript)); + auto stream = onlyStream(usbmuxOf(parsed)); + BOOST_REQUIRE_EQUAL(stream->getMessages().size(), 1u); + BOOST_TEST(!stream->getMessages()[0].outbound); + BOOST_TEST(stream->getMessages()[0].summary() == "Request=StartSession"); + BOOST_TEST(stream->getEncodingIn() == StreamEncoding::Plist); + } + + BOOST_AUTO_TEST_CASE(test_binary_plist) { + plist_t dict = plist_new_dict(); + plist_dict_set_item(dict, "Request", plist_new_string("GetValue")); + plist_dict_set_item(dict, "Key", plist_new_string("ProductVersion")); + char *binary = nullptr; + uint32_t length = 0; + plist_to_bin(dict, &binary, &length); + Bytes message(reinterpret_cast(binary), reinterpret_cast(binary) + length); + free(binary); + plist_free(dict); + + auto transcript = iphone(); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_SYN)); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_ACK, lengthPrefixed(message))); + + auto parsed = parse(capture(Format::Usbmon, transcript)); + auto stream = onlyStream(usbmuxOf(parsed)); + BOOST_REQUIRE_EQUAL(stream->getMessages().size(), 1u); + BOOST_TEST(stream->getMessages()[0].summary() == "Request=GetValue, Key=ProductVersion"); + BOOST_TEST(stream->getMessages()[0].xml().find("ProductVersion") != std::string::npos); + } + + BOOST_AUTO_TEST_CASE(test_start_service_names_later_streams) { + auto transcript = iphone(); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_SYN)); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_ACK, lengthPrefixed(xmlPlist( + "RequestStartService" + "Servicecom.apple.afc")))); + transcript.bulkIn(0x85, in(HOST_PORT, 62078, USB_TCP_FLAG_ACK, lengthPrefixed(xmlPlist( + "Port50123RequestStartService" + "Servicecom.apple.afc")))); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_FIN | USB_TCP_FLAG_ACK)); + transcript.bulkOut(0x04, out(HOST_PORT + 1, 50123, USB_TCP_FLAG_SYN)); + transcript.bulkOut(0x04, out(HOST_PORT + 1, 50123, USB_TCP_FLAG_ACK, bytes("CFA6LPAA("))); + + auto parsed = parse(capture(Format::Darwin, transcript)); + auto usbmux = usbmuxOf(parsed); + BOOST_REQUIRE_EQUAL(usbmux->getStreams().size(), 2u); + + auto lockdown = usbmux->getStreams().begin()->second; + auto afc = usbmux->getStreams().rbegin()->second; + if (lockdown->getDevicePort() != 62078) { std::swap(lockdown, afc); } + + BOOST_TEST(lockdown->isClosed()); + BOOST_TEST(lockdown->getMessages()[1].summary() == "Request=StartService, Service=com.apple.afc, Port=50123"); + BOOST_TEST(afc->getServiceName() == "com.apple.afc"); + BOOST_TEST(afc->getEncodingOut() == StreamEncoding::Raw); + BOOST_TEST(afc->getTransactionCount() == 0u); + BOOST_TEST(usbmux->toString().find("[com.apple.afc]") != std::string::npos); + BOOST_TEST(usbmux->details().find("> Request=StartService, Service=com.apple.afc") != std::string::npos); + } + + BOOST_AUTO_TEST_CASE(test_tls_upgrade_is_detected) { + auto transcript = iphone(); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_SYN)); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_ACK, request("StartSession"))); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_ACK, bytes({0x16, 0x03, 0x01, 0x00, 0x05, 1, 2, 3, 4, 5}))); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_ACK, bytes({0x17, 0x03, 0x03, 0x00, 0x02, 9, 9}))); + + auto parsed = parse(capture(Format::Darwin, transcript)); + auto stream = onlyStream(usbmuxOf(parsed)); + BOOST_TEST(stream->getMessages().size() == 1u); + BOOST_TEST(stream->getEncodingOut() == StreamEncoding::TLS); + BOOST_TEST(stream->getBytesOut() == request("StartSession").size() + 17); + } + + BOOST_AUTO_TEST_CASE(test_connection_opened_before_capture) { + auto transcript = iphone(); + transcript.bulkIn(0x85, in(HOST_PORT, 62078, USB_TCP_FLAG_ACK, request("Goodbye"))); + + auto parsed = parse(capture(Format::Darwin, transcript)); + auto stream = onlyStream(usbmuxOf(parsed)); + BOOST_TEST(stream->getHostPort() == HOST_PORT); + BOOST_TEST(stream->getMessages().size() == 1u); + } + + BOOST_AUTO_TEST_CASE(test_reconnects_create_new_streams) { + auto transcript = iphone(); + for (int i = 0; i < 3; i++) { + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_SYN)); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_ACK, request("QueryType"))); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_RST)); + } + + auto parsed = parse(capture(Format::Darwin, transcript)); + auto usbmux = usbmuxOf(parsed); + BOOST_REQUIRE_EQUAL(usbmux->getStreams().size(), 3u); + for (const auto &[id, stream]: usbmux->getStreams()) { + BOOST_TEST(stream->getMessages().size() == 1u); + BOOST_TEST(stream->isClosed()); + } + } + + BOOST_AUTO_TEST_CASE(test_lost_framing_resynchronises) { + auto transcript = iphone(); + transcript.bulkOut(0x04, bytes({0xde, 0xad, 0xbe, 0xef, 0xff, 0xff, 0xff, 0xff, 1, 2, 3})); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_SYN)); + + auto parsed = parse(capture(Format::Darwin, transcript)); + auto usbmux = usbmuxOf(parsed); + BOOST_TEST(usbmux->getResyncCount() == 1u); + BOOST_TEST(usbmux->getStreams().size() == 1u); + } + + BOOST_AUTO_TEST_CASE(test_artifacts_hold_plist_transcripts) { + auto transcript = iphone(); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_SYN)); + transcript.bulkOut(0x04, out(HOST_PORT, 62078, USB_TCP_FLAG_ACK, request("QueryType"))); + + auto parsed = parse(capture(Format::Darwin, transcript)); + auto artifacts = usbmuxOf(parsed)->getArtifacts(); + BOOST_REQUIRE_EQUAL(artifacts.size(), 1u); + std::string document(reinterpret_cast(artifacts[0].data.data()), artifacts[0].data.size()); + BOOST_TEST(document.find("") != std::string::npos); + BOOST_TEST(document.find("QueryType") != std::string::npos); + } + +BOOST_AUTO_TEST_SUITE_END()