TSK Manager v0.11.0
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
#!/usr/bin/env python3
|
||||
import struct
|
||||
import time
|
||||
from subprocess import check_output, CalledProcessError
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from tqdm import tqdm
|
||||
|
||||
from opendbc.car.isotp import isotp_send
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.uds import UdsClient, ACCESS_TYPE, SESSION_TYPE, DATA_IDENTIFIER_TYPE, SERVICE_TYPE, \
|
||||
ROUTINE_CONTROL_TYPE, InvalidServiceIdError, MessageTimeoutError, NegativeResponseError
|
||||
from panda import Panda
|
||||
from tsk.common.env import is_agnos, PAYLOAD_PATH
|
||||
|
||||
|
||||
class NotAGNOSError(Exception):
|
||||
def __str__(self) -> str:
|
||||
return "Can't run TSK Extractor outside of a comma device."
|
||||
|
||||
|
||||
class BoarddNotRunningError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RetryError(Exception):
|
||||
def __init__(self, message: str):
|
||||
self.message: str = message
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.message}\n\nTry again. If the problem persists, turn off the car, put it back into 'Not Ready to Drive' mode, and then try again."
|
||||
|
||||
|
||||
def format_version_for_error_display(version1, version2=None, length=8):
|
||||
version_str = ""
|
||||
|
||||
version1_str = str(version1)
|
||||
if version1_str.startswith("b'"):
|
||||
version1_str = version1_str[2:]
|
||||
|
||||
version_str = version1_str[:length]
|
||||
|
||||
if version2 and version1 != version2:
|
||||
version2_str = str(version2)
|
||||
if version2_str.startswith("b'"):
|
||||
version2_str = version2_str[2:]
|
||||
|
||||
version_str += ", " + version2_str[:length]
|
||||
|
||||
return version_str
|
||||
|
||||
|
||||
class TSKExtractor:
|
||||
ADDR = 0x7a1
|
||||
DEBUG = False
|
||||
BUS = 0
|
||||
|
||||
SEED_KEY_SECRET = b'\xf0\x5f\x36\xb7\xd7\x8c\x03\xe2\x4a\xb4\xfa\xef\x2a\x57\xd0\x44'
|
||||
|
||||
# These are the key and IV used to encrypt the payload in build_payload.py
|
||||
DID_201_KEY = b'\x00' * 16
|
||||
DID_202_IV = b'\x00' * 16
|
||||
|
||||
# Confirmed working on the following versions
|
||||
APPLICATION_VERSIONS = {
|
||||
b'\x018965B4209000\x00\x00\x00\x00': b'\x01!!!!!!!!!!!!!!!!', # 2021 RAV4 Prime
|
||||
b'\x018965B4233100\x00\x00\x00\x00': b'\x01!!!!!!!!!!!!!!!!', # 2023 RAV4 Prime
|
||||
b'\x018965B4509100\x00\x00\x00\x00': b'\x01!!!!!!!!!!!!!!!!', # 2021 Sienna
|
||||
}
|
||||
|
||||
KEY_STRUCT_SIZE = 0x20
|
||||
CHECKSUM_OFFSET = 0x1d
|
||||
SECOC_KEY_SIZE = 0x10
|
||||
SECOC_KEY_OFFSET = 0x0c
|
||||
|
||||
@classmethod
|
||||
def _get_key_struct(cls, data, key_no):
|
||||
return data[key_no * cls.KEY_STRUCT_SIZE: (key_no + 1) * cls.KEY_STRUCT_SIZE]
|
||||
|
||||
@classmethod
|
||||
def _verify_checksum(cls, key_struct):
|
||||
checksum = sum(key_struct[:cls.CHECKSUM_OFFSET])
|
||||
checksum = ~checksum & 0xff
|
||||
return checksum == key_struct[cls.CHECKSUM_OFFSET]
|
||||
|
||||
@classmethod
|
||||
def _get_secoc_key(cls, key_struct):
|
||||
return key_struct[cls.SECOC_KEY_OFFSET:cls.SECOC_KEY_OFFSET + cls.SECOC_KEY_SIZE]
|
||||
|
||||
@classmethod
|
||||
def hack(cls):
|
||||
"""Initializes the ECU connection and checks if boardd is running."""
|
||||
if not is_agnos():
|
||||
raise NotAGNOSError
|
||||
|
||||
try:
|
||||
check_output(["pidof", "boardd"])
|
||||
# This shouldn't happen since we never started boardd
|
||||
raise BoarddNotRunningError("boardd is running, kill openpilot and run again")
|
||||
except CalledProcessError as e:
|
||||
if e.returncode != 1: # 1 == no process found (boardd not running)
|
||||
raise e
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
panda = Panda()
|
||||
panda.flash() # no-op if firmware is already up to date; required because TSKM kills pandad before it can flash
|
||||
panda.set_safety_mode(CarParams.SafetyModel.elm327)
|
||||
|
||||
uds_client = UdsClient(panda, cls.ADDR, cls.ADDR + 8, cls.BUS, timeout=0.1, response_pending_timeout=0.1)
|
||||
|
||||
print("Getting application versions...")
|
||||
|
||||
try:
|
||||
app_version = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION)
|
||||
print(f" - APPLICATION_SOFTWARE_IDENTIFICATION (application): {str(app_version)}")
|
||||
except (AssertionError, InvalidServiceIdError, MessageTimeoutError, NegativeResponseError):
|
||||
raise RetryError("Car not detected")
|
||||
|
||||
if app_version not in cls.APPLICATION_VERSIONS:
|
||||
print(f"Unexpected application version (ignored): {str(app_version)}")
|
||||
|
||||
# Mandatory flow of diagnostic sessions
|
||||
try:
|
||||
uds_client.diagnostic_session_control(SESSION_TYPE.DEFAULT)
|
||||
uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC)
|
||||
uds_client.diagnostic_session_control(SESSION_TYPE.PROGRAMMING)
|
||||
uds_client.diagnostic_session_control(SESSION_TYPE.DEFAULT)
|
||||
uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC)
|
||||
except (InvalidServiceIdError, MessageTimeoutError, NegativeResponseError):
|
||||
raise RetryError("Car not in 'Not Ready To Drive' mode")
|
||||
|
||||
# Get bootloader version
|
||||
try:
|
||||
bl_version = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION)
|
||||
except (AssertionError, InvalidServiceIdError, MessageTimeoutError, NegativeResponseError):
|
||||
raise RetryError(f"Can't read bootloader version ({format_version_for_error_display(app_version)})")
|
||||
print(f" - APPLICATION_SOFTWARE_IDENTIFICATION (bootloader) {str(bl_version)}")
|
||||
|
||||
try:
|
||||
if bl_version != cls.APPLICATION_VERSIONS[app_version]:
|
||||
print(f"Unexpected bootloader version (ignored): {str(bl_version)}")
|
||||
except KeyError as e: # In case app_version is not found at all
|
||||
print(f"Unexpected bootloader version (ignored): {str(e)}")
|
||||
|
||||
# Go back to programming session
|
||||
try:
|
||||
uds_client.diagnostic_session_control(SESSION_TYPE.PROGRAMMING)
|
||||
except (InvalidServiceIdError, MessageTimeoutError, NegativeResponseError):
|
||||
raise RetryError("Can't enter programming session for reading bootloader version")
|
||||
|
||||
# Security Access - Request Seed
|
||||
try:
|
||||
seed_payload = b"\x00" * 16
|
||||
seed = uds_client.security_access(ACCESS_TYPE.REQUEST_SEED, data_record=seed_payload)
|
||||
|
||||
key = AES.new(cls.SEED_KEY_SECRET, AES.MODE_ECB).decrypt(seed_payload)
|
||||
key = AES.new(key, AES.MODE_ECB).encrypt(seed)
|
||||
|
||||
print("\nSecurity Access...")
|
||||
|
||||
print(" - SEED:", seed.hex())
|
||||
print(" - KEY:", key.hex())
|
||||
|
||||
# Security Access - Send Key
|
||||
uds_client.security_access(ACCESS_TYPE.SEND_KEY, key)
|
||||
print(" - Key OK!")
|
||||
|
||||
except (InvalidServiceIdError, MessageTimeoutError, NegativeResponseError):
|
||||
raise RetryError("Security Access failed")
|
||||
|
||||
# Security Access - Send Key
|
||||
print("\nPreparing to upload payload...")
|
||||
|
||||
try:
|
||||
# Write something to DID 203, not sure why but needed for state machine
|
||||
uds_client.write_data_by_identifier(0x203, b"\x00" * 5)
|
||||
|
||||
# Write KEY and IV to DID 201/202, prerequisite for request download
|
||||
print(" - Write data by identifier 0x201", cls.DID_201_KEY.hex())
|
||||
uds_client.write_data_by_identifier(0x201, cls.DID_201_KEY)
|
||||
|
||||
print(" - Write data by identifier 0x202", cls.DID_202_IV.hex())
|
||||
uds_client.write_data_by_identifier(0x202, cls.DID_202_IV)
|
||||
|
||||
# Request download to RAM
|
||||
data = b"\x01" # [1] Format
|
||||
data += b"\x46" # [2] 4 size bytes, 6 address bytes
|
||||
data += b"\x01" # [3] memoryIdentifier
|
||||
data += b"\x00" # [4]
|
||||
data += struct.pack('!I', 0xfebf0000) # [5] Address
|
||||
data += struct.pack('!I', 0x1000) # [9] Size
|
||||
|
||||
print("\nUpload payload...")
|
||||
|
||||
print(" - Request download")
|
||||
resp = uds_client._uds_request(SERVICE_TYPE.REQUEST_DOWNLOAD, data=data)
|
||||
|
||||
# Upload payload
|
||||
payload = open(PAYLOAD_PATH, "rb").read()
|
||||
assert len(payload) == 0x1000
|
||||
chunk_size = 0x400
|
||||
for i in range(len(payload) // chunk_size):
|
||||
print(f" - Transfer data {i}")
|
||||
uds_client.transfer_data(i + 1, payload[i * chunk_size:(i + 1) * chunk_size])
|
||||
|
||||
uds_client.request_transfer_exit()
|
||||
|
||||
print("\nVerify payload...")
|
||||
|
||||
# Routine control 0x10f0
|
||||
# [0] 0x31 (routine control)
|
||||
# [1] 0x01 (start)
|
||||
# [2] 0x10f0 (routine identifier)
|
||||
# [4] 0x45 (format, 4 size bytes, 5 address bytes)
|
||||
# [5] 0x0
|
||||
# [6] mem addr
|
||||
# [10] mem addr
|
||||
data = b"\x45\x00"
|
||||
data += struct.pack('!I', 0xfebf0000)
|
||||
data += struct.pack('!I', 0x1000)
|
||||
|
||||
uds_client.routine_control(ROUTINE_CONTROL_TYPE.START, 0x10f0, data)
|
||||
print(" - Routine control 0x10f0 OK!")
|
||||
|
||||
except (InvalidServiceIdError, MessageTimeoutError, NegativeResponseError):
|
||||
raise RetryError("Payload upload failed")
|
||||
|
||||
print("\nTrigger payload...")
|
||||
|
||||
# Now we trigger the payload by trying to erase
|
||||
# [0] 0x31 (routine control)
|
||||
# [1] 0x01 (start)
|
||||
# [2] 0xff00 (routine identifier)
|
||||
# [4] 0x45 (format, 4 size bytes, 5 address bytes)
|
||||
# [5] 0x0
|
||||
# [6] mem addr
|
||||
# [10] mem addr
|
||||
data = b"\x45\x00"
|
||||
data += struct.pack('!I', 0xe0000)
|
||||
data += struct.pack('!I', 0x8000)
|
||||
|
||||
# Manually send so we don't get stuck waiting for the response
|
||||
erase = b"\x31\x01\xff\x00" + data
|
||||
isotp_send(panda, erase, cls.ADDR, bus=cls.BUS)
|
||||
|
||||
print("\nDumping keys...")
|
||||
start = 0xfebe6e34
|
||||
end = 0xfebe6ff4
|
||||
|
||||
start_time = time.time()
|
||||
timeout = 30
|
||||
|
||||
extracted = b""
|
||||
|
||||
with open(f'data_{start:08x}_{end:08x}.bin', 'wb') as f:
|
||||
with tqdm(total=end - start) as pbar:
|
||||
while start < end:
|
||||
|
||||
current_time = time.time()
|
||||
if current_time - start_time > timeout:
|
||||
raise RetryError("Key dumping timed out")
|
||||
|
||||
for addr, *_, data, bus in panda.can_recv():
|
||||
if bus != cls.BUS:
|
||||
continue
|
||||
|
||||
if data == b"\x03\x7f\x31\x78\x00\x00\x00\x00": # Skip response pending
|
||||
continue
|
||||
|
||||
if addr != cls.ADDR + 8:
|
||||
continue
|
||||
|
||||
if cls.DEBUG:
|
||||
print(f"{data.hex()}")
|
||||
|
||||
ptr = struct.unpack("<I", data[:4])[0]
|
||||
assert (ptr >> 8) == start & 0xffffff # Check lower 24 bits of address
|
||||
|
||||
extracted += data[4:]
|
||||
f.write(data[4:])
|
||||
f.flush()
|
||||
|
||||
start += 4
|
||||
pbar.update(4)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
key_1_ok = cls._verify_checksum(cls._get_key_struct(extracted, 1))
|
||||
key_4_ok = cls._verify_checksum(cls._get_key_struct(extracted, 4))
|
||||
|
||||
if not key_1_ok or not key_4_ok:
|
||||
raise RetryError(f"SecOC key checksum verification failed ({format_version_for_error_display(app_version, bl_version)})")
|
||||
|
||||
key_1 = cls._get_secoc_key(cls._get_key_struct(extracted, 1))
|
||||
key_4 = cls._get_secoc_key(cls._get_key_struct(extracted, 4))
|
||||
|
||||
print("\nECU_MASTER_KEY ", key_1.hex())
|
||||
print("SecOC Key (KEY_4)", key_4.hex())
|
||||
|
||||
return key_4.hex()
|
||||
|
||||
@classmethod
|
||||
def run(cls):
|
||||
try:
|
||||
secoc_key = cls.hack()
|
||||
except (BoarddNotRunningError, RetryError):
|
||||
raise
|
||||
except Exception as e:
|
||||
e.add_note("\n\n!!!! Unexpected error. Please take a photo, post it on #toyota-security, and ping @calvinspark\n")
|
||||
raise
|
||||
|
||||
print("SecOC key extracted successfully")
|
||||
print("!!!! Take a photo of this screen")
|
||||
return secoc_key
|
||||
Reference in New Issue
Block a user