├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── Adding libusb-win32 filter.png ├── QCSuper architecture.md ├── The Diag protocol.md └── sample_pcaps │ ├── Wireshark screenshot.png │ ├── sample_2g_3g_4g_xperia.pcap │ └── sample_2g_3g_4g_xperia_with_sib_and_nas.pcap ├── qcsuper.py ├── setup.py ├── src ├── __init__.py ├── __main__.py ├── inputs │ ├── README.md │ ├── _base_input.py │ ├── _hdlc_mixin.py │ ├── adb.py │ ├── adb_bridge │ │ ├── Makefile │ │ ├── README.md │ │ ├── adb_bridge │ │ └── adb_bridge.c │ ├── adb_wsl2.py │ ├── adb_wsl2_bridge │ │ ├── README.md │ │ └── adb_wsl2_bridge.ps1 │ ├── dlf_read.py │ ├── external │ │ ├── README.md │ │ ├── adb │ │ │ ├── AdbWinApi.dll │ │ │ ├── AdbWinUsbApi.dll │ │ │ ├── adb_linux │ │ │ ├── adb_macos │ │ │ ├── adb_windows.exe │ │ │ └── lib64 │ │ │ │ └── libc++.dylib │ │ └── update_tools.sh │ ├── json_geo_read.py │ ├── tcp_connector.py │ ├── usb_modem_argparser.py │ ├── usb_modem_pyserial.py │ ├── usb_modem_pyusb.py │ └── usb_modem_pyusb_devfinder.py ├── main.py ├── modules │ ├── README.md │ ├── _enable_log_mixin.py │ ├── _utils.py │ ├── cli.py │ ├── decoded_sibs_dump.py │ ├── dlf_dump.py │ ├── efs_shell.py │ ├── efs_shell_commands │ │ ├── _base_efs_shell_command.py │ │ ├── cat.py │ │ ├── chmod.py │ │ ├── device_info.py │ │ ├── get.py │ │ ├── ln.py │ │ ├── ls.py │ │ ├── md5sum.py │ │ ├── mkdir.py │ │ ├── mv.py │ │ ├── put.py │ │ ├── rm.py │ │ └── stat.py │ ├── info.py │ ├── json_geo_dump.py │ ├── memory_dump.py │ ├── pcap_dump.py │ └── wireshark_plugin │ │ └── diag_nr_rrc_dissector.lua └── protocol │ ├── README.md │ ├── efs2.py │ ├── gsmtap.py │ ├── log_types.py │ ├── messages.py │ └── subsystems.py └── tests ├── tests.py └── tests_usbmodem_argparser.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__/ 3 | config.py 4 | build/ 5 | dist/ 6 | *.egg-info 7 | .~lock.* 8 | -------------------------------------------------------------------------------- /docs/Adding libusb-win32 filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P1sec/QCSuper/f5f1501c7ce09f6c167ae623233f674be09cdf87/docs/Adding libusb-win32 filter.png -------------------------------------------------------------------------------- /docs/QCSuper architecture.md: -------------------------------------------------------------------------------- 1 | QCSuper can be split up into two building blocks: 2 | 3 | * **Inputs** are Python classes exposing interfaces to read and optionally send Diag protocol data. We can distinguish: 4 | * Inputs for communicating live with devices (the `--adb` input for talking with rooted Android phones, the `--usb-modem` for talking with USB modems) 5 | * Inputs for processing saved data (`--dlf-read` is able to read data in a format providing interoperability with the vendor QXDM tool) 6 | 7 | * **Modules** are Python classes using inputs to perform specific tasks using the Diag protocol. For example: 8 | * Capturing raw 2G/3G/4G signalling traffic (`--pcap-dump` will dump to a PCAP file, `--wireshark-live` will open directly Wireshark) 9 | * Gathering generation info about the device (`--info`) 10 | * Capturing raw Diag logs information into reusable formats (`--json-geo-dump`, `--dlf-dump`) 11 | 12 | ## Internal architecture 13 | 14 | QCSuper needs to deal to multiple sources of input: 15 | 16 | * The connected Diag device 17 | * Which can be different kinds of descriptor: 18 | * A pseudo-serial USB device 19 | * A TCP socket communicating with a remote Android device 20 | * A file to replay from 21 | * Which can deliver different kinds of received data: 22 | * Synchronous responses to requests 23 | * Asynchronous logs or messages (see [The Diag protocol.md](The%20Diag%20protocol.md)) 24 | * When the source is a real device, the device can accept only one Diag client at once 25 | 26 | * An optional interactive prompt (`--cli` module): needed to provide a handy way to continue capturing on the Diag client while executing other tasks 27 | 28 | All this requires a form of concurrency to be acheived: either threading, or a way to poll on descriptors through an event loop. 29 | 30 | The design ease/simplicity tradeoff I have chosen was to use threading (but I'm open to rework the architecture if someone has something else to propose). Polling on both a serial port and a featureful command prompt or thread queue (for example) is not doable easily in a multi-platform way, and using asyncio seemed to add some design and syntaxic overhead/external libraries to the equation. 31 | 32 | ### Threading model 33 | 34 | QCSuper makes use of different threads: 35 | 36 | * The main thread contains the loop reading from the device, and is the only place where reading is performed (it will also dispatch asynchronous messages to modules, calling the `on_log`, `on_message` callbacks which may not write neither read, and calling at teardown the `on_deinit` callback which may write) 37 | * A background thread is used for initializing the modules selected through command line arguments (calling the `on_init` callback which may write) 38 | * Edge case only: a background thread may be used for the optional interactive prompt (`--cli`) and initializing the modules called from it (calling the `on_init` callback which may write) 39 | 40 | ### Modules API 41 | 42 | A module is a Python class which may expose different methods: 43 | 44 | * `__init__`: will receive the input object as its first argument, and optionally other arguments from the command line or interactive prompt (passed in sequence from the entry point `qcsuper.py`). 45 | * `on_init`: called when the connection to the Diag device is established. Not called when the input is not a device but a file containing recorded log data. 46 | * Callbacks triggered by a read on the input source: 47 | * `on_log`: called when an asynchronous response Diag protocol raw "log" is received. 48 | * `on_message`: called when an asynchronous response Diag protocol text "message" structure is received. 49 | * `on_deinit`: called when the connection to the Diag device ceased establishment, or the user hit Ctrl+C. 50 | 51 | The methods composing these callbacks may perform request-response operations (using `self.diag_reader.send_recv(opcode, payload)`, where `self.diag_reader` is the input object). 52 | 53 | When a request-response operation is performed, the thread for the callback is paused for the time the response is received, using a thread synchronization primitive shared with the main thread. 54 | 55 | When using the interactive prompt (`--cli`), the moment where the `on_init` callbacks ends is the moment where the user is informed that the task continued to background (or is finished, in the case where there is no further callbacks). 56 | 57 | ### Inputs API 58 | 59 | An input is a Python class which may expose different methods: 60 | 61 | * `__init__`: will optionally receive arguments from the command line or interactive prompt (passed in sequence from the entry point `qcsuper.py`). 62 | * `send_request`: this function will be called when a module wants to send a Diag request packet (involving possible utility functions such as `hdlc_encapsulate` from `HdlcMixin`). 63 | * `read_loop`: Diag responses packets will be read and dispatched from here, involving the use of the private `dispatch_received_diag_packet` method inherited from `BaseInput` (and other possible utility functions, such as `hdlc_decapsulate` from `HdlcMixin`). 64 | 65 | Inputs inherit from the `BaseInput` class which exposes a method called `send_recv`, allowing to write a request then read the response, wrapping transparently thread synchronization primitives. 66 | 67 | `send_recv` is most likely the only method of an input to be called directly from a module. 68 | -------------------------------------------------------------------------------- /docs/The Diag protocol.md: -------------------------------------------------------------------------------- 1 | The **Diag protocol**, also called **QCDM** or **DM** (Diagnostic monitor) is a protocol present on Qualcomm-based phones and USB modems. 2 | 3 | It can be accessed through multiple channels: 4 | 5 | * On Android phones, it is exposed internally through the `/dev/diag` device, created by the "diagchar" kernel module. This device may communicate with the baseband in multiple ways (shared memory, serial). 6 | 7 | * On USB modems, it is exposed externally through an USB pseudo-serial port (likely `/dev/ttyUSB*` or `/dev/ttyHSO*`). 8 | * Most often exposed directly, or requires to send a special AT command (`AT$QCDMG`). 9 | * This interface can also be often exposed on Android phones, but seems to require to be enabled through a vendor-specific way. 10 | 11 | `/dev/diag` should allow to exchange the same data as with the port exposed by USB modems, with some extra framing and IOCTL required. 12 | 13 | # The diag protocol over USB 14 | 15 | In its simplest form, the Diag protocol uses a simple framing (inspired by the [https://en.wikipedia.org/wiki/High-Level_Data_Link_Control](https://en.wikipedia.org/wiki/High-Level_Data_Link_Control) with less fields). 16 | 17 | It is composed of: 18 | 19 | * Contents in which the trailer character `\x7e` is escaped as `\x7d\x5e`, and `\x7d` escaped as `\x7d\x5d`: 20 | * 1 byte: command code 21 | * n bytes: packet payload 22 | * 2 bytes: CCITT CRC-16 checksum (with Python: `crcmod.mkCrcFun(0x11021, initCrc=0, xorOut=0xffff)`) 23 | * 1 byte: trailer character (`\x7e`) 24 | 25 | Looking at the effective contents, these are composed of two parts, the one-byte command code and the payload that ensues. 26 | 27 | Diag is a very feature-rich protocol (even though it is locked down on some recent devices) and exposes a lot of commands. These can be found in certain Internet-facing source code (look for `diagcmd.h`). 28 | 29 | We can break down Diag packets sent over the wire into a few kinds: 30 | * Request packets 31 | * Response packets 32 | * Synchronous responses: will bear the same command code as the request, or an error command code (`DIAG_BAD_CMD_F`, `DIAG_BAD_PARM_F`...). 33 | * Asynchronous **log packets** (`DIAG_LOG_F`): they contain an encoded raw structure of data, logged by the baseband at some point. Most often proprietary structures, but some packets also expose the raw over-the-air data. 34 | * Asynchronous **message packets** (`DIAG_MSG_F`, `DIAG_EXT_MSG_F`, `DIAG_EXT_MSG_TERSE_F`): they contain a string of information (debug, error, info...). Composed of a format string and a variable number of arguments. 35 | 36 | Diag may expose a lot of functionalites, including: 37 | * Gathering generation information about the device (baseband firmware version, model, serial number...) 38 | * Reading/writing memory (fully enabled on older devices, most often restricted to ranges or disabled on newer) 39 | * Reading/writing non-volatile memory (read and alter protocol-specific state variables) 40 | * Commands related to calls and messaging 41 | * Commands related to GPS 42 | * Commands specific to 2G/3G/4G protocols, layer 1 to 3 43 | * Commands related to the tiny internal filesystem of the baseband (EFS) 44 | * Many more. Most features added after some point in the expansion of Diag use the `DIAG_SUBSYS_CMD_F` command which allows to use a 1-byte subsystem number, followed by a 2-bytes subsystem command code. 45 | 46 | # The diag protocol over `/dev/diag` 47 | 48 | On Android devices, you can communicate with `/dev/diag` using the following steps: 49 | 50 | * Open `/dev/diag` read/write (most often requires root rights) 51 | * Trigger the `DIAG_IOCTL_SWITCH_LOGGING` IOCTL as depicted below (will enable reading/writing Diag frames): 52 | 53 | ```c 54 | const unsigned long DIAG_IOCTL_SWITCH_LOGGING = 7; 55 | const int MEMORY_DEVICE_MODE = 2; 56 | 57 | const int mode_param[] = { MEMORY_DEVICE_MODE, -1, 0 }; // diag_logging_mode_param_t 58 | 59 | if(ioctl(diag_fd, DIAG_IOCTL_SWITCH_LOGGING, MEMORY_DEVICE_MODE, 0, 0, 0) < 0 && 60 | ioctl(diag_fd, DIAG_IOCTL_SWITCH_LOGGING, &mode_param, sizeof(mode_param), 0, 0, 0, 0) < 0) { 61 | error("ioctl"); 62 | } 63 | ``` 64 | 65 | * Trigger the `DIAG_IOCTL_REMOTE_DEV` IOCTL which will enable you to know whether you should consider an extra field when reading/writing Diag frames: 66 | 67 | ```c 68 | const unsigned long DIAG_IOCTL_REMOTE_DEV = 32; 69 | int use_mdm = 0; 70 | 71 | if(ioctl(diag_fd, DIAG_IOCTL_REMOTE_DEV, &use_mdm) < 0) { 72 | error("ioctl"); 73 | } 74 | ``` 75 | 76 | You can now read/write on the character device. Everything is little-endian. The format for writing requests is: 77 | 78 | ```c 79 | struct request { 80 | int data_type; // USER_SPACE_DATA_TYPE = 32 81 | int device_type; // MDM = -1 (only if use_mdm returned by the ioctl above is 1, or field not present) 82 | 83 | char payload[]; // Same framing as with the "diag protocol over USB" described above 84 | }; 85 | ``` 86 | 87 | The format for reading responses is: 88 | 89 | ```c 90 | struct response { 91 | int data_type; // USER_SPACE_DATA_TYPE = 32 92 | int device_type; // MDM = -1 (only if use_mdm returned by the ioctl above is 1, or field not present) 93 | 94 | int nb_buffers; // Number of entries in the field below 95 | struct response_buffer response_buffers[nb_buffers]; 96 | }; 97 | 98 | struct response_buffer { 99 | int buffer_size; // Size in bytes of the field below 100 | char payload[buffer_size]; // Same framing as with the "diag protocol over USB" described above; 101 | }; 102 | ``` 103 | 104 | # Implementations of the Diag protocol 105 | 106 | There exists a vendor Windows client provided by Qualcomm, called QXDM (Extensible Diagnostic Monitor), which can trigger almost all functionality of Diag. It can't however perform the specific tasks QCSuper was designed for (generate a standard PCAP, dump all the device's memory...). 107 | 108 | QXDM can save logs received through diag (see the definition of "log packet" above) into a very simple format, consisting of part of the raw Diag packets data, called DLF. 109 | 110 | Recent versions use a way more complex format called ISF. Otherwise, they expose options to convert ISF to DLF and the other way around. 111 | 112 | QCSuper can both save raw logs into the DLF format, providing interoperability (module --dlf-dump) and reprocess them (input --dlf-read). 113 | 114 | There are also a few open source tools implementing bits of the Diag protocol, see the "related tools" section of the README. 115 | 116 | 117 | -------------------------------------------------------------------------------- /docs/sample_pcaps/Wireshark screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P1sec/QCSuper/f5f1501c7ce09f6c167ae623233f674be09cdf87/docs/sample_pcaps/Wireshark screenshot.png -------------------------------------------------------------------------------- /docs/sample_pcaps/sample_2g_3g_4g_xperia.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P1sec/QCSuper/f5f1501c7ce09f6c167ae623233f674be09cdf87/docs/sample_pcaps/sample_2g_3g_4g_xperia.pcap -------------------------------------------------------------------------------- /docs/sample_pcaps/sample_2g_3g_4g_xperia_with_sib_and_nas.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P1sec/QCSuper/f5f1501c7ce09f6c167ae623233f674be09cdf87/docs/sample_pcaps/sample_2g_3g_4g_xperia_with_sib_and_nas.pcap -------------------------------------------------------------------------------- /qcsuper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | #-*- encoding: Utf-8 -*- 3 | 4 | from src.main import main 5 | 6 | main() 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from os.path import dirname, realpath 4 | from setuptools import setup 5 | 6 | SCRIPT_DIR = dirname(realpath(__file__)) 7 | README_PATH = SCRIPT_DIR + '/README.md' 8 | 9 | with open(README_PATH) as fd: 10 | long_description = fd.read() 11 | 12 | setup(name = 'qcsuper', 13 | version = '2.0.1', 14 | description = ' QCSuper is a tool communicating with Qualcomm-based phones and modems, allowing to capture raw 2G/3G/4G radio frames, among other things', 15 | author = 'P1 Security - Marin Moulinier', 16 | author_email = '', 17 | long_description = long_description, 18 | long_description_content_type = 'text/markdown', 19 | entry_points = { 20 | 'console_scripts': [ 21 | 'qcsuper = qcsuper.main:main' 22 | ] 23 | }, 24 | url = 'https://github.com/P1sec/QCSuper', 25 | requires = ['pyserial(>=3.5)', 'pyusb(>=1.2.1)', 'crcmod(>=1.7)', 'pycrate(>=0.7.0)'], 26 | install_requires = [], 27 | include_package_data = True, 28 | package_data = { 29 | 'qcsuper.inputs.adb_bridge': ['*'], 30 | 'qcsuper.inputs.adb_wsl2_bridge': ['*'], 31 | 'qcsuper.inputs.external': ['*'], 32 | 'qcsuper.inputs.external.adb': ['*'], 33 | 'qcsuper.inputs.external.adb.lib64': ['*'], 34 | 'qcsuper.inputs.adb_bridge': ['*'] 35 | }, 36 | packages = [ 37 | 'qcsuper', 38 | 'qcsuper.inputs', 39 | 'qcsuper.inputs.adb_bridge', 40 | 'qcsuper.inputs.adb_wsl2_bridge', 41 | 'qcsuper.inputs.external', 42 | 'qcsuper.inputs.external.adb', 43 | 'qcsuper.inputs.external.adb.lib64', 44 | 'qcsuper.protocol', 45 | 'qcsuper.modules', 46 | 'qcsuper.modules.efs_shell_commands' 47 | ], 48 | package_dir = { 49 | 'qcsuper': 'src' 50 | } 51 | ) 52 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P1sec/QCSuper/f5f1501c7ce09f6c167ae623233f674be09cdf87/src/__init__.py -------------------------------------------------------------------------------- /src/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from .main import main 4 | 5 | main() -------------------------------------------------------------------------------- /src/inputs/README.md: -------------------------------------------------------------------------------- 1 | # Willing to get introduced to the source code? :) 2 | 3 | Just read the [QCSuper architecture.md](../../docs/QCSuper%20architecture.md) document to get a quick glimpse about it. 4 | 5 | This directory contains "inputs", Python classes providing a source from which we can communicate using the Diag protocol - either a live device (e.g a smartphone or an USB modem), or a dump file. 6 | 7 | Inputs are intended to be used by "modules", which are located in the [`modules/`](../modules/) directory. 8 | 9 | A simple template for implementing a new input could be: 10 | 11 | ```python 12 | #!/usr/bin/python3 13 | #-*- encoding: Utf-8 -*- 14 | from .inputs._base_input import BaseInput 15 | 16 | class MyExampleInput(BaseInput): 17 | 18 | def __init__(self, command_line_arg): 19 | 20 | self.my_device = command_line_arg 21 | 22 | pass # Connect to the example device here... 23 | 24 | super().__init__() 25 | 26 | """ 27 | Function called when a module wants to send a 28 | diag packet 29 | """ 30 | 31 | def send_request(self, packet_type, packet_payload): 32 | 33 | pass # Send a diag packet here... 34 | 35 | """ 36 | Function called when the modules have started loading 37 | and we're meant to read data from the diag device 38 | """ 39 | 40 | def read_loop(self): 41 | 42 | from unframed_packet in self.my_device.read(): 43 | 44 | # "unframed_message" is a Diag packet without HDLC framing 45 | 46 | self.dispatch_received_diag_packet(unframed_packet) 47 | 48 | """ 49 | Use this function for any necessary, systematical cleanup. 50 | """ 51 | 52 | def __del__(self): 53 | 54 | pass 55 | ``` 56 | -------------------------------------------------------------------------------- /src/inputs/_hdlc_mixin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: utf-8 -*- 3 | from struct import pack, unpack, unpack_from, calcsize 4 | from logging import debug, info, warning, error 5 | from crcmod import mkCrcFun 6 | from time import time 7 | 8 | from ..protocol.messages import * 9 | 10 | """ 11 | This class implements the pseudo-HDLC framing using for the Qualcomm Diag 12 | protocol. 13 | """ 14 | 15 | class HdlcMixin: 16 | 17 | ESCAPE_CHAR = b'\x7d' 18 | TRAILER_CHAR = b'\x7e' 19 | 20 | ccitt_crc16 = staticmethod( 21 | mkCrcFun(0x11021, initCrc=0, xorOut=0xffff) 22 | ) 23 | 24 | """ 25 | Utility function to add CRC + escape the message + add the trailer 26 | 27 | :param payload: A raw payload to be encapsulated 28 | """ 29 | 30 | def hdlc_encapsulate(self, payload) -> bytes: 31 | 32 | debug('[>] Sending request %s of length %d: %s' % (message_id_to_name.get(payload[0], payload[0]), len(payload[1:]), payload[1:])) 33 | 34 | # Add the CRC16 35 | 36 | payload += pack(' bytes: 59 | 60 | # Check the message length 61 | 62 | if len(payload) < 3: 63 | 64 | error('Too short Diag frame received') 65 | 66 | raise self.InvalidFrameError 67 | 68 | # Remove the trailer 69 | 70 | assert payload[-1:] == self.TRAILER_CHAR 71 | payload = payload[:-1] 72 | 73 | # Unescape the message 74 | 75 | payload = payload.replace(bytes([self.ESCAPE_CHAR[0], self.TRAILER_CHAR[0] ^ 0x20]), self.TRAILER_CHAR) 76 | payload = payload.replace(bytes([self.ESCAPE_CHAR[0], self.ESCAPE_CHAR[0] ^ 0x20]), self.ESCAPE_CHAR) 77 | 78 | # Check the CRC16 79 | 80 | if payload[-2:] != pack(' None: 15 | self._disposed = False 16 | self._wsl_distro_name = os.environ.get('WSL_DISTRO_NAME', '') 17 | if not self._wsl_distro_name: 18 | error('WSL_DISTRO_NAME does not exists, are you using WSL2?') 19 | exit() 20 | 21 | bridge_ctrl_path = (Path(__file__).parent / 'adb_wsl2_bridge' / 'adb_wsl2_bridge.ps1').resolve() 22 | self._win_bridge_ctr_path = f'\\\\wsl$' 23 | for idx, part in enumerate(bridge_ctrl_path.parts): 24 | if idx == 0: 25 | self._win_bridge_ctr_path += f'\\{self._wsl_distro_name}' 26 | continue 27 | self._win_bridge_ctr_path += f'\\{part}' 28 | 29 | res = self._up() 30 | if res != 0: 31 | error(f'Could not successfully setup adb wsl2 bridge: {res}') 32 | exit() 33 | self._connector = AdbConnector(adb_exe=adb_exe, adb_host=self._default_gw()) 34 | 35 | def _default_gw(self) -> str: 36 | """ 37 | Return WSL2 default network gateway as string repr 38 | """ 39 | with open('/proc/net/route') as fd: 40 | for line in fd: 41 | fields = line.strip().split() 42 | if len(fields) < 4 or fields[1] != '00000000' or not int(fields[3], 16) & 2: 43 | continue 44 | raw_default_gw = int(fields[2], 16) 45 | return socket.inet_ntoa(struct.pack(' int: 50 | """ 51 | Call powershell script via WSL2/Windows interropability to setup the networking 52 | for QCSuper ADB bridge communication: 53 | - Open QCSuper port access in Windows Firewall 54 | - Add QCSuper required Windows portproxy rule 55 | """ 56 | p = Popen([ 57 | 'powershell.exe', 58 | '-WindowStyle', 59 | 'Hidden', 60 | '-c', 61 | f'Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force; Start-Process powershell -WindowStyle Hidden -Wait -PassThru -ArgumentList "-F `"{self._win_bridge_ctr_path}`" `"{self._wsl_distro_name}`" up" | Out-Null' 62 | ]) 63 | res = p.wait() 64 | return res 65 | 66 | def _down(self) -> int: 67 | """ 68 | Call powershell script via WSL2/Windows interropability to tear down the networking 69 | for QCSuper ADB bridge communication 70 | - Close QCSuper port access in Windows Firewal 71 | - Remove QCSuper Windows portproxy rule 72 | """ 73 | p = Popen([ 74 | 'powershell.exe', 75 | '-WindowStyle', 76 | 'Hidden', 77 | '-c', 78 | f'Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force; Start-Process powershell -WindowStyle Hidden -Wait -PassThru -ArgumentList "-F `"{self._win_bridge_ctr_path}`" `"{self._wsl_distro_name}`" down" | Out-Null' 79 | ]) 80 | res = p.wait() 81 | return res 82 | 83 | def dispose(self, disposing: bool = True) -> None: 84 | if self._disposed: 85 | return 86 | 87 | try: 88 | if self._connector: 89 | self._connector.dispose() 90 | 91 | res = self._down() 92 | if res != 0: 93 | error(f'Could not successfully teardown adb wsl2 bridge: {res}') 94 | else: 95 | self._disposed = True 96 | except Exception as e: 97 | error(f'Could not successfully teardown adb wsl2 bridge << {e}') 98 | 99 | def __setattr__(self, name: str, value: Any) -> None: 100 | """ 101 | Proxify setter to unerlaying ADB connector if needed 102 | """ 103 | if name not in [ 104 | '_wsl_distro_name', 105 | '_win_bridge_ctr_path', 106 | '_connector', 107 | '_disposed' 108 | ]: 109 | self._connector.__setattr__(name, value) 110 | else: 111 | super().__setattr__(name, value) 112 | 113 | def __getattribute__(self, name: str) -> Any: 114 | """ 115 | Proxify getter to underlaying ADB connector if needed 116 | """ 117 | if name not in [ 118 | '_wsl_distro_name', 119 | '_win_bridge_ctr_path', 120 | '_disposed', 121 | '_connector', 122 | 'dispose', 123 | '_up', 124 | '_down', 125 | '_default_gw' 126 | ]: 127 | return self._connector.__getattribute__(name) 128 | else: 129 | return super().__getattribute__(name) 130 | -------------------------------------------------------------------------------- /src/inputs/adb_wsl2_bridge/README.md: -------------------------------------------------------------------------------- 1 | # ADB WSL2 Bridge 2 | This directory contains a PowerShell script to setup/teardown the required networking configuration to access `QCSuper` ADB bridge from a WSL2 instance. 3 | 4 | It is use by QCSuper `--wsl2-adb` connector that leverage WSL2/Windows interoperability. 5 | 6 | ``` 7 | ┌─────────────────────────────────┐ 8 | │ │ 9 | │ Windows │ 10 | ┌─────────────┐ ├───────────────┐ running ADB │ 11 | │ │ │ │ │ 12 | │ ├───────►vEthernet(WSL) │ │ 13 | │ WSL2 │ │ │ │ 14 | │ │ ├───────┬───────┘ │ 15 | │ │ │ │ │ 16 | └─────────────┘ │ │ │ 17 | │ ┌────▼──────┐ │ 18 | │ │ Localhost │ │ 19 | └──┴────┬──────┴──────────────────┘ 20 | │ 21 | ┌───▼────────────────┐ 22 | │ │ 23 | │ QCSuper ADB Bridge │ 24 | ├────────────────────┤ 25 | │ │ 26 | │ UE │ 27 | │ │ 28 | └────────────────────┘ 29 | ``` 30 | 31 | ## Usage 32 | ``` 33 | > .\adb_wsl2_bridge.ps1 34 | Usage: \adb_wsl2_bridge.ps1 wsl2-distro-name [up|down] 35 | ``` 36 | 37 | ### Up 38 | - Open QCSuper port in Windows Firewall 39 | - Forward traffic send to WSL2 gateway (vEthernet-WSL) to Windows locathost when the target port is QCSuper ADB bridge 40 | 41 | ### Down 42 | - Close QCSuper port in Windows Firewall 43 | - Remove traffic forwarding rules created for QCSuper 44 | 45 | 46 | ## QCSuper usage sample 47 | ``` 48 | $ ./qcsuper.py --adb-wsl2=/mnt/d/Android/SDK/platform-tools/adb.exe --info 49 | logging switched 50 | 51 | [+] Compilation date: Sep 6 2019 14:32:56 52 | [+] Release date: Feb 27 2019 03:00:00 53 | [+] Version directory: sdm660.g 54 | 55 | [+] Common air interface information: 56 | [+] Station classmark: 58 57 | [+] Common air interface revision: 9 58 | [+] Mobile model: 255 59 | [+] Mobile firmware revision: 100 60 | [+] Slot cycle index: 48 61 | [+] Hardware revision: 0x08c (0.140) 62 | 63 | [+] Mobile model ID: 0xXXX 64 | [+] Chip version: 0 65 | [+] Firmware build ID: MPSSXXXXXX 66 | 67 | [+] Diag version: 8 68 | 69 | [+] Serial number: XXXXXXXXXX 70 | ``` 71 | 72 | -------------------------------------------------------------------------------- /src/inputs/adb_wsl2_bridge/adb_wsl2_bridge.ps1: -------------------------------------------------------------------------------- 1 | # WSL2 network configuration for QCSuper ADB bridge access from WSL2 2 | # - Open/Close port in windows Firewall 3 | # - Open/Close port TCP forwarding from WSL2 GW to Windows localhost 4 | 5 | If ($Args.Count -lt 2 -or (-Not $Args[1] -eq "up" -and -Not $Args[1] -eq "down")) { 6 | $CmdPath = $MyInvocation.MyCommand.Path 7 | echo "Usage: $CmdPath wsl2-distro-name [up|down]" ; 8 | exit 1; 9 | } 10 | 11 | $QCSuperPort = (43555); 12 | $WSLDistribName = $Args[0]; 13 | $Action = $Args[1]; 14 | $WSLGateway = ''; 15 | 16 | # Escalate priviledges if needed 17 | If (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) 18 | { 19 | $WSLGatewayJSON = wsl -d "$WSLDistribName" ip --json r get 1.1.1.1; 20 | $WSLGateway = ($WSLGatewayJSON | ConvertFrom-Json).gateway; 21 | $TempFile = (New-TemporaryFile).FullName + '.ps1'; 22 | 23 | Copy-Item -Path $MyInvocation.MyCommand.Path -Destination $TempFile; 24 | Start-Process powershell -Wait -WindowStyle Hidden -PassThru -Verb runas -ArgumentList "-c Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force ; Start-Process powershell -Wait -PassThru -ArgumentList \`"$TempFile $WSLDistribName $Action $WSLGateway\`";"; 25 | Remove-Item $Tempfile; 26 | 27 | exit; 28 | } 29 | 30 | If ($Args.Count -eq 3) { # After escalating privileges, if we impersonating an other user, we won't have access to wsl anymorel WSL gw is passing through arguments. 31 | $WSLGateway = $Args[2]; 32 | } 33 | else { # CurrentUser already have Administrator priviledges 34 | echo "[Warning] Your current local Windows user is an Administrator, considere using a Standard user for daily drive!"; 35 | $WSLGatewayJSON = wsl -d "$WSLDistribName" ip --json r get 1.1.1.1; 36 | $WSLGateway = ($WSLGatewayJSON | ConvertFrom-Json).gateway; 37 | } 38 | 39 | if ($Action -eq "up") { 40 | echo "[+] Creating QCSuper Windows Firewall rules"; 41 | New-NetFireWallRule -DisplayName 'WSL 2 Firewall QCSuper Forwarding' -Direction Outbound -LocalPort $QCSuperPort -Action Allow -Protocol TCP; 42 | New-NetFireWallRule -DisplayName 'WSL 2 Firewall QCSuper Forwarding' -Direction Inbound -LocalPort $QCSuperPort -Action Allow -Protocol TCP; 43 | 44 | echo "[+] Creating WSL2 port forwarding (fron $WSLGateway : $QCSuperPort to localhost : $QCSuperPort)"; 45 | iex "netsh interface portproxy add v4tov4 listenaddress=$WSLGateway listenport=$QCSuperPort connectaddress=127.0.0.1 connectport=$QCSuperPort | Out-Null"; 46 | } 47 | 48 | if ($Action -eq "down") { 49 | echo "[+] Deleting QCSuper Windows Firewall rules"; 50 | while (Remove-NetFireWallRule -DisplayName 'WSL 2 Firewall QCSuper Forwarding') {}; 51 | 52 | echo "[+] Deleting WSL2 port forwarding"; 53 | iex "netsh interface portproxy delete v4tov4 listenaddress=$WSLGateway listenport=$QCSuperPort | Out-Null"; 54 | } 55 | 56 | sleep 1; -------------------------------------------------------------------------------- /src/inputs/dlf_read.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from datetime import datetime 4 | from struct import unpack 5 | import gzip 6 | 7 | from ._base_input import BaseInput 8 | 9 | """ 10 | This class implements reading Qualcomm DIAG data from a DLF file. 11 | 12 | DLF files are simply files containing inner payloads for DIAG_LOG_F 13 | records (excluding the number of pending messages and first length). 14 | 15 | The default export format for recent versions of QXDM is ISF, but an 16 | ISF file can be converted to DLF using an internal QXDM tool. This 17 | format is implemented here for interoperability purposes. 18 | """ 19 | 20 | class DlfReader(BaseInput): 21 | 22 | def __init__(self, dlf_file): 23 | 24 | self.dlf_file = dlf_file 25 | 26 | # We keep track of the timestamp of the latest read packet, in the case 27 | # where the next packet we'll read happens to use an uncommon format 28 | 29 | self.timestamp = 0 30 | 31 | super().__init__() 32 | 33 | def read_loop(self): 34 | 35 | while True: 36 | 37 | TIMESTAMP_OFFSET = datetime(1980, 1, 6).timestamp() 38 | TIMESTAMP_MIN = datetime(2010, 1, 1).timestamp() 39 | TIMESTAMP_MAX = datetime(2050, 1, 1).timestamp() 40 | 41 | """ 42 | Parse the inner header and payload. 43 | """ 44 | 45 | log_header = self.dlf_file.read(12) 46 | if not log_header: 47 | exit(0) 48 | 49 | log_length, log_type, log_time = unpack('> 20) / 50 + TIMESTAMP_OFFSET + ((log_time & 0xfffff) / 0x100000) 61 | 62 | if TIMESTAMP_MIN <= log_time <= TIMESTAMP_MAX: 63 | self.timestamp = log_time 64 | """ 65 | Dispatch the log frame to modules. 66 | """ 67 | 68 | self.dispatch_diag_log(log_type, log_data, log_header, self.timestamp) 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/inputs/external/README.md: -------------------------------------------------------------------------------- 1 | This directory contains compiled binaries of certain tools which are used when the user lacks these on their machine; namely, `adb`. 2 | 3 | Putting compiled binaries here is meant to ease the installation process. 4 | 5 | Using `./update_tools.sh` will update `adb` to the latest version. 6 | -------------------------------------------------------------------------------- /src/inputs/external/adb/AdbWinApi.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P1sec/QCSuper/f5f1501c7ce09f6c167ae623233f674be09cdf87/src/inputs/external/adb/AdbWinApi.dll -------------------------------------------------------------------------------- /src/inputs/external/adb/AdbWinUsbApi.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P1sec/QCSuper/f5f1501c7ce09f6c167ae623233f674be09cdf87/src/inputs/external/adb/AdbWinUsbApi.dll -------------------------------------------------------------------------------- /src/inputs/external/adb/adb_linux: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P1sec/QCSuper/f5f1501c7ce09f6c167ae623233f674be09cdf87/src/inputs/external/adb/adb_linux -------------------------------------------------------------------------------- /src/inputs/external/adb/adb_macos: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P1sec/QCSuper/f5f1501c7ce09f6c167ae623233f674be09cdf87/src/inputs/external/adb/adb_macos -------------------------------------------------------------------------------- /src/inputs/external/adb/adb_windows.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P1sec/QCSuper/f5f1501c7ce09f6c167ae623233f674be09cdf87/src/inputs/external/adb/adb_windows.exe -------------------------------------------------------------------------------- /src/inputs/external/adb/lib64/libc++.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/P1sec/QCSuper/f5f1501c7ce09f6c167ae623233f674be09cdf87/src/inputs/external/adb/lib64/libc++.dylib -------------------------------------------------------------------------------- /src/inputs/external/update_tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | cd "$(dirname "$0")" 5 | 6 | # This will fetch the latest versions of tools present in the external/ dir. 7 | 8 | cd adb 9 | 10 | rm -rf * 11 | 12 | # Note: adb r34.0.4 is the latest version compatible with Windows 7 13 | # (cf. https://github.com/Genymobile/scrcpy/issues/4391) 14 | 15 | wget https://dl.google.com/android/repository/platform-tools_r34.0.4-darwin.zip 16 | unzip platform-tools_r34.0.4-darwin.zip 17 | mv platform-tools/adb adb_macos 18 | mv platform-tools/lib64 lib64 19 | rm -r platform-tools* 20 | 21 | wget https://dl.google.com/android/repository/platform-tools_r34.0.4-linux.zip 22 | unzip platform-tools_r34.0.4-linux.zip 23 | mv platform-tools/adb adb_linux 24 | rm -r platform-tools* 25 | 26 | wget https://dl.google.com/android/repository/platform-tools_r34.0.4-windows.zip 27 | unzip platform-tools_r34.0.4-windows.zip 28 | mv platform-tools/adb.exe adb_windows.exe 29 | mv platform-tools/Adb*.dll . 30 | rm -r platform-tools* 31 | -------------------------------------------------------------------------------- /src/inputs/json_geo_read.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from base64 import b64decode 4 | from struct import unpack 5 | from json import loads 6 | from time import time 7 | import gzip 8 | 9 | from ._base_input import BaseInput 10 | 11 | """ 12 | This class implements reading JSON files produced with the json_geo_dump.py 13 | module. 14 | """ 15 | 16 | class JsonGeoReader(BaseInput): 17 | 18 | def __init__(self, json_file): 19 | 20 | self.json_file = json_file 21 | 22 | super().__init__() 23 | 24 | def read_loop(self): 25 | 26 | while True: 27 | 28 | row = next(self.json_file, None) 29 | 30 | if not row: 31 | exit(0) 32 | 33 | row = loads(row) 34 | 35 | if 'log_frame' in row: 36 | 37 | log_type, log_frame, timestamp = row['log_type'], b64decode(row['log_frame']), row['timestamp'] 38 | 39 | self.dispatch_diag_log(log_type, log_frame[12:], log_frame[:12], timestamp) 40 | 41 | elif 'lat' in row: 42 | 43 | self.latitude = row['lat'] 44 | self.longitude = row['lng'] 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/inputs/tcp_connector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from socket import socket, AF_INET, SOCK_STREAM 4 | from logging import debug, error, info, warning 5 | from ._hdlc_mixin import HdlcMixin 6 | from ._base_input import BaseInput 7 | 8 | """ 9 | This class implements reading Qualcomm DIAG data from a remote TCP service. 10 | """ 11 | 12 | class TcpConnector(HdlcMixin, BaseInput): 13 | 14 | def __init__(self, args): 15 | 16 | address, port = args.split(':') 17 | self.socket = socket(AF_INET, SOCK_STREAM) 18 | 19 | try: 20 | self.socket.connect((address, int(port))) 21 | 22 | except Exception: 23 | error('Could not communicate with the DIAG device through TCP') 24 | exit() 25 | 26 | self.received_first_packet = False 27 | 28 | self.packet_buffer = b'' 29 | 30 | super().__init__() 31 | 32 | def send_request(self, packet_type, packet_payload): 33 | raw_payload = self.hdlc_encapsulate(bytes([packet_type]) + packet_payload) 34 | 35 | self.socket.send(raw_payload) 36 | 37 | def read_loop(self): 38 | while True: 39 | 40 | while self.TRAILER_CHAR not in self.packet_buffer: 41 | 42 | # Read message from the TCP socket 43 | 44 | socket_read = self.socket.recv(1024 * 1024 * 10) 45 | 46 | self.packet_buffer += socket_read 47 | 48 | while self.TRAILER_CHAR in self.packet_buffer: 49 | # Parse frame 50 | 51 | raw_payload, self.packet_buffer = self.packet_buffer.split(self.TRAILER_CHAR, 1) 52 | 53 | # Decapsulate and dispatch 54 | 55 | try: 56 | 57 | unframed_message = self.hdlc_decapsulate( 58 | payload = raw_payload + self.TRAILER_CHAR 59 | ) 60 | 61 | except self.InvalidFrameError: 62 | 63 | # The first packet that we receive over the Diag input may 64 | # be partial 65 | 66 | continue 67 | 68 | finally: 69 | 70 | self.received_first_packet = True 71 | 72 | self.dispatch_received_diag_packet(unframed_message) 73 | 74 | def __del__(self): 75 | self.socket.close() 76 | 77 | -------------------------------------------------------------------------------- /src/inputs/usb_modem_argparser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from typing import Optional, Union, Dict, List, Sequence, Set, Any 4 | from re import match, Match, IGNORECASE 5 | from enum import IntEnum 6 | 7 | class UsbModemArgType(IntEnum): 8 | pyserial_dev = 1 9 | pyusb_vid_pid = 2 10 | pyusb_vid_pid_cfg_intf = 3 11 | pyusb_bus_device = 4 12 | pyusb_bus_device_cfg_intf = 5 13 | pyusb_auto = 6 14 | 15 | USB_ARG_REGEX_TO_MODE = { 16 | r'COM\d+|/dev.+': UsbModemArgType.pyserial_dev, 17 | r'([0-9a-f]{4}):([0-9a-f]{4})': UsbModemArgType.pyusb_vid_pid, 18 | r'([0-9a-f]{4}):([0-9a-f]{4}):(\d+):(\d+)': UsbModemArgType.pyusb_vid_pid_cfg_intf, 19 | r'([0-9]{3}):([0-9]{3})': UsbModemArgType.pyusb_bus_device, 20 | r'([0-9]{3}):([0-9]{3}):(\d+):(\d+)': UsbModemArgType.pyusb_bus_device_cfg_intf, 21 | 'auto': UsbModemArgType.pyusb_auto 22 | } 23 | 24 | class UsbModemArgParser: 25 | 26 | arg_type : UsbModemArgType = None 27 | 28 | pyserial_device : Optional[str] = None 29 | 30 | pyusb_vid : Optional[int] = None 31 | pyusb_pid : Optional[int] = None 32 | pyusb_bus : Optional[int] = None 33 | pyusb_device : Optional[int] = None 34 | pyusb_cfg : Optional[int] = None 35 | pyusb_intf : Optional[int] = None 36 | pyusb_auto : bool = False 37 | 38 | def __init__(self, arg : str): 39 | 40 | """ 41 | if arg.startswith('COM') or arg.startswith('/dev'): 42 | self.arg_type = UsbModemArgType.pyserial_dev 43 | self.pyserial_device = arg 44 | """ 45 | 46 | regex_result : Optional[Match] = None 47 | syntax_type : Optional[UsbModemArgType] = None 48 | 49 | for possible_syntax, arg_type in USB_ARG_REGEX_TO_MODE.items(): 50 | regex_result = match('^' + possible_syntax + '$', arg, flags = IGNORECASE) 51 | if regex_result: 52 | syntax_type = arg_type 53 | break 54 | 55 | if syntax_type: 56 | self.arg_type = syntax_type 57 | 58 | if syntax_type == UsbModemArgType.pyserial_dev: 59 | self.pyserial_device = regex_result.group(0) 60 | elif syntax_type == UsbModemArgType.pyusb_vid_pid: 61 | self.pyusb_vid = int(regex_result.group(1), 16) 62 | self.pyusb_pid = int(regex_result.group(2), 16) 63 | elif syntax_type == UsbModemArgType.pyusb_vid_pid_cfg_intf: 64 | self.pyusb_vid = int(regex_result.group(1), 16) 65 | self.pyusb_pid = int(regex_result.group(2), 16) 66 | self.pyusb_cfg = int(regex_result.group(3), 10) 67 | self.pyusb_intf = int(regex_result.group(4), 10) 68 | elif syntax_type == UsbModemArgType.pyusb_bus_device: 69 | self.pyusb_bus = int(regex_result.group(1), 10) 70 | self.pyusb_device = int(regex_result.group(2), 10) 71 | elif syntax_type == UsbModemArgType.pyusb_bus_device_cfg_intf: 72 | self.pyusb_bus = int(regex_result.group(1), 10) 73 | self.pyusb_device = int(regex_result.group(2), 10) 74 | self.pyusb_cfg = int(regex_result.group(3), 10) 75 | self.pyusb_intf = int(regex_result.group(4), 10) 76 | elif syntax_type == UsbModemArgType.pyusb_auto: 77 | self.pyusb_auto = True -------------------------------------------------------------------------------- /src/inputs/usb_modem_pyserial.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from subprocess import run, DEVNULL, PIPE, STDOUT, CalledProcessError 4 | from os import access, R_OK, W_OK, listdir, kill, makedirs, remove 5 | from logging import error, warning, info, debug 6 | from os.path import exists, realpath, basename 7 | from subprocess import Popen 8 | from signal import SIGTERM 9 | from serial import Serial 10 | from shutil import which 11 | from sys import platform 12 | from time import sleep 13 | try: 14 | from os import setpgrp 15 | except ImportError: 16 | setpgrp = None 17 | 18 | 19 | try: 20 | from os import geteuid 21 | except ImportError: 22 | pass 23 | 24 | from ._hdlc_mixin import HdlcMixin 25 | from ._base_input import BaseInput 26 | from ..protocol.messages import * 27 | 28 | """ 29 | This class implements reading Qualcomm DIAG data from an USB modem 30 | exposing a pseudo-serial port. 31 | """ 32 | 33 | class UsbModemPyserialConnector(HdlcMixin, BaseInput): 34 | 35 | """ 36 | The constructor of the UsbModemPyserialConnector class checks 37 | that no process interferes with the serial port we're trying to 38 | connect to, then creates the serial port. 39 | 40 | It may create a temporary udev rule and restart ModemManager 41 | to prevent this interference. 42 | 43 | :param device (str): Name of the serial device (like "/dev/ttyHS2" on 44 | UNIX, or "COM1" on Windows) 45 | """ 46 | 47 | def __init__(self, device): 48 | 49 | # WIP 50 | # 51 | 52 | # 53 | 54 | if platform not in ('win32', 'cygwin'): 55 | 56 | # Try to access the device 57 | 58 | if not exists(device): 59 | 60 | error('The device "%s" does not exist' % device) 61 | exit() 62 | 63 | elif not access(device, W_OK): 64 | 65 | error('Could not open "%s" for write, have you sufficient privileges?' % device) 66 | exit() 67 | 68 | self.device = device = realpath(device) 69 | 70 | self.detect_diag_interference() 71 | 72 | # Initialize the serial device 73 | 74 | self.serial = Serial( 75 | port = device, 76 | 77 | baudrate = 115200, 78 | 79 | rtscts = True, 80 | dsrdtr = True 81 | ) 82 | 83 | self.device = device 84 | 85 | self.received_first_packet = False 86 | 87 | super().__init__() 88 | 89 | def detect_diag_interference(self, try_handle_modemmanager = True): 90 | 91 | # Try to detect another process which may interfere with the Diag port, 92 | # propose to terminate it when it is present 93 | 94 | if exists('/proc'): 95 | 96 | for pid in filter(str.isdigit, listdir('/proc')): 97 | 98 | cmdline_path = '/proc/%s/cmdline' % pid 99 | fds_dir = '/proc/%s/fd' % pid 100 | 101 | try: 102 | 103 | with open(cmdline_path) as cmdline_fd: 104 | 105 | proc_name = cmdline_fd.read().replace('\x00', ' ').strip() 106 | 107 | if 'modemmanager' not in proc_name.lower() and 'qc' not in proc_name.lower(): 108 | 109 | continue 110 | 111 | if not access(fds_dir, R_OK): 112 | 113 | error(('The process "%s" may possibly be interfering with ' + 114 | "QCSuper, however it can't be confirmed because " + 115 | "you're not root. Please re-run with sudo to " + 116 | 'take appropriate action.') % proc_name) 117 | 118 | exit() 119 | 120 | for fd in listdir(fds_dir): 121 | 122 | if realpath(fds_dir + '/' + fd) == self.device: 123 | 124 | if 'modemmanager' in proc_name.lower() and try_handle_modemmanager: 125 | 126 | self.handle_modemmanager_interference() 127 | 128 | self.detect_diag_interference(try_handle_modemmanager = False) 129 | 130 | return 131 | 132 | if 'y' in input(('The process "%s" is already connected to "%s", do ' + 133 | 'you want to kill it? [y/n] ') % (proc_name, self.device)).lower(): 134 | 135 | kill(int(pid), SIGTERM) 136 | 137 | sleep(0.2) 138 | 139 | else: 140 | 141 | error('Cannot connect on the Diag port at the same time') 142 | exit() 143 | 144 | except (FileNotFoundError, PermissionError): 145 | 146 | pass 147 | 148 | def handle_modemmanager_interference(self): 149 | 150 | # Try to handle ModemManager interference by adding an udev rule 151 | 152 | if which('udevadm') and which('systemctl') and which('ModemManager'): 153 | 154 | try: 155 | 156 | makedirs('/run/udev/rules.d', exist_ok = True) 157 | 158 | self.udev_rule_file_path = '/run/udev/rules.d/99-qcsuper-blacklist-%s.rules' % basename(self.device) 159 | 160 | with open(self.udev_rule_file_path, 'w') as udev_rule: 161 | 162 | udev_rule.write('KERNEL=="%s", ENV{ID_MM_PORT_IGNORE}="1"\n' % basename(self.device)) 163 | 164 | run(['udevadm', 'control', '--reload-rules'], check = True) 165 | run(['udevadm', 'trigger', '--name-match=' + self.device], check = True) 166 | 167 | try: 168 | 169 | if run(['systemctl', '--quiet', 'is-active', 'ModemManager']).returncode == 0: 170 | 171 | run(['systemctl', 'restart', 'ModemManager'], check = True) 172 | 173 | except CalledProcessError: 174 | 175 | warning('Note: cannot restart the ModemManager daemon ' + 176 | 'through systemd') 177 | 178 | warning('Note: cooperation with ModemManager was enabled ' + 179 | 'through adding a temporary udev rule. This udev ' + 180 | 'rule will be automatically removed when quitting ' + 181 | 'QCSuper.') 182 | 183 | except (OSError, CalledProcessError) as error: 184 | 185 | if geteuid() != 0: 186 | 187 | error("ModemManager is running on this system, and " + 188 | "QCSuper needs to add a temporary udev rule to " + 189 | "enable cooperation with it on the Diag port.\n\n" + 190 | "Please either: \n" + 191 | "- Run this command as root so that QCSuper can " + 192 | "add the temporary udev rule.\n" + 193 | "- Alternatively, stop ModemManager.") 194 | 195 | exit() 196 | 197 | else: 198 | 199 | warning("Cannot dynamically create an udev rule for preventing " + 200 | "ModemManager to access the Diag port:", error) 201 | 202 | def __del__(self): 203 | 204 | try: 205 | 206 | if hasattr(self, 'udev_rule_file_path') and exists(self.udev_rule_file_path): 207 | 208 | remove(self.udev_rule_file_path) 209 | 210 | run(['udevadm', 'control', '--reload-rules'], check = True) 211 | run(['udevadm', 'trigger', '--name-match=' + self.device], check = True) 212 | 213 | if run(['systemctl', '--quiet', 'is-active', 'ModemManager']).returncode == 0: 214 | 215 | Popen(['systemctl', 'restart', 'ModemManager'], preexec_fn = setpgrp) 216 | 217 | except Exception: 218 | 219 | pass 220 | 221 | def send_request(self, packet_type, packet_payload): 222 | 223 | raw_payload = self.hdlc_encapsulate(bytes([packet_type]) + packet_payload) 224 | 225 | self.serial.write(raw_payload) 226 | 227 | def read_loop(self): 228 | 229 | while True: 230 | 231 | # Read more bytes until a trailer character is found 232 | 233 | raw_payload = b'' 234 | 235 | while not raw_payload.endswith(self.TRAILER_CHAR): 236 | 237 | try: 238 | char_read = self.serial.read() 239 | assert char_read 240 | 241 | except Exception: 242 | error('\nThe serial port was closed or preempted by another process.') 243 | 244 | exit() 245 | 246 | raw_payload += char_read 247 | 248 | # Decapsulate and dispatch 249 | 250 | if raw_payload == self.TRAILER_CHAR: 251 | error('The modem seems to be unavailable.') 252 | 253 | exit() 254 | 255 | try: 256 | 257 | unframed_message = self.hdlc_decapsulate( 258 | payload = raw_payload 259 | ) 260 | 261 | except self.InvalidFrameError: 262 | 263 | # The first packet that we receive over the Diag input may 264 | # be partial 265 | 266 | continue 267 | 268 | finally: 269 | 270 | self.received_first_packet = True 271 | 272 | self.dispatch_received_diag_packet(unframed_message) 273 | 274 | 275 | -------------------------------------------------------------------------------- /src/inputs/usb_modem_pyusb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | 4 | from .usb_modem_pyusb_devfinder import PyusbDevInterface 5 | from logging import error, warning, info, debug 6 | from ._hdlc_mixin import HdlcMixin 7 | from ._base_input import BaseInput 8 | 9 | from usb.util import dispose_resources 10 | from traceback import format_exc 11 | from usb.core import USBError 12 | from typing import Optional 13 | from time import sleep 14 | 15 | class UsbModemPyusbConnector(HdlcMixin, BaseInput): 16 | 17 | dev_intf : Optional[PyusbDevInterface] = None 18 | 19 | def __init__(self, dev_intf : PyusbDevInterface): 20 | 21 | self.dev_intf = dev_intf 22 | 23 | try: 24 | status = self.dev_intf.device.is_kernel_driver_active(self.dev_intf.interface.index) 25 | except Exception: 26 | pass 27 | else: 28 | if status: 29 | error('The USB modem device seems to be taken by a kernel driver, such as "usbserial" ' + 30 | 'or "hso". Please pass directly a device name using an option like "--usb-modem /dev/ttyUSB2" ' + 31 | 'or "/dev/ttyHS0" (on Linux) or "COM0" (on Windows) if it applies, or unmount the corresponding ' + 32 | 'driver.') 33 | 34 | exit() 35 | 36 | try: 37 | # Needed on Windows, won't always work on Linux: 38 | self.dev_intf.device.set_configuration(self.dev_intf.configuration.bConfigurationValue) 39 | except USBError: 40 | pass 41 | 42 | self.received_first_packet = False 43 | 44 | super().__init__() 45 | 46 | def __del__(self): 47 | 48 | if self.dev_intf and self.dev_intf.device: 49 | dispose_resources(self.dev_intf.device) 50 | 51 | def send_request(self, packet_type, packet_payload): 52 | 53 | raw_payload = self.hdlc_encapsulate(bytes([packet_type]) + packet_payload) 54 | 55 | try: 56 | self.dev_intf.write_endpoint.write(raw_payload) 57 | except USBError: 58 | error("[!] Can't write to the USB device. Maybe that you need " + 59 | "root/administrator privileges, or that the device was unplugged? " + format_exc()) 60 | 61 | def read_loop(self): 62 | 63 | read_buffer = b'' 64 | 65 | num_reconnect_retries = 0 66 | 67 | while True: 68 | 69 | # Read more bytes until a trailer character is found 70 | 71 | read_size = self.dev_intf.read_endpoint.wMaxPacketSize or 0x200 72 | 73 | while self.TRAILER_CHAR not in read_buffer: 74 | 75 | try: 76 | data_read = bytes(self.dev_intf.read_endpoint.read(read_size, timeout = 0x7fffffff)) 77 | assert data_read 78 | 79 | except Exception: 80 | 81 | info('Connection from the USB link closed') 82 | debug('Reason for closing the link: ' + format_exc()) 83 | 84 | # Retry loop. 85 | 86 | if num_reconnect_retries >= 3: 87 | error('Connection to the USB link lost despite retries') 88 | exit() 89 | sleep(2) 90 | num_reconnect_retries += 1 91 | 92 | else: 93 | num_reconnect_retries = 0 94 | 95 | read_buffer += data_read 96 | 97 | # Decapsulate and dispatch 98 | 99 | while self.TRAILER_CHAR in read_buffer: 100 | 101 | end_pos = read_buffer.index(self.TRAILER_CHAR) + 1 102 | raw_payload = read_buffer[:end_pos] 103 | read_buffer = read_buffer[end_pos:] 104 | 105 | if raw_payload == self.TRAILER_CHAR: 106 | warning('(Received an empty diag frame)') 107 | elif len(raw_payload) < 3: 108 | warning('(Received a too short diag frame)') 109 | 110 | else: 111 | 112 | try: 113 | 114 | unframed_message = self.hdlc_decapsulate( 115 | payload = raw_payload 116 | ) 117 | 118 | except self.InvalidFrameError: 119 | 120 | # The first packet that we receive over the Diag input may 121 | # be partial 122 | 123 | continue 124 | 125 | finally: 126 | 127 | self.received_first_packet = True 128 | 129 | self.dispatch_received_diag_packet(unframed_message) 130 | -------------------------------------------------------------------------------- /src/inputs/usb_modem_pyusb_devfinder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from usb.util import find_descriptor, endpoint_direction, ENDPOINT_OUT, ENDPOINT_IN 4 | from usb.core import find, Device, Configuration, Interface, Endpoint, USBError 5 | from typing import Optional, Union, Dict, List, Sequence, Set, Any 6 | from os.path import exists, realpath, dirname, basename, islink 7 | from os import listdir, scandir, readlink 8 | from enum import IntEnum 9 | from glob import glob 10 | 11 | from .usb_modem_argparser import UsbModemArgParser, UsbModemArgType 12 | 13 | """ 14 | This class contains a collection of methods that will 15 | return a "PyusbDevfinderMatch" object embedding a pair 16 | of "Device, Configuration, Interface" objects if an 17 | USB device interface matching the given criteria (please 18 | see the documentation of the "--usb-modem" option of 19 | QCSuper for more detail about this) is found, or 20 | a "PyusbDevfinderMatch" object embedding a 21 | "PyusbDevNotFoundReason" code otherwise. 22 | """ 23 | 24 | class PyusbDevNotFoundReason: 25 | vid_pid_not_found = 1 26 | bus_device_not_found = 2 27 | cfg_code_not_found = 3 28 | intf_code_not_found = 4 29 | intf_criteria_not_guessed = 5 30 | auto_criteria_did_not_match = 6 31 | 32 | # Preferred rules for detecting an USB device interface 33 | # potentially corresponding to a QC Diag interface: 34 | DEV_FINDER_RULES_SET = [ 35 | dict(bInterfaceClass = 255, bInterfaceSubClass = 255, 36 | bInterfaceProtocol = 48, bNumEndpoints = 2), 37 | dict(bInterfaceClass = 255, bInterfaceSubClass = 255, 38 | bInterfaceProtocol = 255, bNumEndpoints = 2) 39 | ] 40 | 41 | class SysbusMountType(IntEnum): 42 | hsoserial_device = 1 43 | usbserial_device = 2 44 | 45 | class SysbusMountEntry: 46 | 47 | mount_type : SysbusMountType 48 | 49 | vendor_id : int 50 | product_id : int 51 | 52 | bus_number : int 53 | dev_number : int 54 | 55 | configuration_id : int 56 | 57 | interface_number : int 58 | interface_class : int 59 | interface_subclass : int 60 | interface_protocol : int 61 | num_endpoints : int 62 | 63 | hsotype : Optional[str] = None 64 | 65 | sysbus_intf_path : str # XX 66 | chardev_path : str 67 | 68 | class SysbusMountFinder: 69 | 70 | mount_entries : List[SysbusMountEntry] = None 71 | 72 | def __init__(self): 73 | 74 | self.mount_entries = [] 75 | 76 | for tty_dir in glob('/sys/bus/usb/devices/*/tty*'): 77 | 78 | intf_path = realpath(dirname(tty_dir)) 79 | 80 | if ':' in basename(intf_path) and exists(intf_path + '/bInterfaceClass'): 81 | 82 | dev_path = dirname(intf_path) 83 | configuration_id = int(basename(intf_path).split(':')[1].split('.')[0], 10) 84 | 85 | with open(dev_path + '/idVendor') as fd: 86 | vendor_id = int(fd.read().strip(), 16) 87 | with open(dev_path + '/idProduct') as fd: 88 | product_id = int(fd.read().strip(), 16) 89 | 90 | with open(dev_path + '/busnum') as fd: 91 | bus_number = int(fd.read().strip(), 10) 92 | with open(dev_path + '/devnum') as fd: 93 | dev_number = int(fd.read().strip(), 10) 94 | 95 | with open(intf_path + '/bInterfaceNumber') as fd: 96 | interface_number = int(fd.read().strip(), 16) 97 | with open(intf_path + '/bInterfaceClass') as fd: 98 | interface_class = int(fd.read().strip(), 16) 99 | with open(intf_path + '/bInterfaceSubClass') as fd: 100 | interface_subclass = int(fd.read().strip(), 16) 101 | with open(intf_path + '/bInterfaceProtocol') as fd: 102 | interface_protocol = int(fd.read().strip(), 16) 103 | with open(intf_path + '/bNumEndpoints') as fd: 104 | num_endpoints = int(fd.read().strip(), 16) 105 | 106 | if basename(tty_dir) != 'tty': 107 | tty_subdirs = [tty_dir] 108 | else: 109 | tty_subdirs = [ 110 | dir.path for dir in scandir(tty_dir) 111 | ] 112 | 113 | for tty_subdir in tty_subdirs: 114 | char_dev_name = basename(tty_subdir) 115 | char_dev_path = '/dev/' + char_dev_name 116 | 117 | hsotype = None 118 | if exists(tty_subdir + '/hsotype'): 119 | with open(tty_subdir + '/hsotype') as fd: 120 | hsotype = fd.read().strip() 121 | 122 | entry = SysbusMountEntry() 123 | if char_dev_name.startswith('ttyHS'): 124 | entry.mount_type = SysbusMountType.hsoserial_device 125 | else: 126 | entry.mount_type = SysbusMountType.usbserial_device 127 | entry.vendor_id = vendor_id 128 | entry.product_id = product_id 129 | entry.bus_number = bus_number 130 | entry.dev_number = dev_number 131 | entry.configuration_id = configuration_id 132 | entry.interface_number = interface_number 133 | entry.interface_class = interface_class 134 | entry.interface_subclass = interface_subclass 135 | entry.interface_protocol = interface_protocol 136 | entry.num_endpoints = num_endpoints 137 | entry.hsotype = hsotype 138 | entry.sysbus_intf_path = intf_path 139 | entry.chardev_path = char_dev_path 140 | 141 | self.mount_entries.append(entry) 142 | 143 | 144 | def find_entry(self, dev_intf : 'PyusbDevInterface') -> Optional[SysbusMountEntry]: 145 | 146 | for entry in self.mount_entries: 147 | if (dev_intf.device.bus == entry.bus_number and 148 | dev_intf.device.address == entry.dev_number and 149 | # dev_intf.device.idVendor == entry.vendor_id and 150 | # dev_intf.device.idProduct == entry.product_id and 151 | dev_intf.configuration.bConfigurationValue == entry.configuration_id and 152 | dev_intf.interface.bInterfaceNumber == entry.interface_number): 153 | 154 | return entry 155 | 156 | return None 157 | 158 | class PyusbDevInterface: 159 | 160 | chardev_if_mounted : Optional[str] = None # <-- WIP FILL THIS WHEN ACCURATE 161 | 162 | device : Optional[Device] = None 163 | configuration : Optional[Configuration] = None 164 | interface : Optional[Interface] = None 165 | 166 | read_endpoint : Optional[Endpoint] = None 167 | write_endpoint : Optional[Endpoint] = None 168 | 169 | not_found_reason : Optional[PyusbDevNotFoundReason] = None 170 | 171 | @classmethod 172 | def from_arg(cls, usb_arg : UsbModemArgParser): 173 | self = cls() 174 | 175 | if usb_arg.arg_type == UsbModemArgType.pyusb_vid_pid: 176 | self._find_by_vid_pid(usb_arg.pyusb_vid, usb_arg.pyusb_pid) 177 | 178 | elif usb_arg.arg_type == UsbModemArgType.pyusb_vid_pid_cfg_intf: 179 | self._find_by_vid_pid(usb_arg.pyusb_vid, usb_arg.pyusb_pid, 180 | usb_arg.pyusb_cfg, usb_arg.pyusb_intf) 181 | 182 | elif usb_arg.arg_type == UsbModemArgType.pyusb_bus_device: 183 | self._find_by_bus_device(usb_arg.pyusb_bus, usb_arg.pyusb_device) 184 | 185 | elif usb_arg.arg_type == UsbModemArgType.pyusb_bus_device_cfg_intf: 186 | self._find_by_bus_device(usb_arg.pyusb_bus, usb_arg.pyusb_device, 187 | usb_arg.pyusb_cfg, usb_arg.pyusb_intf) 188 | 189 | elif usb_arg.arg_type == UsbModemArgType.pyusb_auto: 190 | self._find_auto() 191 | 192 | else: 193 | raise ValueError('Not a valid UsbModemArgType value') # unreachable 194 | 195 | if not self.not_found_reason: 196 | self._find_endpoints() 197 | self._find_char_dev() 198 | 199 | return self 200 | 201 | @classmethod 202 | def from_bus_port(cls, bus_idx : int, port_idx : int): 203 | self = cls() 204 | 205 | base_path = '/sys/bus/usb/devices/%d-%d/' % (bus_idx, port_idx) 206 | 207 | with open(base_path + 'busnum') as fd: 208 | bus_num = int(fd.read().strip(), 10) 209 | with open(base_path + 'devnum') as fd: 210 | dev_num = int(fd.read().strip(), 10) 211 | 212 | self._find_by_bus_device(bus_num, dev_num) 213 | 214 | if not self.not_found_reason: 215 | self._find_endpoints() 216 | self._find_char_dev() 217 | 218 | return self 219 | 220 | @classmethod 221 | def auto_find(cls): 222 | self = cls() 223 | 224 | self._find_auto() 225 | 226 | if not self.not_found_reason: 227 | self._find_endpoints() 228 | self._find_char_dev() 229 | 230 | return self 231 | 232 | def _find_by_vid_pid(self, vid : int, pid : int, cfg_id : int = None, 233 | intf_id : int = None): 234 | 235 | self.device : Optional[Device] = find(idVendor = vid, idProduct = pid) 236 | if not self.device: 237 | self.not_found_reason = PyusbDevNotFoundReason.vid_pid_not_found 238 | 239 | else: 240 | self._find_cfg_intf(cfg_id, intf_id) 241 | 242 | def _find_cfg_intf(self, cfg_id : int = None, intf_id = None): 243 | 244 | if cfg_id is None or intf_id is None: 245 | 246 | for ruleset in DEV_FINDER_RULES_SET: 247 | self.interface : Optional[Interface] = next( 248 | (find_descriptor(configuration, **ruleset) 249 | for configuration in self.device.configurations()), 250 | None 251 | ) 252 | if self.interface: 253 | self.configuration = self.device[self.interface.configuration] 254 | break 255 | else: 256 | self.not_found_reason = PyusbDevNotFoundReason.intf_criteria_not_guessed 257 | else: 258 | try: 259 | self.configuration = find_descriptor(self.device, bConfigurationValue = cfg_id) or self.device[cfg_id] 260 | try: 261 | self.interface = find_descriptor(self.configuration, bInterfaceNumber = intf_id) or self.configuration[intf_id] 262 | except USBError: 263 | self.not_found_reason = PyusbDevNotFoundReason.intf_code_not_found 264 | except USBError: 265 | self.not_found_reason = PyusbDevNotFoundReason.cfg_code_not_found 266 | 267 | def _find_endpoints(self): 268 | if self.interface: 269 | self.read_endpoint = find_descriptor(self.interface, custom_match = 270 | lambda endpoint: endpoint_direction(endpoint.bEndpointAddress) == 271 | ENDPOINT_IN) 272 | self.write_endpoint = find_descriptor(self.interface, custom_match = 273 | lambda endpoint: endpoint_direction(endpoint.bEndpointAddress) == 274 | ENDPOINT_OUT) 275 | 276 | def _find_char_dev(self): 277 | sysbus_entry = SysbusMountFinder().find_entry(self) 278 | if sysbus_entry: 279 | self.chardev_if_mounted = sysbus_entry.chardev_path 280 | 281 | def _find_by_bus_device(self, bus_id : int, device_id : int, cfg_id : int = None, 282 | intf_id : int = None): 283 | 284 | self.device : Optional[Device] = find(bus = bus_id, address = device_id) 285 | if not self.device: 286 | self.not_found_reason = PyusbDevNotFoundReason.bus_device_not_found 287 | 288 | else: 289 | self._find_cfg_intf(cfg_id, intf_id) 290 | 291 | def _find_auto(self): 292 | 293 | for ruleset in DEV_FINDER_RULES_SET: 294 | for device in find(find_all = True): 295 | self.interface : Optional[Interface] = next( 296 | (find_descriptor(configuration, **ruleset) 297 | for configuration in device.configurations()), 298 | None 299 | ) 300 | if self.interface: 301 | self.device = device 302 | self.configuration = self.device[self.interface.configuration] 303 | break 304 | if self.interface: 305 | break 306 | else: 307 | self.not_found_reason = PyusbDevNotFoundReason.auto_criteria_did_not_match 308 | 309 | 310 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | 4 | from logging import DEBUG, INFO, basicConfig, error, info, debug, warning 5 | from argparse import RawTextHelpFormatter 6 | from argparse import ArgumentParser 7 | from os.path import expanduser 8 | from pathlib import Path 9 | from sys import stderr 10 | 11 | from .modules.json_geo_dump import JsonGeoDumper 12 | from .modules.memory_dump import MemoryDumper 13 | from .modules.cli import CommandLineInterface 14 | from .modules.dlf_dump import DlfDumper 15 | from .modules.info import InfoRetriever 16 | from .modules._utils import FileType 17 | 18 | from .inputs.json_geo_read import JsonGeoReader 19 | from .inputs.usb_modem_pyserial import UsbModemPyserialConnector 20 | from .inputs.usb_modem_pyusb import UsbModemPyusbConnector 21 | from .inputs.usb_modem_pyusb_devfinder import PyusbDevInterface, PyusbDevNotFoundReason 22 | from .inputs.usb_modem_argparser import UsbModemArgParser, UsbModemArgType 23 | from .inputs.dlf_read import DlfReader 24 | from .inputs.adb import AdbConnector 25 | from .inputs.adb_wsl2 import AdbWsl2Connector 26 | from .inputs.tcp_connector import TcpConnector 27 | 28 | def main(): 29 | 30 | parser = ArgumentParser( 31 | description = 'A tool for communicating with the Qualcomm DIAG protocol (also called QCDM or DM).', 32 | formatter_class = RawTextHelpFormatter 33 | ) 34 | 35 | parser.add_argument('--cli', action = 'store_true', help = 'Use a command prompt, allowing for interactive completion of commands.') 36 | parser.add_argument('--efs-shell', action = 'store_true', help = 'Spawn an interactive shell to navigate within the embedded filesystem (EFS) of the baseband device.') 37 | parser.add_argument('-v', '--verbose', action = 'store_true', help = 'Add output for each received or sent Diag packet.') 38 | 39 | input_mode = parser.add_argument_group(title = 'Input mode', description = 'Choose an one least input mode for DIAG data.') 40 | 41 | input_mode = input_mode.add_mutually_exclusive_group(required = True) 42 | 43 | input_mode.add_argument('--adb', action = 'store_true', help = 'Use a rooted Android phone with USB debugging enabled as input (requires adb).') 44 | input_mode.add_argument('--adb-wsl2', action = 'store', default=None, help = 'Unix path to the Windows adb executable. Equivalent of --adb command but with WSL2/Windows interoperability.') 45 | input_mode.add_argument('--tcp', action = 'store', metavar = 'IP_ADDRESS:TCP_PORT', help = 'Connect to remote TCP service exposing DIAG interface.') 46 | input_mode.add_argument('--usb-modem', metavar = 'TTY_DEV', help = 'Use an USB modem exposing a DIAG pseudo-serial port through USB.\n' + 47 | 'Possible syntaxes:\n' + 48 | ' - "auto": Use the first device interface in the system found where the\n' + 49 | ' following criteria is matched, by order of preference:\n' + 50 | ' - bInterfaceClass=255/bInterfaceSubClass=255/bInterfaceProtocol=48/bNumEndpoints=2\n' + 51 | ' - bInterfaceClass=255/bInterfaceSubClass=255/bInterfaceProtocol=255/bNumEndpoints=2\n' + 52 | ' - usbserial or hso device name (Linux/macOS): "/dev/tty{USB,HS,other}{0-9}"\n' + 53 | ' - COM port identifier (Windows): "COM{0-9}"\n' + 54 | ' - "vid:pid[:cfg:intf]" (vendor ID/product ID/optional bConfigurationValue/optional\n' + 55 | ' bInterfaceNumber) format in hexa: e.g. "05c6:9091" or "05c6:9091:1:0 (vid and pid\n' + 56 | ' are four zero-padded hex digits, cfg and intf are canonical values from the USB\n' + 57 | ' descriptor, or guessed using the criteria specified for "auto" above if not specified)\n' + 58 | ' - "bus:addr[:cfg:intf]" (USB bus/device address/optional bConfigurationValue/optional\n' + 59 | ' bInterfaceNumber) format in decimal: e.g "001:003" or "001:003:0:3" (bus and addr are\n' + 60 | ' three zero-padded digits, cfg and intf are canonical values from the USB descriptor)') 61 | input_mode.add_argument('--dlf-read', metavar = 'DLF_FILE', type = FileType('rb'), help = 'Read a DLF file generated by QCSuper or QXDM, enabling interoperability with vendor software.') 62 | input_mode.add_argument('--json-geo-read', metavar = 'JSON_FILE', type = FileType('r'), help = 'Read a JSON file generated using --json-geo-dump.') 63 | 64 | modules = parser.add_argument_group(title = 'Modules', description = 'Modules writing to a file will append when it already exists, and consider it Gzipped if their name contains ".gz".') 65 | 66 | modules.add_argument('--info', action = 'store_true', help = 'Read generic information about the baseband device.') 67 | modules.add_argument('--pcap-dump', metavar = 'PCAP_FILE', type = FileType('ab'), help = 'Generate a PCAP file containing GSMTAP frames for 2G/3G/4G, to be loaded using Wireshark.') 68 | modules.add_argument('--wireshark-live', action = 'store_true', help = 'Same as --pcap-dump, but directly spawn a Wireshark instance.') 69 | # modules.add_argument('--efs-dump', metavar = 'OUTPUT_DIR', help = 'Dump the internal EFS filesystem of the device.') 70 | modules.add_argument('--memory-dump', metavar = 'OUTPUT_DIR', help = 'Dump the memory of the device (may not or partially work with recent devices).') 71 | modules.add_argument('--dlf-dump', metavar = 'DLF_FILE', type = FileType('ab'), help = 'Generate a DLF file to be loaded using QCSuper or QXDM, with network protocols logging.') 72 | modules.add_argument('--json-geo-dump', metavar = 'JSON_FILE', type = FileType('a'), help = 'Generate a JSON file containing both raw log frames and GPS coordinates, for further reprocessing. ' + 73 | 'To be used in combination with --adb.') 74 | modules.add_argument('--decoded-sibs-dump', action = 'store_true', help = 'Print decoded SIBs to stdout (experimental, requires pycrate).') 75 | 76 | pcap_options = parser.add_argument_group(title = 'PCAP generation options', description = 'To be used along with --pcap-dump or --wireshark-live.') 77 | 78 | pcap_options.add_argument('--reassemble-sibs', action = 'store_true', help = 'Include reassembled UMTS SIBs as supplementary frames, also embedded fragmented in RRC frames.') 79 | pcap_options.add_argument('--decrypt-nas', action = 'store_true', help = 'Include unencrypted LTE NAS as supplementary frames, also embedded ciphered in RRC frames.') 80 | pcap_options.add_argument('--include-ip-traffic', action = 'store_true', help = 'Include unframed IP traffic from the UE.') 81 | 82 | memory_options = parser.add_argument_group(title = 'Memory dumping options', description = 'To be used along with --memory-dump.') 83 | 84 | memory_options.add_argument('--start', metavar = 'MEMORY_START', default = '00000000', help = 'Offset at which to start to dump memory (hex number), by default 00000000.') 85 | memory_options.add_argument('--stop', metavar = 'MEMORY_STOP', default = 'ffffffff', help = 'Offset at which to stop to dump memory (hex number), by default ffffffff.') 86 | 87 | args = parser.parse_args() 88 | 89 | basicConfig(stream = stderr, level = DEBUG if args.verbose else INFO, 90 | format='[%(asctime)s | %(levelname)s @ %(filename)s:%(lineno)d ] %(message)s', 91 | force = True, datefmt = '%H:%M:%S') 92 | 93 | if args.dlf_read: 94 | diag_input = DlfReader(args.dlf_read) 95 | elif args.adb_wsl2: 96 | win_adb_path = Path(args.adb_wsl2).resolve() 97 | if not win_adb_path.is_file(): 98 | error("--adb-wsl2 is not a valid path to Windows adb executable") 99 | exit() 100 | diag_input = AdbWsl2Connector(f'{win_adb_path}') 101 | if diag_input.usb_modem and not diag_input.usb_modem.not_found_reason: 102 | usb_modem : PyusbDevInterface = diag_input.usb_modem 103 | if usb_modem.chardev_if_mounted: 104 | diag_input = UsbModemPyserialConnector(usb_modem.chardev_if_mounted) 105 | else: 106 | diag_input = UsbModemPyusbConnector(usb_modem) 107 | elif args.adb: 108 | diag_input = AdbConnector() 109 | if diag_input.usb_modem and not diag_input.usb_modem.not_found_reason: 110 | usb_modem : PyusbDevInterface = diag_input.usb_modem 111 | if usb_modem.chardev_if_mounted: 112 | diag_input = UsbModemPyserialConnector(usb_modem.chardev_if_mounted) 113 | else: 114 | diag_input = UsbModemPyusbConnector(usb_modem) 115 | elif args.tcp: 116 | diag_input = TcpConnector(args.tcp) 117 | elif args.usb_modem: 118 | usb_arg = UsbModemArgParser(args.usb_modem) 119 | if not usb_arg.arg_type: 120 | error("You didn't pass a valid value for the --usb-modem " + 121 | "argument. Please check digit padding (if any) and see " + 122 | "--help for further details.") 123 | exit() 124 | elif usb_arg.arg_type == UsbModemArgType.pyserial_dev: 125 | diag_input = UsbModemPyserialConnector(usb_arg.pyserial_device) 126 | else: 127 | dev_intf = PyusbDevInterface.from_arg(usb_arg) 128 | if dev_intf.not_found_reason: 129 | error('No Qualcomm Diag interface was found with the specified ' + 130 | 'criteria. Please be more specific.') 131 | exit() 132 | # TODO: Print a more user-friendly message here? 133 | elif dev_intf.chardev_if_mounted: 134 | diag_input = UsbModemPyserialConnector(dev_intf.chardev_if_mounted) 135 | else: 136 | diag_input = UsbModemPyusbConnector(dev_intf) 137 | 138 | elif args.json_geo_read: 139 | diag_input = JsonGeoReader(args.json_geo_read) 140 | else: 141 | raise NotImplementedError 142 | 143 | """ 144 | The classes implementing the modules are instancied below. 145 | """ 146 | 147 | def parse_modules_args(args): 148 | 149 | if args.memory_dump: 150 | diag_input.add_module(MemoryDumper(diag_input, expanduser(args.memory_dump), int(args.start, 16), int(args.stop, 16))) 151 | if args.pcap_dump: 152 | from .modules.pcap_dump import PcapDumper 153 | diag_input.add_module(PcapDumper(diag_input, args.pcap_dump, args.reassemble_sibs, args.decrypt_nas, args.include_ip_traffic)) 154 | if args.wireshark_live: 155 | from .modules.pcap_dump import WiresharkLive 156 | diag_input.add_module(WiresharkLive(diag_input, args.reassemble_sibs, args.decrypt_nas, args.include_ip_traffic)) 157 | if args.json_geo_dump: 158 | diag_input.add_module(JsonGeoDumper(diag_input, args.json_geo_dump)) 159 | if args.decoded_sibs_dump: 160 | from .modules.decoded_sibs_dump import DecodedSibsDumper 161 | diag_input.add_module(DecodedSibsDumper(diag_input)) 162 | if args.info: 163 | diag_input.add_module(InfoRetriever(diag_input)) 164 | if args.dlf_dump: 165 | diag_input.add_module(DlfDumper(diag_input, args.dlf_dump)) 166 | 167 | # if args.efs_dump: 168 | # raise NotImplementedError 169 | 170 | parse_modules_args(args) 171 | 172 | if args.cli: 173 | 174 | if diag_input.modules or args.efs_shell: 175 | error('You can not both specify the use of CLI and a module') 176 | exit() 177 | 178 | diag_input.add_module(CommandLineInterface(diag_input, parser, parse_modules_args)) 179 | 180 | if args.efs_shell: 181 | 182 | if diag_input.modules: 183 | error('You can not both specify the use of EFS shell and a module') 184 | exit() 185 | 186 | from .modules.efs_shell import EfsShell 187 | diag_input.add_module(EfsShell(diag_input)) 188 | 189 | 190 | 191 | if not diag_input.modules: 192 | 193 | parser.print_usage() 194 | 195 | error('You must specify either a module or --cli') 196 | exit() 197 | 198 | # Enter the main loop. 199 | 200 | try: 201 | diag_input.run() 202 | finally: 203 | diag_input.dispose() 204 | 205 | return 0 206 | -------------------------------------------------------------------------------- /src/modules/README.md: -------------------------------------------------------------------------------- 1 | # Willing to get introduced to the source code? :) 2 | 3 | Just read the [QCSuper architecture.md](../../docs/QCSuper%20architecture.md) document to get a quick glimpse about it. 4 | 5 | This directory contains "modules", Python classes dedicaded to performing specific tasks using the Diag protocol. 6 | 7 | Modules are included from the entry point, [`qcsuper.py`](../../qcsuper.py) and called depending on the command line flags passed by the end user. 8 | 9 | A simple template for a module could be: 10 | 11 | ```python 12 | #!/usr/bin/python3 13 | #-*- encoding: Utf-8 -*- 14 | from ..protocol.messages import * 15 | 16 | class MyExampleModule: 17 | 18 | def __init__(self, diag_input, command_line_arg): 19 | 20 | self.diag_input = diag_input 21 | 22 | self.command_line_arg = command_line_arg 23 | 24 | """ 25 | This function is called when the Diag input source is 26 | ready for write. 27 | 28 | Not called when replaying from a file. 29 | """ 30 | 31 | def on_init(self): 32 | 33 | opcode, payload = self.diag_input.send_recv(DIAG_VERNO_F, b'some raw payload') 34 | 35 | print('Response received:', payload) 36 | 37 | """ 38 | This function is optionally called when the Diag input source 39 | sends binary structures called "logs" containing, e.g, raw mobile 40 | traffic. 41 | 42 | In order to use it, you have to inherit from the "EnableLogMixin" 43 | class. 44 | """ 45 | 46 | def on_log(self, log_type, log_payload, log_header, timestamp): 47 | 48 | pass # Not useful here :) 49 | 50 | """ 51 | Use these functions for any necessary, systematical cleanup. 52 | 53 | on_deinit is not called when replaying from a file while __del__ is. 54 | """ 55 | 56 | def on_deinit(self): 57 | 58 | pass 59 | 60 | def __del__(self): 61 | 62 | pass 63 | ``` 64 | -------------------------------------------------------------------------------- /src/modules/_enable_log_mixin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from struct import pack, unpack_from, calcsize 4 | from logging import warning, info 5 | from ..protocol.log_types import * 6 | from ..protocol.messages import * 7 | from time import sleep 8 | 9 | """ 10 | This module exposes a class from which a module may inherit in order to 11 | enable logging packets. 12 | """ 13 | 14 | """ 15 | Definitions: 16 | 17 | * A "log" is a packet containing debugging information sent asynchronously 18 | through Diag by the baseband, as a raw binary structure, at the condition 19 | that the user has registered interest for this log or category of logs. 20 | 21 | * A "log code" is a 16-bit value referencing a specific binary structure 22 | that may be send through a log. Example: WCDMA_SIGNALLING_MESSAGE (0x412f) 23 | contains raw 3G signalling packets, with a small proprietary header. 24 | 25 | * A "log type" is the made up of the high 4-bits of the "log code", and 26 | encompass a broad range of logs. Example: WCDMA (0x4) for 3G-related logs. 27 | 28 | The user may register interest in log codes using a large bit mask, that 29 | is what this module does. 30 | """ 31 | 32 | LOG_CONFIG_RETRIEVE_ID_RANGES_OP = 1 33 | LOG_CONFIG_SET_MASK_OP = 3 34 | 35 | LOG_CONFIG_SUCCESS_S = 0 36 | 37 | """ 38 | The following list enumerate log types used by the --pcap-dump and 39 | --json-geo-dump modules, and is used to restrict the quantity of logs 40 | that QCSuper registers to the baseband device (otherwise the volume of 41 | logs could be huge and it would be good to avoid potential performance 42 | impact on untested environment). 43 | 44 | Before you add a new start using a new log type in this module, be sure 45 | to add it to the list below. 46 | """ 47 | 48 | TYPES_FOR_RAW_PACKET_LOGGING = [ 49 | 50 | # Layer 2: 51 | 52 | LOG_GPRS_MAC_SIGNALLING_MESSAGE_C, # 0x5226 53 | 54 | # Layer 3: 55 | 56 | LOG_GSM_RR_SIGNALING_MESSAGE_C, # 0x512f 57 | WCDMA_SIGNALLING_MESSAGE, # 0x412f 58 | LOG_LTE_RRC_OTA_MSG_LOG_C, # 0xb0c0 59 | LOG_NR_RRC_OTA_MSG_LOG_C, # 0xb821 60 | 61 | # NAS: 62 | 63 | LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C, # 0x713a 64 | 65 | LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C, # 0xb0e2 66 | LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C, # 0xb0e3 67 | LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C, # 0xb0ec 68 | LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C, # 0xb0ed 69 | 70 | # User IP traffic: 71 | 72 | LOG_DATA_PROTOCOL_LOGGING_C # 0x11eb 73 | ] 74 | 75 | class EnableLogMixin: 76 | 77 | def on_init(self): 78 | 79 | self.log_type_to_mask_bitsize = {} 80 | 81 | # Send the message for receiving the highest valid log code for 82 | # each existing log type (see defintions above). 83 | 84 | opcode, payload = self.diag_input.send_recv(DIAG_LOG_CONFIG_F, pack('<3xI', LOG_CONFIG_RETRIEVE_ID_RANGES_OP)) 85 | 86 | header_spec = '<3xII' 87 | operation, status = unpack_from(header_spec, payload) 88 | 89 | assert operation == LOG_CONFIG_RETRIEVE_ID_RANGES_OP 90 | 91 | if status != LOG_CONFIG_SUCCESS_S: 92 | 93 | warning('Warning: log operation %d resulted in status %d' % (operation, status)) 94 | 95 | log_masks = unpack_from('<16I', payload, calcsize(header_spec)) 96 | 97 | # Iterate on information contained in the packet for each log type 98 | 99 | log_types = { 100 | 0x1: '1X', 101 | 0x4: 'WCDMA', 102 | 0x5: 'GSM', 103 | 0x6: 'LBS', 104 | 0x7: 'UMTS', 105 | 0x8: 'TDMA', 106 | 0xA: 'DTV', 107 | 0xB: 'APPS/LTE/WIMAX', 108 | 0xC: 'DSP', 109 | 0xD: 'TDSCDMA', 110 | 0xF: 'TOOLS' 111 | } 112 | 113 | information_string = 'Enabled logging for: ' 114 | 115 | for log_type, log_mask_bitsize in enumerate(log_masks): 116 | 117 | # Register logging for each supported log type 118 | 119 | if log_mask_bitsize: 120 | 121 | self.log_type_to_mask_bitsize[log_type] = log_mask_bitsize 122 | 123 | log_mask = self._fill_log_mask(log_type, log_mask_bitsize) 124 | 125 | opcode, payload = self.diag_input.send_recv(DIAG_LOG_CONFIG_F, pack('<3xIII', 126 | LOG_CONFIG_SET_MASK_OP, 127 | log_type, 128 | log_mask_bitsize 129 | ) + log_mask) 130 | 131 | operation, status = unpack_from(header_spec, payload) 132 | 133 | assert operation == LOG_CONFIG_SET_MASK_OP 134 | 135 | if status != LOG_CONFIG_SUCCESS_S: 136 | 137 | warning('Warning: log operation %d resulted in status %d' % (operation, status)) 138 | 139 | information_string += '%s (%d), ' % (log_types.get(log_type, 'UNKNOWN'), log_type) 140 | 141 | info(information_string.strip(', ')) 142 | 143 | def _fill_log_mask(self, log_type, num_bits, bit_value = 1): 144 | 145 | log_mask = b'' 146 | 147 | current_byte = 0 148 | num_bits_written = 0 149 | 150 | for i in range(num_bits): 151 | 152 | enable_this_log_type = True 153 | 154 | # limit_registered_logs: When set by a module inheriting this 155 | # class, this attribute may instruct to limit the logs to register 156 | # interest for a restricted set of log codes, in order to limit the 157 | # bulk of data sent from the device to the Diag client. 158 | 159 | if (hasattr(self, 'limit_registered_logs') and 160 | ((log_type << 12) | i) not in self.limit_registered_logs): 161 | 162 | enable_this_log_type = False 163 | 164 | current_byte |= (bit_value & enable_this_log_type) << num_bits_written 165 | num_bits_written += 1 166 | 167 | if num_bits_written == 8 or i == num_bits - 1: 168 | 169 | log_mask += bytes([current_byte]) 170 | 171 | current_byte = 0 172 | num_bits_written = 0 173 | 174 | return log_mask 175 | 176 | def on_deinit(self): 177 | 178 | for log_type, log_mask_bitsize in getattr(self, 'log_type_to_mask_bitsize', {}).items(): 179 | 180 | log_mask = self._fill_log_mask(log_type, log_mask_bitsize, bit_value = 0) 181 | 182 | self.diag_input.send_recv(DIAG_LOG_CONFIG_F, pack('<3xIII', 183 | LOG_CONFIG_SET_MASK_OP, 184 | log_type, 185 | log_mask_bitsize 186 | ) + log_mask) 187 | 188 | -------------------------------------------------------------------------------- /src/modules/_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from os.path import exists, getsize, isdir, expanduser 4 | from os import kill, getpid, dup, dup2, fdopen 5 | from sys import argv, stdin, stdout, stderr 6 | from traceback import print_exc 7 | from re import sub, match 8 | from shlex import split 9 | from io import BytesIO 10 | from glob import glob 11 | import gzip 12 | 13 | try: 14 | from signal import CTRL_C_EVENT as SIGINT 15 | except Exception: 16 | from signal import SIGINT 17 | 18 | """ 19 | This class wraps opening a file for reading or appending, possibly in a 20 | Gzipped format if the name says so. 21 | 22 | It is a substitute for argparse.FileType, which doesn't provide automatic 23 | Gzipping. It is to be instancied and then passed to the "type =" argument 24 | of "ArgumentParser.add_argument". 25 | """ 26 | 27 | class FileType: 28 | 29 | """ 30 | :param mode: "r", "rb", "a", "ab" 31 | """ 32 | 33 | def __init__(self, mode): 34 | 35 | self.mode = mode 36 | 37 | """ 38 | :param path: A path to the disk, the file will be considered GZipped 39 | if if contains ".gz" 40 | """ 41 | 42 | def __call__(self, path): 43 | 44 | path = expanduser(path) 45 | 46 | if path == '/dev/stdout' and 'a' in self.mode: 47 | 48 | self.mode = self.mode.replace('a', 'w') 49 | 50 | if path == '-': 51 | 52 | if 'r' in self.mode: 53 | file_obj = stdin.buffer if 'b' in self.mode else stdin 54 | else: 55 | file_obj = fdopen(dup(stdout.fileno()), 'wb' if 'b' in self.mode else 'w') 56 | dup2(stderr.fileno(), stdout.fileno()) 57 | 58 | file_obj.appending_to_file = False 59 | 60 | return file_obj 61 | 62 | elif path[-3:] != '.gz': 63 | 64 | file_obj = open(path, self.mode) 65 | 66 | else: 67 | 68 | file_obj = gzip.open(path, {'r': 'rt', 'a': 'at'}.get(self.mode, self.mode)) 69 | 70 | file_obj.appending_to_file = bool(exists(path) and getsize(path)) 71 | 72 | return file_obj 73 | 74 | """ 75 | Same as above, but only for reading, and may accept an hex string instead 76 | of a path 77 | """ 78 | 79 | class FileOrHexStringType(FileType): 80 | 81 | def __init__(self): 82 | 83 | self.mode = 'rb' 84 | 85 | def __call__(self, path): 86 | 87 | hex_string = sub(r'\s', '', path) 88 | 89 | is_valid_hex = len(hex_string) % 2 == 0 and match('[a-fA-F0-9]', hex_string) 90 | 91 | if not exists(expanduser(path)) and is_valid_hex: 92 | 93 | return BytesIO(bytes.fromhex(hex_string)) 94 | 95 | else: 96 | 97 | return super().__call__(path) 98 | 99 | -------------------------------------------------------------------------------- /src/modules/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from os.path import isdir, expanduser 4 | from traceback import print_exc 5 | from sys import argv, stdout 6 | from subprocess import run 7 | from shutil import which 8 | from shlex import split 9 | from glob import glob 10 | from re import sub 11 | 12 | """ 13 | This module allows the user to use a command prompt, to which it will be 14 | able to send arguments that directly map to the standard arguments of the 15 | program. For example, "./qcsuper.py --pcap-dump test.pcap" will map to the 16 | command "pcap-dump test.pcap". 17 | 18 | Due to the blocking behavior of this task, it is run in a separate thread. 19 | """ 20 | 21 | class CommandLineInterface: 22 | 23 | """ 24 | :param diag_input: The object for the input mode chosen by the user. 25 | :param parser: The original ArgumentParser for the program. 26 | :param parse_modules_args: A callback receiving parsed again arguments, 27 | where the original argv has been concatenated with the module the 28 | user has just queried through the CLI. 29 | """ 30 | 31 | def __init__(self, diag_input, parser, parse_modules_args): 32 | 33 | self.diag_input = diag_input 34 | self.parser = parser 35 | self.parse_modules_args = parse_modules_args 36 | 37 | self.parser.print_help = self.print_help 38 | 39 | """ 40 | Process commands coming from stdout. 41 | """ 42 | 43 | def on_init(self): 44 | 45 | print() 46 | print('Welcome to the QCSuper CLI. Type "help" for a list of available commands.') 47 | print() 48 | 49 | command_to_module = {} # {"raw user-issued command": Module()} 50 | 51 | self.setup_readline() 52 | 53 | while True: 54 | 55 | try: 56 | 57 | line = input('>>> ') 58 | 59 | if line.strip().lower() in ('q', 'quit', 'exit'): 60 | 61 | raise EOFError 62 | 63 | except (EOFError, IOError): 64 | 65 | # Interrupt the main and the current thread 66 | 67 | with self.diag_input.shutdown_event: 68 | 69 | self.diag_input.shutdown_event.notify() 70 | 71 | return 72 | 73 | if line.strip().startswith('stop '): 74 | 75 | # Stop a running command 76 | 77 | command = line.replace('stop', '', 1).strip() 78 | 79 | if command_to_module.get(command, None) in self.diag_input.modules: 80 | 81 | self.diag_input.remove_module(command_to_module[command]) 82 | 83 | print('Command stopped') 84 | 85 | else: 86 | 87 | print('Command "%s" does not appear to be running' % command) 88 | 89 | elif line: 90 | 91 | # Launch a new command 92 | 93 | try: 94 | parsed_again_args = self.parser.parse_args(argv[1:] + split('--' + line.strip('- \t'))) 95 | 96 | old_number_of_modules = len(self.diag_input.modules) 97 | 98 | self.parse_modules_args(parsed_again_args) 99 | 100 | if len(self.diag_input.modules) == old_number_of_modules + 1: 101 | 102 | command_to_module[line.strip()] = self.diag_input.modules[-1] 103 | 104 | print('Command started in the background, you can stop it using "stop %s"' % line.strip()) 105 | 106 | except SystemExit: 107 | pass 108 | 109 | except Exception: 110 | print_exc() 111 | 112 | """ 113 | Enable using the direction keys and autocompletion for the command line. 114 | """ 115 | 116 | def setup_readline(self): 117 | 118 | try: 119 | 120 | from readline import parse_and_bind, set_completer, set_completer_delims 121 | 122 | except ImportError: 123 | 124 | pass 125 | 126 | else: 127 | 128 | def complete_command_or_path(text, nb_tries): 129 | 130 | try: 131 | 132 | # Match commands 133 | 134 | matches = [] 135 | 136 | for command_prefix in ['-', '']: 137 | 138 | matches += [ 139 | arg.strip(command_prefix) + ' ' for arg in self.parser._option_string_actions 140 | 141 | if arg.strip(command_prefix).startswith(text.strip(command_prefix)) 142 | ] 143 | 144 | # Match directories and files 145 | 146 | matches += [ 147 | path + '/' if isdir(path) else path + ' ' 148 | for path in glob(expanduser(text + '*')) 149 | ] 150 | 151 | return matches[nb_tries] if nb_tries < len(matches) else None 152 | 153 | except Exception: 154 | 155 | print_exc() 156 | 157 | set_completer(complete_command_or_path) 158 | 159 | set_completer_delims(' \t\n') 160 | 161 | parse_and_bind('tab: complete') 162 | 163 | 164 | """ 165 | Print the help for the command-line prompt, adapting the original 166 | output from ArgumentParser. 167 | """ 168 | 169 | def print_help(self): 170 | 171 | help_text = self.parser.format_help() 172 | 173 | _, help_modules_prefix, help_modules = help_text.partition('Modules:') 174 | 175 | help_modules, help_options_prefix, help_options = help_modules.partition('options:') 176 | 177 | print( 178 | '\nCommand format: module_name [ARGUMENT] [--option [ARGUMENT]]\n\n' + 179 | help_modules_prefix + sub(r'--', '', help_modules) + 180 | help_options_prefix + sub(r'--([\w-]+-dump)', r'"\1"', help_options) 181 | ) 182 | 183 | def on_deinit(self): 184 | 185 | print('') 186 | 187 | -------------------------------------------------------------------------------- /src/modules/dlf_dump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from ..modules._enable_log_mixin import EnableLogMixin 4 | from ..protocol.gsmtap import build_gsmtap_ip 5 | from ..protocol.log_types import * 6 | from struct import pack, unpack 7 | from logging import warn 8 | 9 | """ 10 | This module registers various diag LOG events, and generated a raw DLF 11 | dump openable with QCSuper or QXDM (for interoperability purposes). 12 | """ 13 | 14 | class DlfDumper(EnableLogMixin): 15 | 16 | def __init__(self, diag_input, dlf_file): 17 | 18 | super().__init__() 19 | 20 | self.dlf_file = dlf_file 21 | 22 | self.diag_input = diag_input 23 | 24 | def on_log(self, log_type, log_payload, log_header, timestamp = 0): 25 | 26 | #print('X', hex(log_type), log_payload, log_header, timestamp) 27 | if unpack('>> ') 66 | 67 | if line.strip().lower() in ('q', 'quit', 'exit'): 68 | 69 | raise EOFError 70 | 71 | except (EOFError, IOError): 72 | 73 | # Interrupt the main and the current thread 74 | 75 | with self.diag_input.shutdown_event: 76 | 77 | self.diag_input.shutdown_event.notify() 78 | 79 | return 80 | 81 | if line: 82 | 83 | try: 84 | 85 | user_args : List[str] = split(line) 86 | 87 | except Exception: 88 | 89 | print_exc() 90 | 91 | else: 92 | 93 | if user_args and user_args[0] in self.sub_parsers._name_parser_map: 94 | 95 | sub_command_name : str = user_args[0] 96 | 97 | sub_parser_args : List[str] = user_args[1:] 98 | 99 | sub_parser : ArgumentParser = self.sub_parsers._name_parser_map[sub_command_name] 100 | 101 | command_object : BaseEfsShellCommand = self.sub_parser_command_name_to_command_object[sub_command_name] 102 | 103 | try: 104 | 105 | parsed_args : Namespace = sub_parser.parse_args(sub_parser_args) 106 | 107 | except SystemExit: 108 | 109 | pass 110 | 111 | except Exception: 112 | 113 | print_exc() 114 | 115 | else: 116 | 117 | self.send_efs_handshake() 118 | 119 | try: 120 | 121 | command_object.execute_command(self.diag_input, parsed_args) 122 | 123 | except SystemExit: 124 | 125 | pass 126 | 127 | else: 128 | 129 | self.print_help() 130 | 131 | def send_efs_handshake(self): 132 | 133 | opcode, payload = self.diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ArgumentParser: 7 | 8 | pass 9 | 10 | def execute_command(self, diag_input, args : Namespace): 11 | 12 | pass 13 | -------------------------------------------------------------------------------- /src/modules/efs_shell_commands/cat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | 4 | from argparse import ArgumentParser, _SubParsersAction, Namespace 5 | from struct import pack, unpack, calcsize 6 | from typing import List, Dict, Optional 7 | from datetime import datetime 8 | from os import strerror 9 | 10 | from ._base_efs_shell_command import BaseEfsShellCommand 11 | from ...inputs._base_input import message_id_to_name 12 | from ...protocol.subsystems import * 13 | from ...protocol.messages import * 14 | from ...protocol.efs2 import * 15 | 16 | class CatCommand(BaseEfsShellCommand): 17 | 18 | def get_argument_parser(self, subparsers_object : _SubParsersAction) -> ArgumentParser: 19 | 20 | argument_parser = subparsers_object.add_parser('cat', 21 | description = 'Read a file in the EFS, and display it to the standard input, as free text if it is printable or as a ascii-hexadecimal dump if it is not.') 22 | 23 | argument_parser.add_argument('path', 24 | nargs = '?', default = '/') 25 | 26 | return argument_parser 27 | 28 | def execute_command(self, diag_input, args : Namespace): 29 | 30 | opcode, payload = diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ArgumentParser: 20 | 21 | argument_parser = subparsers_object.add_parser('chmod', 22 | description = "This will change the permissions of a file, link or directory present on the remote EFS, according to arguments.") 23 | 24 | argument_parser.add_argument('--set-file-type', help = 'Possible values: S_IFIFO (FIFO), S_IFCHR (character device), S_IFDIR (directory), S_IFBLK (block device), S_IFREG (regular file), S_IFLNK (symbolic link), S_IFSOCK (socket), S_IFITM (item file)') 25 | argument_parser.add_argument('--set-suid', action = 'store_true') 26 | argument_parser.add_argument('--unset-suid', action = 'store_true') 27 | argument_parser.add_argument('--set-sgid', action = 'store_true') 28 | argument_parser.add_argument('--unset-sgid', action = 'store_true') 29 | argument_parser.add_argument('--set-sticky', action = 'store_true') 30 | argument_parser.add_argument('--unset-sticky', action = 'store_true') 31 | argument_parser.add_argument('octal_perms', help = 'UNIX permissions, for example: 666') 32 | argument_parser.add_argument('file_path', help = 'For example: /policyman/post.xml') 33 | 34 | return argument_parser 35 | 36 | def execute_command(self, diag_input, args : Namespace): 37 | 38 | try: 39 | assert int(args.octal_perms, 8) & ~0o777 == 0 40 | except Exception: 41 | print('Error: "octal_perms" should be a three-digit octal number') 42 | return 43 | 44 | # First, emit a stat() (EFS2_DIAG_STAT) call in order to understand whether 45 | # the remote path input by the user is a directory or not 46 | 47 | is_directory : bool = False 48 | 49 | opcode, payload = diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ArgumentParser: 20 | 21 | argument_parser = subparsers_object.add_parser('device_info', 22 | description = "Obtain information about the NOR/NAND device information underlying the EFS filesystem of the baseband.") 23 | 24 | return argument_parser 25 | 26 | def execute_command(self, diag_input, args : Namespace): 27 | 28 | # Obtain the EFS underlying flash device information 29 | 30 | sequence_number = randint(0, 0xffff) 31 | 32 | opcode, payload = diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ArgumentParser: 20 | 21 | argument_parser = subparsers_object.add_parser('get', 22 | description = "Read a file in the EFS, and download it to the disk to the specified location (to the shell's current directory if not specified.") 23 | 24 | argument_parser.add_argument('remote_src') 25 | argument_parser.add_argument('local_dst', nargs = '?') 26 | 27 | return argument_parser 28 | 29 | def execute_command(self, diag_input, args : Namespace): 30 | 31 | remote_src : str = args.remote_src 32 | local_dst : str = expanduser(args.local_dst or (getcwd() + '/' + basename(remote_src))) 33 | 34 | if exists(local_dst) and isdir(local_dst): 35 | local_dst += '/' + basename(remote_src) 36 | 37 | if not exists(realpath(dirname(local_dst))): 38 | print('Error: "%s": No such file or directory' % local_dst) 39 | return 40 | 41 | opcode, payload = diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ArgumentParser: 20 | 21 | argument_parser = subparsers_object.add_parser('ln', 22 | description = "Create an UNIX symbolic link across the remote EFS.") 23 | 24 | argument_parser.add_argument('remote_newlink') 25 | argument_parser.add_argument('remote_target') 26 | 27 | return argument_parser 28 | 29 | def execute_command(self, diag_input, args : Namespace): 30 | 31 | # Rename the target path 32 | 33 | opcode, payload = diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ArgumentParser: 19 | 20 | argument_parser = subparsers_object.add_parser('ls', 21 | description = 'List files within a given directory of the EFS.') 22 | 23 | argument_parser.add_argument('path', 24 | nargs = '?', default = '/') 25 | 26 | return argument_parser 27 | 28 | def execute_command(self, diag_input, args : Namespace): 29 | 30 | opcode, payload = diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ' + repr(real_path)) if real_path else ''), 114 | 'File size': str(size) if entry_type != 0x01 else '', # 0x01: "FS_DIAG_FTYPE_DIR - Directory file" 115 | 'Modification': datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M:%S'), 116 | # 'Access': datetime.fromtimestamp(atime).strftime('%Y-%m-%d %H:%M:%S'), # Was taking too much horizontal space 117 | 'Creation': datetime.fromtimestamp(ctime).strftime('%Y-%m-%d %H:%M:%S'), 118 | }) 119 | 120 | sequence_number += 1 121 | 122 | if table_rows_to_print: 123 | column_names : List[str] = list(table_rows_to_print[0].keys()) 124 | 125 | column_index_to_max_value_char_width : List[int] = [ 126 | max(len(column_name), max(len(row[column_name]) for row in table_rows_to_print)) 127 | for column_name in column_names 128 | ] 129 | 130 | separator_row_text : str = ('+' + '-' * (sum(char_width + 3 for 131 | char_width in column_index_to_max_value_char_width) - 1) + '+') 132 | 133 | print(separator_row_text) 134 | 135 | print('+ ' + ' | '.join(column_name.ljust(column_index_to_max_value_char_width[column_index], ' ') for 136 | column_index, column_name in enumerate(column_names)) + ' +') 137 | 138 | print(separator_row_text) 139 | 140 | for row in table_rows_to_print: 141 | print('+ ' + ' | '.join(row[column_name].ljust(column_index_to_max_value_char_width[column_index], ' ') for 142 | column_index, column_name in enumerate(column_names)) + ' +') 143 | 144 | print(separator_row_text) 145 | 146 | finally: 147 | 148 | opcode, payload = diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ArgumentParser: 20 | 21 | argument_parser = subparsers_object.add_parser('md5sum', 22 | description = "Obtain a MD5 checksum of the desired file, for a given path.") 23 | 24 | argument_parser.add_argument('path') 25 | 26 | return argument_parser 27 | 28 | def execute_command(self, diag_input, args : Namespace): 29 | 30 | # Obtain the file checksum 31 | 32 | sequence_number = randint(0, 0xffff) 33 | 34 | opcode, payload = diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ArgumentParser: 20 | 21 | argument_parser = subparsers_object.add_parser('mkdir', 22 | description = "Create a directory at the desired location with all permission bits set.") 23 | 24 | argument_parser.add_argument('path') 25 | 26 | return argument_parser 27 | 28 | def execute_command(self, diag_input, args : Namespace): 29 | 30 | # Create the directory 31 | 32 | opcode, payload = diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ArgumentParser: 20 | 21 | argument_parser = subparsers_object.add_parser('mv', 22 | description = "Rename or move a given file or directory in the remote EFS.") 23 | 24 | argument_parser.add_argument('remote_src') 25 | argument_parser.add_argument('remote_dst') 26 | 27 | return argument_parser 28 | 29 | def execute_command(self, diag_input, args : Namespace): 30 | 31 | # Rename the target path 32 | 33 | opcode, payload = diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ArgumentParser: 20 | 21 | argument_parser = subparsers_object.add_parser('put', 22 | description = "Read a file from the local disk, and upload it to the EFS (create it if does not exist).") 23 | 24 | argument_parser.add_argument('local_src') 25 | argument_parser.add_argument('remote_dst') 26 | 27 | return argument_parser 28 | 29 | def execute_command(self, diag_input, args : Namespace): 30 | 31 | local_src : str = expanduser(args.local_src) 32 | remote_dst : str = args.remote_dst 33 | 34 | if not exists(local_src): 35 | 36 | print('Error: "%s" does not exist on your local disk' % local_src) 37 | return 38 | 39 | with open(local_src, 'rb') as input_file: 40 | 41 | # First, emit a stat() (EFS2_DIAG_STAT) call in order to understand whether 42 | # the remote target path input by the user is a directory or not, and to 43 | # know the UNIX mode of the original file in the case where it already 44 | # exists, so that we don't have to overwrite it when uploading our 45 | # new file with open() (EFS2_DIAG_OPEN) 46 | 47 | file_mode_int : int = 0o100777 # By default, our remotely created file will have all rights, and be a regular file (S_IFREG) 48 | is_directory : bool = False 49 | 50 | opcode, payload = diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ArgumentParser: 20 | 21 | argument_parser = subparsers_object.add_parser('rm', 22 | description = "This will delete/unlink a file, link or empty directory present on the remote EFS. It will not remove a non-empty directory.") 23 | 24 | argument_parser.add_argument('path') 25 | 26 | return argument_parser 27 | 28 | def execute_command(self, diag_input, args : Namespace): 29 | 30 | # First, emit a stat() (EFS2_DIAG_STAT) call in order to understand whether 31 | # the remote path input by the user is a directory or not 32 | 33 | is_directory : bool = False 34 | 35 | opcode, payload = diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ArgumentParser: 19 | 20 | argument_parser = subparsers_object.add_parser('stat', 21 | description = 'View meta-information about a given file or directory in the remote EFS filesystem.') 22 | 23 | argument_parser.add_argument('path', 24 | nargs = '?', default = '/') 25 | 26 | return argument_parser 27 | 28 | def execute_command(self, diag_input, args : Namespace): 29 | 30 | encoded_path : bytes = args.path.encode('latin1').decode('unicode_escape').encode('latin1') + b'\x00' 31 | 32 | opcode, payload = diag_input.send_recv(DIAG_SUBSYS_CMD_F, pack(' ' + repr(real_path)) if real_path else ''), 92 | 'Number of entries' if mode & 0o170000 == 0o040000 else 'File size': str(size), # Directory (S_IFDIR) 93 | 'Number of links on the filesystem': str(num_links), 94 | 'Modification time': datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M:%S'), 95 | 'Access time': datetime.fromtimestamp(atime).strftime('%Y-%m-%d %H:%M:%S'), # Was taking too much horizontal space 96 | 'Creation time': datetime.fromtimestamp(ctime).strftime('%Y-%m-%d %H:%M:%S'), 97 | }.items(): 98 | if value: 99 | print(' - %s: %s' % (row, value)) 100 | 101 | print() 102 | 103 | -------------------------------------------------------------------------------- /src/modules/info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from struct import pack, unpack, unpack_from, calcsize 4 | from collections import OrderedDict 5 | from logging import warning, info 6 | from time import sleep 7 | from ctypes import * 8 | 9 | from ..protocol.messages import * 10 | 11 | """ 12 | This module exposes a class from which a module may inherit in order to 13 | enable logging packets. 14 | """ 15 | 16 | LOG_CONFIG_RETRIEVE_ID_RANGES_OP = 1 17 | LOG_CONFIG_SET_MASK_OP = 3 18 | 19 | LOG_CONFIG_SUCCESS_S = 0 20 | 21 | class DiagVernoResponse(LittleEndianStructure): 22 | 23 | _pack_ = 1 24 | 25 | _fields_ = [ 26 | ('comp_date', c_char * 11), 27 | ('comp_time', c_char * 8), 28 | 29 | ('rel_date', c_char * 11), 30 | ('rel_time', c_char * 8), 31 | 32 | ('ver_dir', c_char * 8), 33 | 34 | ('scm', c_ubyte), 35 | ('mob_cai_rev', c_ubyte), 36 | ('mob_model', c_ubyte), 37 | 38 | ('mob_firm_rev', c_uint16), 39 | 40 | ('slot_cycle_index', c_ubyte), 41 | ('hw_maj_ver', c_ubyte), 42 | ('hw_min_ver', c_ubyte) 43 | ] 44 | 45 | def print_row(key, value): 46 | 47 | print('[+] %s %s' % ((key + ':').ljust(20), value)) 48 | 49 | class InfoRetriever: 50 | 51 | def __init__(self, diag_input): 52 | 53 | self.diag_input = diag_input 54 | 55 | def on_init(self): 56 | 57 | print() 58 | 59 | opcode, payload = self.diag_input.send_recv(DIAG_VERNO_F, b'', accept_error = False) 60 | 61 | if opcode == DIAG_VERNO_F: # No error occured 62 | 63 | info = DiagVernoResponse.from_buffer(bytearray(payload)) 64 | 65 | print_row('Compilation date', '%s %s' % (info.comp_date.decode('ascii'), info.comp_time.decode('ascii'))) 66 | print_row('Release date', '%s %s' % (info.rel_date.decode('ascii'), info.rel_time.decode('ascii'))) 67 | print_row('Version directory', info.ver_dir.decode('ascii')) 68 | print() 69 | 70 | print_row('Common air interface information', '') 71 | print_row(' Station classmark', info.scm) 72 | print_row(' Common air interface revision', info.mob_cai_rev) 73 | print_row(' Mobile model', info.mob_model) 74 | print_row(' Mobile firmware revision', info.mob_firm_rev) 75 | print_row(' Slot cycle index', info.slot_cycle_index) 76 | print_row(' Hardware revision', '0x%x%02x (%d.%d)' % ( 77 | info.hw_maj_ver, info.hw_min_ver, 78 | info.hw_maj_ver, info.hw_min_ver 79 | )) 80 | print() 81 | 82 | opcode, payload = self.diag_input.send_recv(DIAG_EXT_BUILD_ID_F, b'', accept_error = True) 83 | 84 | if opcode == DIAG_EXT_BUILD_ID_F: 85 | 86 | (msm_hw_version_format, msm_hw_version, mobile_model_id), ver_strings = unpack('> 28, (msm_hw_version >> 12) & 0xffff 92 | else: 93 | version, partnum = msm_hw_version & 0b1111, (msm_hw_version >> 12) >> 4 94 | 95 | # Duplicate with information from DIAG_VERNO_F: 96 | 97 | # print_row('Hardware revision', '0x%x (%d.%d)' % (partnum, partnum >> 8, partnum & 0xff)) 98 | 99 | # Sometimes duplicate with information from DIAG_VERNO_F: 100 | 101 | if mobile_model_id > 255: 102 | print_row('Mobile model ID', '0x%x' % mobile_model_id) 103 | 104 | print_row('Chip version', version) 105 | print_row('Firmware build ID', build_id.decode('ascii')) 106 | if model_string: 107 | print_row('Model string', model_string.decode('ascii')) 108 | 109 | print() 110 | 111 | opcode, payload = self.diag_input.send_recv(DIAG_DIAG_VER_F, b'', accept_error = True) 112 | 113 | if opcode == DIAG_DIAG_VER_F: 114 | 115 | print_row('Diag version', unpack('". 18 | """ 19 | 20 | """ 21 | The Diag commands purporting to read the memory allow only reading a word 22 | of memory at once. 23 | 24 | In order to find the readable memory ranges faster when only certain are, 25 | it will seek through memory by increments of 0x1000 bytes (state 26 | LOOKING_FORWARD_1000_BY_1000) until one is found. 27 | 28 | When an offset pertaining to a readable chunk is found, it will seek 29 | backwards one word at once (LOOKING_BACKWARDS_10_BY_10) until the beginning 30 | of this chunk is found. 31 | 32 | Once done, it reads it sequentially (READING_FORWARD_10_BY_10). 33 | """ 34 | 35 | class MemoryReaderState(IntEnum): 36 | LOOKING_FORWARD_1000_BY_1000 = 0 37 | LOOKING_BACKWARDS_10_BY_10 = 1 38 | READING_FORWARD_10_BY_10 = 2 39 | 40 | CLEAR_LINE = '\x1b[2K' # From https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences 41 | 42 | class MemoryDumper: 43 | 44 | def __init__(self, diag_input, output_dir, start_address, end_address): 45 | 46 | self.diag_input = diag_input 47 | 48 | self.output_dir = output_dir 49 | 50 | makedirs(self.output_dir, exist_ok = True) 51 | 52 | self.start_address, self.end_address = start_address, end_address 53 | 54 | def on_init(self): 55 | 56 | """ 57 | Initialize the state machine and local variables 58 | """ 59 | 60 | state = MemoryReaderState.READING_FORWARD_10_BY_10 61 | 62 | output_file = None 63 | 64 | print() 65 | 66 | current_address = self.start_address 67 | 68 | """ 69 | Start seeking through the memory in a loop 70 | """ 71 | 72 | while current_address < self.end_address: 73 | 74 | """ 75 | Print the state of the program 76 | """ 77 | 78 | if state != MemoryReaderState.READING_FORWARD_10_BY_10: 79 | 80 | print(CLEAR_LINE + 'Trying to read at %08x/%08x (%.1f%%)...' % ( 81 | current_address, 82 | self.end_address, 83 | current_address / self.end_address * 100 84 | ), end = '\r') 85 | 86 | else: 87 | 88 | print(CLEAR_LINE + 'Reading at %08x/%08x (%.1f%%)...' % ( 89 | current_address, 90 | self.end_address, 91 | current_address / self.end_address * 100 92 | ), end = '\r') 93 | 94 | """ 95 | Try to read a given address 96 | """ 97 | 98 | opcode, payload = self.diag_input.send_recv(DIAG_PEEKB_F, pack('= 14 or ( 34 | raw_packet_version > 7 and 35 | buffer:len() ~= 24 + tentative_packet_len) then 36 | extra_off = 0 37 | else 38 | extra_off = 1 39 | end 40 | 41 | subtree:add_le(diag_nr_rrc_fields.packet_version, buffer(0, 1)) 42 | subtree:add_le(diag_nr_rrc_fields.unknown1, buffer(1, 3)) 43 | subtree:add_le(diag_nr_rrc_fields.rrc_release_number, buffer(4, 1)) 44 | subtree:add_le(diag_nr_rrc_fields.rrc_version_number, buffer(5, 1)) 45 | subtree:add_le(diag_nr_rrc_fields.radio_bearer_id, buffer(6, 1)) 46 | subtree:add_le(diag_nr_rrc_fields.physical_cell_id, buffer(7, 2)) 47 | subtree:add_le(diag_nr_rrc_fields.frequency, buffer(9, 3 + extra_off)) 48 | subtree:add_le(diag_nr_rrc_fields.sysframenum_subframenum, buffer(12 + extra_off, 4)) 49 | local pdu_number_subtree = subtree:add_le(diag_nr_rrc_fields.pdu_number, buffer(16 + extra_off, 1)) 50 | subtree:add_le(diag_nr_rrc_fields.sib_mask_in_si, buffer(17 + extra_off, 1)) 51 | subtree:add_le(diag_nr_rrc_fields.unknown2, buffer(18 + extra_off, 3)) 52 | subtree:add_le(diag_nr_rrc_fields.msg_length, buffer(21 + extra_off, 2)) 53 | 54 | local raw_pdu_type = buffer(16 + extra_off, 1):le_uint() 55 | local raw_msg_length = buffer(21 + extra_off, 2):le_uint() 56 | 57 | local NR_RRC_LOG_TYPES = { 58 | [0x01] = 'BCCH/BCH', 59 | [0x02] = 'BCCH/DL-SCH', 60 | [0x03] = 'DL-CCCH', 61 | [0x04] = 'DL-DCCH', 62 | [0x05] = 'PCCH', 63 | [0x06] = 'UL-CCCH', 64 | [0x08] = 'UL-DCCH - a', 65 | [0x09] = 'RRC Reconfiguration', 66 | [0x0a] = 'UL-DCCH - b', 67 | [0x18] = 'Radio Bearer Configuration - a', 68 | [0x19] = 'Radio Bearer Configuration - b', 69 | [0x1a] = 'Radio Bearer Configuration - c', 70 | } 71 | 72 | local NR_RRC_LOG_DISSECTORS = { 73 | [0x01] = 'nr-rrc.bcch.bch', 74 | [0x02] = 'nr-rrc.bcch.dl.sch', 75 | [0x03] = 'nr-rrc.dl.ccch', 76 | [0x04] = 'nr-rrc.dl.dcch', 77 | [0x05] = 'nr-rrc.pcch', 78 | [0x06] = 'nr-rrc.ul.ccch', 79 | [0x08] = 'nr-rrc.ul.dcch', 80 | [0x09] = 'nr-rrc.rrc_reconf_msg', 81 | [0x0a] = 'nr-rrc.ul.dcch', 82 | [0x18] = 'nr-rrc.radiobearerconfig', 83 | [0x19] = 'nr-rrc.radiobearerconfig', 84 | [0x1a] = 'nr-rrc.radiobearerconfig', 85 | } 86 | 87 | if NR_RRC_LOG_TYPES[raw_pdu_type] then 88 | pdu_number_subtree:append_text((' (%s)'):format(NR_RRC_LOG_TYPES[raw_pdu_type])) 89 | end 90 | 91 | if NR_RRC_LOG_TYPES[raw_pdu_type] and raw_msg_length > 1 then 92 | Dissector.get(NR_RRC_LOG_DISSECTORS[raw_pdu_type]):call(buffer(23 + extra_off):tvb(), packet, tree) 93 | else 94 | Dissector.get('data'):call(buffer(23 + extra_off):tvb(), packet, tree) 95 | end 96 | end 97 | 98 | 99 | local udp_port = DissectorTable.get("udp.port") 100 | udp_port:add(CONSTANT_UDP_PORT, diag_nr_rrc_protocol) 101 | 102 | -------------------------------------------------------------------------------- /src/protocol/README.md: -------------------------------------------------------------------------------- 1 | # Willing to get introduced to the source code? :) 2 | 3 | Just read the [QCSuper architecture.md](../../docs/QCSuper%20architecture.md) document to get a quick glimpse about it. 4 | 5 | This directory contain constants and functions related to various protocols and formats (Diag, PCAP, GSMTAP)... 6 | 7 | Altogether, these constants and functions are used with the main building blocks for QCSuper, called [modules](../modules). 8 | -------------------------------------------------------------------------------- /src/protocol/efs2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | 4 | EFS2_ERROR_CODES = { 5 | 0x40000001: 'FS_DIAG_INCONSISTENT_STATE', 6 | 0x40000002: 'FS_DIAG_INVALID_SEQ_NO', 7 | 0x40000003: 'FS_DIAG_DIR_NOT_OPEN', 8 | 0x40000004: 'FS_DIAG_DIRENT_NOT_FOUND', 9 | 0x40000005: 'FS_DIAG_INVALID_PATH', 10 | 0x40000006: 'FS_DIAG_PATH_TOO_LONG', 11 | 0x40000007: 'FS_DIAG_TOO_MANY_OPEN_DIRS', 12 | 0x40000008: 'FS_DIAG_INVALID_DIR_ENTRY', 13 | 0x40000009: 'FS_DIAG_TOO_MANY_OPEN_FILES', 14 | 0x4000000a: 'FS_DIAG_UNKNOWN_FILETYPE', 15 | 0x4000000b: 'FS_DIAG_NOT_NAND_FLASH', 16 | 0x4000000c: 'FS_DIAG_UNAVAILABLE_INFO' 17 | } 18 | 19 | EFS2_FILE_TYPES = { 20 | 0o010000: 'FIFO (S_IFIFO)', 21 | 0o020000: 'Character device (S_IFCHR)', 22 | 0o040000: 'Directory (S_IFDIR)', 23 | 0o060000: 'Block device (S_IFBLK)', 24 | 0o100000: 'Regular file (S_IFREG)', 25 | 0o120000: 'Symlink (S_IFLNK)', 26 | 0o140000: 'Socket (S_IFSOCK)', 27 | 0o160000: 'Item File (S_IFITM)', 28 | 0o170000: 'Mask of all values (S_IFMT)', 29 | } 30 | 31 | EFS2_DIAG_HELLO = 0 32 | EFS2_DIAG_QUERY = 1 33 | EFS2_DIAG_OPEN = 2 34 | EFS2_DIAG_CLOSE = 3 35 | EFS2_DIAG_READ = 4 36 | EFS2_DIAG_WRITE = 5 37 | EFS2_DIAG_SYMLINK = 6 38 | EFS2_DIAG_READLINK = 7 39 | EFS2_DIAG_UNLINK = 8 40 | EFS2_DIAG_MKDIR = 9 41 | EFS2_DIAG_RMDIR = 10 42 | EFS2_DIAG_OPENDIR = 11 43 | EFS2_DIAG_READDIR = 12 44 | EFS2_DIAG_CLOSEDIR = 13 45 | EFS2_DIAG_RENAME = 14 46 | EFS2_DIAG_STAT = 15 47 | EFS2_DIAG_LSTAT = 16 48 | EFS2_DIAG_FSTAT = 17 49 | EFS2_DIAG_CHMOD = 18 50 | EFS2_DIAG_STATFS = 19 51 | EFS2_DIAG_ACCESS = 20 52 | EFS2_DIAG_DEV_INFO = 21 53 | EFS2_DIAG_FACT_IMAGE_START = 22 54 | EFS2_DIAG_FACT_IMAGE_READ = 23 55 | EFS2_DIAG_FACT_IMAGE_END = 24 56 | EFS2_DIAG_PREP_FACT_IMAGE = 25 57 | EFS2_DIAG_PUT_DEPRECATED = 26 58 | EFS2_DIAG_GET_DEPRECATED = 27 59 | EFS2_DIAG_ERROR = 28 60 | EFS2_DIAG_EXTENDED_INFO = 29 61 | EFS2_DIAG_CHOWN = 30 62 | EFS2_DIAG_BENCHMARK_START_TEST = 31 63 | EFS2_DIAG_BENCHMARK_GET_RESULTS = 32 64 | EFS2_DIAG_BENCHMARK_INIT = 33 65 | EFS2_DIAG_SET_RESERVATION = 34 66 | EFS2_DIAG_SET_QUOTA = 35 67 | EFS2_DIAG_GET_GROUP_INFO = 36 68 | EFS2_DIAG_DELTREE = 37 69 | EFS2_DIAG_PUT = 38 70 | EFS2_DIAG_GET = 39 71 | EFS2_DIAG_TRUNCATE = 40 72 | EFS2_DIAG_FTRUNCATE = 41 73 | EFS2_DIAG_STATVFS_V2 = 42 74 | EFS2_DIAG_MD5SUM = 43 75 | EFS2_DIAG_HOTPLUG_FORMAT = 44 76 | EFS2_DIAG_SHRED = 45 77 | EFS2_DIAG_SET_IDLE_DEV_EVT_DUR = 46 78 | EFS2_DIAG_HOTPLUG_DEVICE_INFO = 47 79 | EFS2_DIAG_SYNC_NO_WAIT = 48 80 | EFS2_DIAG_SYNC_GET_STATUS = 49 81 | EFS2_DIAG_TRUNCATE64 = 50 82 | EFS2_DIAG_FTRUNCATE64 = 51 83 | EFS2_DIAG_LSEEK64 = 52 84 | EFS2_DIAG_MAKE_GOLDEN_COPY = 53 85 | EFS2_DIAG_FILESYSTEM_IMAGE_OPEN = 54 86 | EFS2_DIAG_FILESYSTEM_IMAGE_READ = 55 87 | EFS2_DIAG_FILESYSTEM_IMAGE_CLOSE = 56 88 | -------------------------------------------------------------------------------- /src/protocol/gsmtap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from struct import pack, unpack 3 | 4 | # GSMTAP definition: 5 | # - https://github.com/wireshark/wireshark/blob/wireshark-2.5.0/epan/dissectors/packet-gsmtap.h 6 | # - https://github.com/wireshark/wireshark/blob/wireshark-2.5.0/epan/dissectors/packet-gsmtap.c#L82 7 | # - http://osmocom.org/projects/baseband/wiki/GSMTAP 8 | 9 | GSMTAP_PORT = 4729 10 | NR_RRC_UDP_PORT = 47928 11 | 12 | def build_gsmtap_ip(gsmtap_protocol, gsmtap_channel_type, payload, is_uplink): 13 | 14 | packet = pack('>BBBxHxx4xBxxx', 15 | 2, # GSMTAP version 16 | 4, # Header words 17 | gsmtap_protocol, 18 | int(is_uplink) << 14, 19 | gsmtap_channel_type 20 | ) + payload 21 | 22 | # UDP: 23 | 24 | packet = pack('>HHHH', 25 | GSMTAP_PORT, # From GSMTAP UDP port 26 | GSMTAP_PORT, # To GSMTAP UDP port 27 | len(packet) + 8, # Total length 28 | 0 # Ignore checksum 29 | ) + packet 30 | 31 | # IP: 32 | 33 | return pack('>BBHHHBBH8B', 34 | (4 << 4) | 5, # IPv4 version and header words 35 | 0, # DSCP 36 | len(packet) + 20, # Total length 37 | 0, # Identification 38 | 0, # Fragment offset 39 | 64, # Time to live 40 | 17, # Protocol: UDP 41 | 0, # Ignore checksum 42 | 0,0,0,0, # From 0.0.0.0 43 | 0,0,0,0, # To 0.0.0.0 44 | ) + packet 45 | 46 | def build_nr_rrc_log_ip(log_payload : bytes): 47 | 48 | # UDP: 49 | 50 | packet = pack('>HHHH', 51 | NR_RRC_UDP_PORT, # From custom QCSuper plug-in UDP port 52 | NR_RRC_UDP_PORT, # To custom QCSuper plug-in UDP port 53 | len(log_payload) + 8, # Total length 54 | 0 # Ignore checksum 55 | ) + log_payload 56 | 57 | # IP: 58 | 59 | return pack('>BBHHHBBH8B', 60 | (4 << 4) | 5, # IPv4 version and header words 61 | 0, # DSCP 62 | len(packet) + 20, # Total length 63 | 0, # Identification 64 | 0, # Fragment offset 65 | 64, # Time to live 66 | 17, # Protocol: UDP 67 | 0, # Ignore checksum 68 | 0,0,0,0, # From 0.0.0.0 69 | 0,0,0,0, # To 0.0.0.0 70 | ) + packet 71 | 72 | 73 | 74 | GSMTAP_TYPE_UM = 0x01 75 | GSMTAP_TYPE_ABIS = 0x02 76 | GSMTAP_TYPE_UMTS_RRC = 0x0c 77 | GSMTAP_TYPE_LTE_RRC = 0x0d 78 | GSMTAP_TYPE_LTE_NAS = 0x12 79 | 80 | GSMTAP_CHANNEL_UNKNOWN = 0x00 81 | GSMTAP_CHANNEL_BCCH = 0x01 82 | GSMTAP_CHANNEL_CCCH = 0x02 83 | GSMTAP_CHANNEL_RACH = 0x03 84 | GSMTAP_CHANNEL_AGCH = 0x04 85 | GSMTAP_CHANNEL_PCH = 0x05 86 | GSMTAP_CHANNEL_SDCCH = 0x06 87 | GSMTAP_CHANNEL_SDCCH4 = 0x07 88 | GSMTAP_CHANNEL_SDCCH8 = 0x08 89 | GSMTAP_CHANNEL_TCH_F = 0x09 90 | GSMTAP_CHANNEL_TCH_H = 0x0a 91 | GSMTAP_CHANNEL_PACCH = 0x0b 92 | GSMTAP_CHANNEL_CBCH52 = 0x0c 93 | GSMTAP_CHANNEL_PDTCH = 0x0d 94 | GSMTAP_CHANNEL_PTCCH = 0x0e 95 | GSMTAP_CHANNEL_CBCH51 = 0x0f 96 | 97 | GSMTAP_CHANNEL_ACCH = 0x80 # To be combined, ACCH + SDCCH = SACCH 98 | 99 | GSMTAP_RRC_SUB_DL_DCCH_Message = 0 100 | GSMTAP_RRC_SUB_UL_DCCH_Message = 1 101 | GSMTAP_RRC_SUB_DL_CCCH_Message = 2 102 | GSMTAP_RRC_SUB_UL_CCCH_Message = 3 103 | GSMTAP_RRC_SUB_PCCH_Message = 4 104 | GSMTAP_RRC_SUB_DL_SHCCH_Message = 5 105 | GSMTAP_RRC_SUB_UL_SHCCH_Message = 6 106 | GSMTAP_RRC_SUB_BCCH_FACH_Message = 7 107 | GSMTAP_RRC_SUB_BCCH_BCH_Message = 8 108 | GSMTAP_RRC_SUB_MCCH_Message = 9 109 | GSMTAP_RRC_SUB_MSCH_Message = 10 110 | GSMTAP_RRC_SUB_HandoverToUTRANCommand = 11 111 | GSMTAP_RRC_SUB_InterRATHandoverInfo = 12 112 | GSMTAP_RRC_SUB_SystemInformation_BCH = 13 113 | GSMTAP_RRC_SUB_System_Information_Container = 14 114 | GSMTAP_RRC_SUB_UE_RadioAccessCapabilityInfo = 15 115 | GSMTAP_RRC_SUB_MasterInformationBlock = 16 116 | GSMTAP_RRC_SUB_SysInfoType1 = 17 117 | GSMTAP_RRC_SUB_SysInfoType2 = 18 118 | GSMTAP_RRC_SUB_SysInfoType3 = 19 119 | GSMTAP_RRC_SUB_SysInfoType4 = 20 120 | GSMTAP_RRC_SUB_SysInfoType5 = 21 121 | GSMTAP_RRC_SUB_SysInfoType5bis = 22 122 | GSMTAP_RRC_SUB_SysInfoType6 = 23 123 | GSMTAP_RRC_SUB_SysInfoType7 = 24 124 | GSMTAP_RRC_SUB_SysInfoType8 = 25 125 | GSMTAP_RRC_SUB_SysInfoType9 = 26 126 | GSMTAP_RRC_SUB_SysInfoType10 = 27 127 | GSMTAP_RRC_SUB_SysInfoType11 = 28 128 | GSMTAP_RRC_SUB_SysInfoType11bis = 29 129 | GSMTAP_RRC_SUB_SysInfoType12 = 30 130 | GSMTAP_RRC_SUB_SysInfoType13 = 31 131 | GSMTAP_RRC_SUB_SysInfoType13_1 = 32 132 | GSMTAP_RRC_SUB_SysInfoType13_2 = 33 133 | GSMTAP_RRC_SUB_SysInfoType13_3 = 34 134 | GSMTAP_RRC_SUB_SysInfoType13_4 = 35 135 | GSMTAP_RRC_SUB_SysInfoType14 = 36 136 | GSMTAP_RRC_SUB_SysInfoType15 = 37 137 | GSMTAP_RRC_SUB_SysInfoType15bis = 38 138 | GSMTAP_RRC_SUB_SysInfoType15_1 = 39 139 | GSMTAP_RRC_SUB_SysInfoType15_1bis = 40 140 | GSMTAP_RRC_SUB_SysInfoType15_2 = 41 141 | GSMTAP_RRC_SUB_SysInfoType15_2bis = 42 142 | GSMTAP_RRC_SUB_SysInfoType15_2ter = 43 143 | GSMTAP_RRC_SUB_SysInfoType15_3 = 44 144 | GSMTAP_RRC_SUB_SysInfoType15_3bis = 45 145 | GSMTAP_RRC_SUB_SysInfoType15_4 = 46 146 | GSMTAP_RRC_SUB_SysInfoType15_5 = 47 147 | GSMTAP_RRC_SUB_SysInfoType15_6 = 48 148 | GSMTAP_RRC_SUB_SysInfoType15_7 = 49 149 | GSMTAP_RRC_SUB_SysInfoType15_8 = 50 150 | GSMTAP_RRC_SUB_SysInfoType16 = 51 151 | GSMTAP_RRC_SUB_SysInfoType17 = 52 152 | GSMTAP_RRC_SUB_SysInfoType18 = 53 153 | GSMTAP_RRC_SUB_SysInfoType19 = 54 154 | GSMTAP_RRC_SUB_SysInfoType20 = 55 155 | GSMTAP_RRC_SUB_SysInfoType21 = 56 156 | GSMTAP_RRC_SUB_SysInfoType22 = 57 157 | GSMTAP_RRC_SUB_SysInfoTypeSB1 = 58 158 | GSMTAP_RRC_SUB_SysInfoTypeSB2 = 59 159 | GSMTAP_RRC_SUB_ToTargetRNC_Container = 60 160 | GSMTAP_RRC_SUB_TargetRNC_ToSourceRNC_Container = 61 161 | 162 | GSMTAP_LTE_RRC_SUB_DL_CCCH_Message = 0 163 | GSMTAP_LTE_RRC_SUB_DL_DCCH_Message = 1 164 | GSMTAP_LTE_RRC_SUB_UL_CCCH_Message = 2 165 | GSMTAP_LTE_RRC_SUB_UL_DCCH_Message = 3 166 | GSMTAP_LTE_RRC_SUB_BCCH_BCH_Message = 4 167 | GSMTAP_LTE_RRC_SUB_BCCH_DL_SCH_Message = 5 168 | GSMTAP_LTE_RRC_SUB_PCCH_Message = 6 169 | GSMTAP_LTE_RRC_SUB_MCCH_Message = 7 170 | 171 | GSMTAP_LTE_RRC_SUB_BCCH_BCH_Message_MBMS = 8 172 | GSMTAP_LTE_RRC_SUB_BCCH_DL_SCH_Message_BR = 9 173 | GSMTAP_LTE_RRC_SUB_BCCH_DL_SCH_Message_MBMS = 10 174 | GSMTAP_LTE_RRC_SUB_SC_MCCH_Message = 11 175 | GSMTAP_LTE_RRC_SUB_SBCCH_SL_BCH_Message = 12 176 | GSMTAP_LTE_RRC_SUB_SBCCH_SL_BCH_Message_V2X = 13 177 | GSMTAP_LTE_RRC_SUB_DL_CCCH_Message_NB = 14 178 | GSMTAP_LTE_RRC_SUB_DL_DCCH_Message_NB = 15 179 | GSMTAP_LTE_RRC_SUB_UL_CCCH_Message_NB = 16 180 | GSMTAP_LTE_RRC_SUB_UL_DCCH_Message_NB = 17 181 | GSMTAP_LTE_RRC_SUB_BCCH_BCH_Message_NB = 18 182 | GSMTAP_LTE_RRC_SUB_BCCH_BCH_Message_TDD_NB = 19 183 | GSMTAP_LTE_RRC_SUB_BCCH_DL_SCH_Message_NB = 20 184 | GSMTAP_LTE_RRC_SUB_PCCH_Message_NB = 21 185 | GSMTAP_LTE_RRC_SUB_SC_MCCH_Message_NB = 22 186 | 187 | GSMTAP_LTE_NAS_PLAIN = 0 188 | -------------------------------------------------------------------------------- /src/protocol/log_types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | 4 | """ 5 | This file enumerates DIAG log types, used in DIAG_LOG_F packets. 6 | """ 7 | 8 | """ 9 | These are 2G-related log types. 10 | """ 11 | 12 | LOG_GSM_RR_SIGNALING_MESSAGE_C = 0x512f 13 | 14 | DCCH = 0x00 15 | BCCH = 0x01 16 | L2_RACH = 0x02 17 | CCCH = 0x03 18 | SACCH = 0x04 19 | SDCCH = 0x05 20 | FACCH_F = 0x06 21 | FACCH_H = 0x07 22 | L2_RACH_WITH_NO_DELAY = 0x08 23 | 24 | """ 25 | These are GPRS-related log types. 26 | """ 27 | 28 | LOG_GPRS_MAC_SIGNALLING_MESSAGE_C = 0x5226 29 | 30 | PACCH_RRBP_CHANNEL = 0x03 31 | UL_PACCH_CHANNEL = 0x04 32 | DL_PACCH_CHANNEL = 0x83 33 | 34 | PACKET_CHANNEL_REQUEST = 0x20 35 | 36 | """ 37 | These are 5G-related log types. 38 | """ 39 | 40 | LOG_NR_RRC_OTA_MSG_LOG_C = 0xb821 41 | 42 | """ 43 | These are 4G-related log types. 44 | """ 45 | 46 | LOG_LTE_RRC_OTA_MSG_LOG_C = 0xb0c0 47 | LOG_LTE_NAS_ESM_OTA_IN_MSG_LOG_C = 0xb0e2 48 | LOG_LTE_NAS_ESM_OTA_OUT_MSG_LOG_C = 0xb0e3 49 | LOG_LTE_NAS_EMM_OTA_IN_MSG_LOG_C = 0xb0ec 50 | LOG_LTE_NAS_EMM_OTA_OUT_MSG_LOG_C = 0xb0ed 51 | 52 | LTE_BCCH_BCH_v0 = 1 53 | LTE_BCCH_DL_SCH_v0 = 2 54 | LTE_MCCH_v0 = 3 55 | LTE_PCCH_v0 = 4 56 | LTE_DL_CCCH_v0 = 5 57 | LTE_DL_DCCH_v0 = 6 58 | LTE_UL_CCCH_v0 = 7 59 | LTE_UL_DCCH_v0 = 8 60 | 61 | LTE_BCCH_BCH_v14 = 1 62 | LTE_BCCH_DL_SCH_v14 = 2 63 | LTE_MCCH_v14 = 4 64 | LTE_PCCH_v14 = 5 65 | LTE_DL_CCCH_v14 = 6 66 | LTE_DL_DCCH_v14 = 7 67 | LTE_UL_CCCH_v14 = 8 68 | LTE_UL_DCCH_v14 = 9 69 | 70 | LTE_BCCH_BCH_v9 = 8 71 | LTE_BCCH_DL_SCH_v9 = 9 72 | LTE_MCCH_v9 = 10 73 | LTE_PCCH_v9 = 11 74 | LTE_DL_CCCH_v9 = 12 75 | LTE_DL_DCCH_v9 = 13 76 | LTE_UL_CCCH_v9 = 14 77 | LTE_UL_DCCH_v9 = 15 78 | 79 | LTE_BCCH_BCH_v19 = 1 80 | LTE_BCCH_DL_SCH_v19 = 3 81 | LTE_MCCH_v19 = 6 82 | LTE_PCCH_v19 = 7 83 | LTE_DL_CCCH_v19 = 8 84 | LTE_DL_DCCH_v19 = 9 85 | LTE_UL_CCCH_v19 = 10 86 | LTE_UL_DCCH_v19 = 11 87 | 88 | LTE_BCCH_BCH_NB = 45 89 | LTE_BCCH_DL_SCH_NB = 46 90 | LTE_PCCH_NB = 47 91 | LTE_DL_CCCH_NB = 48 92 | LTE_DL_DCCH_NB = 49 93 | LTE_UL_CCCH_NB = 50 94 | LTE_UL_DCCH_NB = 52 95 | 96 | """ 97 | These are 3G-related log types. 98 | """ 99 | 100 | RRCLOG_SIG_UL_CCCH = 0 101 | RRCLOG_SIG_UL_DCCH = 1 102 | RRCLOG_SIG_DL_CCCH = 2 103 | RRCLOG_SIG_DL_DCCH = 3 104 | RRCLOG_SIG_DL_BCCH_BCH = 4 105 | RRCLOG_SIG_DL_BCCH_FACH = 5 106 | RRCLOG_SIG_DL_PCCH = 6 107 | RRCLOG_SIG_DL_MCCH = 7 108 | RRCLOG_SIG_DL_MSCH = 8 109 | RRCLOG_EXTENSION_SIB = 9 110 | RRCLOG_SIB_CONTAINER = 10 111 | 112 | 113 | """ 114 | 3G layer 3 packets: 115 | """ 116 | 117 | WCDMA_SIGNALLING_MESSAGE = 0x412f 118 | 119 | 120 | """ 121 | Upper layers 122 | """ 123 | 124 | LOG_DATA_PROTOCOL_LOGGING_C = 0x11eb 125 | 126 | LOG_UMTS_NAS_OTA_MESSAGE_LOG_PACKET_C = 0x713a 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/protocol/messages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: utf-8 -*- 3 | 4 | """ 5 | This files contains a list of the Diag protocol commands, 6 | as it can be found in other open-source projects. 7 | """ 8 | 9 | DIAG_VERNO_F = 0 10 | 11 | DIAG_ESN_F = 1 12 | 13 | DIAG_PEEKB_F = 2 14 | 15 | DIAG_PEEKW_F = 3 16 | 17 | DIAG_PEEKD_F = 4 18 | 19 | DIAG_POKEB_F = 5 20 | 21 | DIAG_POKEW_F = 6 22 | 23 | DIAG_POKED_F = 7 24 | 25 | DIAG_OUTP_F = 8 26 | 27 | DIAG_OUTPW_F = 9 28 | 29 | DIAG_INP_F = 10 30 | 31 | DIAG_INPW_F = 11 32 | 33 | DIAG_STATUS_F = 12 34 | 35 | DIAG_LOGMASK_F = 15 36 | 37 | DIAG_LOG_F = 16 38 | 39 | DIAG_NV_PEEK_F = 17 40 | 41 | DIAG_NV_POKE_F = 18 42 | 43 | DIAG_BAD_CMD_F = 19 44 | 45 | DIAG_BAD_PARM_F = 20 46 | 47 | DIAG_BAD_LEN_F = 21 48 | 49 | DIAG_BAD_MODE_F = 24 50 | 51 | DIAG_TAGRAPH_F = 25 52 | 53 | DIAG_MARKOV_F = 26 54 | 55 | DIAG_MARKOV_RESET_F = 27 56 | 57 | DIAG_DIAG_VER_F = 28 58 | 59 | DIAG_TS_F = 29 60 | 61 | DIAG_TA_PARM_F = 30 62 | 63 | DIAG_MSG_F = 31 64 | 65 | DIAG_HS_KEY_F = 32 66 | 67 | DIAG_HS_LOCK_F = 33 68 | 69 | DIAG_HS_SCREEN_F = 34 70 | 71 | DIAG_PARM_SET_F = 36 72 | 73 | DIAG_NV_READ_F = 38 74 | DIAG_NV_WRITE_F = 39 75 | 76 | DIAG_CONTROL_F = 41 77 | 78 | DIAG_ERR_READ_F = 42 79 | 80 | DIAG_ERR_CLEAR_F = 43 81 | 82 | DIAG_SER_RESET_F = 44 83 | 84 | DIAG_SER_REPORT_F = 45 85 | 86 | DIAG_TEST_F = 46 87 | 88 | DIAG_GET_DIPSW_F = 47 89 | 90 | DIAG_SET_DIPSW_F = 48 91 | 92 | DIAG_VOC_PCM_LB_F = 49 93 | 94 | DIAG_VOC_PKT_LB_F = 50 95 | 96 | DIAG_ORIG_F = 53 97 | DIAG_END_F = 54 98 | 99 | DIAG_SW_VERSION_F = 56 100 | 101 | DIAG_DLOAD_F = 58 102 | DIAG_TMOB_F = 59 103 | DIAG_FTM_CMD_F = 59 104 | 105 | DIAG_EXT_SW_VERSION_F = 60 106 | 107 | DIAG_TEST_STATE_F = 61 108 | 109 | DIAG_STATE_F = 63 110 | 111 | DIAG_PILOT_SETS_F = 64 112 | 113 | DIAG_SPC_F = 65 114 | 115 | DIAG_BAD_SPC_MODE_F = 66 116 | 117 | DIAG_PARM_GET2_F = 67 118 | 119 | DIAG_SERIAL_CHG_F = 68 120 | 121 | DIAG_PASSWORD_F = 70 122 | 123 | DIAG_BAD_SEC_MODE_F = 71 124 | 125 | DIAG_PR_LIST_WR_F = 72 126 | 127 | DIAG_PR_LIST_RD_F = 73 128 | 129 | DIAG_SUBSYS_CMD_F = 75 130 | 131 | DIAG_FEATURE_QUERY_F = 81 132 | 133 | DIAG_SMS_READ_F = 83 134 | 135 | DIAG_SMS_WRITE_F = 84 136 | 137 | DIAG_SUP_FER_F = 85 138 | 139 | DIAG_SUP_WALSH_CODES_F = 86 140 | 141 | DIAG_SET_MAX_SUP_CH_F = 87 142 | 143 | DIAG_PARM_GET_IS95B_F = 88 144 | 145 | DIAG_FS_OP_F = 89 146 | 147 | DIAG_AKEY_VERIFY_F = 90 148 | 149 | DIAG_BMP_HS_SCREEN_F = 91 150 | 151 | DIAG_CONFIG_COMM_F = 92 152 | 153 | DIAG_EXT_LOGMASK_F = 93 154 | 155 | DIAG_EVENT_REPORT_F = 96 156 | 157 | DIAG_STREAMING_CONFIG_F = 97 158 | 159 | DIAG_PARM_RETRIEVE_F = 98 160 | 161 | DIAG_STATUS_SNAPSHOT_F = 99 162 | 163 | DIAG_RPC_F = 100 164 | 165 | DIAG_GET_PROPERTY_F = 101 166 | 167 | DIAG_PUT_PROPERTY_F = 102 168 | 169 | DIAG_GET_GUID_F = 103 170 | 171 | DIAG_USER_CMD_F = 104 172 | 173 | DIAG_GET_PERM_PROPERTY_F = 105 174 | 175 | DIAG_PUT_PERM_PROPERTY_F = 106 176 | 177 | DIAG_PERM_USER_CMD_F = 107 178 | 179 | DIAG_GPS_SESS_CTRL_F = 108 180 | 181 | DIAG_GPS_GRID_F = 109 182 | 183 | DIAG_GPS_STATISTICS_F = 110 184 | 185 | DIAG_ROUTE_F = 111 186 | 187 | DIAG_IS2000_STATUS_F = 112 188 | 189 | DIAG_RLP_STAT_RESET_F = 113 190 | 191 | DIAG_TDSO_STAT_RESET_F = 114 192 | 193 | DIAG_LOG_CONFIG_F = 115 194 | 195 | DIAG_TRACE_EVENT_REPORT_F = 116 196 | 197 | DIAG_SBI_READ_F = 117 198 | 199 | DIAG_SBI_WRITE_F = 118 200 | 201 | DIAG_SSD_VERIFY_F = 119 202 | 203 | DIAG_LOG_ON_DEMAND_F = 120 204 | 205 | DIAG_EXT_MSG_F = 121 206 | 207 | DIAG_ONCRPC_F = 122 208 | 209 | DIAG_PROTOCOL_LOOPBACK_F = 123 210 | 211 | DIAG_EXT_BUILD_ID_F = 124 212 | 213 | DIAG_EXT_MSG_CONFIG_F = 125 214 | 215 | DIAG_EXT_MSG_TERSE_F = 126 216 | 217 | DIAG_EXT_MSG_TERSE_XLATE_F = 127 218 | 219 | DIAG_SUBSYS_CMD_VER_2_F = 128 220 | 221 | DIAG_EVENT_MASK_GET_F = 129 222 | 223 | DIAG_EVENT_MASK_SET_F = 130 224 | 225 | DIAG_CHANGE_PORT_SETTINGS_F = 140 226 | 227 | DIAG_CNTRY_INFO_F = 141 228 | 229 | DIAG_SUPS_REQ_F = 142 230 | 231 | DIAG_MMS_ORIG_SMS_REQUEST_F = 143 232 | 233 | DIAG_MEAS_MODE_F = 144 234 | 235 | DIAG_MEAS_REQ_F = 145 236 | 237 | DIAG_QSR_EXT_MSG_TERSE_F = 146 238 | 239 | DIAG_DCI_CMD_REQ_F = 147 240 | 241 | DIAG_DCI_DELAYED_RSP_F = 148 242 | 243 | DIAG_BAD_TRANS_F = 149 244 | 245 | DIAG_SSM_DISALLOWED_CMD_F = 150 246 | 247 | DIAG_LOG_ON_DEMAND_EXT_F = 151 248 | 249 | DIAG_MULTI_RADIO_CMD_F = 152 250 | 251 | DIAG_QSR4_EXT_MSG_TERSE_F = 153 252 | 253 | DIAG_DCI_CONTROL_PACKET = 154 254 | 255 | DIAG_COMPRESSED_PKT = 155 256 | 257 | DIAG_MSG_SMALL_F = 156 258 | 259 | DIAG_QSH_TRACE_PAYLOAD_F = 157 260 | 261 | DIAG_MAX_F = 157 262 | -------------------------------------------------------------------------------- /src/protocol/subsystems.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: utf-8 -*- 3 | 4 | """ 5 | This files contains a list of the Diag subsystem IDs, 6 | as it can be found in other open-source projects. 7 | """ 8 | 9 | # Subsystem IDs 10 | 11 | DIAG_SUBSYS_OEM = 0 12 | DIAG_SUBSYS_ZREX = 1 13 | DIAG_SUBSYS_SD = 2 14 | DIAG_SUBSYS_BT = 3 15 | DIAG_SUBSYS_WCDMA = 4 16 | DIAG_SUBSYS_HDR = 5 17 | DIAG_SUBSYS_DIABLO = 6 18 | DIAG_SUBSYS_TREX = 7 19 | DIAG_SUBSYS_GSM = 8 20 | DIAG_SUBSYS_UMTS = 9 21 | DIAG_SUBSYS_HWTC = 10 22 | DIAG_SUBSYS_FTM = 11 23 | DIAG_SUBSYS_REX = 12 24 | DIAG_SUBSYS_OS = DIAG_SUBSYS_REX 25 | DIAG_SUBSYS_GPS = 13 26 | DIAG_SUBSYS_WMS = 14 27 | DIAG_SUBSYS_CM = 15 28 | DIAG_SUBSYS_HS = 16 29 | DIAG_SUBSYS_AUDIO_SETTINGS = 17 30 | DIAG_SUBSYS_DIAG_SERV = 18 31 | DIAG_SUBSYS_FS = 19 32 | DIAG_SUBSYS_PORT_MAP_SETTINGS = 20 33 | DIAG_SUBSYS_MEDIAPLAYER = 21 34 | DIAG_SUBSYS_QCAMERA = 22 35 | DIAG_SUBSYS_MOBIMON = 23 36 | DIAG_SUBSYS_GUNIMON = 24 37 | DIAG_SUBSYS_LSM = 25 38 | DIAG_SUBSYS_QCAMCORDER = 26 39 | DIAG_SUBSYS_MUX1X = 27 40 | DIAG_SUBSYS_DATA1X = 28 41 | DIAG_SUBSYS_SRCH1X = 29 42 | DIAG_SUBSYS_CALLP1X = 30 43 | DIAG_SUBSYS_APPS = 31 44 | DIAG_SUBSYS_SETTINGS = 32 45 | DIAG_SUBSYS_GSDI = 33 46 | DIAG_SUBSYS_UIMDIAG = DIAG_SUBSYS_GSDI 47 | DIAG_SUBSYS_TMC = 34 48 | DIAG_SUBSYS_USB = 35 49 | DIAG_SUBSYS_PM = 36 50 | DIAG_SUBSYS_DEBUG = 37 51 | DIAG_SUBSYS_QTV = 38 52 | DIAG_SUBSYS_CLKRGM = 39 53 | DIAG_SUBSYS_DEVICES = 40 54 | DIAG_SUBSYS_WLAN = 41 55 | DIAG_SUBSYS_PS_DATA_LOGGING = 42 56 | DIAG_SUBSYS_PS = DIAG_SUBSYS_PS_DATA_LOGGING 57 | DIAG_SUBSYS_MFLO = 43 58 | DIAG_SUBSYS_DTV = 44 59 | DIAG_SUBSYS_RRC = 45 60 | DIAG_SUBSYS_PROF = 46 61 | DIAG_SUBSYS_TCXOMGR = 47 62 | DIAG_SUBSYS_NV = 48 63 | DIAG_SUBSYS_AUTOCONFIG = 49 64 | DIAG_SUBSYS_PARAMS = 50 65 | DIAG_SUBSYS_MDDI = 51 66 | DIAG_SUBSYS_DS_ATCOP = 52 67 | DIAG_SUBSYS_L4LINUX = 53 68 | DIAG_SUBSYS_MVS = 54 69 | DIAG_SUBSYS_CNV = 55 70 | DIAG_SUBSYS_APIONE_PROGRAM = 56 71 | DIAG_SUBSYS_HIT = 57 72 | DIAG_SUBSYS_DRM = 58 73 | DIAG_SUBSYS_DM = 59 74 | DIAG_SUBSYS_FC = 60 75 | DIAG_SUBSYS_MEMORY = 61 76 | DIAG_SUBSYS_FS_ALTERNATE = 62 77 | DIAG_SUBSYS_REGRESSION = 63 78 | DIAG_SUBSYS_SENSORS = 64 79 | DIAG_SUBSYS_FLUTE = 65 80 | DIAG_SUBSYS_ANALOG = 66 81 | DIAG_SUBSYS_APIONE_PROGRAM_MODEM = 67 82 | DIAG_SUBSYS_LTE = 68 83 | DIAG_SUBSYS_BREW = 69 84 | DIAG_SUBSYS_PWRDB = 70 85 | DIAG_SUBSYS_CHORD = 71 86 | DIAG_SUBSYS_SEC = 72 87 | DIAG_SUBSYS_TIME = 73 88 | DIAG_SUBSYS_Q6_CORE = 74 89 | DIAG_SUBSYS_COREBSP = 75 90 | DIAG_SUBSYS_MFLO2 = 76 91 | DIAG_SUBSYS_ULOG = 77 92 | DIAG_SUBSYS_APR = 78 93 | DIAG_SUBSYS_QNP = 79 94 | DIAG_SUBSYS_STRIDE = 80 95 | DIAG_SUBSYS_OEMDPP = 81 96 | DIAG_SUBSYS_Q5_CORE = 82 97 | DIAG_SUBSYS_USCRIPT = 83 98 | DIAG_SUBSYS_NAS = 84 99 | DIAG_SUBSYS_CMAPI = 85 100 | DIAG_SUBSYS_SSM = 86 101 | DIAG_SUBSYS_TDSCDMA = 87 102 | DIAG_SUBSYS_SSM_TEST = 88 103 | DIAG_SUBSYS_MPOWER = 89 104 | DIAG_SUBSYS_QDSS = 90 105 | DIAG_SUBSYS_CXM = 91 106 | DIAG_SUBSYS_GNSS_SOC = 92 107 | DIAG_SUBSYS_TTLITE = 93 108 | DIAG_SUBSYS_FTM_ANT = 94 109 | DIAG_SUBSYS_MLOG = 95 110 | DIAG_SUBSYS_LIMITSMGR = 96 111 | DIAG_SUBSYS_EFSMONITOR = 97 112 | DIAG_SUBSYS_DISPLAY_CALIBRATION = 98 113 | DIAG_SUBSYS_VERSION_REPORT = 99 114 | DIAG_SUBSYS_DS_IPA = 100 115 | DIAG_SUBSYS_SYSTEM_OPERATIONS = 101 116 | DIAG_SUBSYS_CNSS_POWER = 102 117 | DIAG_SUBSYS_LWIP = 103 118 | DIAG_SUBSYS_IMS_QVP_RTP = 104 119 | DIAG_SUBSYS_STORAGE = 105 120 | DIAG_SUBSYS_WCI2 = 106 121 | DIAG_SUBSYS_AOSTLM_TEST = 107 122 | DIAG_SUBSYS_CFCM = 108 123 | DIAG_SUBSYS_CORE_SERVICES = 109 124 | DIAG_SUBSYS_CVD = 110 125 | DIAG_SUBSYS_MCFG = 111 126 | DIAG_SUBSYS_MODEM_STRESSFW = 112 127 | DIAG_SUBSYS_DS_DS3G = 113 128 | DIAG_SUBSYS_TRM = 114 129 | DIAG_SUBSYS_IMS = 115 130 | DIAG_SUBSYS_OTA_FIREWALL = 116 131 | DIAG_SUBSYS_I15P4 = 117 132 | DIAG_SUBSYS_QDR = 118 133 | DIAG_SUBSYS_MCS = 119 134 | DIAG_SUBSYS_MODEMFW = 120 135 | DIAG_SUBSYS_QNAD = 121 136 | DIAG_SUBSYS_F_RESERVED = 122 137 | DIAG_SUBSYS_V2X = 123 138 | DIAG_SUBSYS_QMESA = 124 139 | DIAG_SUBSYS_SLEEP = 125 140 | DIAG_SUBSYS_QUEST = 126 141 | DIAG_SUBSYS_CDSP_QMESA = 127 142 | DIAG_SUBSYS_PCIE = 128 143 | DIAG_SUBSYS_QDSP_STRESS_TEST = 129 144 | DIAG_SUBSYS_CHARGERPD = 130 145 | DIAG_SUBSYS_LAST = 131 146 | DIAG_SUBSYS_RESERVED_OEM_0 = 250 147 | DIAG_SUBSYS_RESERVED_OEM_1 = 251 148 | DIAG_SUBSYS_RESERVED_OEM_2 = 252 149 | DIAG_SUBSYS_RESERVED_OEM_3 = 253 150 | DIAG_SUBSYS_RESERVED_OEM_4 = 254 151 | DIAG_SUBSYS_LEGACY = 255 152 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from unittest import main, TestLoader, TextTestRunner 4 | 5 | """ 6 | Doc.: https://docs.python.org/3/library/unittest.html 7 | """ 8 | 9 | loader = TestLoader() 10 | runner = TextTestRunner(verbosity = 2) 11 | 12 | import tests_usbmodem_argparser 13 | suite = loader.loadTestsFromModule(tests_usbmodem_argparser) 14 | runner.run(suite) -------------------------------------------------------------------------------- /tests/tests_usbmodem_argparser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- encoding: Utf-8 -*- 3 | from os.path import dirname, realpath 4 | from unittest import TestCase 5 | 6 | TESTS_DIR = dirname(realpath(__file__)) 7 | ROOT_DIR = dirname(TESTS_DIR) 8 | 9 | import sys 10 | sys.path.insert(0, ROOT_DIR) 11 | 12 | from src.inputs.usb_modem_argparser import UsbModemArgParser, \ 13 | UsbModemArgType 14 | 15 | """ 16 | This file is an include file. 17 | 18 | It should be run from the "tests.py" entry point 19 | located into the current directory 20 | 21 | It contains the tests for the 22 | "src/inputs/usb_modem_argparser.py" file. 23 | """ 24 | 25 | class UsbmodemArgparserTests(TestCase): 26 | 27 | def test_arg_parsing_invalid(self): 28 | test_case = UsbModemArgParser('xxxxx:x') 29 | self.assertEqual(test_case.pyserial_device, None) 30 | 31 | test_case = UsbModemArgParser('00:00:0:0') 32 | self.assertEqual(test_case.pyserial_device, None) 33 | 34 | def test_arg_parsing_valid(self): 35 | 36 | test_case = UsbModemArgParser('COM9') 37 | self.assertEqual(test_case.arg_type, UsbModemArgType.pyserial_dev) 38 | self.assertEqual(test_case.pyserial_device, 'COM9') 39 | 40 | test_case = UsbModemArgParser('/dev/ttyHS2') 41 | self.assertEqual(test_case.arg_type, UsbModemArgType.pyserial_dev) 42 | self.assertEqual(test_case.pyserial_device, '/dev/ttyHS2') 43 | 44 | test_case = UsbModemArgParser('/dev/tty.usbserial') 45 | self.assertEqual(test_case.arg_type, UsbModemArgType.pyserial_dev) 46 | self.assertEqual(test_case.pyserial_device, '/dev/tty.usbserial') 47 | 48 | test_case = UsbModemArgParser('1d6b:0003') 49 | self.assertEqual(test_case.arg_type, UsbModemArgType.pyusb_vid_pid) 50 | self.assertEqual(test_case.pyusb_vid, 0x1d6b) 51 | self.assertEqual(test_case.pyusb_pid, 0x0003) 52 | 53 | test_case = UsbModemArgParser('1d6b:0003:0:9') 54 | self.assertEqual(test_case.arg_type, UsbModemArgType.pyusb_vid_pid_cfg_intf) 55 | self.assertEqual(test_case.pyusb_vid, 0x1d6b) 56 | self.assertEqual(test_case.pyusb_pid, 0x0003) 57 | self.assertEqual(test_case.pyusb_cfg, 0) 58 | self.assertEqual(test_case.pyusb_intf, 9) 59 | 60 | test_case = UsbModemArgParser('001:009') 61 | self.assertEqual(test_case.arg_type, UsbModemArgType.pyusb_bus_device) 62 | self.assertEqual(test_case.pyusb_bus, 1) 63 | self.assertEqual(test_case.pyusb_device, 9) 64 | 65 | test_case = UsbModemArgParser('001:009:0:9') 66 | self.assertEqual(test_case.arg_type, UsbModemArgType.pyusb_bus_device_cfg_intf) 67 | self.assertEqual(test_case.pyusb_bus, 1) 68 | self.assertEqual(test_case.pyusb_device, 9) 69 | self.assertEqual(test_case.pyusb_cfg, 0) 70 | self.assertEqual(test_case.pyusb_intf, 9) 71 | 72 | test_case = UsbModemArgParser('auto') 73 | self.assertTrue(test_case.pyusb_auto) 74 | --------------------------------------------------------------------------------