- Hello, everyone. My name is Christopher Wade, and today I’m gonna be talking about breaking secure bootloaders.
00:06 - The purpose of this talk is to outline how smartphones use signature verification mechanisms to protect their firmware, both the core chips and the peripheral hardware.
00:13 - This is implemented at the bootloader level, which provides facilities for firmware updates as well, and often other interfaces for management of the device.
00:20 - We’re gonna be outlining two weaknesses in two different chips used by smartphones which can be exploited to bypass these signature protections.
00:27 - A bit about me before we start. My name is Christopher Wade, and I’m a security consultant at Pen Test Partners, where I mainly work in hardware and IoT testing.
00:34 - So the first project we’ll be discussing is the Qualcomm Snapdragon 660 bootloader.
00:39 - I purchased an Android phone to do some mobile research which contained a similar chip to this and I realized that I needed root access in order to use all of my testing tools.
00:47 - Now, root access on an Android smartphone can only really be achieved in a meaningful way by unlocking the bootloader by which disables signature verification mechanisms and allows you to modify the Android boot image in order to add your own functionality.
01:01 - This required an unlock tool from the manufacturer in this case, which had some different limitations on it.
01:06 - So manufacturers often add protections to the bootloaders on top of what Qualcomm provides in order to require custom tooling provided by them to allow for bootloader unlocking or to remove the ability to unlock the bootloader entirely.
01:20 - So this often requires creating a user account for that particular brand and waiting for a period of time, between seven to 28 days.
01:27 - Unlocks are performed using custom USB fastboot commands, fastboot being the USB protocol used for Android bootloaders, which we’ll be discussing in a second.
01:35 - And there are a few reasons why they would do this, such as preventing inexperienced users from being tricked into deliberately deploying malicious software to their phones, stopping third parties from deploying malicious software to their phones as part of the supply chain, or allowing the manufacturer to track who is unlocking that bootloader in the first place.
01:52 - So in my case, I found that the manufacturer had implemented bootloader unlocking production in a very standard way, which was to add a signature verification mechanism.
02:02 - So using the USBPCAP tool, which allows you to man in the middle USB from a Windows host to USB devices, I analyzed how this was performed and I identified that what happened was using the tool, it would send fastboot commands, which included a 256-byte signature downloaded from the manufacturer servers after the timeout, which was found to be matched against some internal data on the phone.
02:25 - When this was verified, it unlocked the bootloader and allowed the device to be used.
02:29 - I decided because it had this timeout, it would be very interesting to see if I could bypass this restriction in order to unlock the bootloader before the timeout.
02:36 - So after sort of seven days, I decided to set myself a challenge to break the bootloader on an older smartphone in this series before the end of the seven-day waiting period.
02:45 - So the target device I went for was a mid-range phone released in 2017, which used the Qualcomm Snapdragon 660 chipset and the ARM64 architecture.
02:53 - I’d previously unlocked the bootloader on this phone via the same manner, but because I could lock it again for the project just using fastboot commands, I decided to use it as a target.
03:01 - The bootloader had been modified with the same functionality as the phone that I started with.
03:07 - So fastboot is a command interface used by most Android bootloaders.
03:11 - It’s a basic USB interface which sends raw text commands to the phone and gets raw text commands back, in general, apart from in very specific circumstances.
03:19 - These include commands like reboot, flash data, write data, and some specific OEM functionality that can be added by OEMs just by virtue of the fact that the bootloaders are open source and can be modified by them.
03:32 - It can be implemented very simply using standard USB libraries.
03:35 - You can send bulk requests, including the ASCII commands, and get human-readable responses back via asynchronous reads via the bulk interface.
03:44 - There are libraries that exist for this purpose, but most of them are unnecessary.
03:49 - The ABL bootloader provides this fastboot USB interface and verifies and executes the Android operating system, as well.
03:55 - So it’s part of a multi-stage boot process, which starts with the phone booting, loading this bootloader into RAM, which then verifies the Android boot image and gets things going.
04:03 - It can be accessed via ADB during ADB reboot bootloader or using button combinations on boot, which is usually the volume down button and power.
04:11 - It’s stored in the ABL partition on the device.
04:14 - Qualcomm’s base bootloader for this has source code available, which can be analyzed, but because vendors often modify this, this can only be used as a reference rather than building from scratch.
04:24 - So the bootloader itself is stored as an ELF in the ABL partition as outlined, but contains no executable code.
04:30 - Instead it contains a UEFI filesystem. Using the tool uefi-firmware-parser, I could find a portable executable called Linux loader, which could be loaded directly into IDA for analysis.
04:44 - Fastboot commands in this were stored in a table for function callbacks.
04:48 - So it’d be a text command followed by function callback just in order, which allowed me to analyze every single command that was available on the phone and identifying custom ones, hidden ones, or any non-standard functionality that would be in the functions that were already there.
05:02 - Because this is quite a verbose bootloader which sends a lot of strings back, it was really easy to analyze what each part of the boot data was doing just by analyzing what the strings were saying.
05:12 - I’d identified that the flash command, which usually only allows for flashing of data partitions on the Android phone, had been modified so that on this particular phone, it could flash a specific extra bootloader, a specific extra partition called crclist.
05:26 - These partitions were handled differently and did some parsing and all sorts of things on string data sent to it and were not standard as directly created by Qualcomm.
05:34 - As such, I thought there’d be potential for some kind of memory corruption or partition overwrite in this custom functionality, so I decided to start my focus there.
05:43 - I’d made some assumptions when I was building my flash, my fastboot tool, which meant that my command sequence wasn’t exactly correct.
05:49 - What’s meant to happen is that you send a download command with a payload size, then send the raw binary of the payload and then the flash and then the partition you want to send to.
05:58 - I didn’t quite understand this because I hadn’t read the documentation at this point, so what I tried was sending the flash command and the partition I wanted to go to and then sending a payload.
06:07 - Also, in my code, I’d left an incorrect flash command after that payload in the command sequence.
06:12 - This resulted in the bootloader crashing after sending the second flash command.
06:16 - The lack of a download command was deemed to be the reason for this, meaning that it was likely that because I was sending a huge buffer without any data previous to that, it was not handling it as data and was actually handling it as a very large command.
06:31 - So analysis of the crash showed that the USB connectivity on the phone stopped functioning entirely.
06:35 - I could unplug the phone and plug it back in again, and it wouldn’t enumerate over USB, and it required a hard reset back to the bootloader.
06:41 - So holding down volume and power for 10 seconds.
06:45 - I sent a smaller payload size in the same manner, but this didn’t crash the phone.
06:49 - So what I attempted to do was provide a binary search approach where I’d take a large payload and a small payload and half the size in between of those and send it and try and find the maximum size that would be sent without a crash.
07:01 - By rebooting and sending sizes between the minimum and maximum value, I found that the maximum size would be 0x11bae0.
07:10 - Because of this unusual memory size, I assumed that this was going to be a buffer overflow which I could probably exploit to a bypass some of the functionality on the bootloader or add my own functionality.
07:20 - However, with no debugging functionality available on the phone, identifying what memory region was being written to or anything like that would be very difficult.
07:27 - The bootloader was found to also use stack canaries which could be identified using the strings in the code and looking at the source code of the bootloader, which could be potentially triggered by this.
07:37 - I decided to manually increment the next byte, so sending 0x11bae1 bytes of data, and setting that last bite to different bytes of data to see what’d happen, and I found that the last byte was meant to be 0xff.
07:51 - So I looped through until I got to ff and I found that that didn’t crash the phone when I sent that payload, or at least didn’t crash it immediately.
08:00 - By constantly power cycling, incrementing that byte value, and moving to the next byte when I found a valid one that didn’t crash the phone, I can probably create a reasonable facsimile of the memory that was in the bootloader at that point.
08:11 - It wouldn’t be an exact memory necessarily, but it would be enough not to crash the bootloader, and I can probably use that to find what kind of data it was and possibly modify it to gain code execution.
08:20 - But I required a way of automating this process rather than doing it by hand by manually turning the power on and off.
08:26 - It was suggested by my colleagues that I could take the battery out of the phone and use a USB relay to turn the power on and off while automating power cycling on the phone.
08:33 - However, this would require removing glue from the case to access the battery, and hardware attacks like that were something I just didn’t wanna go for at this point.
08:41 - Instead, I wrapped a hair tie around the power and volume down buttons of the phone, which caused the boot loop.
08:46 - So it would constantly loop reboot the phone while allowing enough time for the USB to be enumerated and for a payload to be sent.
08:53 - So it’s sufficient time to test whether the overflow was valid.
08:57 - My custom fastboot tool was modified to do this.
08:59 - So it would loop around, wait for the thing to crash, and then start again, and made it verify two key events.
09:05 - First, I wanted it to send a flashing failed response.
09:07 - So because I sent an incorrect partition as my last command, which was causing the crash, it would tell me that flashing was not allowed on a locked bootloader.
09:17 - I also checked whether the bootloader would crash after this.
09:21 - Each iteration of this took about 10 to 30 seconds on reboots, but eventually this would allow me to create some kind of payload.
09:29 - So I left the phone overnight performing this loop, and by the end of the, by the morning the next day, I’d got 0x34 bytes of data which didn’t crash the phone.
09:37 - Because there were repeated word values and lack of any default stack canary, which I identified in the code, I probably thought that this was probably not likely to be the stack.
09:46 - However, I did notice that all of the 32-bit words that were outlined were actually valid ARM64 opcodes.
09:54 - Most opcodes, while valid operations, would probably not be the same as a bootloader, just due to the fact that things like stack handling in general, the management of how the code is running, doesn’t necessarily need to be exactly right.
10:03 - For instance, if there’s things relating to stacks that’s not actually handled or if there’s registers that are set but never read from, they could be whatever we wanted to be and it wouldn’t really cause a problem.
10:14 - However, pushing the stack pointer down and any brunch and link operations would probably have to be pretty accurate.
10:20 - So I decided to search for those in IDA using the values that have been generated by my code.
10:27 - There’s a few reasons why this search didn’t work, however.
10:29 - So there’s often unused bits that can be flipped on operations which don’t alter functionality or only alter it superficially.
10:37 - Also, registers can be accessed in both 32 and 64-bit mode, meaning that if a register was being used as a 32-bit register but accessed in the code as a 64-bit register, accidentally setting it as a 32-bit one wouldn’t cause any problems and everything would work as intended.
10:52 - Also, branch instructions can have conditions for jumping like zero, not zero, et cetera, and if these were identified but didn’t cause any issues, then also this would mean that text searching for these opcodes just wouldn’t work.
11:07 - So I decided to identify similar instructions.
11:09 - I decided to use the branch and link construction because it was most likely to be unique in the code.
11:14 - Because stack pointing, stack pushing is often very accurate, but also about the same on most functions that are compiled in ARM64, I thought a branch and link construction which would jump to specific relative addresses would probably be easier to note.
11:29 - I performed a text search while removing the first nybble from the opcode to see if I could find any branches that were in a similar relative address space to it.
11:37 - For instance, if there were two string comparison functions that were next to each other and I was jumping to one and not the other.
11:44 - I identified a single valid instruction with this branch and link without the first nybble being the same, and this was in the parser for the crclist partition, which was the partition that we were gonna access at the start of this whole process.
11:57 - And I found that the opcodes were very similar in both the start and end and pretty much everything in between, apart from a few very superficial things.
12:06 - So analysis of the offsets in IDA compared to the buffer overflow I performed found that it was overwriting the entire bootloader at 101000, which was possible because the bootloader was executed from RAM, as demonstrated by the overflow.
12:20 - Because I could overwrite code as it was running, it meant that I was probably overwriting memory that could be modified as needed.
12:27 - I took the original bootloader binary, which I found in the ABL partition, and extracted for this purpose and could overwrite it over the partition that was already in, or rather, the RAM that was already there, meaning that I could perform a buffer overflow without actually breaking the bootloader and keeping everything working.
12:42 - Because of this, I was essentially overwriting it with unsigned code and could modify any aspect of the bootloader to run anything I wanted to.
12:51 - So I wanted to unlock the bootloader, which was the entire purpose of this project.
12:55 - So what I first did was looked into how it was performing the unlock.
13:00 - So what it would do was verify the RSA signature, as outlined at the start, and then send a fastboot unlock command.
13:05 - What I wanted to do was make it so it jumped past the RSA check as part of the signature verification and just jumped straight into that unlock.
13:12 - I could use a simple branch instruction, even from the crclist code that I was in, to jump to the relative address of the bootloader unlock function.
13:20 - Online ARM64 assemblers such as shell-storm can be used to do this and generate very quick rapid code that will just allow you to jump from one piece of code to another.
13:29 - It would be difficult to debug this, but if I had achieved unlocking the bootloader, I’d know fairly simply because the phone would reboot, and then it would tell me that the partitions had corrupted because I was going from a locked bootloader to an unlocked one.
13:40 - And we’ll be discussing that again soon. So here’s a quick outline of this attack going.
13:46 - So it’s just a piece of C code that runs and send the buffer overflow, send the flash:crclist command, and then send an okay response.
13:56 - Now, that okay response was actually from the unlock command and not from the flash:crclist command, ‘cause I’d essentially jumped from one function to another.
14:05 - And then because I hadn’t handled the stack properly at this point, I also got a big chunk of data which was invalid just due to the fact that this was very quickly and rapidly implemented from what I had.
14:16 - So because I could root the phone, I could deploy custom recovery images to it and root the phone itself.
14:21 - However, because Qualcomm chips can encrypt the user data partition using internal keys, if I went from a locked to an unlocked bootloader, I would not actually be able to access my old data.
14:32 - So essentially I’d have to erase my entire phone in order to unlock it and then use root access.
14:38 - I did find that with a few code changes, I could dump RAM off the phone and some of the memory address spaces.
14:45 - However, I couldn’t use this for things like a cold boot attack because I couldn’t access any RAM outside of what was specifically partitioned for the second stage bootloader we were attacking.
14:54 - I achieved this entire process over four days, which was just short of the seven days that would be required for this bootloader unlocking using the tool.
15:04 - But also I attempted to replicate this vulnerability on the newer phone, which used an SDM665, so just the next chip up from what I had.
15:12 - However, these weren’t effective, meaning that this vulnerability was probably only workable on the phone I had.
15:20 - However, I was able to procure a second smartphone which also used the SDM660.
15:25 - This was from a different manufacturer and released at a different time and had actually had all bootloader unlocking functionality disabled completely.
15:33 - So even though you could access fastboot, you couldn’t do fastboot unlock with any tools available.
15:38 - I found that it was using a similar signature verification mechanism to the original phone.
15:41 - However, the keys and all the tools for this were not available publicly.
15:47 - Using an OTA image of this phone, however, I could analyze the bootloader again.
15:50 - So rather than getting it from the partition, I could download it from the Android zip image that was used for updates and just extract it that way.
15:59 - This meant that I could find the code which blocked the bootloader unlock and it just had a response that said cannot unlock bootloader without having the correct signature.
16:07 - And I also found that there were no hidden bootloader commands identifying the device, meaning I couldn’t just unlock the bootloader to find anything secret.
16:14 - So initially, I tried the old crash attempt, so sending the same data I’d sent before to see if it crashed.
16:19 - However, the device still functioned, implying that the vulnerability may not be present.
16:22 - However, I then sent a much larger payload size, eight megabytes worth.
16:26 - This crashed the phone, implying that the vulnerability was still there, but the memory layout may have been different to the original.
16:32 - I did some manual analysis, as I did before, using smaller and larger payload sizes, and found that it was overwritten after 403000 bytes rather than the 101000 bytes used by the original phone.
16:47 - With this, I could really quickly create a bootloader unlock tool.
16:52 - So a single brunch instruction was identified which sent an error response when unlocking the bootloader saying whether you couldn’t verify the signature or whether you could just unlock the bootloader.
17:01 - I could replace this with a single NOP instruction in the bootloader and bypass this check.
17:06 - I could then root the phone using Magisk and have full access to all of its capabilities.
17:11 - Because this vulnerability was found on two different phones from different manufacturers using the same Qualcomm chip, I disclosed it directly to Qualcomm because it was likely to be on every phone that used the SDM660.
17:24 - Now, bootloader access isn’t required for users in contexts where unlocking isn’t permitted.
17:28 - A lot of phone manufacturers nowadays, they just don’t allow you to unlock your bootloader at all, just ‘cause they don’t want you to do research.
17:35 - It’s possible to just disable fastboot access because it is open source and they can modify it as they want to, to bypass all the USB interfaces or prevent attacks against it.
17:44 - What someone manufacturers now do is allow it to be reactivated via signed engineering apps, which have system access within the Android operating system, which allows you to boot back into fastboot.
17:53 - However, without that, you can’t get back into the bootloader.
17:56 - Manufacturers who disable the bootloader unlocking functionality often use this approach just ‘cause it’s much quicker to implement.
18:03 - You can just cut out all the USB functionality until you want it to be accessible.
18:09 - So one of the fun things that I mentioned earlier is that I wanted to read back memory.
18:12 - So the download command, which is used to send data to the phone, could be patched to parse the hex value used for reading data for certain payload sizes, rather, to make it read data.
18:25 - I could use this to read back the bootloader code, the stack memory, and everything for a bit of minor debugging, but I couldn’t read any arbitrary memory.
18:32 - This really restricts the ability for any cold boot attacks.
18:34 - It meant that memory was protected, even if I had this buffer overflow.
18:39 - However, what I really wanted to do at this point, because I’d gone so far, is bypass the encryption on the user data protection on the phone.
18:46 - So Qualcomm’s chips encrypt the user data partition, which contains all the data users have available and is erased on factory resets, even when there’s no passwords or pins used, using an internal security mechanism on the chip.
18:59 - This prevents forensic analysis of the chip, and means that if you unlock your bootloader a phone, you either get told that the partition is corrupted or it just deletes the partition itself on reboot.
19:10 - Bypassing this protection meant that we could access user data via physical access, but be able to do other few fun things.
19:18 - So using Qualcomm’s source code, I decided to see how this encryption process could be implemented while protecting the device, and found that the encryption keys were intentionally inaccessible even with this code execution.
19:30 - I also found that it used an internal API which I couldn’t access with this code.
19:33 - It could jump to the code and execute certain things, but I couldn’t modify it with my exploits.
19:38 - This API was found to verify whether the phone was unlocked and whether the image was appropriately signed before decrypting the partition for user data.
19:47 - So the boot fastboot command loads and executes Android images that are deployed via USB.
19:51 - So in fastboot, you can say try and boot this image.
19:54 - Now, if the phone is locked, then it tells you can’t do that.
19:58 - But if it’s unlocked, it will start booting the phone.
20:01 - I noted that in this code, the verification functionality and the booting functionality were two separate functions separated by quite a lot of different code.
20:09 - So there’s a high likelihood that I’d be able to swap two images, a signed one and an unsigned one, in order to bypass these protections.
20:18 - So the boot command receives the full Android boot image via the fastboot download command, which is then loaded into RAM, verified and executed.
20:25 - By patching it, I could then alter it to perform this time of check to time of use attack, and instead of sending one image, I can send two.
20:32 - So what I did is I decided to modify the tool I’d already written to perform this functionality.
20:36 - So what I did is instead of sending just one boot image, I configured it to send three pieces of data, a four byte offset to an unsigned image, the signed image, and then the unsigned malicious image.
20:48 - What this would do is mean that I’d have the functionality to then, if I modified the bootloader to swap between a signed and an unsigned image.
20:57 - So as I said before, the boot command does not function on a locked bootloader.
21:00 - So the first thing I need to do was bypass that check by patching the code.
21:05 - So the check for the lock state was replaced by an operation which moved from the image pointer from the offset to the unsigned image to the signed image.
21:13 - This meant that the verification could then be formed without anything really changing about the functionality.
21:20 - I then noted that the function calls that occurred between the verification and the booting were unnecessary for actual functionality.
21:26 - So if I knocked these out, the device would still boot and reboot as needed and do everything else it needed to.
21:31 - They’re pretty much there for just cleaning up everything before starting the Linux image or the Android image.
21:37 - Because I could cut these out, I got about five spare instructions I could use to then swap around the two images as needed.
21:43 - So I needed four additional instructions for this on top of the first instruction in which I moved the pointer.
21:48 - The first thing I had to do was move the pointer back from the signed image back to the offset at the start of the data I’d sent, read that offset value, jump to that offset value, and then point this pointer the structure that sent for booting to the new unsigned malicious code.
22:05 - This would be sufficient to swap between the signed and unsigned image, and meant I could facilitate this time of check to time of use attack.
22:12 - And I found this to be effective, allowing me to run unsigned Android images without unlocking the bootloader at all.
22:19 - So there’s a few certain things you can do with this.
22:21 - The first one is running tethered root. So unlocking the bootloader obviously wipes all the user data, and permanently rooting the phone exposes it to quite a lot of risk.
22:31 - A device being permanently rooted is not something that’s necessary unless you’re constantly using it for research, especially for most phone users.
22:37 - If you wanna play around with the internals of your phone, that’s fine.
22:40 - But if you want to go back to the phone functioning normally, that’s something you can’t really do without then locking the bootloader and doing a lot of other things.
22:48 - But by deploying a rooted Android image using Magisk or any other rooting tool you like, but Magisk is definitely the best one, you can use this time of check to time of use attack and then boot into a image with root access, but then reboot when you want to and remove all these root capabilities, just ‘cause they’re stored in a boot image that was running from RAM.
23:08 - However, you can also use this to bypass the lock screen.
23:10 - So by accessing unencrypted user data via this attack, one can remove the lock screen restrictions.
23:16 - There’s a file that’s used for implementing lock screens, and by using a custom recovery image or any other functionality, you could just modify this and remove it or remove the lock screen entirely from the Android image you’re deploying and get access to all the user data directly.
23:33 - So here’s a quick outline of how this time of check to time of use attack is performed.
23:37 - So again, it’s just make and run like before from my C++ code.
23:40 - It uploads the two images just via fastboot and then starts booting.
23:44 - And then from here, it will just swap between the two images, not requiring any signing verification, and boot into the unsigned image on the locked bootloader.
23:56 - Further to this, with developer functionality enabled on a phone, you can add further encryption.
24:01 - Rather than just using the internal keys provided by Qualcomm, you can also add your own passwords or PIN numbers to your phone in order to increase the amount of encryption there.
24:11 - So rather than just having that one layer of encryption, you also can require a password to be input on boot, which means that it won’t decrypt the user data partition without adding extra functionality.
24:21 - So this can be bypassed by taking the Android boot image, which you have, and sticking the back door on it.
24:28 - So taking a user’s phone off them for a short amount of time or having access to it for a short amount of time, you can deploy this exploit of the time of check to time of use attack with a reverse shell built into the Android boot image and put there by yourself and then reboot the phone.
24:46 - What will happen then is the person will put in their PIN and it will boot that phone, and when that’s booted, you will have full access to all of the user data via a reverse shell.
24:54 - So updated in the same way, taking a init script, which are usually part of the boot image, and then just for this particular demonstration, putting an interpreter shell in.
25:05 - So this attack was disclosed to Qualcomm, as well, but because it was only possible via the initial buffer overflow, there’s not huge amount of risk by this being here.
25:13 - If someone could perform this buffer overflow on that phone and then perform this attack, it’s bad, but it’s an attack chain that’s not always going to be viable.
25:22 - Patching the phone to prevent this attack would be extremely difficult, as well, because it would require modifying code that isn’t directly performed by the Android bootloader.
25:30 - However, these could allow an attacker with physical access to an SDM660-based phone that hadn’t been patched now that this has been patched by Qualcomm to bypass all of these bootloader locking mechanisms.
25:43 - So let’s move on to project two. So the NXP PN series is a series of NFC chips used for basically NFC communication on both smartphones and some embedded electronics.
25:53 - By breaking the firmware protections on these chips, one could add new NFC capabilities to both that phone with root access or to their embedded products which use this particular chip sets series.
26:03 - It’s extremely popular. It’s not understating it to say that these chips are used in millions and millions of smartphones and any exploits in them would be transferable between a large number of devices if you had root access to them.
26:16 - So in this particular instance, I was using the NXP PN553, which is an NFC chip used solely in mobile devices.
26:23 - It bears similarity to similar chips in embedded devices and smartphones, such as PN547, PN548, PN551, and PN5180.
26:34 - They all were found to use a similar firmware update mechanism, and they were all found to use the Cortex-M architecture.
26:40 - There’s other chips in the PN series, such as the PN544, which don’t use ARM Cortex-M architectures, but instead use 8051, other things like that.
26:51 - I also found that these chips had very little public research available.
26:55 - So on smartphones, these chips are communicated with via the I2C interface.
27:00 - For the PN series, this is on the /dev/nq-nci device file, which uses NCI for NFC communication the standard NFC protocol, and then a custom protocol for firmware updates, which we’re gonna attack for this project.
27:15 - I could trace all communication from the phone to this chip via ADB logcat.
27:21 - So what I really wanted to do is force firmware updates so I could see how they could function and then analyze any weaknesses in them.
27:27 - So what I did is I took the two firmware images that were on the phones, the libpn553_fw. so and libpn553_rec. so, which are the recovery image and the main firmware image, and swapped their two file names.
27:42 - And what would happen is when you started up the NFC functionality, the phone would then verify which firmware it was, verify the version number, and see if it was different to what was on the device itself.
27:52 - Swapping these files meant that I could force the update to occur, trace it, and analyze how it worked, and also trace it against some aspects of the source code.
28:01 - So the bootloader update critical was found to be unique to NXP chips, and I think unique to that PN series.
28:07 - It consists of a one-byte status, a one-byte size, a one-byte command, and then a variable size of parameters, and then a CRC at the end.
28:15 - This was encapsulated into 0xfc byte chunks for large payloads, meaning you could send large payloads of data while only having a size of one byte.
28:25 - Reads and writes to /dev/nq-nci directly translated to I2C commands.
28:30 - So because Android devices are essentially embedded Linux devices, it would send I2C communication via this interface.
28:38 - I also found that using IO control functions, I could turn the chip on and off and also set it into bootloader mode as needed.
28:47 - So the firmware files were found to be kept in ELF files like libpn553_fw. so.
28:52 - However, these ELF files were found to only contained one sector, which contained binary formatted data, but not the code actually being executed in any meaningful capacity.
29:02 - It actually contained commands which needed to be run in sequence for firmware updates to occur, and these commands could be updated to rebuild the firmware image as needed to do some cursory analysis in IDA.
29:14 - So the CO write command was found to be used, the only command that was used for this process, and the first command in the sequence contained a lot of unknown high entropy data, which was likely to be a kind of checksum or signature or anything.
29:26 - All subsequent payloads were found to contain a 24-bit address, a 16-bit size, and a data payload with an unknown hash.
29:32 - These commands were all required to be sent in sequence as they were stored in this update file or they wouldn’t function properly.
29:40 - So because memory addresses at the start of commands aided the reconstruction of the firmware, I could generate a firmware binary containing all the firmware data that was there.
29:47 - However, I did note that the firmware that was created from this was extremely small, definitely not enough to perform all the NFC functionality that these chips were capable of.
29:55 - I also found multiple code references to memory that wasn’t accessible via my firmware updates, meaning that the core system functionality was likely to be stored further in the bootloader or as part of some kind of immutable data on the chip.
30:08 - I found that there were two commands in the bootloader for allowing for readback of memory, A2 and E0.
30:12 - A2 was just the standard read memory command, which allowed you to read memory from some arbitrary addresses, but only limited to the memory that could also be written to via firmware updates, and could mean I could directly read back the bootloader.
30:24 - However, the E0 command was found to calculate checksums of memory and also provide four bytes of configuration data from an EEPROM, which I could then reconstruct from these four bytes.
30:35 - So I found in this EEPROM data that there was a large chunk of randomized data size of xC0, ending with what looked like a standard RSA exponent, 10001.
30:47 - This meant that it was likely to be the modulus and exponent of the public RSA key used by the chip for signature verification, which meant that when I went back to looking at the firmware updates, it’d be easier for me to spot where the signature was.
30:59 - I also found an additional write command over the C01, which was found to allow a writing of 64 bytes of configuration data to this EEPROM.
31:07 - This memory had no bearing on any functionality and its size was only restricted to those 64 bytes, meaning that I couldn’t use it for any attacks, but it was likely to be used for some kind of logging data.
31:17 - As outlined before, the block write commands all ended with an unknown 256-bit hash.
31:21 - I assumed it was SHA-256, but because it didn’t match the contents of the packet I was looking at, I wasn’t quite sure if that was correct.
31:30 - Multiple other hashing algorithms were tried for this, but with no valid results.
31:34 - After a while, I discovered this was actually the hash for the next block in the sequence, and I’ll explain that now.
31:40 - So the first CO command contains a version number, the SHA-256 hash, and a signature of that specific hash and the version number.
31:49 - This is the hash of the next block, which also contains a hash.
31:52 - So what happens is each block has a hash, which is then hashed for the next block in its hash, creating a sort of hash chain, which means that you only need to verify the signature at the start while also only being able to write valid blocks of data if they match that hash sequence.
32:08 - I also noted that the final block in the sequence had no hash because it had no subsequent block, and that became very important later.
32:17 - I performed some targeted fuzzing on a chip on both the firmware update functionality and the NCI interfaces.
32:21 - I’d taken quite a lot of time in this project trying to look through each aspect of how this chip functions.
32:27 - So I looked at both the firmware update protocol and the NCI protocol, and what I found was that the chip was found to contain some vendor-specific conflicts that are only accessible via NCI using the configuration write command.
32:39 - What I did was send incrementing bitwise values while rebooting the chip to see if it did anything strange to both the NCI interface or the firmware updates itself.
32:49 - But what I found would actually happen was that it bricked the chip itself, but only on the firmware side.
32:55 - The bootloader still functioned, but I couldn’t now rewrite that configuration data to unbrick the chip ‘cause it was only accessible from the firmware.
33:04 - But I decided to persevere with the bootloader side rather than the NCI side ‘cause I felt that there was probably something usable there.
33:11 - It was noted that the last block of the firmware update could be written multiple times despite this hash chain.
33:15 - So I could send the same block, the last block several times over, and it would still take it and still write it, which implied that the hash of the previous block remained in memory and meant that this sequence was using very static global memory as it went along.
33:28 - There was a potential opportunity here for me here to overwrite this hash in memory using some kind of memory corruption, such as an invalid command.
33:36 - So I sent an invalid command the same size of the firmware update block, including that SHA-256, to see what would happen.
33:41 - At the end of the sequence after sending this block, before the last packet, the last block was prevented from being written, implying that the hash had been overwritten in the static RAM.
33:53 - So because of this, I could create a modified hash and stick it at the end of my custom command or my corrupted command and use this to override the hash chain.
34:02 - So because I could overwrite this hash, I could modify what the last block contained and use this to essentially write arbitrary memory to the chip.
34:10 - And I could do this to bypass all the secretary verification mechanisms and bypass the entire hash chain just by adding my own hashes and doing this override over and over and over again, meaning I could modify any aspect of the code.
34:23 - First thing I wanted to do with this as I had signature bypass, signature verification bypass, was repair the firmware.
34:29 - So using a dump of the working configuration, which I got at the start of this whole process, I took it and tried to write it to the correct portion of memory in the EEPROM.
34:42 - This repaired the chip and meant that I could also prove that these arbitrary memory writes took hold and worked appropriately, meaning that I could then repair the chip if I needed to, but I could also add custom functionality.
34:52 - So the next goal was to dump the bootloader from the chip.
34:56 - All standard functions were stored in the bootloader with limited functionality and firmware updates, such as mem copies and things like that.
35:02 - However, I noted that the NCI version number command, which was part of the firmware update, so the version number used by the firmware would be changed when the code changed rather than it being some kind of static memory that was written.
35:15 - Because this version number was easy to identify in memory and it’s function meant (indistinct) were easy to identify alongside it, I could override this in order to alter the functionality.
35:24 - So I noted that after getting the version number, it would call a function as part of the NCI configuration version number read command.
35:34 - I decided to see if I could alter this, and what I noted was this was likely to be a mem copy function.
35:42 - So the branch instruction to the function could be overwritten using the signature bypass and taking a bit of C code and the gcc -c flag, I could write a custom function and modify it as I went along as performing firmware updates to alter what could be read.
35:57 - So I could then observe its effect on this version number command and see what the responses were like.
36:03 - Because I had overwritten the function, not added any functionality to it, I saw that the lack of data in the response after doing this implied that the mem copy was part of that return and meant I had a decent way to return arbitrary memory from the chip.
36:17 - So I assumed the location of RAM was gonna be at 10000 due to the fact that the firmware was referencing this space for reads and writes and parts of all sorts of global memory access.
36:26 - In the overridden memory and copy function, I changed it to search for a unique value in RAM.
36:32 - I sent the value FACFAC in NCI and then searched for it in RAM when it was sent.
36:39 - This provided a global pointer right to the start of RAM 10007.
36:43 - I could set this pointer to be, this value to now store an arbitrary memory address, which I could then use to dump the bootloader directly.
36:52 - So by constantly resetting the chip and re-sending the version number read command, which can only be done at the start of the chip starting up, I could use this to dump the bootloader from the chip.
37:04 - Of course, this functionality could then be extended to modify other core NFC functionality, but I decided to leave it here and disclose it to NXP.
37:13 - So I decided to replicate this vulnerability, as well, to prove that it was on other chips.
37:16 - I decided to go for a chip mainly used in embedded devices rather than smartphones for this particular part of the project.
37:22 - So the PN5180 is a chip often used by hobbyists for NFC connectivity, and you can buy them very cheaply online, and they come with all sorts of accessories, and they’re very, quite easy to work with, lots of libraries available.
37:32 - I found that it had a similar architecture to the PN553, but used a custom communication protocol instead of NCI for the core firmware.
37:40 - It can be communicated with via an SPI interface and GPIO pins.
37:44 - And I used a Raspberry PI for this project.
37:46 - The firmware update process was the same apart from adding a byte at the start because it was using a slightly different communication protocol, SPI rather than I2C.
37:54 - But this allowed me to replicate the signature bypass.
38:00 - So a command in the chip’s communication protocol was found to read memory from a specific part of the EEPROM.
38:07 - Basically the version number, again. This point was found in the firmware payload, and I found that I could just override the entire firmware and modify this pointer in read memory in order to dump the bootloader from this chip, as well.
38:18 - I could read it without any functional code changes and just modifying this memory pointer.
38:22 - Obviously, though, this took a lot longer than just doing it via arbitrary code execution.
38:28 - The vulnerability was likely to be available on all of the similar chipsets, not just the PN5180 and the PN553.
38:35 - It’s likely to be on all the other ones apart from the later versions of the series, like such as the NXP SN series, which are completely immune to this, and also use encryption on top of their signature verification, adding some quite strong protections.
38:50 - These could allow an attack with access to the firmware updates, though, to completely take over the chips, the vulnerable chips, and modify the functionality or add NFC functionality, add some kind of NFC backdoor, or just to add functionality to a hobbyist product where you want to use this chip, but didn’t have the capability to do certain things just because of limitations of the firmware.
39:12 - On smartphones, of course, it’s required for root access, which isn’t always available, but it can add some functionality such as NFC chip emulation.
39:22 - The vulnerability was disclosed to NXP in June 2020, and they confirmed that it affected multiple chips in their product line.
39:28 - They requested a long remediation period, which I agreed to, with a public release of August 2021, due to the fact that modifying a single stage bootloader like this is incredibly dangerous.
39:40 - When you’re altering a primary bootloader from the core firmware running from it, if anything goes wrong during this process, you essentially brick your chip.
39:49 - And because these chips are so pervasive, there’s no way to really unbrick them via any other means.
39:54 - It’s something they wanted to be very careful with.
39:56 - So it was done in phased rollouts and it was done in quite an impressive manner.
40:01 - The current generation of NXP NFC products, including the SN series, are definitely not affected.
40:06 - I’ve checked this myself and found that the encryption in place and the modifications to the code means that this particular vulnerability does not allow for this initial bypass.
40:15 - And remediation across all the affected chipsets has now been performed.
40:20 - Special thanks to Qualcomm and NXP for remediating these findings.
40:24 - They did a really good job being communicative throughout this entire process and were very helpful in helping me understand some of the vulnerabilities I found.
40:31 - Firmware signature protection is only good as its implementation, and because there were slight weaknesses in these implementations, I could use these to bypass them completely and gain quite privileged access to quite pervasive chips.
40:44 - Common chips, however, are great targets as they have a high impact and they don’t always have all of their weaknesses remediated.
40:50 - And of course, bootloader vulnerabilities are common, even in popular hardware.
40:54 - Thank you very much. .