Broadcom Bluetooth: Unpatching the unpatchable

Due to some vulnerabilities found recently, Broadcom started to see the "Bluetooth host -> Bluetooth controller" communication as an attack surface. In Bluetooth terminology, the host is the operating system with a Bluetooth daemon, e.g. iOS or Android, and the controller is Broadcom's Bluetooth chip. A lot of manufacturers see that as attack surface - to protect their intellectual property or to prevent that people use their smartphone as software-defined radio. Moreover, it raises the bar for initial exploit development.

Other manufacturers protect their chips with secure boot. This means that they sign their firmware. The chip has a minimal bootloader, which loads the vendor's firmware for that chip, checks the signature, and then boots it. Thus, for a lot of chips, the first step is to find a secure boot bypass or to find a vendor/device that has secure boot disabled.

Broadcom had an interesting idea on that, for both Bluetooth and Wi-Fi chips. They store their firmware in a ROM. Thus, it cannot be manipulated and does not need to be verified :)

Patchram concept

Okay. But how do they apply patches then? Their chips are ARM (Cortex M3/M4 for Bluetooth and I think R4 for Wi-Fi). ARM has a Flashpatch unit, or, as Broadcom calls and maps it, Patchram. The Patchram works similar to debug breakpoints. It defines 128 (M3-flavored chips) or 256 (M4) slots. If a slot is active, it has a mapping of addresses in ROM and patches. Each patch is 4 bytes and must be 4-byte-aligned. On ARM, a branch instruction is exactly 4 bytes. As a result, the Patchram can define up to 256 positions in the ROM that branch to RAM and execute something there. This works fully transparent, meaning that without looking into the Patchram, one can just execute and read from the ROM and would always get the patched contents.

This has a couple of interesting implications:

  • The ROM cannot be changed.
  • The Patchram is limited.
  • The RAM is limited, thus, Patchram cannot replace the main thread or similar to compensate for the limit.
  • If the ROM does not implement any signature verification, the firmware remains open for creative hacking :)
  • There are always RAM regions that can execute code \o/ RWX FTW.
  • Vendors need to choose which patches they apply once they run out of Patchram.
  • Only publicly released vulnerabilities are patched.
The initial firmware is flashed on the ROM when the chip is built. Using the firmware compile date, we can indicate how old the firmware is. There are some exceptions, though, where the compile date is fairly recent but it uses quite outdated code and libraries.

Patchram implementation


So, lets look into the firmware of the Broadcom Bluetooth chip used in the Samsung Galaxy S10 and S20 series, which includes variants like the S10e and the Note 20 5G. (Only the European version has Broadcom chips, though, you might also come across Qualcomm on the worldwide model.) The build date is copied to a variable also referred to as bootcheck. So, even though it is stored in ROM, after startup, you would find it shortly after 0x200400 on any chip:

   ____     __                    _____  __ 
  /  _/__  / /____ _______  ___ _/ / _ )/ /_ _____
 _/ // _ \/ __/ -_) __/ _ \/ _ `/ / _  / / // / -_)
/___/_//_/\__/\__/_/ /_//_/\_,_/_/____/_/\_,_/\__/

type <help -v> for usage information!
> hd 0x200400
00200400: ee 0a 3e 19  68 3d 81 90  00 00 00 00  00 00 00 00   |··>·|h=··|····|····|
00200410: 08 42 18 92  41 70 72 20  31 33 20 32  30 31 38 00   |·B··|Apr |13 2|018·|
00200420: 32 32 3a 35  35 3a 34 31  34 33 37 35  42 31 00 00   |22:5|5:41|4375|B1··|
00200430: 10 00 69 43  0e 00 00 00  00 00 00 00  00 00 00 00   |··iC|····|····|····|

This means that I call this chip BCM4375B1. Sometimes, the packing or firmware naming within the operating system diverges. And it was built on April 13 2018. The Samsung Galaxy S10 had been released in March 2019, which is the first phone with this chip.

And now let's take a look into the Patchram with all the patches applied that are contained in the January 2021 update:

