- Hello, everyone.
My name is Christopher Wade,
and today I'm gonna be talking about
breaking secure bootloaders.
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.
This is implemented at the bootloader level,
which provides facilities for firmware updates as well,
and often other interfaces for management of the device.
We're gonna be outlining two weaknesses
in two different chips used by smartphones
which can be exploited to bypass
these signature protections.
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.
So the first project we'll be discussing
is the Qualcomm Snapdragon 660 bootloader.
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.
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.
This required an unlock tool
from the manufacturer in this case,
which had some different limitations on it.
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.
So this often requires creating a user account
for that particular brand
and waiting for a period of time,
between seven to 28 days.
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.
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.
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.
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.
When this was verified,
it unlocked the bootloader
and allowed the device to be used.
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.
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.
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.
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.
The bootloader had been modified with the same functionality
as the phone that I started with.
So fastboot is a command interface used
by most Android bootloaders.
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.
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.
It can be implemented very simply
using standard USB libraries.
You can send bulk requests,
including the ASCII commands,
and get human-readable responses back
via asynchronous reads via the bulk interface.
There are libraries that exist for this purpose,
but most of them are unnecessary.
The ABL bootloader provides this fastboot USB interface
and verifies and executes
the Android operating system, as well.
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.
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.
It's stored in the ABL partition on the device.
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.
So the bootloader itself is stored as an ELF
in the ABL partition as outlined,
but contains no executable code.
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.
Fastboot commands in this were stored in a table
for function callbacks.
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.
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.
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.
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.
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.
I'd made some assumptions when I was building my flash,
my fastboot tool,
which meant that my command sequence wasn't exactly correct.
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.
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.
Also, in my code,
I'd left an incorrect flash command
after that payload in the command sequence.
This resulted in the bootloader crashing
after sending the second flash command.
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.
So analysis of the crash showed that the USB connectivity
on the phone stopped functioning entirely.
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.
So holding down volume and power for 10 seconds.
I sent a smaller payload size in the same manner,
but this didn't crash the phone.
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.
By rebooting and sending sizes between the minimum
and maximum value,
I found that the maximum size would be 0x11bae0.
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.
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.
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.
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.
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.
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.
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.
But I required a way of automating this process
rather than doing it by hand
by manually turning the power on and off.
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.
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.
Instead, I wrapped a hair tie around the power
and volume down buttons of the phone,
which caused the boot loop.
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.
So it's sufficient time to test
whether the overflow was valid.
My custom fastboot tool was modified to do this.
So it would loop around,
wait for the thing to crash,
and then start again,
and made it verify two key events.
First, I wanted it to send a flashing failed response.
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.
I also checked whether the bootloader
would crash after this.
Each iteration of this
took about 10 to 30 seconds on reboots,
but eventually this would allow me
to create some kind of payload.
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.
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.
However, I did notice that all of the 32-bit words
that were outlined were actually valid ARM64 opcodes.
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.
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.
However, pushing the stack pointer down
and any brunch and link operations
would probably have to be pretty accurate.
So I decided to search for those in IDA using the values
that have been generated by my code.
There's a few reasons why this search didn't work, however.
So there's often unused bits
that can be flipped on operations
which don't alter functionality
or only alter it superficially.
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.
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.
So I decided to identify similar instructions.
I decided to use the branch and link construction
because it was most likely to be unique in the code.
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.
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.
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.
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.
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.
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.
Because I could overwrite code as it was running,
it meant that I was probably overwriting memory
that could be modified as needed.
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.
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.
So I wanted to unlock the bootloader,
which was the entire purpose of this project.
So what I first did was looked into
how it was performing the unlock.
So what it would do was verify the RSA signature,
as outlined at the start,
and then send a fastboot unlock command.
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.
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.
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.
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.
And we'll be discussing that again soon.
So here's a quick outline of this attack going.
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.
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.
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.
So because I could root the phone,
I could deploy custom recovery images to it
and root the phone itself.
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.
So essentially I'd have to erase my entire phone
in order to unlock it
and then use root access.
I did find that with a few code changes,
I could dump RAM off the phone
and some of the memory address spaces.
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.
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.
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.
However, these weren't effective,
meaning that this vulnerability was probably only workable
on the phone I had.
However, I was able to procure a second smartphone
which also used the SDM660.
This was from a different manufacturer
and released at a different time
and had actually had all bootloader
unlocking functionality disabled completely.
So even though you could access fastboot,
you couldn't do fastboot unlock with any tools available.
I found that it was using a similar
signature verification mechanism to the original phone.
However, the keys and all the tools for this
were not available publicly.
Using an OTA image of this phone, however,
I could analyze the bootloader again.
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.
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.
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.
So initially, I tried the old crash attempt,
so sending the same data I'd sent before
to see if it crashed.
However, the device still functioned,
implying that the vulnerability may not be present.
However, I then sent a much larger payload size,
eight megabytes worth.
This crashed the phone,
implying that the vulnerability was still there,
but the memory layout
may have been different to the original.
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.
With this, I could really quickly create
a bootloader unlock tool.
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.
I could replace this with a single NOP instruction
in the bootloader and bypass this check.
I could then root the phone using Magisk
and have full access to all of its capabilities.
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.
Now, bootloader access isn't required for users in contexts
where unlocking isn't permitted.
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.
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.
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.
However, without that,
you can't get back into the bootloader.
Manufacturers who disable the bootloader
unlocking functionality often use this approach
just 'cause it's much quicker to implement.
You can just cut out all the USB functionality
until you want it to be accessible.
So one of the fun things that I mentioned earlier is that
I wanted to read back memory.
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.
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.
This really restricts the ability for any cold boot attacks.
It meant that memory was protected,
even if I had this buffer overflow.
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.
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.
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.
Bypassing this protection meant that we could access
user data via physical access,
but be able to do other few fun things.
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.
I also found that it used an internal API
which I couldn't access with this code.
It could jump to the code and execute certain things,
but I couldn't modify it with my exploits.
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.
So the boot fastboot command loads
and executes Android images that are deployed via USB.
So in fastboot,
you can say try and boot this image.
Now, if the phone is locked,
then it tells you can't do that.
But if it's unlocked,
it will start booting the phone.
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.
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.
So the boot command receives the full Android boot image
via the fastboot download command,
which is then loaded into RAM,
verified and executed.
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.
So what I did is I decided to modify the tool
I'd already written to perform this functionality.
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.
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.
So as I said before,
the boot command does not function on a locked bootloader.
So the first thing I need to do was bypass that check
by patching the code.
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.
This meant that the verification could then be formed
without anything really changing about the functionality.
I then noted that the function calls that occurred
between the verification
and the booting were unnecessary for actual functionality.
So if I knocked these out,
the device would still boot and reboot as needed
and do everything else it needed to.
They're pretty much there for
just cleaning up everything before starting the Linux image
or the Android image.
Because I could cut these out,
I got about five spare instructions I could use
to then swap around the two images as needed.
So I needed four additional instructions for this
on top of the first instruction
in which I moved the pointer.
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.
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.
And I found this to be effective,
allowing me to run unsigned Android images
without unlocking the bootloader at all.
So there's a few certain things you can do with this.
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.
A device being permanently rooted
is not something that's necessary
unless you're constantly using it for research,
especially for most phone users.
If you wanna play around
with the internals of your phone, that's fine.
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.
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.
However, you can also use this to bypass the lock screen.
So by accessing unencrypted user data via this attack,
one can remove the lock screen restrictions.
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.
So here's a quick outline of how this time of check
to time of use attack is performed.
So again, it's just make and run like before
from my C++ code.
It uploads the two images just via fastboot
and then starts booting.
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.
Further to this,
with developer functionality enabled on a phone,
you can add further encryption.
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.
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.
So this can be bypassed by taking the Android boot image,
which you have,
and sticking the back door on it.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
So in this particular instance,
I was using the NXP PN553,
which is an NFC chip used solely in mobile devices.
It bears similarity to similar chips
in embedded devices and smartphones,
such as PN547, PN548, PN551,
and PN5180.
They all were found to use
a similar firmware update mechanism,
and they were all found to use the Cortex-M architecture.
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.
I also found that these chips
had very little public research available.
So on smartphones,
these chips are communicated with via the I2C interface.
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.
I could trace all communication from the phone
to this chip via ADB logcat.
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.
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.
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.
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.
So the bootloader update critical
was found to be unique to NXP chips,
and I think unique to that PN series.
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.
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.
Reads and writes to /dev/nq-nci directly
translated to I2C commands.
So because Android devices are essentially
embedded Linux devices,
it would send I2C communication via this interface.
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.
So the firmware files were found
to be kept in ELF files like libpn553_fw.so.
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.
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.
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.
All subsequent payloads were found to contain
a 24-bit address, a 16-bit size,
and a data payload with an unknown hash.
These commands were all required to be sent in sequence
as they were stored in this update file
or they wouldn't function properly.
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.
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.
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.
I found that there were two commands in the bootloader
for allowing for readback of memory, A2 and E0.
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.
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.
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.
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.
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.
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.
As outlined before,
the block write commands all ended
with an unknown 256-bit hash.
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.
Multiple other hashing algorithms were tried for this,
but with no valid results.
After a while,
I discovered this was actually the hash
for the next block in the sequence,
and I'll explain that now.
So the first CO command contains a version number,
the SHA-256 hash,
and a signature of that specific hash
and the version number.
This is the hash of the next block,
which also contains a hash.
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.
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.
I performed some targeted fuzzing on a chip
on both the firmware update functionality
and the NCI interfaces.
I'd taken quite a lot of time in this project
trying to look through each aspect
of how this chip functions.
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.
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.
But what I found would actually happen
was that it bricked the chip itself,
but only on the firmware side.
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.
But I decided to persevere with the bootloader side
rather than the NCI side 'cause I felt
that there was probably something usable there.
It was noted that the last block of the firmware update
could be written multiple times despite this hash chain.
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.
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.
So I sent an invalid command
the same size of the firmware update block,
including that SHA-256,
to see what would happen.
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.
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.
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.
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.
First thing I wanted to do with this
as I had signature bypass,
signature verification bypass,
was repair the firmware.
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.
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.
So the next goal was to dump the bootloader from the chip.
All standard functions were stored in the bootloader
with limited functionality and firmware updates,
such as mem copies and things like that.
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.
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.
So I noted that after getting the version number,
it would call a function as part of the NCI configuration
version number read command.
I decided to see if I could alter this,
and what I noted was this was likely to be
a mem copy function.
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.
So I could then observe its effect
on this version number command
and see what the responses were like.
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.
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.
In the overridden memory and copy function,
I changed it to search for a unique value in RAM.
I sent the value FACFAC in NCI
and then searched for it in RAM when it was sent.
This provided a global pointer
right to the start of RAM 10007.
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.
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.
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.
So I decided to replicate this vulnerability, as well,
to prove that it was on other chips.
I decided to go for a chip mainly used in embedded devices
rather than smartphones
for this particular part of the project.
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.
I found that it had a similar architecture to the PN553,
but used a custom communication protocol
instead of NCI for the core firmware.
It can be communicated with via an SPI interface
and GPIO pins.
And I used a Raspberry PI for this project.
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.
But this allowed me to replicate the signature bypass.
So a command in the chip's communication protocol
was found to read memory from a specific part of the EEPROM.
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.
I could read it without any functional code changes
and just modifying this memory pointer.
Obviously, though, this took a lot longer
than just doing it via arbitrary code execution.
The vulnerability was likely to be available
on all of the similar chipsets,
not just the PN5180 and the PN553.
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.
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.
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.
The vulnerability was disclosed to NXP in June 2020,
and they confirmed that it affected multiple chips
in their product line.
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.
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.
And because these chips are so pervasive,
there's no way to really unbrick them via any other means.
It's something they wanted to be very careful with.
So it was done in phased rollouts
and it was done in quite an impressive manner.
The current generation of NXP NFC products,
including the SN series,
are definitely not affected.
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.
And remediation across all the affected chipsets
has now been performed.
Special thanks to Qualcomm
and NXP for remediating these findings.
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.
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.
Common chips, however,
are great targets as they have a high impact
and they don't always have
all of their weaknesses remediated.
And of course, bootloader vulnerabilities are common,
even in popular hardware.
Thank you very much.