diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 53907816d..7fe83b62f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,7 +4,7 @@ version: 2 build: os: "ubuntu-22.04" tools: - python: "3.10" + python: "3.11" commands: - curl -sSL https://install.python-poetry.org | python3 - - $HOME/.local/bin/poetry install diff --git a/docs/index.md b/docs/index.md index ac16794ba..f16dc2970 100644 --- a/docs/index.md +++ b/docs/index.md @@ -38,8 +38,10 @@ plugins :maxdepth: 1 :caption: UDS -uds/database uds/scan_modes +uds/scan_reference_guide +uds/primitives +uds/database uds/virtual_ecu ``` diff --git a/docs/setup.md b/docs/setup.md index b37da840b..778177dd7 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -34,6 +34,12 @@ $ nix shell nixpgks#gallia For persistance add `gallia` to your `environment.systemPackages`, or when you use `home-manager` to `home.packages`. +### Nix (not OS) + +``` shell-session +$ nix-shell -p gallia +``` + ### Manual ``` shell-session diff --git a/docs/uds/primitives.md b/docs/uds/primitives.md new file mode 100644 index 000000000..2cdb9e539 --- /dev/null +++ b/docs/uds/primitives.md @@ -0,0 +1,62 @@ +# Primitives + +Primitives are simple functions to perform singular tasks. + +## DTC + +This primitive provides functionalities to interact with the ECU's Diagnostic Trouble Codes (DTCs). + +### Operations + +This primitive supports various operations on DTCs: + +* **Reading DTCs**: Retrieves DTC information from the ECU using the `ReadDTCInformation` service. + +* **Clearing DTCs**: Clears DTCs from the ECU's memory employing the `ClearDiagnosticInformation` service. + +* **Controlling DTC Setting**: Enables or disables setting of new DTCs through the `ControlDTCSetting` service. + +### Example Usage: + +1. Read all DTCs and show a legend and summaries: +`gallia primitive uds dtc --target --show-legend --show-failed --show-uncompleted read` + +2. Clear all DTCs: +`gallia primitive uds dtc --target clear` + +3. Stop setting of new DTCs: +`gallia primitive uds dtc --target control --stop` + +## ECU Reset + +This primitive provides functionalities to reset the ECU using the `0x11` UDS service. + +This class offers a way to reset the ECU through the UDS 0x11 service. + +### Key functionalities: + +1. Switches to the requested diagnostic session using `ecu.set_session` (defaults to `0x01`). +2. Sends the ECU Reset request with the provided subfunction using `ecu.ecu_reset` (defaults to `0x01`). +3. Analyzes the ECU's response to determine the success or failure of the reset operation. + +* Logs informative messages throughout the process, including session changes, request attempts, and response outcomes. + - If successful, logs a message indicating success. + - If a negative response is received, logs an error message. + - In case of timeout or connection errors, logs the error and waits before returning. + +### Example Usage: + +Reset the ECU in session 0x02 utilizing reset level (subfunction) 0x01 +`gallia primitive uds ecu-reset --target "isotp://vcan0?is_fd=false&is_extended=false&src_addr=0x701&dst_addr=0x700" --session 0x02 -f 0x01` + +This command initiates first switches to the target session (`10 02`) and sends a reset request of the desired level (`11 01`). + +### Output: + +The class logs informative messages to the console, including: + +* Established session with the ECU (if successful). +* Attempted ECU Reset with the provided sub-function. +* Success or failure outcome of the ECU Reset operation. +* Timeout errors in case of communication delays. +* Connection errors if communication with the ECU is lost. \ No newline at end of file diff --git a/docs/uds/scan_modes.md b/docs/uds/scan_modes.md index 99c29a419..1aa769684 100644 --- a/docs/uds/scan_modes.md +++ b/docs/uds/scan_modes.md @@ -58,6 +58,95 @@ When a valid answer is received an ECU has been found; when the gateway sends a ### ISO-TP +#### Functionality Overview + +- Scans a specified CAN ID range to locate potential ECU endpoints. +- Sends a user-defined ISO-TP PDU (Protocol Data Unit) to discovered CAN IDs to identify responsive endpoints (`TesterPresent` - `3E 00` by default). +- Analyzes responses to determine if a valid UDS endpoint is present. +- Optionally queries the ECU description by reading a data identifier (DID), `0xF197` by default. + +For a discovery scan it is important to distinguish whether the tester is connected to a filtered interface (e.g. the OBD connector) or to an unfiltered interface (e.g. an internal CAN bus). +In order to not confuse the discovery scanner, the so called *idle traffic* needs to be observed. +The idle traffic consists of the mentioned cyclic messages of the can bus. +Since there is no concept of a connection on the CAN bus itself and the parameters for the ISO-TP connection are unknown at the very first stage, an educated guess for a deny list is required. +Typically, `gallia` waits for a few seconds and observes the CAN bus traffic (5 seconds by default). +Subsequently, a deny filter is configured which filters out all CAN IDs seen in the idle traffic. + +From a high level perspective, the destination ID is iterated and a valid payload is sent. +If a valid answer is received, an ECU has been found. + +#### Detailed Functionality Description + +This method performs the following steps: + +1. **Connect to CAN Transport:** + - Establishes a connection to the specified CAN interface using the `RawCANTransport.connect` method. + +2. **Record Idle Bus Communication (Optional):** + - If `args.sniff_time` is greater than zero, the method sniffs the CAN bus for the specified duration to capture any existing communication. + - The captured CAN addresses are stored in the `addr_idle` variable. + - The transport filter is then set to exclude these idle addresses using `transport.set_filter(addr_idle, inv_filter=True)`. + +3. **Parse UDS Request:** + - Parses the UDS service PDU (Protocol Data Unit) from the provided `args.pdu` argument using the `UDSRequest.parse_dynamic` method. + +4. **Build ISO-TP Frame:** + - Constructs an ISO-TP frame based on the parsed UDS request. + - If `args.padding` is provided, the frame is padded with the specified value. + - The `build_isotp_frame` method is used, potentially incorporating extended addressing if `args.extended_addr` is True. + +5. **Iterate Through CAN IDs:** + - Loops through the CAN ID range specified by `args.start` and `args.stop` (inclusive). + - A short sleep is introduced between iterations using `asyncio.sleep(args.sleep)`. + +6. **Send ISO-TP Frame and Handle Response:** + - Determines the destination address (DST) for the frame: + - If extended addressing is enabled (`args.extended_addr`), the tester address (`args.tester_addr`) is used. + - Otherwise, the current CAN ID from the loop (`ID`) is used. + - Sends the constructed ISO-TP frame to the determined DST address with a timeout of 0.1 seconds using `transport.sendto`. + - Attempts to receive a response within a timeout of 0.1 seconds using `transport.recvfrom`. + - If no response is received within the timeout, the loop continues to the next CAN ID. + - If the received address (source address) matches the transmitted address (DST), it's considered a self-response and the loop skips to the next CAN ID. + + - Handles received responses: + - If multiple responses are received for the same CAN ID, it's potentially indicative of a broadcast triggered by the request. + - The method logs a message and continues iterating. + - If the response size suggests a large ISO-TP packet, it might be a multi-frame response. + - The method logs a message and continues iterating. + +7. **Identify UDS Endpoint:** + - If a valid response is received from a different address than the transmitted one, a UDS endpoint is potentially discovered on the current CAN ID. + - The method logs a success message and extracts details from the response: + - Source and destination CAN IDs. + - Response payload in hexadecimal format. + - A `TargetURI` object is constructed representing the discovered endpoint, incorporating relevant details like transport scheme, hostname, addresses (source and destination), extended addressing settings (if applicable), and potentially padding values (if used). + - The discovered endpoint is appended to the `found` list. + - The loop exits, as a UDS endpoint has been identified on the current CAN ID. + +8. **Compile Results and Write to File:** + - After iterating through the CAN ID range, the method logs the total number of discovered UDS endpoints. + - It constructs the file path for storing the discovered endpoints in a text file named "ECUs.txt" within the `artifacts_dir` directory. + - The `write_target_list` method is called asynchronously to write the list of discovered endpoints along with any associated database information (using `self.db_handler`) to the file. + +9. **Optional: Query ECU Description (Diagnostics):** + - If `args.query` is True, the method calls the `query_description` method to retrieve the ECU description for each discovered endpoint using the specified DID (Data Identifier) from `args.info_did`. + +#### Usage + +```bash +gallia discover uds isotp --start --stop --target +``` + +**Example:** + +This example command discovers UDS endpoints on a CAN bus using virtual interface vcan0 within a CAN ID range of 0x000 to 0x7FF, sending a default UDS PDU and logging discovered endpoints: + +```bash +gallia discover uds isotp --start 0 --stop 0x7FF --target can-raw://vcan0 +``` + +#### ISO-TP Details + [ISO-TP](https://www.iso.org/standard/66574.html) is a standard for a transport protocol on top of the [CAN bus](https://www.iso.org/standard/63648.html) system. The CAN bus is a field bus which acts as a broadcast medium; any connected participant can read all messages. On the CAN bus there is no concept of a connection. @@ -66,12 +155,7 @@ However, in order to implement a connection channel for the UDS protocol (which In contrast to DoIP special CAN hardware is required. The ISO-TP protocol and the interaction with CAN interfaces is handled by the [networking stack](https://www.kernel.org/doc/html/latest/networking/can.html) of the Linux kernel. -For a discovery scan it is important to distinguish whether the tester is connected to a filtered interface (e.g. the OBD connector) or to an unfiltered interface (e.g. an internal CAN bus). -In order to not confuse the discovery scanner, the so called *idle traffic* needs to be observed. -The idle traffic consists of the mentioned cyclic messages of the can bus. -Since there is no concept of a connection on the CAN bus itself and the parameters for the ISO-TP connection are unknown at the very first stage, an educated guess for a deny list is required. -Typically, `gallia` waits for a few seconds and observes the CAN bus traffic. -Subsequently, a deny filter is configured which filters out all CAN IDs seen in the idle traffic. +##### ISO-TP addressing methods ISO-TP provides multiple different addressing methods: * normal addressing with normal CAN IDs, @@ -97,12 +181,12 @@ ISO-TP provides the following parameters: * **extended source address**: When extended addressing is in use, often set to a static value, e.g. `0xf1`. * **extended destination address**: When extended addressing is in use, it is the address of the ECU. -The discovery procedure is dependend on the used addressing scheme. -From a high level perspective, the destination id is iterated and a valid payload is sent. -If a valid answer is received, an ECU has been found. +The discovery procedure is dependent on the used addressing scheme. ## Session Scan +The UDS session scan discovers available diagnostic sessions and their transition paths within a target Electronic Control Unit (ECU). This scan explores the hierarchical structure of UDS sessions, starting from the default session and recursively identifying other accessible sessions. + UDS has the concept of sessions. Different sessions can for example offer different services. A session is identified by a 1 byte session ID. @@ -117,19 +201,178 @@ In case of a negative response, the session is considered not available from the To detect sessions, which are only reachable from a session different to the default session, a recursive approach is used. The scan for new sessions starts at each previously identified session. The maximum depth is limited to avoid endless scans in case of transition cycles, such as `0x01 -> 0x03 -> 0x05 -> 0x03`. -The scan is finished, if no new session transition is found. +The scan is finished if no new session transition is found. + +### Functionality + +1. **Recursive Session Exploration:** + - Starts with the default session (0x01). + - Attempts to transition into each possible session (1-0x7F) using the `DiagnosticSessionControl` service. + - Tracks successful transitions, building a graph of reachable sessions and their paths. + - Limits the search depth (`--depth`) to prevent infinite loops in case of cyclical session transitions. + +2. **Error Handling and Recovery:** + - **ConditionsNotCorrect:** If a session change fails due to "ConditionsNotCorrect," the scan can optionally retry using diagnostic session control hooks (`--with-hooks`). + - **ECU Reset:** If enabled (`--reset`), the ECU is reset at specific intervals to potentially overcome limitations and access additional sessions. + - **Stack Recovery:** If an error occurs during session switching, the scan attempts to recover by resetting the ECU and traversing the known path to the current session. + +3. **Optimized Scan (Optional):** + - The `--fast` flag enables a quicker scan mode, where new sessions are searched for only once per session, potentially missing some transitions. + +4. **Session Transition Logging:** + - Records all successful and failed session transitions, including the path taken to reach each session. + - Optionally stores the session transition information in a database for further analysis. + +### Key Advantages + +- **Comprehensive Discovery:** Thoroughly explores the entire session space of the ECU, uncovering hidden or non-standard sessions. +- **Session Transition Mapping:** Provides a clear picture of how different sessions can be reached, aiding in understanding the ECU's behavior and potential attack surfaces. +- **Error Resilience:** Handles various errors encountered during session switching, ensuring the scan continues even in the face of unexpected responses from the ECU. +- **Customization:** Offers flexibility in scan depth, ECU reset options, and hook usage to tailor the scan to specific ECU behavior. + +### Usage + +```bash +gallia scan uds sessions --help +``` + +```{note} +The specific command-line arguments and their behavior are subject to change. Always refer to the latest `--help` output for accurate usage information. +``` + +## Reset Scan + +The ECU reset scan assesses the response of an Electronic Control Unit (ECU) to various reset commands. It systematically probes the ECU with different reset sub-functions to gauge its response and identify potential vulnerabilities. + +It iterates and sends UDS requests `11 01` to `11 7F` and identifies the responses. If the response was positive (`51 xx`), it assumes the reset was successful. + +### Key Features + +* **Sub-Function Probing:** The scanner tests a range of ECU reset sub-functions (0x01 to 0x7F) as defined in the UDS standard. This comprehensive approach helps to reveal specific reset levels that may trigger unintended behavior or expose weaknesses in the ECU's reset logic. +* **Session Handling:** It can operate across multiple diagnostic sessions, allowing for a broader evaluation of the ECU's reset behavior in different states or configurations. +* **Configurable Skips:** Users can specify certain sub-functions or sessions to be excluded from the scan, providing flexibility for targeted testing or avoiding known problematic areas. +* **Error Recovery:** The scanner includes mechanisms to handle communication errors and timeouts that may occur during testing. It can attempt to recover the connection and resume the scan, ensuring thoroughness even in the face of unexpected issues. + +### Benefits + +* **Vulnerability Detection:** By systematically triggering different reset levels, the scanner can uncover vulnerabilities that could be exploited to disrupt the ECU's operation or gain unauthorized access, such as a reset triggering an unauthorised Diagnostic Session switch. +* **Robustness Assessment:** The results of the scan provide valuable insights into the ECU's resilience to various reset scenarios, helping to identify areas for improvement in its design and implementation. +* **Customized Testing:** The ability to configure session switching and selectively skip specific sub-functions allows for tailored testing based on the specific requirements and concerns of the user. + +### Usage + +To run the ECU reset scan, use the following command: + +```bash +gallia scan uds reset --target [OPTIONS] +``` + +Replace `` with the appropriate connection details for the ECU (e.g., `isotp://vcan0?is_fd=false&is_extended=false&src_addr=0x701&dst_addr=0x700`). Refer to the CLI `--help` for available options to customize the scan. Make sure the target URI is in quotes in the command, as not enclosing it in quotes might alter the execution of the command. + +```{note} +The specific command-line arguments and their behavior are subject to change. Always refer to the latest `--help` output for accurate usage information. +``` + +The scan results will be displayed in the console, indicating which reset levels were successful, timed out, or resulted in errors. This information can be used to further analyze the ECU's behavior and identify potential security risks. + +### Workflow Overview + +The ECU reset scan leverages the Unified Diagnostic Services (UDS) protocol, a standardized communication framework for vehicle diagnostics and reprogramming. Specifically, it interacts with the ECU Reset service (UDS service ID `0x11`). + +1. **Session Handling:** + * Optionally, if the `--sessions` argument is provided, the scan iterates through a list of specified diagnostic sessions. + * For each session: + * A `DiagnosticSessionControl` (UDS service ID `0x10`) request is sent to switch the ECU to the desired session. + * The scan proceeds if the session change is successful. + +2. **Sub-Function Iteration:** + * The scan systematically iterates through ECU Reset sub-functions, ranging from 0x01 to 0x7F. + * Skipping of specific sub-functions or sessions can be configured using the `--skip` argument. + +3. **ECU Reset Request:** + * For each sub-function, a UDS `ECUReset` request is sent with the corresponding sub-function byte. + * The request is configured with the `ANALYZE` tag to prompt detailed logging of the response. + +4. **Response Analysis:** + * **Positive Response:** If the ECU responds positively (acknowledges the reset), the sub-function is recorded as "ok". + * The script waits for the ECU to recover using `ecu.wait_for_ecu()`. + * A hard reset (sub-function 0x01) is issued to restore the ECU to a known state. + * **Negative Response:** If the ECU sends a negative response: + * `subFunctionNotSupported`: The sub-function is not implemented and is logged accordingly. + * Other negative responses are recorded as errors along with their response codes. + * **Timeout:** If the ECU does not respond within a timeout period: + * The sub-function is recorded as "timeout". + * If the `--power-cycle` flag is set, the script attempts to power-cycle the ECU and reconnect before continuing. + +5. **Session Verification (Optional):** + * If `--sessions` is used and `--skip-check-session` is not set, the script verifies that the ECU remains in the intended session after each reset. + * If the session has changed, it attempts to re-enter the correct session. + +6. **Result Summary:** + * Upon completion, the script outputs lists of sub-functions categorized as "ok", "timeout", and "error". + +**Technical Details:** + +* **Transport Protocol:** The scan relies on a UDS transport layer (e.g., ISOTP or DoIP), which is configured separately using the `--target` argument. +* **Error Handling:** The script employs `try-except` blocks to catch and handle `TimeoutError`, `ConnectionError`, and UDS-specific exceptions like `IllegalResponse` and `UnexpectedNegativeResponse`. +* **Logging:** Detailed logging is used to record each step of the scan, including sent requests, received responses, and any errors encountered. ## Service Scan -The service scan operates at the UDS protocol level. -UDS provides several endpoints called *services*. -Each service has an identifier and a specific list of arguments or sub-functions. +The UDS service scan identifies available UDS services on a target UDS Server. It accomplishes this through a methodical process of iterating through service IDs and analyzing UDS responses. Each service has an identifier and a specific list of arguments or sub-functions. In order to identify available services, a reverse matching is applied. According to the UDS standard, ECUs reply with the error codes `serviceNotSupported` or `serviceNotSupportedInActiveSession` when an unimplemented service is requested. Therefore, each service which responds with a different error code is considered available. To address the different services and their varying length of arguments and sub-functions the scanner automatically appends `\x00` bytes if the received response was `incorrectMessageLengthOrInvalidFormat`. +### Key Advantages + +- **Thoroughness:** The scan systematically covers the entire UDS service ID range, ensuring no potential services are overlooked. +- **Session Awareness:** By optionally testing across multiple sessions, it can reveal services that are only available in specific ECU states. +- **Adaptability:** The scan automatically adapts to services with different parameter lengths, ensuring accurate identification. +- **Error Handling:** Robustly handles timeouts and unexpected responses, providing diagnostic information for troubleshooting. + +### Usage + +The service scan is initiated through the Gallia command-line interface. For detailed usage instructions and available options, refer to the help information by running: + +```bash +gallia scan uds services --target +``` + +```{note} +The specific command-line arguments and their behavior are subject to change. Always refer to the latest `--help` output for accurate usage information. +``` + +### Workflow Overview + +1. **Session Handling (Optional):** + - If desired, the scan can operate across multiple diagnostic sessions. + - For each specified session, it attempts to switch the ECU to that session using the `DiagnosticSessionControl` service. + - If the session switch fails, it moves on to the next session. + +2. **Service ID Iteration:** + - Systematically scans through UDS service IDs (0x00 to 0xFF). + - Can optionally include service IDs that have the "SuppressPositiveResponse" bit set (those that do not typically send positive responses). + +3. **Request Generation and Transmission:** + - Constructs a UDS request for each service ID, experimenting with different payload lengths (1, 2, 3, and 5 bytes) if the received response was `incorrectMessageLengthOrInvalidFormat` to accommodate services with varying parameters. + - Sends the request to the ECU. + +4. **Response Analysis:** + - Handles various types of ECU responses: + - **Positive Response:** Indicates the service is available. + - **Negative Response:** + - Specific negative response codes (`serviceNotSupported` or `serviceNotSupportedInActiveSession`) signal that the service is not supported and iteration for the current session ends. + - `incorrectMessageLengthOrInvalidFormat` suggests adjusting the payload length, prompting further probing. + - **Timeout:** The ECU fails to respond within the expected time, indicating the service might not be available or accessible. + - **Malformed Response:** An unexpected or invalid response is logged for further investigation. + +5. **Result Logging:** + - Records the availability of each service along with its corresponding response for each session (if applicable). + - Outputs a summary of discovered services in each session. + ## Identifier Scan The identifier scan operates at the UDS protocol level; to be more specific it operates at the level of a specific UDS service. @@ -147,6 +390,153 @@ For discovering available subFunctions the following error codes indicate the su Each identifier or subFunction which responds with a different error code is considered available. -## Memory Scan +### Functionality + +1. **Service Selection:** + - Users specify the target service ID (`--sid`) they want to scan. + - Currently supported services include: + - `0x27`: SecurityAccess + - `0x22`: ReadDataByIdentifier + - `0x2e`: WriteDataByIdentifier + - `0x31`: RoutineControl + +2. **Session Handling (Optional):** + - The scan can optionally operate across multiple diagnostic sessions (`--sessions`). + - It attempts to switch to each specified session before initiating the identifier scan. + - Session switching can be verified at intervals using the `--check-session` option. + +3. **DID/Sub-Function Iteration:** + - Systematically scans through the specified range of data identifiers (DIDs) or sub-functions. + - The start and end values of the scan range are customizable with `--start` and `--end` arguments. + +4. **Request Generation and Transmission:** + - Constructs a UDS request for each DID or sub-function, tailored to the specified service. + - Appends an optional payload to the request if provided using `--payload`. + - Transmits the request to the ECU. + +5. **Response Analysis:** + - Handles various types of ECU responses: + - **Positive Response:** Indicates the DID or sub-function is available. + - **Negative Response:** + - Specific negative response codes (e.g., `requestOutOfRange`, `subFunctionNotSupported`) are interpreted based on the service being scanned, potentially leading to the end of the current session's scan (`--skip-not-supported`). + - Other negative responses are logged for further analysis. + - **Timeout:** The ECU fails to respond within the expected time, indicating the DID or sub-function might not be available or accessible. + - **Illegal Response:** An unexpected or invalid response is logged as a warning. + +6. **Result Categorization and Logging:** + - Categorizes responses as "Positive", "Abnormal", or "Timeout". + - Logs detailed information about each response, including the DID/sub-function and session (if applicable). + - Outputs a summary of the scan results, including the count of each response category. + +### Key Advantages + +- **Service-Specific Focus:** Tailors the scan to the unique requirements of each UDS service, maximizing the accuracy of DID/sub-function discovery. +- **Customizable Scan Range:** Allows users to define the starting and ending DIDs/sub-functions, focusing on specific areas of interest. +- **Session Support:** Optionally scans across multiple sessions to identify DIDs/sub-functions that are session-dependent. +- **Payload Flexibility:** Permits appending custom payloads to requests for advanced testing scenarios. + +### Usage + +```bash +gallia scan uds identifiers --help +``` + +```{note} +The specific command-line arguments and their behavior are subject to change. Always refer to the latest `--help` output for accurate usage information. +``` + +## Memory Functions Scan + +This scanner targets Electronic Control Units (ECUs) and explores functionalities that provide direct access to their memory. + +### Functionality + +The scanner focuses on the following Unified Diagnostic Service (UDS) services: + +* **ReadMemoryByAddress (service ID 0x23):** Retrieves data from a specified memory location. +* **WriteMemoryByAddress (service ID 0x3D):** Writes data to a specified memory location. +* **RequestDownload (service ID 0x34):** Tester downloads a block of data from the ECU. +* **RequestUpload (service ID 0x35):** ECU uploads a block of data to the tester. + +These services all share a similar packet structure, with the exception of WriteMemoryByAddress which requires an additional data field. +It iterates through a range of memory addresses and attempts to: + +* Read or write data using the chosen UDS service. +* Handle potential timeouts during communication with the ECU. +* Analyze the ECU's response to these attempts, which might reveal vulnerabilities or security mechanisms. + +The scanner offers several configuration options through command-line arguments to customize its behavior: +* Target diagnostic session (default: 0x03). +* Optionally verify and potentially recover the session before each memory access. +* Specify the UDS service to use for memory access (required, choices: 0x23, 0x3D, 0x34, 0x35). +* Provide data to write for service 0x3D WriteMemoryByAddress (8 bytes of zeroes by default). + +### Usage + +```bash +gallia scan uds memory --target --sid +``` + +```{note} +The specific command-line arguments and their behavior are subject to change. Always refer to the latest `--help` output for accurate usage information. +``` + +**Example:** + +```bash +gallia scan uds memory --sid 0x23 --target "isotp://can2?is_fd=false&is_extended=true&src_addr=0x22bbfbfa&dst_addr=0x22bbfafb&tx_padding=0&rx_padding=0" --db ecu_test --session 1 +``` + +The provided command invokes the scanner to utilize the UDS service `ReadMemoryByAddress` (service ID 0x23) on the target ECU reachable through the specified ISO-TP connection in session `0x01`. It will iterate through a range of memory addresses and attempt to read data from those locations. The results will be saved in a database file called `ecu_test`. + +## Dump Security Access Seeds + +The `dump-seeds` scanner attempts to retrieve security access seeds from the connected ECU. These seeds are (ideally) random values used by the ECU's security mechanisms. By capturing these seeds, attackers might be able to potentially bypass certain security checks or unlock higher access levels. + +The scanner offers several functionalities: + +* **Session Management:** + * Can switch between diagnostic sessions on the ECU based on the provided session ID. + * Optionally verifies the current session before proceeding. + * Re-enters the session after potential ECU resets. +* **Seed Request:** + * Requests security access seeds at a specified level from the ECU. + * Allows attaching additional data to the seed request message. + * Handles timeouts and errors that might occur during communication with the ECU. +* **Key Sending (Optional):** + * Simulates sending a key filled with zeros after requesting a seed. + * This technique might bypass certain brute-force protection mechanisms implemented by the ECU. +* **ECU Reset (Optional):** + * Can be configured to periodically reset the ECU. + * This aims to overcome limitations imposed by the ECU, such as seed rate limiting. + * Handles ECU recovery and reconnection after reset. + +### Usage + +The seed dumper is invoked using the following command: + +```bash +gallia scan uds dump-seeds --target [OPTIONS] +``` + +```{note} +The specific command-line arguments and their behavior are subject to change. Always refer to the latest `--help` output for accurate usage information. +``` + +**Example:** + +This example demonstrates how to capture seeds from security level 0x11 with session 0x02, resetting the ECU every 10th seed request, and running for 30 minutes: + +``` +gallia scan uds dump-seeds --target "isotp://vcan0?is_fd=false&is_extended=false&src_addr=0x701&dst_addr=0x700" --session 0x02 --data-record 0xCAFE00 --level 0x11 --reset 10 --duration 30 --db ecu_test +``` + +This command would: +* Connect to the ECU specified by ``. +* Switch to diagnostic session 0x02 (if possible). +* Request seeds from security level 0x11 with data record 0xCAFE00. (`27 11 CA FE 00`) +* Reset the ECU after every 10th seed request. +* Run for 30 minutes (or indefinitely if `--duration` is not set). +* Log the results in a database file called `ecu_test` -TODO +The dumped seeds will be written to a file named "seeds.bin" located in the scanner's artifacts directory. diff --git a/docs/uds/scan_reference_guide.md b/docs/uds/scan_reference_guide.md new file mode 100644 index 000000000..bfaf643d0 --- /dev/null +++ b/docs/uds/scan_reference_guide.md @@ -0,0 +1,414 @@ +# Scan Reference Guide + +This reference guide provides detailed information on the various scan modes of `gallia` used to assess the security and diagnostic capabilities of ECUs (Electronic Control Units) in vehicles. Each section outlines the purpose, message structure, success and failure criteria, and the type of information to include in test reports for each scan. + +### Terminology Definition + +* **ECU** - Electronic Control Unit, a piece of hardware that could contain multiple diagnostic endpoints/servers on various interfaces (UDS on CAN, DoIP, XCP, etc.) +* **UDS Server** - a single UDS source-target address pair. A single ECU can have multiple UDS servers on different communication channels or interfaces. + +## Discovery Scan + +### Purpose + +Identify ECUs (Electronic Control Units) present on a vehicle network, either through DoIP (Diagnostics over IP) or ISO-TP (ISO Transport Protocol) over CAN (Controller Area Network). This is a foundational step for further diagnostic and security testing. + +To avoid identifying unrelated CAN traffic as UDS endpoints, the scan observes the bus for a period to identify regular, non-diagnostic messages. These messages are then filtered out, allowing the scan to focus on potential UDS responses. + +### Messages Sent + +* **DoIP:** + * `RoutingActivationRequest`: Initializes the DoIP connection. + * `DiagnosticMessage`: Contains a standard UDS (Unified Diagnostic Services) request (e.g., `10 01` for session control) sent to a range of target addresses. +* **ISO-TP (CAN):** + * ISO-TP frames containing standard UDS requests (first `3E 00` for TesterPresent and subsequently `10 01` for DiagnosticSessionControl) are sent to a specified range of CAN IDs. + +### Success Criteria + +* **DoIP:** A valid UDS response from an ECU indicates its presence at the tested target address. +* **ISO-TP (CAN):** A valid ISO-TP response (not an error frame) from a CAN ID indicates a potential UDS endpoint. + +### Failure Criteria + +* **DoIP:** No response (e.g., indicating no ECU at that address), or a network-level error. +* **ISO-TP (CAN):** No response, an ISO-TP error frame, or a response only seen in the idle traffic (suggesting a non-UDS node). + +### Report Information + +* **Discovered ECUs (DoIP):** + * List of DoIP target addresses (IP addresses and ports) where ECUs were found. + * ECU identification details (if obtained through subsequent UDS requests). +* **Discovered ECUs (ISO-TP):** + * List of CAN IDs where UDS endpoints were discovered. + * Source and destination CAN IDs used for communication. + * ECU identification details (if obtained through further diagnostic requests). +* **Unusual Responses:** Any responses that deviate from the standard UDS or ISO-TP specifications, which might indicate non-standard ECU implementations. + +### Additional Information (for ISO-TP) + +* **Addressing Mode:** Specify whether normal or extended addressing was used for ISO-TP. +* **Tester Address:** If applicable, mention the tester address used in extended addressing mode. +* **Querying ECU Description:** If enabled, note if the scan queried ECU descriptions using a specific DID (Data Identifier) and include the retrieved descriptions. + +### Example Report Snippet (DoIP) + +``` +Discovery Scan Report (DoIP) + +Discovered ECUs: + +* Target Address: 192.168.0.10:13400 +* ECU Identification: Engine Control Module (via DID read F190) +* Target Address: 192.168.0.20:13400 +* ECU Identification: Transmission Control Module (via DID read F190) + +Analysis: Two ECUs were successfully discovered on the DoIP network. Further diagnostic testing is recommended to assess their functionality and security. +``` + +### Example Report Snippet (ISO-TP) + +``` +Discovery Scan Report (ISO-TP) + +Discovered ECUs: + +* CAN ID: 0x7E8 (Source: 0x7E0, Target: 0x7E8) +* ECU Identification: Body Control Module (via DID read F190) +* CAN ID: 0x7E8 (Source: 0x7FF, Target: 0x7E8) +* ECU Identification: Body Control Module (via DID read F190) +* CAN ID: 0x72A (Source: 0x7FF, Target: 0x72A) +* ECU Identification: Not obtained (further testing required) + +Analysis: Three potential UDS endpoints were discovered on the CAN bus. Two of them were identified as the same physical ECU. Multiple CAN IDs pointing to the same ECU (in this case, the Body Control Module) are common and often represent different diagnostic addresses or functionalities within the ECU. Additional diagnostic requests are needed to identify the other ECU and assess their capabilities. +``` + +## Session Scan + +### Purpose + +Discover all available diagnostic sessions within a UDS server and the valid transition paths between them. This scan helps assess the ECU's state management and identify potential vulnerabilities related to unauthorized access to privileged sessions. + +### Messages Sent + +* **DiagnosticSessionControl (0x10):** This request is used to attempt transitions to different sessions. It's sent with varying session IDs (0x01 to 0x7F) to check for supported sessions. + +### Success Criteria + +* **Positive Response:** The ECU responds with a positive acknowledgment (`50 xx`) to the session change request, indicating that the requested session is available and the transition was successful. +* **Negative Response:** The ECU responds with a specific negative response code: + * **conditionsNotCorrect (0x22):** Session exists, but the conditions to enter the requested session are not met. + * **subFunctionNotSupportedInActiveSession (0x7E):** The session is supported but cannot be transitioned into from the current session. + +### Failure Criteria + +* **Negative Response:** The ECU responds with a negative response code, such as: + * **subFunctionNotSupported (0x12):** The session ID is not supported. +* **Timeout:** The ECU fails to respond within the expected time, suggesting the session transition was not successful. + +### Report Information + +* **Entered Sessions:** A list of all sessions successfully entered during the scan, along with the path (sequence of transitions) used to reach each session. +* **Session Transition Failed:** A list of sessions that could not be entered (but exist), either due to negative responses or conditions not being met, along with the associated error codes. +* **Session-Specific Details:** + * **Session Name:** If known, the name or description of each discovered session (e.g., "Default Session," "Extended Session," etc.). + * **Entry Requirements:** If available, any conditions or prerequisites that need to be met to enter a specific session. +* **Error Handling:** Details of how the scan handled "conditionsNotCorrect" errors (using hooks) and ECU resets, if applicable. +* **Analysis:** A summary of the scan results, emphasizing any unusual session behavior, potential security vulnerabilities, or non-standard session transitions. + +### Example Report Snippet + +``` +Session Scan Report + +Sessions Entered: + +- Default Session (0x01) +- Extended Session (0x03) - Accessible from Default Session (0x01) +- Programming Session (0x02) - Accessible from Extended Session (0x03) + +Session Transitions: + +Default Session (0x01) -> Extended Session (0x03) -> Programming Session (0x02) + +Session Transition Failed (existing, but not entered): + +- Session 0x41 (conditionsNotCorrect) +- Session 0x42 (securityAccessDenied) + +Analysis: The ECU supports standard sessions with a linear transition path. Session 0x41 might be accessible under specific conditions not tested in this scan. Session 0x42 might be accessible after a securityAccess condition satisfaction. +``` + +## Reset Scan + +### Purpose + +Evaluate the resilience and behavior of an ECU (Electronic Control Unit) under various reset conditions. This scan helps identify potential vulnerabilities in reset handling and assess the impact of different reset types on the ECU's state and functionality. + +### Messages Sent + +* **ECUReset (0x11):** This request is sent with varying sub-function parameters (0x01 to 0x7F) to trigger different types of resets as defined in the UDS standard. + +### Success Criteria + +* **Positive Response:** The ECU acknowledges the reset request with a positive response (`51 xx`) indicating a successful reset. +* **Expected Recovery:** After a successful reset, the ECU recovers to a known, default state (often the default session) or a state as specified by the sub-function. + +### Failure Criteria + +* **Negative Response:** The ECU responds with a negative response code, such as: + * **subFunctionNotSupported (0x12):** The specific reset sub-function is not supported. + * **conditionsNotCorrect (0x22):** Conditions are not met for the requested reset. +* **Timeout:** The ECU fails to respond within the expected time, suggesting the reset may have caused an issue. +* **Unexpected Recovery:** The ECU recovers to an unexpected state after reset, potentially indicating a security flaw. + +### Report Information + +* **Supported Reset Sub-functions:** A list of all reset sub-functions that resulted in a positive response and expected recovery behavior. +* **Session Impact:** If the scan tests multiple sessions, details on how resets affect the active session and whether the ECU correctly returns to the intended session. +* **Unexpected Behavior:** A description of any anomalies observed during the scan, such as the ECU recovering to an unexpected session or state after reset. +* **Potential Vulnerabilities:** Any identified cases where a reset triggers an unexpected behavior that could be exploited for malicious purposes (e.g., transitioning to a privileged session without proper authorization). + +### Example Report Snippet + +``` +Reset Scan Report + +Default Session (0x01): +- Supported Resets: HardReset (0x01), KeyOffOnReset (0x02), SoftReset (0x05) +- Unsupported Resets: (0x11) - securityAccessDenied +- Unexpected Behavior: ECU remained in Extended Session after SoftReset + +Extended Session (0x03): +- Supported Resets: HardReset (0x01) +- Timeouts: EnableRapidPowerShutDown (0x04), ResetToBootLoader (0x07) + +Analysis: +- The ECU supports basic reset functions in the Default Session. +- In the Extended Session, several resets either timed out or resulted in unexpected behavior, suggesting potential vulnerabilities. Further investigation is recommended. +``` + +## Service Scan + +### Purpose + +Identify the UDS services supported by an ECU. This scan helps assess the ECU's diagnostic capabilities and identify potential entry points for further security analysis. + +### Messages Sent + +* **Service Requests:** The scan sends requests for all standard UDS service IDs (0x00 to 0x7F). If configured, it also includes service IDs with the "SuppressPositiveResponse" bit set (up to 0xFF). The UDS server should not display behavior different to a request without the suppress bit, but it is important to test all edge-cases. +* **Payload Variations:** For services potentially requiring additional parameters (such as WriteDataByIdentifier), the scan varies the payload length to accommodate different payload structures. + +### Success Criteria + +* **Positive Response:** The ECU acknowledges the service request with a positive response specific to that service. +* **Negative Response (Generic):** The ECU responds with a negative response code other than `serviceNotSupported` or `serviceNotSupportedInActiveSession`. Each service which responds with a different error code is considered available. + +### Failure Criteria + +* **Negative Response (Specific):** According to the UDS standard, ECUs reply with the error codes `serviceNotSupported` or `serviceNotSupportedInActiveSession` when an unimplemented service is requested. +* **Timeout:** The ECU doesn't respond within the expected time frame, potentially indicating a lack of support for the service. + +### Report Information + +* **Supported Services:** A comprehensive list of all positively identified services, categorized by the diagnostic session in which they were found (if multiple sessions were scanned). +* **Illegal Responses:** In case of an illegal response, providing additional insight into the ECU's behavior. + +### Example Report Snippet + +``` +Service Scan Report + +Default Session (0x01): +DiagnosticSessionControl (0x10), ECUReset (0x11), ReadDataByIdentifier (0x22), + +Programming Session (0x02): +DiagnosticSessionControl (0x10), ECUReset (0x11), SecurityAccess (0x27), WriteDataByIdentifier (0x2E), + +Analysis: The ECU supports standard diagnostic services in the default session. SecurityAccess and programming-related services are only available in the programming session. +``` + +## Identifier Scan + +### Purpose +Read data from data identifiers (DIDs) to assess potential sensitive data or discover sub-functions supported by a specific UDS service within a UDS server. + +### Target Services +* `0x27`: SecurityAccess +* `0x22`: ReadDataByIdentifier +* `0x2e`: WriteDataByIdentifier +* `0x31`: RoutineControl + +### Messages Sent +* UDS requests tailored to the selected service. + * Requests contain the service ID, followed by the DID or sub-function being tested. + * Optionally, a custom payload can be appended to the request. + +* **Service-Specific Requests:** The scan sends requests using one of the targeted services, along with parameters like: + * **DID/sub-function:** The specific sub-function to target (e.g., try to read, write or launch). + * **Custom payload (optional):** A custom payload can be appended to the request. + +### Success Criteria +* **Positive Response:** The ECU acknowledges the DID or sub-function with a positive response specific to the service. This indicates that the DID/sub-function is valid and supported. + +### Failure Criteria +* **Negative Response:** The ECU responds with a negative response code. These responses are categorized as: + * **Service-Specific:** Negative responses that are expected based on the selected service and indicate that the DID/sub-function is not supported or unavailable. Examples include `requestOutOfRange`, `subFunctionNotSupported`, or `serviceNotSupportedInActiveSession`. + * **Other:** Other negative response codes might signal unexpected behavior or issues with the communication. + * **Timeout**: The ECU does not respond within the expected timeframe. This might indicate that the DID/sub-function is not supported, or there are communication issues. + * **Illegal Response**: The ECU responds with an unexpected or invalid response that doesn't conform to the UDS standard. + +### Report Information +* **Supported DIDs/Sub-functions:** A list of all positively identified DIDs or sub-functions, organized by the session in which they were found (if multiple sessions were scanned). +* **Session Details:** If multiple sessions were tested, a breakdown of supported identifiers per session, along with any session-switching issues encountered. +* **Analysis:** A brief analysis highlighting any patterns or anomalies found in the scan results, potentially pointing towards security vulnerabilities or non-standard implementations. + +### Example Report Snippet + +``` +Identifier Scan Report + +Service: ReadDataByIdentifier (0x22) + +Default Session (0x01): +F180: 56 31 2E 35 2E 34 = V1.5.4 +F190: 31 47 31 4A 43 35 32 34 58 59 37 32 31 38 38 36 39 = 1G1JC524XY7218869 + +Extended Session (0x03): +F180: 56 31 2E 35 2E 34 = V1.5.4 +F181: 50 72 65 52 65 6C 65 61 73 65 56 31 2E 35 2E 34 = PreReleaseV1.5.4 +F190: 31 47 31 4A 43 35 32 34 58 59 37 32 31 38 38 36 39 = 1G1JC524XY7218869 +F191: Timeout +F192: Timeout + +Analysis: The ECU supports most standard DIDs in both sessions. Timeouts in the extended session might indicate communication issues or intentional restrictions. +``` + +## Memory Scan + +### Purpose + +Test and evaluate UDS services that allow direct access to an ECU's memory. By interacting with services like `ReadMemoryByAddress` and `WriteMemoryByAddress`, this scan helps identify potential vulnerabilities in memory access control mechanisms and uncover sensitive data or executable code that might be stored in the ECU's memory. + +### Target Services + +* `0x23`: ReadMemoryByAddress +* `0x3D`: WriteMemoryByAddress +* `0x34`: RequestDownload +* `0x35`: RequestUpload + +### Messages Sent + +* **Service-Specific Requests:** The scan sends requests using one of the targeted services, along with parameters like: + * **Memory Address:** The location in memory to read from or write to. + * **Memory Size (if applicable):** The amount of data to read or write. + * **Data (for WriteMemoryByAddress):** The data to be written. + +### Success Criteria + +* **Positive Response:** The ECU responds with the requested data (for read operations) or acknowledges the successful write operation (for write operations). This indicates the service is functional and the specified memory location is accessible. + +### Failure Criteria + +* **Negative Response:** The ECU responds with an error code, such as: + * `requestOutOfRange`: The specified memory address is outside the valid range. + * `generalReject`: The service is not supported or the request is invalid. + * `securityAccessDenied`: The current security level doesn't permit the requested memory access. + * `conditionsNotCorrect`: Certain preconditions (like a specific session) haven't been met for memory access. + +* **Timeout:** The ECU doesn't respond within the expected time, suggesting the service is not available or the memory address is not accessible. + +### Report Information + +* **Tested Memory Range:** The range of memory addresses that were scanned. +* **Accessible Memory:** A list of memory addresses that responded positively to read or write requests, potentially indicating regions without strict access controls. +* **Inaccessible Memory:** A list of memory addresses that resulted in negative responses or timeouts, suggesting protected or unavailable areas. +* **Security Observations:** Details of any security mechanisms encountered (e.g., securityAccessDenied responses) and potential vulnerabilities discovered. +* **Retrieved Data:** If data was successfully read using `ReadMemoryByAddress`, a sample of the retrieved data might be included, provided it doesn't contain sensitive information. + +### Example Report Snippet + +``` +Memory Scan Report + +Service Tested: ReadMemoryByAddress (0x23) +Session: Extended Diagnostic Session (0x03) + +Memory Range Tested: 0x000000 - 0x001000 + +Accessible Memory: +- 0x000000 - 0x0000FF (responded with data) + +Inaccessible Memory: +- 0x000100 - 0x001000 (requestOutOfRange) + +Security Observations: +- Memory range 0x000800 - 0x000FFF returned securityAccessDenied, indicating potential sensitive data. + +Recommendations: +- Further investigate the accessible memory range to determine if it contains sensitive information. +- Attempt to bypass the securityAccessDenied response in the protected memory range to assess potential vulnerabilities. +``` + +## Seed Dumping Scan + +### Purpose + +Extract security access seeds from the ECU (Electronic Control Unit) to analyze the ECU's security mechanisms and potentially identify vulnerabilities. Seeds are random values used for cryptographic operations in security access procedures. + +### Targeted Service + +* `0x27`: SecurityAccess (specifically the "Request Seed" and "Send Key" sub-functions) + +### Messages Sent + +* **RequestSeed (`27 01`):** Requests a seed from a specific security level, optionally including a custom data record. +* **SendKey (`27 02`):** (Optional) Sends a key (typically derived from the seed) to the ECU, usually with the intent to bypass brute-force protection mechanisms. +* **ECUReset (0x10):** (Optional) Sends a restart request to the ECU with the intent to bypass brute-force protection mechanisms. + +### Success Criteria + +* **Valid Seed Retrieval:** The ECU responds with a positive response (`67 xx`) containing a seed value. + +### Failure Criteria + +* **Negative Response:** The ECU responds with a negative response code, such as: + * **securityAccessDenied (0x33):** Insufficient security level to access the requested seed. + * **requiredTimeDelayNotExpired (0x37):** A time delay is required before requesting another seed. + * **exceededNumberOfAttempts (0x36):** Too many incorrect keys have been sent for the current security level. +* **Timeout:** The ECU fails to respond within the expected time, indicating potential communication issues or seed request throttling. + +### Report Information + +* **Session:** The diagnostic session used for seed extraction. +* **Security Level:** The security level from which seeds were requested. +* **Number of Seeds Dumped:** The total number of seeds successfully retrieved. +* **Seed Dump Rate:** The rate at which seeds were dumped (e.g., seeds per second). +* **Dump Duration:** The total time taken for the seed dumping process. +* **Data Record:** If used, the data record appended to seed requests. +* **Zero Key Strategy:** Whether zero-filled keys were sent (and if successful). +* **Reset Strategy:** If ECU resets were used to bypass rate limiting, details on the reset frequency and success. +* **Unusual Responses:** Any unexpected negative responses or patterns that might indicate non-standard ECU behavior or potential security vulnerabilities. +* **Analysis:** Assessment of the seed randomness, potential weaknesses in the seed generation algorithm, and any observed limitations in the ECU's security access implementation. + +### Example Report Snippet + +``` +Seed Dumping Scan Report + +Session: Extended Diagnostic Session (0x03) +Security Level: 0x11 +Number of Seeds Dumped: 26000 +Total Seed Dump Rate: 20.8 seeds/minute +Dump Duration: 120 minutes + +Appended Data Record after `27 xx`: 0x12345678 +Zero Key Strategy: Not Used +Reset Strategy: Used (every 10 seeds) + +Filename: seeds_03_sa11.bin + +Analysis: +- The ECU successfully responded to seed requests with valid seeds. +- The reset strategy was effective in bypassing the ECU's rate limiting. +- Further analysis of the dumped seeds is required to assess their randomness and potential vulnerabilities. +``` \ No newline at end of file diff --git a/src/gallia/command/base.py b/src/gallia/command/base.py index 3cba4e3e3..32dbed0fb 100644 --- a/src/gallia/command/base.py +++ b/src/gallia/command/base.py @@ -539,7 +539,7 @@ async def setup(self, args: Namespace) -> None: # traffic might be missing. if args.dumpcap: if shutil.which("dumpcap") is None: - self.parser.error("--dumpcap specified but `dumpcap` is not available") + self.parser.error("--dumpcap specified but `dumpcap` is not available. `wireshark` is likely not installed. Install `wireshark` if you want to use `dumpcap`.") self.dumpcap = await Dumpcap.start(args.target, self.artifacts_dir) if self.dumpcap is None: logger.error("Dumpcap could not be started!") diff --git a/src/gallia/commands/discover/uds/isotp.py b/src/gallia/commands/discover/uds/isotp.py index 27384d9d3..02c8a5717 100644 --- a/src/gallia/commands/discover/uds/isotp.py +++ b/src/gallia/commands/discover/uds/isotp.py @@ -17,11 +17,22 @@ class IsotpDiscoverer(UDSDiscoveryScanner): - """Discovers all UDS endpoints on an ECU using ISO-TP normal addressing. - This is the default protocol used by OBD. - When using normal addressing, the ISO-TP header does not include an address and there is no generic tester address. - Addressing is only done via CAN IDs. Every endpoint has a source and destination CAN ID. - Typically, there is also a broadcast destination ID to address all endpoints.""" + """This class, `IsotpDiscoverer`, implements a UDS discovery scanner specifically designed to discover UDS endpoints on an Electronic Control Unit (ECU) using the ISO-TP normal addressing scheme. + + *Methods:* + + * **constructor (__init__())** - Initializes the class and inherits functionalities from the parent class `UDSDiscoveryScanner`. + * **configure_parser(self) -> None:** Defines the command-line arguments specific to the IsotpDiscoverer scanner. + * **setup(self, args: Namespace) -> None:** Performs initial setup tasks based on the provided arguments. Validates arguments and ensures compatibility. + * **query_description(self, target_list: list[TargetURI], did: int) -> None:** Queries the ECU description for each discovered endpoint using the specified DID. + * **_build_isotp_frame_extended(self, pdu: bytes, ext_addr: int) -> bytes:** Constructs an ISO-TP frame with extended addressing. + * **_build_isotp_frame(self, pdu: bytes) -> bytes:** Constructs a standard ISO-TP frame without extended addressing. + * **build_isotp_frame(self, req: UDSRequest, ext_addr: int | None = None, padding: int | None = None) -> bytes:** Builds an ISO-TP frame based on the provided UDS request, incorporating extended addressing and padding if specified. + * **main(self, args: Namespace) -> None:** The main execution function that orchestrates the discovery process. + + This IsotpDiscoverer class offers a comprehensive solution to discover UDS endpoints on an ECU utilizing ISO-TP normal addressing. + It provides informative logging and allows for various configuration options to tailor the scanning process. + """ SUBGROUP = "uds" COMMAND = "isotp" @@ -33,65 +44,77 @@ def configure_parser(self) -> None: metavar="INT", type=auto_int, required=True, - help="set start address", + help="Starting CAN ID for the scanning range", ) self.parser.add_argument( "--stop", metavar="INT", type=auto_int, required=True, - help="set end address", + help="Ending CAN ID for the scanning range", ) self.parser.add_argument( "--padding", type=auto_int, default=None, - help="set isotp padding", + help="Set isotp padding (no padding by default)", ) self.parser.add_argument( "--pdu", type=unhexlify, default=bytes([0x3E, 0x00]), - help="set pdu used for discovery", + help="Defines the UDS PDU used for discovery (TesterPresent by default)", ) self.parser.add_argument( "--sleep", type=float, default=0.01, - help="set sleeptime between loop iterations", + help="Set sleep time between loop iterations", ) self.parser.add_argument( "--extended-addr", action="store_true", - help="use extended isotp addresses", + help="Enables the use of extended ISO-TP addresses.", ) self.parser.add_argument( "--tester-addr", type=auto_int, default=0x6F1, - help="tester address for --extended", + help="Sets the tester address when `--extended-addr` addressing is enabled (default: 0x%(default)x)", ) self.parser.add_argument( "--query", action="store_true", - help="query ECU description via RDBID", + help="Triggers querying the ECU description via DID read (DID specified by `--info-did`, `0xF197` by default)", ) self.parser.add_argument( "--info-did", metavar="DID", type=auto_int, default=0xF197, - help="DID to query ECU description", + help="Specify DID to read providing ECU description (default: 0x%(default)x)", ) self.parser.add_argument( "--sniff-time", default=5, type=int, metavar="SECONDS", - help="Time in seconds to sniff on bus for current traffic", + help="Time in seconds to sniff on bus for current traffic before initiating the scan to configure the deny filter (default: %(default)d seconds)", ) async def setup(self, args: Namespace) -> None: + """ + Performs initial setup and validation based on provided arguments. + + Raises: + argparse.ArgumentError: If an unsupported transport schema is provided or + if extended addressing is used with start/stop + values exceeding 0xFF. + + Calls the parent class `UDSDiscoveryScanner` setup method after performing + initial checks. + """ + if args.target is not None and not args.target.scheme == RawCANTransport.SCHEME: self.parser.error( f"Unsupported transport schema {args.target.scheme}; must be can-raw!" @@ -101,6 +124,15 @@ async def setup(self, args: Namespace) -> None: await super().setup(args) async def query_description(self, target_list: list[TargetURI], did: int) -> None: + """ + Queries the ECU description for each discovered endpoint in the target list + using the specified DID (Data Identifier). + + Args: + target_list: List of TargetURI objects representing discovered endpoints. + did: The DID (Data Identifier) used to query the ECU description. + """ + logger.info("reading info DID from all discovered endpoints") for target in target_list: logger.result("----------------------------") @@ -123,10 +155,31 @@ def _build_isotp_frame_extended( pdu: bytes, ext_addr: int, ) -> bytes: + """ + Constructs an ISO-TP frame using extended addressing. + + Args: + pdu: The UDS Request PDU (Protocol Data Unit) to be encapsulated within the frame. + ext_addr: The extended ISO-TP address (1 byte). + + Returns: + The complete ISO-TP frame with extended addressing prepended to the PDU. + """ + isotp_hdr = bytes([ext_addr, len(pdu) & 0x0F]) return isotp_hdr + pdu def _build_isotp_frame(self, pdu: bytes) -> bytes: + """ + Constructs a standard ISO-TP frame without extended addressing. + + Args: + pdu: The UDS Request PDU (Protocol Data Unit) to be encapsulated within the frame. + + Returns: + The complete ISO-TP frame with standard addressing prepended to the PDU. + """ + isotp_hdr = bytes([len(pdu) & 0x0F]) return isotp_hdr + pdu @@ -136,6 +189,27 @@ def build_isotp_frame( ext_addr: int | None = None, padding: int | None = None, ) -> bytes: + """ + Constructs an ISO-TP frame based on the provided UDS request, incorporating extended addressing and padding if specified. + + Args: + req: The UDSRequest object containing the PDU to be transmitted. + ext_addr: The extended ISO-TP address to be used (optional). + padding: The padding value to be inserted in the frame (optional). + + Raises: + ValueError: If the provided UDS request PDU exceeds the maximum allowed length for a single ISO-TP frame. + + Returns: + The complete ISO-TP frame ready for transmission. + + This method first retrieves the PDU from the UDS request object. It then checks the PDU size against the maximum allowed length for a single ISO-TP frame. If the PDU is too large, a ValueError is raised. + + Depending on the presence of the `ext_addr` argument, the method calls either `_build_isotp_frame_extended` (for extended addressing) or `_build_isotp_frame` (for standard addressing) to construct the base frame. + + Finally, if padding is specified (`padding` argument is not None), the method calculates the required padding length and appends the padding bytes to the frame. + """ + pdu = req.pdu max_pdu_len = 7 if ext_addr is None else 6 if len(pdu) > max_pdu_len: @@ -153,6 +227,14 @@ def build_isotp_frame( return frame async def main(self, args: Namespace) -> None: + """ + The main execution function that orchestrates the UDS endpoint discovery process on ISOTP. + + See `https://fraunhofer-aisec.github.io/gallia/uds/scan_modes.html#detailed-functionality-description` for more information. + + Args: + args: A Namespace object containing parsed command-line arguments. + """ transport = await RawCANTransport.connect(args.target) found = [] diff --git a/src/gallia/commands/primitive/uds/dtc.py b/src/gallia/commands/primitive/uds/dtc.py index a510f5a97..9683e018f 100644 --- a/src/gallia/commands/primitive/uds/dtc.py +++ b/src/gallia/commands/primitive/uds/dtc.py @@ -23,7 +23,12 @@ class DTCPrimitive(UDSScanner): - """Read out the Diagnostic Troube Codes (DTC)""" + """ + Read or manipulate Diagnostic Trouble Codes (DTCs) + + This class provides functionalities to interact with the ECU's Diagnostic Trouble Codes (DTCs) using the UDS protocol. + It inherits from the UDSScanner class of the gallia.command module to establish communication with the ECU. + """ GROUP = "primitive" COMMAND = "dtc" @@ -36,7 +41,7 @@ def configure_parser(self) -> None: "--session", default=DiagnosticSessionControlSubFuncs.defaultSession.value, type=auto_int, - help="Session to perform test in", + help="Diagnostic session to perform the test in (default: %(default)x)", ) sub_parser = self.parser.add_subparsers(dest="cmd", required=True) read_parser = sub_parser.add_parser( @@ -47,12 +52,12 @@ def configure_parser(self) -> None: type=partial(int, base=16), default=0xFF, help="The bitmask which is sent to the ECU in order to select the relevant DTCs according to their " - "error state. By default, all error codes are returned (c.f. ISO 14229-1,D.2).", + "error state. By default, all error codes are returned (c.f. ISO 14229-1,D.2). (default: 0x%(default)x)", ) read_parser.add_argument( "--show-legend", action="store_true", - help="Show the legend of the bit interpretation according to ISO 14229-1,D.2", + help="Displays a legend explaining the bit interpretation of the error state according to ISO 14229-1,D.2", ) read_parser.add_argument( "--show-failed", @@ -72,25 +77,40 @@ def configure_parser(self) -> None: type=int, default=0xFFFFFF, help="Only clear a particular DTC or the DTCs belonging to the given group. " - "By default, all error codes are cleared.", + "(default: 0x%(default)x - clears all)", ) control_parser = sub_parser.add_parser( "control", - help="Stop or resume the setting of DTCs using the " "ControlDTCSetting service", + help="Stop or resume setting of new DTCs using the " "ControlDTCSetting service", ) control_group = control_parser.add_mutually_exclusive_group(required=True) control_group.add_argument( "--stop", action="store_true", - help="Stop the setting of DTCs. If already disabled, this has no effect.", + help="Stops setting of new DTCs. If already disabled, this has no effect.", ) control_group.add_argument( "--resume", action="store_true", - help="Resume the setting of DTCs. If already enabled, this has no effect.", + help="Resumes setting of new DTCs. If already enabled, this has no effect.", ) async def fetch_error_codes(self, mask: int, split: bool = True) -> dict[int, int]: + """Fetches DTC information from the ECU using the ReadDTCInformation service. + + This method retrieves DTCs from the ECU based on the provided bitmask, which filters the results according to their error state. + + Args: + mask (int): Bitmask to select DTCs based on error state. + split (bool, optional): Attempts to fetch DTCs in chunks if the response is too large (default: True). + + Returns: + dict[int, int]: Dictionary containing DTCs as keys and their corresponding error state as values. + + Raises: + UDSErrorCodes: If a negative response is received from the ECU with a specific error code (e.g., response too long). + """ + ecu_response = await self.ecu.read_dtc_information_report_dtc_by_status_mask(mask) dtcs = {} @@ -118,6 +138,15 @@ async def fetch_error_codes(self, mask: int, split: bool = True) -> dict[int, in return dtcs async def read(self, args: Namespace) -> None: + """Reads DTCs and presents them along with summaries based on user options. + + This method retrieves DTCs using the `fetch_error_codes` method and categorizes them based on their error state. + It then presents the DTC information and optional summaries according to user-specified flags. + + Args: + args (Namespace): Namespace object containing parsed command-line arguments. + """ + dtcs = await self.fetch_error_codes(args.mask) failed_dtcs: list[list[str]] = [] @@ -156,6 +185,14 @@ async def read(self, args: Namespace) -> None: self.show_summary(uncompleted_dtcs) def show_bit_legend(self) -> None: + """ + Presents a legend explaining the bit interpretation of the DTC error state. + + This method iterates through a list of bit descriptions and logs them using the logger.result function. + Each description corresponds to a specific bit in the error state value and explains its meaning. + This legend helps users understand the detailed information provided in the DTC output. + """ + bit_descriptions = [ "0 = testFailed: most recent test failed", "1 = testFailedThisOperationCycle: failed in current cycle", @@ -173,6 +210,14 @@ def show_bit_legend(self) -> None: logger.result(line) def show_summary(self, dtcs: list[list[str]]) -> None: + """ + Generates a summary table for the provided DTC information. + + This method sorts the DTC list and then creates a table with headers including "DTC", "error state", and individual bits (0 to 7). + It uses the `tabulate` library to format the table in a user-friendly way and logs each line using the logger.result function. + This summary provides a concise overview of the DTCs and their error states. + """ + dtcs.sort() header = [ @@ -192,6 +237,18 @@ def show_summary(self, dtcs: list[list[str]]) -> None: logger.result(line) async def clear(self, args: Namespace) -> None: + """ + Clears DTCs from the ECU's memory based on the specified group or DTC. + + This method retrieves the group of DTCs or a specific DTC to clear from the command-line arguments. + It then validates the provided value to ensure it falls within the acceptable range. + Finally, it calls the `ecu.clear_diagnostic_information` method to send the clear request to the ECU. + The method logs the response, indicating success or failure. + + Args: + args (Namespace): Namespace object containing parsed command-line arguments. + """ + group_of_dtc: int = args.group_of_dtc min_group_of_dtc = 0 @@ -210,6 +267,13 @@ async def clear(self, args: Namespace) -> None: logger.result("Success") async def control(self, args: Namespace) -> None: + """ + Enables or disables setting of new DTCs based on user selection. + + This method checks the `--stop` or `--resume` argument from the `args` object to determine whether to stop or resume setting new DTCs. + It then calls the `ecu.control_dtc_setting` method with the corresponding sub-function (CDTCSSubFuncs.OFF or CDTCSSubFuncs.ON) to send the control request to the ECU. + """ + if args.stop: await self.ecu.control_dtc_setting(CDTCSSubFuncs.OFF) else: diff --git a/src/gallia/commands/primitive/uds/ecu_reset.py b/src/gallia/commands/primitive/uds/ecu_reset.py index 69c092335..50165f5ac 100644 --- a/src/gallia/commands/primitive/uds/ecu_reset.py +++ b/src/gallia/commands/primitive/uds/ecu_reset.py @@ -15,30 +15,40 @@ class ECUResetPrimitive(UDSScanner): - """Use the ECUReset UDS service to reset the ECU""" + """Use the ECUReset UDS service to reset the ECU + + This class implements the ECU Reset functionality using the Unified Diagnostic Service (UDS) + protocol. It leverages the UDSScanner class from the gallia.command module to execute + the diagnostic communication. + """ GROUP = "primitive" COMMAND = "ecu-reset" SHORT_HELP = "ECUReset" def configure_parser(self) -> None: + """Configures the argument parser for the ECU Reset command. + """ + self.parser.set_defaults(properties=False) self.parser.add_argument( "--session", type=auto_int, default=0x01, - help="set session perform test in", + help="The diagnostic session to switch into before resetting (Default: 0x%(default)x).", ) self.parser.add_argument( "-f", "--subfunc", type=auto_int, default=0x01, - help="subfunc", + help="The subfunction of the ECU Reset service (reset level - 11 xx) to execute (Default: 0x%(default)x).", ) async def main(self, args: Namespace) -> None: + """The main execution function for the ECU Reset command.""" + resp: UDSResponse = await self.ecu.set_session(args.session) if isinstance(resp, NegativeResponse): logger.error(f"could not change to session: {g_repr(args.session)}") diff --git a/src/gallia/commands/scan/uds/identifiers.py b/src/gallia/commands/scan/uds/identifiers.py index effc7a458..8c460404b 100644 --- a/src/gallia/commands/scan/uds/identifiers.py +++ b/src/gallia/commands/scan/uds/identifiers.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 +"""Module for scanning UDS identifiers within specific services.""" + import binascii import reprlib from argparse import Namespace @@ -25,8 +27,12 @@ class ScanIdentifiers(UDSScanner): - """This scanner scans DataIdentifiers of various - services. Specific requirements such as for RoutineControl or SecurityAccess + """This scanner scans DataIdentifiers of various services. + + Supports various services like ReadDataByIdentifier, WriteDataByIdentifier, RoutineControl, and SecurityAccess. + Handles session switching, skips specific DIDs, and provides detailed scan results. + + Specific requirements such as for RoutineControl or SecurityAccess are considered and implemented in the script. """ @@ -34,29 +40,34 @@ class ScanIdentifiers(UDSScanner): SHORT_HELP = "identifier scan of a UDS service" def configure_parser(self) -> None: + """Configures the argument parser for the identifier scan.""" self.parser.add_argument( "--sessions", type=auto_int, nargs="*", - help="Set list of sessions to be tested; all if None", + metavar="SESSION_ID", + help="List of diagnostic sessions to scan (e.g., '1 3'). If not specified, uses the current session.", ) self.parser.add_argument( "--start", type=auto_int, default=0, - help="start scan at this dataIdentifier (default: 0x%(default)x)", + metavar="DID", + help="Starting dataIdentifier to scan (default: 0x%(default)x)", ) self.parser.add_argument( "--end", type=auto_int, default=0xFFFF, - help="end scan at this dataIdentifier (default: 0x%(default)x)", + metavar="DID", + help="Ending dataIdentifier to scan (default: 0x%(default)x)", ) self.parser.add_argument( "--payload", default=None, type=binascii.unhexlify, - help="Payload which will be appended for each request as hex string", + metavar="HEX_STRING", + help="Optional payload to append to each request (hex string)", ) self.parser.add_argument( "--sid", @@ -64,11 +75,12 @@ def configure_parser(self) -> None: default=0x22, help=""" Service ID to scan; defaults to ReadDataByIdentifier (default: 0x%(default)x); - currently supported: - 0x27 Security Access; - 0x22 Read Data By Identifier; - 0x2e Write Data By Identifier; - 0x31 Routine Control; + + Currently supported: + - 0x27: SecurityAccess + - 0x22: ReadDataByIdentifier + - 0x2e: WriteDataByIdentifier + - 0x31: RoutineControl """, ) self.parser.add_argument( @@ -84,17 +96,21 @@ def configure_parser(self) -> None: default={}, type=str, action=ParseSkips, + metavar="SESSION_ID:DIDS", help=""" - The data identifiers to be skipped per session. - A session specific skip is given by : - where is a comma separated list of single ids or id ranges using a dash. - Examples: - - 0x01:0xf3 - - 0x10-0x2f - - 0x01:0xf3,0x10-0x2f - Multiple session specific skips are separated by space. - Only takes affect if --sessions is given. - """, + Skip specific data identifiers within sessions. Format: 'SESSION_ID:DIDS' + + SESSION_ID: ID of the session + DIDS: Comma-separated list of: + - Single DIDs (e.g., 0xf190) + - DID ranges (e.g., 0x1000-0x10FF) + + Examples: + - 1:0xf190 (Skips DID 0xf190 in session 1) + - 0x1000-0x10FF (Skips DIDs 0x1000 to 0x10FF in all sessions) + + Only applicable if --sessions is used. + """, ) self.parser.add_argument( "--skip-not-supported", @@ -103,6 +119,12 @@ def configure_parser(self) -> None: ) async def main(self, args: Namespace) -> None: + """ + Main execution of the identifier scan. + + Manages session switching (if applicable) and calls `perform_scan` to execute the scan within each session. + """ + if args.sessions is None: logger.notice("Performing scan in current session") await self.perform_scan(args) @@ -132,6 +154,13 @@ async def main(self, args: Namespace) -> None: await self.ecu.leave_session(session, sleep=args.power_cycle_sleep) async def perform_scan(self, args: Namespace, session: None | int = None) -> None: + """ + Performs the scan for data identifiers within the specified service and session. + + Iterates over DIDs and sub-functions, sending requests and analyzing responses. + Handles scanning different UDS services (SecurityAccess, RoutineControl, etc.) and logs the results. + """ + positive_DIDs = 0 abnormal_DIDs = 0 timeout_DIDs = 0 diff --git a/src/gallia/commands/scan/uds/memory.py b/src/gallia/commands/scan/uds/memory.py index 6cd631706..cfe4be5de 100644 --- a/src/gallia/commands/scan/uds/memory.py +++ b/src/gallia/commands/scan/uds/memory.py @@ -16,45 +16,62 @@ class MemoryFunctionsScanner(UDSScanner): - """This scanner scans functions with direct access to memory. - Specifically, these are service 0x3d WriteMemoryByAddress, 0x34 RequestDownload - and 0x35 RequestUpload, which all share the same packet structure, except for - 0x3d which requires an additional data field. + """This scanner targets ECUs (Electronic Control Units) and scans functions that provide direct access to memory. + + Currently supports: + * ReadMemoryByAddress (0x23) + * WriteMemoryByAddress (0x3D) - requires additional data field + * RequestDownload (0x34) + * RequestUpload (0x35) """ SHORT_HELP = "scan services with direct memory access" COMMAND = "memory" def configure_parser(self) -> None: + """Adds arguments specific to the memory scanner to the argument parser. + """ + self.parser.add_argument( "--session", type=auto_int, default=0x03, - help="set session to perform test", + help="Set a session to perform the test in (default: 0x%(default)x)", ) self.parser.add_argument( "--check-session", nargs="?", const=1, type=int, - help="Check current session via read DID [for every nth MemoryAddress] and try to recover session", + help="Check the current session via a DID read (`0xF186`) [for every nth MemoryAddress] and try to recover session if lost (e.g., `--check-session 10` to check after every 10th address).", ) self.parser.add_argument( "--sid", required=True, choices=[0x23, 0x3D, 0x34, 0x35], type=auto_int, - help="Choose between 0x23 ReadMemoryByAddress 0x3d WriteMemoryByAddress, " - "0x34 RequestDownload and 0x35 RequestUpload", + help="UDS Service ID to test. Choose between: " + "0x23 ReadMemoryByAddress; 0x3d WriteMemoryByAddress; " + "0x34 RequestDownload; 0x35 RequestUpload", ) self.parser.add_argument( "--data", default="0000000000000000", type=unhexlify, - help="Service 0x3d requires a data payload which can be specified with this flag as a hex string", + help="Service 0x3d WriteMemoryByAddress requires a data payload which can be specified with this flag as a hex string (8 bytes of zeroes by default).", ) async def main(self, args: Namespace) -> None: + """ + The main entry point for the memory scanner. + + Establishes the target session, then scans several memory addresses using + `scan_memory_address`. + + Args: + args: Namespace object containing parsed command-line arguments. + """ + resp = await self.ecu.set_session(args.session) if isinstance(resp, NegativeResponse): logger.critical(f"could not change to session: {resp}") @@ -64,6 +81,15 @@ async def main(self, args: Namespace) -> None: await self.scan_memory_address(args, i) async def scan_memory_address(self, args: Namespace, addr_offset: int = 0) -> None: + """ + Scans a single memory address using the specified service and parameters. + + Args: + args: Namespace object containing parsed command-line arguments. + addr_offset: Optional offset to apply to the base memory address during scanning + (default: 0). Useful for scanning consecutive memory regions. + """ + sid = args.sid data = args.data if sid == 0x3D else None # Only service 0x3d has a data field memory_size = len(data) if data else 0x1000 diff --git a/src/gallia/commands/scan/uds/reset.py b/src/gallia/commands/scan/uds/reset.py index a7476898b..67cf60a64 100644 --- a/src/gallia/commands/scan/uds/reset.py +++ b/src/gallia/commands/scan/uds/reset.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 +"""Module for scanning ECU reset functionality.""" + import reprlib import sys from argparse import Namespace @@ -20,45 +22,57 @@ logger = get_logger("gallia.scan.reset") - class ResetScanner(UDSScanner): - """Scan ecu_reset""" + """Scan ECU reset functionality. + + This scanner tests various ECU reset sub-functions (0x01 to 0x7F) and observes the ECU's response. It can + handle session switching, skips specific sub-functions, and attempts recovery in case of errors. + """ SHORT_HELP = "identifier scan in ECUReset" COMMAND = "reset" def configure_parser(self) -> None: + """Configure arguments for the command line parser.""" self.parser.add_argument( "--sessions", type=auto_int, nargs="*", - help="Set list of sessions to be tested; all if None", + metavar="SESSION_ID", + help="List of session IDs to scan (e.g., 1 3). If not provided, all sessions will be scanned.", ) self.parser.add_argument( "--skip", nargs="+", default={}, type=str, + metavar="SESSION_ID:SUB_FUNCTIONS", action=ParseSkips, help=""" - The sub functions to be skipped per session. - A session specific skip is given by : - where is a comma separated list of single ids or id ranges using a dash. - Examples: - - 0x01:0xf3 - - 0x10-0x2f - - 0x01:0xf3,0x10-0x2f - Multiple session specific skips are separated by space. - Only takes affect if --sessions is given. - """, + Skip specific sub-functions within sessions. Format: 'SESSION_ID:SUB_FUNCTIONS' + + SESSION_ID: ID of the session + SUB_FUNCTIONS: Comma-separated list of: + - Single sub-function IDs (e.g., 0xf3) + - Sub-function ID ranges (e.g., 0x10-0x2f) + + Examples: + - '0x01:0xf3' (Skips sub-function 0xf3 in session 0x01) + - '0x10-0x2f' (Skips sub-functions 0x10 to 0x2f in all sessions) + - '0x01:0xf3,0x10-0x2f' (Multiple skips in session 0x01) + + Multiple session-specific skips can be provided, separated by spaces. + Only applicable if the --sessions option is used. + """, ) self.parser.add_argument( "--skip-check-session", action="store_true", - help="skip check current session; only takes affect if --sessions is given", + help="Disable checking the current session before each sub-function test. Only applicable if the --sessions option is used.", ) async def main(self, args: Namespace) -> None: + """Execute the ECU reset scan, potentially across multiple sessions.""" if args.sessions is None: await self.perform_scan(args) else: diff --git a/src/gallia/commands/scan/uds/sa_dump_seeds.py b/src/gallia/commands/scan/uds/sa_dump_seeds.py index 087dc8d65..dbc3b898c 100644 --- a/src/gallia/commands/scan/uds/sa_dump_seeds.py +++ b/src/gallia/commands/scan/uds/sa_dump_seeds.py @@ -21,7 +21,10 @@ class SASeedsDumper(UDSScanner): - """This scanner tries to enable ProgrammingSession and dump seeds for 12h.""" + """ + This scanner attempts to switch to a specified Diagnostic Session + and continuously dumps security access seeds from the ECU. + """ COMMAND = "dump-seeds" SHORT_HELP = "dump security access seeds" @@ -37,20 +40,20 @@ def configure_parser(self) -> None: metavar="INT", type=auto_int, default=0x02, - help="Set diagnostic session to perform test in", + help="Set diagnostic session to switch into before performing the seed dumping (default: 0x%(default)x)", ) self.parser.add_argument( "--check-session", action="store_true", default=False, - help="Check current session with read DID", + help="Verify the current session by reading DID `0xF186`.", ) self.parser.add_argument( "--level", default=0x11, metavar="INT", type=auto_int, - help="Set security access level to request seed from", + help="Set security access level to request seeds from (default: 0x%(default)x)", ) self.parser.add_argument( "--send-zero-key", @@ -59,8 +62,8 @@ def configure_parser(self) -> None: const=96, default=0, type=int, - help="Attempt to fool brute force protection by pretending to send a key after requesting a seed " - "(all zero bytes, length can be specified)", + help="Attempt to fool brute force protection by sending a zero-filled key after requesting a seed " + "(specify key length in bytes). (default: 0 - disabled)", ) self.parser.add_argument( "--reset", @@ -68,24 +71,38 @@ def configure_parser(self) -> None: const=1, default=None, type=int, - help="Attempt to fool brute force protection by resetting the ECU after every nth requested seed.", + help="Attempt to fool brute force protection by resetting the ECU after every Nth requested seed (default: None - no reset).", ) self.parser.add_argument( "--duration", default=0, type=float, metavar="FLOAT", - help="Run script for N minutes; zero or negative for infinite runtime (default)", + help="Run the dumping for a specified number of minutes (0 or negative for infinite runtime). (default: 0 - infinite)", ) self.parser.add_argument( "--data-record", metavar="HEXSTRING", type=binascii.unhexlify, default=b"", - help="Append an optional data record to each seed request", + help="Optional data record to be appended to the seed request message (provide as hex string). (default: empty data)", ) async def request_seed(self, level: int, data: bytes) -> bytes | None: + """This coroutine requests a security access seed from the connected ECU. + + - Calls the `ecu.security_access_request_seed` method to send the seed request. + - Handles potential `NegativeResponse` from the ECU, logging the error and returning None. + - If the request is successful, extracts and returns the security seed from the response. + + :param level: The security access level to request the seed from. + :type level: int + :param data: Optional data to be included in the seed request message. + :type data: bytes + :return: The requested security access seed on success, None otherwise. + :rtype: bytes | None + """ + resp = await self.ecu.security_access_request_seed( level, data, config=UDSRequestConfig(tags=["ANALYZE"]) ) @@ -105,6 +122,38 @@ async def send_key(self, level: int, key: bytes) -> bool: return True def log_size(self, path: Path, time_delta: float) -> None: + """This method calculates and displays the size and dump speed of captured data. + + **Details:** + + 1. **Get File Size:** + - Calls `path.stat().st_size` to get the size of the file pointed to by `path` in bytes. + - Divides the file size by 1024 to convert it to KiB (Kilobytes). + + 2. **Calculate Data Dump Speed:** + - Checks if `time_delta` (elapsed time) is zero. + - If zero, sets the dump speed (`rate`) to 0 as there's no time for calculation. + - Otherwise, calculates the dump speed in KiB/h (Kilobytes per hour): + - Divides the file size (`size`) by the elapsed time (`time_delta`) and multiplies by 3600 (conversion factor from seconds to hours). + + 3. **Format Units (Size and Speed):** + - Checks if the calculated `rate` is greater than 1024. + - If so, converts `rate` to MiB (Megabytes) by dividing by 1024 and updates the unit (`rate_unit`). + - Similarly, checks if the file `size` is greater than 1024. + - If so, converts `size` to MiB and updates the unit (`size_unit`). + + 4. **Log Information:** + - Uses the logger object (`logger`) to log a message at the 'notice' level. + - The message includes: + - Dump speed (formatted with 2 decimal places) followed by the unit (KiB/h or MiB/h). + - Captured data size (formatted with 2 decimal places) followed by the unit (KiB or MiB). + + :param path: Path object representing the file containing the captured data. + :type path: Path + :param time_delta: Time elapsed since the start of data capture in seconds. + :type time_delta: float + """ + size = path.stat().st_size / 1024 size_unit = "KiB" rate = size / time_delta * 3600 if time_delta != 0 else 0 @@ -118,6 +167,37 @@ def log_size(self, path: Path, time_delta: float) -> None: logger.notice(f"Dumping seeds with {rate:.2f}{rate_unit}/h: {size:.2f}{size_unit}") async def main(self, args: Namespace) -> None: + """This coroutine is the main entry point for the SASeedsDumper scanner. + + **Functionality:** + + 1. **Session Management:** + - Attempts to switch to the diagnostic session specified by `args.session`. + - Logs errors if the ECU fails to change sessions using `logger.critical()`. + - Optionally verifies the current session before proceeding (`--check-session`). + + 2. **Seed Dumping Loop:** + - Opens a file named "seeds.bin" in the scanner's artifacts directory for writing seeds. + - Enters a loop that continues for a user-defined duration (`--duration`) or indefinitely. + - Within the loop: + - Requests a security access seed from the ECU at the specified level (`--level`). + - Handles potential timeouts and errors during seed requests with logging and termination. + - Appends the received seed to the open file. + - Optionally sends a zero-filled key after requesting a seed (`--send-zero-key`). + - Useful for bypassing certain ECU security mechanisms. + - Optionally resets the ECU periodically (`--reset`). + - Aims to overcome seed rate limiting imposed by the ECU. + - Handles ECU recovery and reconnection after reset. + + 3. **Cleanup:** + - Closes the seed data file. + - Logs the total size and dump speed of the captured seeds. + - Leaves the current diagnostic session on the ECU (optional sleep after power cycle). + + :param args: Namespace object containing parsed command-line arguments. + :type args: Namespace + """ + session = args.session logger.info(f"scanning in session: {g_repr(session)}") diff --git a/src/gallia/commands/scan/uds/services.py b/src/gallia/commands/scan/uds/services.py index e2264e5f6..4888df569 100644 --- a/src/gallia/commands/scan/uds/services.py +++ b/src/gallia/commands/scan/uds/services.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 +"""Module for scanning available UDS services.""" + import reprlib from argparse import BooleanOptionalAction, Namespace from typing import Any @@ -23,37 +25,45 @@ class ServicesScanner(UDSScanner): - """Iterate sessions and services and find endpoints""" + """ + Scans for available UDS services on an ECU. + + This scanner iterates through UDS sessions (optional) and service IDs to determine which services are supported by the ECU. + It handles responses, errors, and session management for efficient service discovery. + """ COMMAND = "services" SHORT_HELP = "service scan on an ECU" EPILOG = "https://fraunhofer-aisec.github.io/gallia/uds/scan_modes.html#service-scan" def configure_parser(self) -> None: + """Configures command-line arguments for the service scan.""" + self.parser.add_argument( "--sessions", nargs="*", type=auto_int, default=None, - help="Set list of sessions to be tested; all if None", + metavar="SESSION_ID", + help="List of session IDs to scan (e.g., 1 3). Scans all sessions if not specified.", ) self.parser.add_argument( "--check-session", action="store_true", default=False, - help="check current session; only takes affect if --sessions is given", + help="Verify the current session before each request (only if --sessions is used).", ) self.parser.add_argument( "--scan-response-ids", default=False, action=BooleanOptionalAction, - help="Include IDs in scan with reply flag set", + help="Include service IDs with the 'SuppressPositiveResponse' bit set.", ) self.parser.add_argument( "--auto-reset", action="store_true", default=False, - help="Reset ECU with UDS ECU Reset before every request", + help="Reset the ECU with UDS ECU Reset before each request.", ) self.parser.add_argument( "--skip", @@ -62,19 +72,31 @@ def configure_parser(self) -> None: type=str, action=ParseSkips, help=""" - The service IDs to be skipped per session. - A session specific skip is given by : - where is a comma separated list of single ids or id ranges using a dash. - Examples: - - 0x01:0xf3 - - 0x10-0x2f - - 0x01:0xf3,0x10-0x2f - Multiple session specific skips are separated by space. - Only takes affect if --sessions is given. - """, + Skip specific services within sessions. Format: SESSION_ID:SERVICES + + SESSION_ID: ID of the session (integer) + SERVICES: Comma-separated list of: + - Single service IDs (e.g., 0x22) + - Service ID ranges (e.g., 0x10-0x1F) + + Examples: + - '0x01:0x22' (Skips service 0x22 in session 0x01) + - '0x10-0x1F' (Skips services 0x10 to 0x1F in all sessions) + - '0x01:0xf3,0x10-0x2f' (Multiple skips in session 0x01) + + Multiple session-specific skips can be provided, separated by spaces. + Only applicable if --sessions is used. + """, ) async def main(self, args: Namespace) -> None: + """ + Main execution function for the service scan. + + Organizes the scan process, handling session switching (if enabled) and calling `perform_scan` for each session. + Aggregates and logs results across sessions. + """ + self.result: list[tuple[int, int]] = [] self.ecu.max_retry = 1 found: dict[int, dict[int, Any]] = {} @@ -124,6 +146,14 @@ async def main(self, args: Namespace) -> None: logger.result(f" [{g_repr(sid)}] vendor specific sid: {data}") async def perform_scan(self, args: Namespace, session: None | int = None) -> dict[int, Any]: + """ + Performs the scan for supported services within a specific session. + + Iterates through service IDs, sending requests with varying payloads to determine service availability. + Handles timeouts, malformed responses, and negative responses from the ECU. + Returns a dictionary of found services and their responses. + """ + result: dict[int, Any] = {} # Starts at 0x00, see first loop iteration. diff --git a/src/gallia/commands/scan/uds/sessions.py b/src/gallia/commands/scan/uds/sessions.py index 2f9274c5a..34a8c07fa 100644 --- a/src/gallia/commands/scan/uds/sessions.py +++ b/src/gallia/commands/scan/uds/sessions.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 +"""Module for scanning available UDS sessions and their transitions.""" + import asyncio import sys from argparse import Namespace @@ -24,21 +26,30 @@ class SessionsScanner(UDSScanner): - """Iterate Sessions""" + """ + Scans for available UDS diagnostic sessions and their transitions. + + Starts from the default session (0x01) and recursively explores other sessions, + handling session switching, potential errors, and ECU reset. + """ COMMAND = "sessions" SHORT_HELP = "session scan on an ECU" def configure_parser(self) -> None: self.parser.add_argument( - "--depth", type=auto_int, default=None, help="Specify max scanning depth." + "--depth", + type=auto_int, + default=None, + metavar="DEPTH", + help="Maximum depth to explore for session transitions (default: unlimited).", ) self.parser.add_argument( "--sleep", metavar="SECONDS", type=auto_int, default=0, - help="Sleep this amount of seconds after changing to DefaultSession", + help="Seconds to wait after switching to the DefaultSession (0x01).", ) self.parser.add_argument( "--skip", @@ -46,30 +57,36 @@ def configure_parser(self) -> None: type=auto_int, default=[], nargs="*", - help="List with session IDs to skip while scanning", + help="List of session IDs to skip during the scan (e.g., '1 3').", ) self.parser.add_argument( "--with-hooks", action="store_true", - help="Use hooks in case of a ConditionsNotCorrect error", + help="Use diagnostic session control hooks to handle 'ConditionsNotCorrect' errors.", ) self.parser.add_argument( "--reset", nargs="?", default=None, - const=0x01, + const=0x01, # Default reset level if --reset is provided without a value type=lambda x: int(x, 0), - help="Reset the ECU after each iteration with the optionally given reset level", + help="Reset the ECU after each iteration using the specified reset level (hex, e.g., '0x01').", ) self.parser.add_argument( "--fast", action="store_true", - help="Only search for new sessions once in a particular session, i.e. ignore different stacks", + help="Search for new sessions only once per session, ignoring alternative paths (faster, but less thorough).", ) async def set_session_with_hooks_handling( self, session: int, use_hooks: bool ) -> NegativeResponse | DiagnosticSessionControlResponse: + """ + Attempts to set the specified session, optionally using diagnostic session control hooks. + + Handles conditionsNotCorrect responses by retrying with hooks if enabled. + """ + resp = await self.ecu.set_session( session, config=UDSRequestConfig(skip_hooks=True), use_db=False ) @@ -102,6 +119,12 @@ async def set_session_with_hooks_handling( return resp async def recover_stack(self, stack: list[int], use_hooks: bool) -> bool: + """ + Attempts to recover the session stack by sequentially setting sessions from the given stack. + + Returns True if the stack recovery was successful, False otherwise. + """ + for session in stack: try: resp = await self.set_session_with_hooks_handling(session, use_hooks) @@ -121,6 +144,13 @@ async def recover_stack(self, stack: list[int], use_hooks: bool) -> bool: return True async def main(self, args: Namespace) -> None: + """ + Main execution of the session scan. + + Performs a depth-first search to discover session transitions, starting from the default session. + Handles session skipping, ECU resets (if enabled), and logs the found sessions. + """ + self.result: list[int] = [] found: dict[int, list[list[int]]] = {0: [[0x01]]} positive_results: list[dict[str, Any]] = [] diff --git a/src/gallia/db/handler.py b/src/gallia/db/handler.py index 4ea1d8906..840722ab8 100644 --- a/src/gallia/db/handler.py +++ b/src/gallia/db/handler.py @@ -2,6 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 +"""Module for database handling in Gallia.""" + import asyncio import json from datetime import datetime @@ -24,6 +26,7 @@ def bytes_repr(data: bytes) -> str: + """Converts bytes data to a hexadecimal string representation without a '0x' prefix.""" return bytes_repr_(data, False, None) @@ -135,7 +138,11 @@ def bytes_repr(data: bytes) -> str: class DBHandler: + """Handles database operations for logging and storing results of Gallia scans.""" + def __init__(self, database: Path): + """Initializes the database handler.""" + self.tasks: list[asyncio.Task[None]] = [] self.path = database self.connection: aiosqlite.Connection | None = None @@ -145,6 +152,8 @@ def __init__(self, database: Path): self.meta: int | None = None async def connect(self) -> None: + """Establishes a connection to the SQLite database.""" + assert self.connection is None, "Already connected to the database" self.path.parent.mkdir(exist_ok=True, parents=True) @@ -162,6 +171,8 @@ async def connect(self) -> None: await self.check_version() async def disconnect(self) -> None: + """Closes the database connection after completing pending tasks.""" + assert self.connection is not None, "Not connected to the database" for task in self.tasks: @@ -177,6 +188,8 @@ async def disconnect(self) -> None: self.connection = None async def check_version(self) -> None: + """Verifies the compatibility of the database schema version.""" + assert self.connection is not None, "Not connected to the database" query = 'SELECT version FROM version WHERE schema = "main"' @@ -204,6 +217,8 @@ async def insert_run_meta( # noqa: PLR0913 start_time: datetime, path: Path, ) -> None: + """Inserts metadata about the scan run into the database.""" + assert self.connection is not None, "Not connected to the database" query = ( @@ -229,6 +244,8 @@ async def insert_run_meta( # noqa: PLR0913 await self.connection.commit() async def complete_run_meta(self, end_time: datetime, exit_code: int, path: Path) -> None: + """Completes the run metadata by adding the end time, exit code, and final path.""" + assert self.connection is not None, "Not connected to the database" assert self.meta is not None, "Run meta not yet created" @@ -240,6 +257,8 @@ async def complete_run_meta(self, end_time: datetime, exit_code: int, path: Path await self.connection.commit() async def insert_scan_run(self, target: str) -> None: + """Inserts information about the target ECU and the associated scan run.""" + assert self.connection is not None, "Not connected to the database" assert self.meta is not None, "Run meta not yet created" @@ -256,6 +275,8 @@ async def insert_scan_run(self, target: str) -> None: await self.connection.commit() async def insert_scan_run_properties_pre(self, properties_pre: dict[str, Any]) -> None: + """Inserts the properties of the target ECU before the scan.""" + assert self.connection is not None, "Not connected to the database" assert self.scan_run is not None, "Scan run not yet created" @@ -264,6 +285,8 @@ async def insert_scan_run_properties_pre(self, properties_pre: dict[str, Any]) - await self.connection.commit() async def complete_scan_run(self, properties_post: dict[str, Any]) -> None: + """Updates the scan run entry with the ECU's properties after the scan.""" + assert self.connection is not None, "Not connected to the database" assert self.scan_run is not None, "Scan run not yet created" @@ -272,6 +295,8 @@ async def complete_scan_run(self, properties_post: dict[str, Any]) -> None: await self.connection.commit() async def insert_discovery_run(self, protocol: str) -> None: + """Inserts a record for a discovery run using the given protocol.""" + assert self.connection is not None, "Not connected to the database" assert self.meta is not None, "Run meta not yet created" @@ -281,6 +306,8 @@ async def insert_discovery_run(self, protocol: str) -> None: await self.connection.commit() async def insert_discovery_result(self, target: str) -> None: + """Inserts a discovered target address from a discovery run.""" + assert self.connection is not None, "Not connected to the database" assert self.discovery_run is not None, "Discovery run not yet created" @@ -302,6 +329,8 @@ async def insert_scan_result( # noqa: PLR0913 log_mode: LogMode, commit: bool = True, ) -> None: + """Inserts detailed results of a single UDS request and its response (or exception).""" + assert self.connection is not None, "Not connected to the database" assert self.scan_run is not None, "Scan run not yet created" @@ -391,6 +420,8 @@ async def execute() -> None: self.tasks.append(asyncio.create_task(execute())) async def insert_session_transition(self, destination: int, steps: list[int]) -> None: + """Records a transition to a new session, along with the steps taken to reach it.""" + assert self.connection is not None, "Not connected to the database" query = "INSERT INTO session_transition VALUES(?, ?, ?)" @@ -398,6 +429,8 @@ async def insert_session_transition(self, destination: int, steps: list[int]) -> await self.connection.execute(query, parameters) async def get_sessions(self) -> list[int]: + """Retrieves a list of all discovered sessions for the current target.""" + assert self.connection is not None, "Not connected to the database" assert self.target is not None, "Scan run not yet created, target unknown" @@ -415,6 +448,8 @@ async def get_sessions(self) -> list[int]: return [x[0] for x in await cursor.fetchall()] async def get_session_transition(self, destination: int) -> list[int] | None: + """Retrieves the steps taken to transition to a specific session.""" + assert self.connection is not None, "Not connected to the database" assert self.target is not None, "Scan run not yet created, target unknown"