> info patchram
[*] ### | Patchram Table ###
[*] [000] 0x000F0B7C: 3d3e0600 (subs r6, #61 ; 0x3d;  movs r6, r0)
[*] [001] 0x000177AC: 1cf2debd (b.w  23436c <.text+0x21cbc0>)
[*] [002] 0x000178F0: 1cf232bf (b.w  234758 <.text+0x21ce68>)
[*] [003] 0x0001655C: 49f16cbf (b.w  160438 <.text+0x149edc>)
...
[*] [249] 0x000C422C: baf0bcb8 (b.w  17e3a8 <.text+0xba17c>)
[*] [250] 0x000C43D4: baf048b8 (b.w  17e468 <.text+0xba094>)
[*] [251] 0x0006E570: 0ff1ecbf (b.w  17e54c <.text+0x10ffdc>)
[*] [252] 0x0006E478: 10f1fcb8 (b.w  17e674 <.text+0x1101fc>)

Okay, the slot at position 0 is a little hack by me, more on that later! But you can see that each slot defines an address and a branch instruction.

When I first looked into that chip, it already had 200 slots in use. That was on my flight to DEF CON 2019, where I added some ugly shell-scripting based implementation to InternalBlue that allows (slower) interaction with the chip without patching around in Android drivers. The applied patches were on a June 2019 level, as far as I remember :)

Broadcom is running out of Patchram slots and their customers need to choose between performance and security patches. 

Broadcom's approach towards locking down the chips

After the chip started, it already has a functional Bluetooth firmware. On some devices, this firmware is not fully functional without the patches, because Broadcom really screwed something up. Like in the Nexus 5... but I think they made a PCIe-related mistake in the iPhone 11 again that also leads to similar issues. While you can run most firmwares without patches, you really shouldn't do that ;)

So the simplest approach - removing all patches to remove everything that Broadcom patched to lock down a chip - works in general, but then you can no longer establish active connections on some chips. Thus, it is useless for research purposes that require normal chip behavior.

To lock down the chips, Broadcom removes the HCI command that enables the host to write to the controller's RAM, called Write_RAM in the following. However, Write_RAM is essential to apply patches. The solution is to blocklist the Write_RAM command, which has the code 0xfc4c. And while we're on that, also the Read_RAM command 0xfc4d on some devices, along with Launch_RAM 0xfc4e, etc. The implementation of this varies a bit depending on the vendor. I first reverse-engineered this when the patch was introduced to the iPhone SE2 on iOS 13.5, the Fiti firmware:


I put significantly more effort into my Fiti database, so my Samsung Note 20 5G database does not look that nice. Anyway, this is the thing that we want to remove in the following:


The compare instructions only check one half of the command. 0xfc indicates that a command is vendor-specific and we are already in the vendor-specific handler, thus, we only compare 0x4c, 0x4e, etc. Depending on the compiler's mood, the register in the comparison seems to be r2, r4 or r5. This ends up in similar byte representations, which makes it easier to patch.

Typically you do not even need to go the whole steps of getting the full firmware image. Just some chunks of the .hcd file are sufficient, then you can search for  0x4c2a and will find the patched commands.

On Fiti, the .hcd file, which contains the patches, was still on the file system and easy to modify. A .hcd file contains a series of Write_RAM commands that apply patches. During this, the firmware is put in a so-called "Download Minidriver" mode, and all other threads are disabled, i.e., you cannot have connections in parallel. Then, it does a soft reboot into the patched firmware. This specific implementation doesn't matter as of now, all we need to do is to identify the byte series and replace the byte 0x4c by a handler that we want to remove, restart Bluetooth, and then we get Write_RAM again even after firmware initialization \o/

Second lockdown

The same approach also worked in June 2020 on Android on a Samsung Galaxy S8. But when I tried this on the January 2021 patch level on a Samsung Galaxy S10e to that I copied the firmware of a Samsung Galaxy Note 20 5G (I'm sick of Odin, you know...), this did not work any more.

I was able to flip bytes in the first region of the .hcd file, but once I changed bytes in the second half, the driver wouldn't initialize any more. adb logcat looks like this:

01-19 01:19:04.665 22238 22238 E bt_hwcfg: Start CFG HW, HCI reset
01-19 01:19:04.676 22238 22242 E bt_hwcfg: Read Local Name after HCI reset
01-19 01:19:04.701 22238 22242 D bt_hwcfg: Chipset BCM4375B1
01-19 01:19:04.701 22238 22242 D bt_hwcfg: Target name = [BCM4375B1]
01-19 01:19:04.701 22238 22242 I bt_hwcfg: module_type[semco_sem_e43_cs51].
01-19 01:19:04.701 22238 22242 I bt_hwcfg: Found patchfile: /vendor/firmware/bcm4375B1_semco.hcd
01-19 01:19:04.701 22238 22242 E bt_hwcfg: fw ver (org)0.0
01-19 01:19:04.701 22238 22242 E bt_hwcfg: Final Patchram is /vendor/firmware/bcm4375B1_semco.hcd
01-19 01:19:04.702 22238 22242 I bt_hwcfg: Axi patch failure or not include AXI patching
01-19 01:19:04.704 22238 22242 I bt_hwcfg: bt vendor lib baud_1: set UART baud 4000000
01-19 01:19:07.008 22238 22242 I bt_hwcfg: bt vendor lib: set UART baud 115200
01-19 01:19:07.008 22238 22242 I bt_hwcfg: FW Download delta = 2332269
01-19 01:19:07.008 22238 22242 D bt_hwcfg: Settlement delay -- 100 ms
01-19 01:19:07.008 22238 22242 I bt_hwcfg: Setting fw settlement delay to 100 
01-19 01:19:07.169 22238 22242 E bt_hwcfg: vendor lib fwcfg aborted!!!

Oh no :(

With the original firmware or bytes flipped in the first parts of the .hcd file, it still worked and finished with the following message:

01-19 01:22:52.753 24910 24914 I bt_hwcfg: vendor lib fwcfg completed

There is also a state in between, where the chip bootloops despite finishing with the completed message. This can happen while patching things that are valid but do not execute correctly etc.

Any checksum mechanism Broadcom can introduce at this point has to be in the .hcd file. So I took a look at this and separated lines to start at 0xfc4c, the Write_RAM command.

4cfc 46 00002400 4252434d6...
4cfc ff 42002400 4252434d6...
...
4cfc e5 48312400 000018cd17001
             ^----------- any change in this upper part accepted
4cfc 05 380720 00 01
             v----------- any change from here on rejected
4cfc 10 00041600 fd8a480c154eaa50713a2f93
4cfc ff 30e52200 314923006...

Each Write_RAM command has a length field, reverse order 4 byte address, and then the patch contents.

The patches are split in 3 regions:
  • A patch configuration region at 0x240000. This replaced the TLV format Dennis Mantz reverse-engineered in his Master thesis.
  • A lower Patchram region at 0x164000.
  • A higher Patchram region at 0x22e530, probably located exactly after all dynamic memory needed on this chip.
  • The weird bit in between sets some boot configuration flag. The patch file can theoretically patch everything and the compiler does strange things etc. :)
Do to the compiler's weird mood of putting a 16 byte command first, while most others were 255 byte, I thought that this might be the checksum I'm looking for. Maybe fd8a480c154eaa50713a2f93, is a checksum, but that's not the way I solved it in the end.

Lockdown light

The .hcd file can be parsed by the Nexmon HCD extractor. It still generates sparse files but hey, it still works on the new format. I did some command line magic to split the regions and then merge them with dd. I'm a bit ashamed of what I did there and you'll likely do it better xD

For better readability, I loaded the ROM I had previously extracted at my flight to DEF CON and then merged it with these memory chunks in IDA. And then, I tried to find the checksum check. CRC32 is indeed used within the Patchram but for something completely different :( And even with the sparse symbols I had previously reverse-engineered and now also bindiffed, it just didn't look that great.

So, if we cannot unpatch the Patchram because reversing is too hard, why not reconfigure the Patchram configuration region?

The TLV format reverse-engineered by Dennis does no longer exist. Instead, a different format is used and luckily WICED Studio leaks information about it. In the .hdf files, there are comments like this:

     COMMAND "Function Call" 0x0106
     {
         doc "Config item code used to call a function directly while processing config data. It takes a"
             "single parameter in such an ENTRY: construct, which is function_addresss."
         PARAM "Address"
             uint32
             doc "The address of the function to be called.";
     }

     COMMAND "Patch Entry" 0x0110
     {
         doc "Installs a code patch entry.  There are three types of patches.  The first type is a"
             "simple replacement of an 8-byte block of instructions with another, and therefore does"
             "not make use of the Code byte array.  The second type contains replacement instructions"
             "which vector to a contiguous block of code, loaded from the Code parameter.  The third"
             "type contains replacement instructions which vector to a fragmented block of code, "
             "loaded from the Code byte array of the Patch Entry config item, with other associated"
             "code segments being loaded from one or more Data Patch config items."
         PARAM "Patch index"
             uint8
             doc "The patch register index to be used"
             max = 255; 
         PARAM "Break out block address"
             uint32
             doc "The address of the block of code to be replaced.  It must fall on a four-byte"
                 "boundary.";
         PARAM "Replacement instructions"
             uint8[4]
             doc "The instructions to be executed in place of the block of code to be replaced.";
         PARAM "Code size"
             uint16
             doc "The size of the code segment to be loaded."
             binary_message_only
             encode_value = ByteArrayValidLength("Code");
         PARAM "Code address"
             uint32
             doc "The address to which to load the code bytes in the Code byte array.";
         PARAM "Code"
             uint8[0xFF00] omit_pad_bytes
             doc "The actual code instructions to be loaded to the address specified by Code address";
     }


Looks like what we need. 

Initially, I tried to use the Patchram configuration to write to the Patchram RAM region. This also lets the check fail :( So, the check is applied later.

Thus, let's just patch something in the ROM and *only* use the Patchram configuration for this. The Patchram configuration also has a length field, which is not contained in the documentation above.


While the "Code address" is set, it does not matter since the "Code size" is 0. We can just set it to 0 as well.

A well-working approach is to patch the HCI handler directly. In ROM, they are a simple table that defines the amount of arguments/return bytes as well as the actual function call.


To only replace one Patchram slot, it's easier to replace an existing handler that is not needed. This is the case for the MWS handlers, because Samsung doesn't use MWS (but iOS does). So I picked this one:


This means that after the patch we need to redefine InternalBlues hci.py as follows:

VSC_Write_RAM = 0xC6F 

A bit hacky, but let's go!

The new Patchram configuration entry looks like this:

10 01
0f
00
7c 0b 0f 00 ; bthci_cmd_cbb_HandleSet_External_Frame_Configuration at 000F0B7C
3D 3E 06 00     ; pointer to bthci_cmd_vs_HandleWrite_RAM 
00 00 ; code size
00 00 00 00     ; we don't need this

With this patched .hcd file we can execute Write_RAM commands! You can find the patched .hcd file here.

Lockdown zero

The original command handler that does not contain the patch is called bthci_cmd_GetDefaultCommandHandler and located at 0x5E980. The filter for this command handler is located at 0x17A210, and if it cannot find anything, it continues to bthci_cmd_GetDefaultCommandHandler.


We can just remove the full handler by overwriting the start address (0x17A210) with a branch to the original handler.

With the new .hcd file installed, this works as follows, directly via InternalBlue:

> writeasm 0x17A210 b.w 0x5E980
[*] Assembler was successful. Machine code (len = 4 bytes) is:
[!] _sendThreadFunc: No response from the firmware.
[!] sendHciCommand: waiting for response timed out!
[|] Writing Memory: Write failed!
0017a210: e4 f6 b6 bb
> hd 0x200400
00200400: ee 0a 3e 19  68 3d 81 90  00 00 00 00  00 00 00 00   |··>·|h=··|····|····|
00200410: 08 42 18 92  41 70 72 20  31 33 20 32  30 31 38 00   |·B··|Apr |13 2|018·|
00200420: 32 32 3a 35  35 3a 34 31  34 33 37 35  42 31 00 00   |22:5|5:41|4375|B1··|

The HCI success status seems to be broken with this patch, but it does not matter here. One memory write to the Patchram RAM region is all we need.

We also got Read_RAM back, which was blocked on this particular Samsung chip as well :) And all the other debug and diagnostic commands.

Comments

  1. Hi Nadine,

    what a wonderful technical post! I kinda lost you at "VSC_Write_RAM = 0xC6F". Where does that value come from? My first thought was that this is part of an address or an offset.

    If I had played around with Internal Blue's code I would probably know that...

    ReplyDelete
    Replies
    1. Hi :)

      So, one part that is missing here is how I did the diffing between the old and the new firmware version to get symbols. But assuming you know where a HCI handler table is located within the firmware, it just has all command handlers in increasing order. So, the usual declaration would be this one (taken from https://github.com/seemoo-lab/internalblue/blob/master/internalblue/hci.py#L189):

      Set_External_Frame_Configuration = 0xC6F

      The Bluetooth Core Specification also defines those codes, except from vendor-specific handlers. So 0xC6F is just the identifier of a handler in a specification-compliant format to use it via HCI on Android.

      Delete

Post a Comment

Popular posts from this blog

Always-on Processor magic: How Find My works while iPhone is powered off

BlueZ: Linux Bluetooth Stack Overview

Embedding Frida in iOS TestFlight Apps