MEDICAL DEVICE Security Analysis
October 9th, 2025 by Brian
At Caesar Creek Software, we routinely perform cybersecurity assessments of medical devices to help companies achieve FDA 510(k) approval for new products. As part of our ongoing research and training, we also conduct internal audits of popular medical devices to stay current with the latest security trends and provide our engineers with hands-on experience.
In this post, we share the results of one such internal assessment. We selected a widely used medical device, evaluated its security posture, and uncovered several vulnerabilities—subsequently reported to the vendor. The most rewarding part of the process, however, was the vendor’s response. Their engineering team collaborated with us to review our findings, discuss potential risks, and work toward meaningful resolutions.
The following is our story and summary of the findings. The specific vendor and product details have been redacted pending FDA approval of the fixes. This project was conducted by Jackson S., Rishav B., and Benton E.
Overview
The following information has been intentionally redacted to remove vendor and product specific information until such time as the vulnerabilities can be addressed and fielded.
Introduction
Software has become a vital component of many systems which previously required manual or mechanical means to operate, and medical devices are understandably a popular use-case. However, software is susceptible to bugs and security vulnerabilities, which could have major implications, especially for medical devices. Someone with malicious intent who gains knowledge of any security vulnerabilities may attempt to exploit them to cause harm in some way. For example, if an attacker can exploit a vulnerability to send false health data to an unsuspecting user, the user may take action with potentially fatal consequences.
One of our goals as security researchers is to analyze devices like these to identify security vulnerabilities and responsibly disclose them to the manufacturer before potential attackers find them. Doing so helps manufacturers quickly address potential concerns before they become issues, and keeps users safe from those with malicious intent. In this series of blog posts, we will describe how we extracted the firmware from a popular MEDICAL DEVICE (PMD), analyzed it for vulnerabilities, and the vulnerabilities we found. All findings were disclosed responsibly to the manufacturer, who was very cooperative and receptive to our disclosure.
Hardware Research
Introduction
In our investigation of a PMD, we discovered the usage of Nordic’s nRF52832 chip as the main microcontroller (MCU) responsible for Bluetooth communication with external devices. This particular MCU provides an excellent attack surface for developing exploits whose execution is predicated upon arbitrary input from remote devices. Nevertheless, in order to develop such an exploit, successfully pulling the proprietary firmware from the MCU is an ideal first step. In this section, we will outline the specific hardware details of the nRF52832 chip, to better understand its features and possible methods of firmware retrieval. We will use the nRF52832 Product Specification as a reference for outlining many of these details.
Schematic

Pictured above is a reference schematic of the nRF52832 package from Nordic Semiconductor.
The pins of note include SWDIO and SWDCLK, which implement the Serial Wire Debug (SWD) interface, used for reading, writing and debugging the MCU. Unfortunately, this interface was locked down by default on our PMD. Additionally, the VDD and DEC pins are also of interest, particularly for their role in a fault injection exploit used to bypass the aforementioned debug protections.
Memory Map
Additionally, the nRF52832 uses the 32-bit ARM Cortex M4 processor, with our particular MCU containing 64kb of RAM and 512kb of Flash. As such, the memory map of the nRF52832 follows the standard Cortex M4 System Address Map. However, the nRF52832 implements additional mappings on top of the standard M4 structure, as pictured below:

As seen above, the flash memory of the MCU, which contains all relevant code, is mapped to address range 0x0 – 0x80000.
Additionally, peripheral registers are mapped starting at 0x40000000 and 0x50000000. These registers are used for managing the various peripherals included in the nRF52832, such as the real-time clock (RTC), random number generator, and radio.
Other interesting regions include the Factory Information Configuration Registers (FICR), which contains hardware details such as the device ID and flash size, and the Data RAM section, which is the volatile data region used by the code.
One of the most important register banks is the User Information Configuration Register (UICR) section, located at address 0x10001000. This section contains the APPROTECT register, which controls the protection settings of the SWD interface.
Debugging and Protection Mechanism
As mentioned above, the SWD interface on our PMD’s nRF52832 was locked down by default. The method of doing so is accomplished via the Access Port Protection mechanism supported by the nRF52832.
As mentioned in the previous section, within the UICR section resides the APPROTECT register, which can be set to the following potential values:
- Disabled: The access port protection is disabled, allowing SWD access.
- Enabled: Access port protection is enabled, locking down the SWD interface.
Once access port protection is enabled, all registers and addresses are inaccessible via SWD, preventing any sort of firmware retrieval or debugging. However, access port protection can be disabled via the Control Access Port (CTRL-AP), which is a special access port that remains open even when APPROTECT is enabled.
In order to disable access port protection via CTRL-AP, an ERASEALL command can be invoked, which will erase the entirety of flash and RAM. Additionally, ERASEALL will clear the APPROTECT register, effectively unlocking the SWD mechanism.
Obviously this strategy of unlocking SWD is less than ideal, considering our goal is preserving the firmware of the MCU for retrieval and RE. As such, another strategy must be used for bypassing the APPROTECT mechanism. In the next major section, we will explore one of these strategies in the form of fault injection.
A Note on Other Chips and the Revision 3 nRF
Alongside the nRF52832, our PMD also featured an undocumented proprietary chip. According to our research online, and based on a cursory overview of the PCB, this chip is a custom ASIC designed to interface with the PMD’s needle.
Additionally, in regards to the access port protection mechanism of the nRF52832, later revisions of the MCU require a slightly different procedure for disabling access port protections. In the newer revisions, access port protections must be disabled in both hardware and software, as opposed to the hardware-only requirement of previous revisions.
As such, in order to disable access port protections in later revisions of the chip, the APPROTECT register must be set to the HwDisabled value, and firmware must write SwDisable to the APPROTECT.Disable register. Otherwise, upon the first reset after APPROTECT is updated, SWD will return to a locked state.
These software updates, combined with additional hardware changes, were implemented with the goal of thwarting the fault injection attack outlined in the next section. This revision 3 design of the nRF52832 has since been incorporated into the manufacturing of our PMD.
Fault Injection
Introduction
As previously mentioned, the intended method of unlocking the SWD interface for our nRF52832 consists of invoking a full flash erasure in order to reset the register which determines the access port protection state. This requirement of erasing flash is unacceptable in our case, considering our goal is to retrieve the firmware for analysis. Therefore, unintended methods, such as fault injection, are required for bypassing access port protection. In our setup, a fault/glitch is defined by a period of shorting the CPU power line to ground. This period of low voltage has the possibility of disrupting the CPU’s hardware mechanisms, including possibly the access port protection mechanism. Previous security research has shown that the nRF52840 chip is vulnerable to fault injection methods during startup, which bypasses access port protection, as seen in this article. Additionally, we’ve successfully performed the same fault injection attack on one of our own nRF52840 development boards in the past. Therefore, our efforts will involve performing a similar attack on the nRF52832.
PCB Analysis
Before we can begin glitching, an analysis of the PCB and MCU’s power sources must be conducted to determine promising lines to glitch during startup.
In analyzing our PMD’s PCB, a CT scan of the device was found, which, when correlated with the ball-out diagram given by the MCU’s product specification, reveals the pads and capacitors connected to our pads of interest.

Notably, the bypass capacitors connected to each of the DEC pins are identified. We’d like to analyze the raw startup behavior of each DEC pad, yet these capacitors serve to stabilize the voltage across each pad, and are not strictly necessary for providing power. According to the nRF52840 research, DEC1 serves as the CPU power line, which is an ideal glitch line candidate. Therefore, C4 is removed, and a glitch line is soldered to DEC1.
Startup Behavior
In order to successfully glitch the nRF52832, the correct delay for glitching must be determined. Analysis of our glitch line’s startup curve can help inform such a delay.

Pictured above is the logic analysis of our glitch line’s startup behavior. After startup, a period of activity can be seen for about 4600 microseconds, with a brief dip during this period outlined on the left. Our nRF52840 dev board exhibited a similar period of activity, albeit without the brief dip early in startup.
The glitch we found for the nRF52840 dev board was located at approximately the same time as the final dip in voltage, outlined on the right. However, because we’re seeing a slight variation with the dev board, we’ll attempt glitches throughout the 0 – 5000 microsecond timeframe, as we can confidently say that our glitch should be found within this period.
Setup
With a glitch line and delay range defined, we can now begin outlining our actual glitching setup. The procedure we will follow in finding a glitch will be brute-force in nature. The space for a glitch is defined by the following parameters:
- DELAY: How long to wait after start-up to perform the glitch
- WIDTH: How long the glitch should last, or more accurately, how long the MCU power line will be grounded
Each glitch attempt consists of powercycling the PMD, waiting DELAY microseconds, shorting DEC1 for WIDTH microseconds, then checking if we can establish a connection with the MCU via SWD. If the connection fails, DELAY is incremented and we try again. Because our device is locked, an SWD connection will only succeed if a glitch succeeds.
In order to automate this process, we used an ESP32 with custom firmware to act as our “glitching probe”, pictured below:
The PMD itself is censored for security purposes
Glitching Firmware
The firmware used for our glitching setup is based on this ESP32 glitching firmware, which we extended to support a serial interface and more flexible glitching parameters.
The firmware provides two types of functionality for the ESP32, the first of which is a basic SWD probe, designed to initiate SWD connections and execute SWD read/writes, and the second being a glitcher. The glitcher functionality automates the process of glitching the MCU, as outlined in the aforementioned section, and provides a command-line interface for setting various glitching parameters.
The firmware allows the definition of five parameters: MAX_DELAY, MIN_DELAY, MAX_WIDTH, MIN_WIDTH, and REPEAT.
Until a successful glitch is found, the glitcher will begin glitching at delay MIN_DELAY microseconds, and for each delay from MIN_DELAY to MAX_DELAY microseconds, the glitcher will test pulses of MIN_WIDTH to MAX_WIDTH microseconds in length. Each width at a given delay is tested REPEAT times. Once all possible delays are exhausted, the current delay will reset to MIN_DELAY.
Therefore, the total number of glitch attempts for one run-through of the delay range is given as:
TOTAL = (MAX_DELAY – MIN_DELAY) * (MAX_WIDTH – MIN_WIDTH) * REPEAT
Our glitch setup will use the following parameters:
- MAX_DELAY = 5000us
- MIN_DELAY = 0us
- MIN_WIDTH = 16us
- MAX_WIDTH = 16us
- REPEAT = 30
We are not exploring different width ranges, as a width of 16us worked well for our dev board, and sticking to a single width sped up our glitching procedure. From here, we fired up the glitcher and let it run over the weekend.
Glitch Results
Our first successful glitch came a few hours later, using a delay of 3752 microseconds:
...
Next glitch
Delay: 3752 Width: 16
Num left: 27
DP Write reg: 0x00 : 0x0000001e
DP Write reg: 0x04 : 0x50000000
AP Read reg: 0x0c : 0x00000001
1111 Read Register: 0x10000010 : 0x00001000
1111 Read Register: 0x10000014 : 0x00000080
Flash size: 524288
1111 Read Register: 0x1000005c : 0xffff014f
1111 Read Register: 0x10000060 : 0x6abbbdd3
1111 Read Register: 0x10000064 : 0xe174adcf
1111 Read Register: 0x10000100 : 0x00052832
1111 Read Register: 0x10000104 : 0x41414531
1111 Read Register: 0x10000108 : 0x00002002
1111 Read Register: 0x0000300c : 0xffff00cb
1111 Read Register: 0x10001208 : 0x00000000
SWD Id: 0x2ba01477
1111 Read Register: 0x10000100 : 0x00052832
We Have a good glitchInterestingly, this glitch came nearly 1000 microseconds before the first glitch we found on our nRF52840 dev board. In the hopes of reproducing the same glitch, we set the glitcher to only test the delay of 3752 microseconds, and fired it off. However, this went on without any hints of a glitch.
Switching back to the ranged strategy, we began testing the delay range of 3700 – 4600 microseconds, which should take much less time than testing from 0 – 5000 microseconds. After a couple of hours, we hit several more glitches:
4445 Microseconds:
...
Next glitch
Delay: 4445 Width: 16
Num left: 26
DP Write reg: 0x00 : 0x0000001e
DP Write reg: 0x04 : 0x50000000
AP Read reg: 0x0c : 0x00000001
1111 Read Register: 0x10000010 : 0x00001000
1111 Read Register: 0x10000014 : 0x00000080
Flash size: 524288
1111 Read Register: 0x1000005c : 0xffff014f
1111 Read Register: 0x10000060 : 0x6abbbdd3
1111 Read Register: 0x10000064 : 0xe174adcf
1111 Read Register: 0x10000100 : 0x00052832
1111 Read Register: 0x10000104 : 0x41414531
1111 Read Register: 0x10000108 : 0x00002002
1111 Read Register: 0x0000300c : 0xffff00cb
1111 Read Register: 0x10001208 : 0x00000000
SWD Id: 0x2ba01477
1111 Read Register: 0x10000100 : 0x00052832
We Have a good glitch4311 Microseconds:
...
Next glitch
Delay: 4311 Width: 16
Num left: 22
DP Write reg: 0x00 : 0x0000001e
DP Write reg: 0x04 : 0x50000000
AP Read reg: 0x0c : 0x00000001
1111 Read Register: 0x10000010 : 0x00001000
1111 Read Register: 0x10000014 : 0x00000080
Flash size: 524288
1111 Read Register: 0x1000005c : 0xffff014f
1111 Read Register: 0x10000060 : 0x6abbbdd3
1111 Read Register: 0x10000064 : 0xe174adcf
1111 Read Register: 0x10000100 : 0x00052832
1111 Read Register: 0x10000104 : 0x41414531
1111 Read Register: 0x10000108 : 0x00002002
1111 Read Register: 0x0000300c : 0xffff00cb
1111 Read Register: 0x10001208 : 0x00000000
SWD Id: 0x2ba01477
1111 Read Register: 0x10000100 : 0x00052832
We Have a good glitch4422 Microseconds
...
Next glitch
Delay: 4422 Width: 16
Num left: 16
DP Write reg: 0x00 : 0x0000001e
DP Write reg: 0x04 : 0x50000000
AP Read reg: 0x0c : 0x00000001
1111 Read Register: 0x10000010 : 0x00001000
1111 Read Register: 0x10000014 : 0x00000080
Flash size: 524288
1111 Read Register: 0x1000005c : 0xffff014f
1111 Read Register: 0x10000060 : 0x6abbbdd3
1111 Read Register: 0x10000064 : 0xe174adcf
1111 Read Register: 0x10000100 : 0x00052832
1111 Read Register: 0x10000104 : 0x41414531
1111 Read Register: 0x10000108 : 0x00002002
1111 Read Register: 0x0000300c : 0xffff00cb
1111 Read Register: 0x10001208 : 0x00000000
SWD Id: 0x2ba01477
1111 Read Register: 0x10000100 : 0x00052832
We Have a good glitchAdditionally, we were able to capture the startup curve behavior during a successful glitch, versus a failed glitch:
Successful glitch:

Failed Glitch:

All in all, we found that a delay range of 4200 – 4600 microseconds, with 30 retries at each delay, provided a solid balance between speed and reliability, as each attempt in this range was able to find a glitch without having to wrap back around to the beginning, and a glitch could be found within as soon as half an hour.
Dumping Firmware
Once the MCU is successfully glitched, procuring a firmware dump should be as simple as invoking the relevant command on the ESP32, or hooking up an SWD probe such as a JLink and dumping the firmware in GDB.
However, there is an important nuance to consider in that the current unlocked state of the nRF52832 is transient in nature, as the APPROTECT register is still set such that the access port protections should be enabled. This means that upon resetting the MCU, the device will revert back to its locked state, and will require glitching to become unlocked again. As such, one must be careful not to powercycle the PMD in between glitching and dumping the firmware.
In anticipation of this, we modified our ESP32’s firmware to support a custom command which would read the entire contents of flash and UICR via SWD, then output the hex of the binaries over serial, allowing us to save the firmware binaries to our local machine. With our firmware binary in hand, we can begin reverse engineering to find vulnerabilities in the PMD itself.
Furthermore, with a dump of the MCU’s UICR binary, we can also now permanently unlock the PMD. This way, we can freely debug the device during our RE efforts without the need for glitching.
Bonus: Permanent Unlocking and Reflashing
As a refresher, the APPROTECT register, located at offset 0x208 in the UICR section of memory, is what determines whether or not our PMD’s MCU has a locked down SWD interface. By default, our PMD has “Enabled” written to this register, preventing us from accessing the SWD interface without either mass erasing flash, or glitching.
However, because we’ve successfully dumped flash and UICR thanks to glitching, we can now permanently unlock the device by mass erasing flash, which will unlock the device and clear the APPROTECT register. Then, we can reflash our firmware dump back onto the device, restoring its actual functionality. Finally, we can patch the APPROTECT register in our UICR dump to disable access port protections, then reflash the UICR region with our patched UICR binary.
The result is a fully unlocked PMD with all of its code intact. This serves to greatly help our reverse engineering efforts, in that we can now freely debug a PMD to test and develop exploits.
Firmware Reverse Engineering
Once we extracted the firmware from the MEDICAL DEVICE, we reverse engineered the firmware to develop a better understanding of the device and to search for vulnerabilities. We used a variety of techniques to make the firmware and the MEDICAL DEVICE easier to reverse engineer.
Populating SoftDevice Functions
Based on the fact that the firmware came from a Nordic chip, and based on a version information field within the firmware, we learned that the firmware was built on top of version S132 of SoftDevice, Nordic’s Bluetooth Low Energy implementation.
Because our PMD uses the S132 SoftDevice, a large number of the functions bundled into the firmware are SoftDevice functions. Of these SoftDevice functions, a large percentage are “SVCall” wrappers, which are analogous to system call wrappers in the context of operating systems. These SVCalls make up the interface between application code and SoftDevice code. Because the SoftDevice provides a Bluetooth interface to the SDK, these SVCalls are used extensively in our firmware binary, particularly in code related to Bluetooth communication. As such, a quick and automatic way to identify these functions would be a great boon to our reverse engineering efforts.
Each of these SVCall wrapper functions have the following simple structure:
function:
svc SVCALL_ID
bx lrThe SVCALL_ID is a unique integer associated with each SVCall. The association between each SVCall and their corresponding ID is given in the headers which ship with the SoftDevice binary provided by Nordic. They can be downloaded here.
Looking through the SoftDevice headers, one can see that many include an SVC enum located near the top of the file.
For example, in ble_gap.h:
enum BLE_GATTC_SVCS
{
SD_BLE_GATTC_PRIMARY_SERVICES_DISCOVER = BLE_GATTC_SVC_BASE,
SD_BLE_GATTC_RELATIONSHIPS_DISCOVER,
SD_BLE_GATTC_CHARACTERISTICS_DISCOVER,
SD_BLE_GATTC_DESCRIPTORS_DISCOVER,
SD_BLE_GATTC_ATTR_INFO_DISCOVER,
SD_BLE_GATTC_CHAR_VALUE_BY_UUID_READ,
SD_BLE_GATTC_READ,
SD_BLE_GATTC_CHAR_VALUES_READ,
SD_BLE_GATTC_WRITE,
SD_BLE_GATTC_HV_CONFIRM,
SD_BLE_GATTC_EXCHANGE_MTU_REQUEST,
};This enum associates SVCall functions with their corresponding ID. We can use Ghidra’s C header file parser to import these enums into Ghidra.
These header files also include the association between these enums and their corresponding function signatures. For example, in ble_gap.h:
SVCALL(SD_BLE_GAP_ADDR_SET, uint32_t, sd_ble_gap_addr_set(ble_gap_addr_t const *p_addr));
...
SVCALL(SD_BLE_GAP_ADDR_GET, uint32_t, sd_ble_gap_addr_get(ble_gap_addr_t *p_addr));
...
SVCALL(SD_BLE_GAP_ADV_ADDR_GET, uint32_t, sd_ble_gap_adv_addr_get(uint8_t adv_handle, ble_gap_addr_t *p_addr));
...We created a Ghidra script that uses these mappings and the enums described above to rename the SVCall wrappers with their correct names and signatures:
# USAGE
# 1. Download the header files for your SoftDevice version from
# https://www.nordicsemi.com/Products/Development-software/S132/Download,
# replacing S132 with your version if step 6 brings up a prompt telling
# you your firmware uses a different version.
# 2. Use File -> Parse C Source to import all of the
# SoftDevice types from the SoftDevice header files.
# * The __STATIC_INLINE types may need to be changed to static inline.
# * It is fine if Ghidra gives some errors saying it can't resolve
# some #include-ed header files.
# 3. Put this script into ~/ghidra_scripts or add the directory it is in to
# your Script Directories as described in ghidra-tools/README.md.
# 4. Go to Window -> Script Manager.
# 5. Search for FixSoftDeviceSVCWrappers.py and double-click on it.
# 6. Select the directory with the SoftDevice header files; it should contain
# the folders doc/ and include/.
#
# TODOS
# * Make the script automatically import the SoftDevice types from the
# SoftDevice header files.
#
# @category Nordic.SoftDevice
import os
import sys
from ghidra.program.model.data import Enum
from ghidra.program.model.symbol import SourceType
from ghidra.app.util.cparser.C import CParserUtils
from ghidra.program.model.listing.Function import FunctionUpdateType
from ghidra.program.model.listing import ParameterImpl
from docking.widgets.filechooser import GhidraFileChooser
from javax.swing import JOptionPane
listing = currentProgram.getListing()
fm = currentProgram.getFunctionManager()
af = currentProgram.getAddressFactory()
memory = currentProgram.getMemory()
dtm = currentProgram.getDataTypeManager()
def main():
id, version = sd_info()
INTENDED_ID = "s132"
INTENDED_VERSION = "7.0.1"
if id != INTENDED_ID or version != INTENDED_VERSION:
choice = JOptionPane.showConfirmDialog(
None,
"Your firmware uses SoftDevice version {}/v{}, whereas this script was written for version {}/v{}. This script will probably work for other SoftDevice versions, but may require modifications. Do you want to continue?".format(
id, version, INTENDED_ID, INTENDED_VERSION
),
"Version Mismatch Warning",
JOptionPane.YES_NO_OPTION,
)
if choice != 0:
print("Exiting as requested.")
sys.exit(1)
file_chooser = GhidraFileChooser(state.getTool().getActiveWindow())
file_chooser.setFileSelectionMode(GhidraFileChooser.DIRECTORIES_ONLY)
directory = file_chooser.getSelectedFile()
if directory is None:
print("You must choose a folder for this to work.")
sys.exit(1)
if set(directory.list()) != set(["doc", "include"]):
print(
"The folder you chose is likely incorrect; it should contain exactly two folders named doc/ and include/."
)
sys.exit(1)
service_calls = parse_directory(directory.getPath())
funcs = fm.getFunctions(True)
for func in funcs:
address = func.getEntryPoint()
code_units = listing.getCodeUnits(address, True)
first = code_units.next()
second = code_units.next()
if first is None or second is None:
continue
first = first.toString()
second = second.toString()
if not first.startswith("svc ") or second != "bx lr":
continue
old_name = func.getName()
_, _, svc_number = first.rpartition(" ")
service_call = find_service_call_from_number(service_calls, int(svc_number, 16))
function_name, _, _ = service_call["signature"].partition("(")
function_name = function_name.replace("sd_", "svcall_")
signature = CParserUtils.parseSignature(
None,
currentProgram,
service_call["return_type"] + " " + service_call["signature"],
)
parameters = list(
map(parameterDefinitionImpl_to_parameterImpl, signature.getArguments())
)
return_type = signature.getReturnType()
print(
"Updating function "
+ old_name
+ " (svc #"
+ svc_number
+ ") to have:\n"
+ "\treturn type:\t"
+ return_type.getName()
+ "\n\tname:\t\t"
+ function_name
+ "\n\tparameters:\t"
+ str(parameters)
+ "\n"
)
func.setReturnType(return_type, SourceType.USER_DEFINED)
func.setName(function_name, SourceType.USER_DEFINED)
func.replaceParameters(
parameters,
FunctionUpdateType.DYNAMIC_STORAGE_ALL_PARAMS,
False,
SourceType.USER_DEFINED,
)
def sd_info():
SD_INFO_STRUCT_OFFSET = 0x3000
SD_INFO_ID_OFFSET = 16
SD_INFO_VERSION_OFFSET = 20
sd_id_address = af.getAddress(hex(SD_INFO_STRUCT_OFFSET + SD_INFO_ID_OFFSET))
sd_version_address = af.getAddress(
hex(SD_INFO_STRUCT_OFFSET + SD_INFO_VERSION_OFFSET)
)
sd_id = memory.getInt(sd_id_address)
sd_version = memory.getInt(sd_version_address)
major_version = sd_version // 1000000
sd_version -= major_version * 1000000
minor_version = sd_version // 1000
sd_version -= minor_version * 1000
bugfix_version = sd_version
return "s{}".format(sd_id), "{}.{}.{}".format(
major_version, minor_version, bugfix_version
)
def parse_directory(folder):
service_calls = []
for path, _, filenames in os.walk(folder):
for filename in filenames:
absolute_path = os.path.join(path, filename)
content = None
with open(absolute_path, "r") as file:
content = file.read()
service_calls += parse_file(content)
return service_calls
def parse_file(content):
service_calls = []
content = content.split("\n")
for line in content:
line = line.strip(" \t\r\n")
if line.startswith("SVCALL("):
service_call = parse_line(line)
service_calls.append(service_call)
return service_calls
def parse_line(line):
assert line.startswith("SVCALL(")
assert line.endswith(");"), "Line doesn't end with );: " + line
# removeprefix and removesuffix aren't supported by Ghidra's Python version
line = line[len("SVCALL(") :] # line.removeprefix("SVCALL(")
line = line[: -len(");")] # line.removesuffix(");")
segments = line.split(", ", 2)
assert len(segments) == 3, "len(" + segments + ") != 3"
[sv_name, return_type, signature] = segments
# Ghidra seems to be incapable of parsing function signatures containing const in their parameters
signature = signature.replace("const ", " ")
service_number = find_service_number_from_name(sv_name)
return {
"service_number": service_number,
"signature": signature,
"return_type": return_type,
}
def find_service_number_from_name(name):
for data_type in dtm.getAllDataTypes():
if not isinstance(data_type, Enum):
continue
if not data_type.contains(name):
continue
return data_type.getValue(name)
raise Exception("could not find service with name {}.".format(name))
def find_service_call_from_number(service_calls, number):
for service_call in service_calls:
if service_call["service_number"] == number:
return service_call
raise Exception("found no service call with number {}".format(number))
def parameterDefinitionImpl_to_parameterImpl(parameterDefinitionImpl):
return ParameterImpl(
parameterDefinitionImpl.getName(),
parameterDefinitionImpl.getDataType(),
currentProgram,
)
main()Running this script in Ghidra, we can see that the SVC wrapper functions are correctly identified, renamed, and retyped:
FixSoftDeviceSVCWrappers.py> Running...
Updating function FUN_0003b78c (svc #0x47) to have:
return type: uint32_t
name: svcall_ecb_blocks_encrypt
parameters: [[uint8_t block_count@<UNASSIGNED>], [nrf_ecb_hal_data_block_t * p_data_blocks@<UNASSIGNED>]]
Updating function FUN_0003c0f4 (svc #0x6c) to have:
return type: uint32_t
name: svcall_ble_gap_addr_set
parameters: [[ble_gap_addr_t * p_addr@<UNASSIGNED>]]
Updating function FUN_0003c0f8 (svc #0x6e) to have:
return type: uint32_t
name: svcall_ble_gap_whitelist_set
parameters: [[ble_gap_addr_t * * pp_wl_addrs@<UNASSIGNED>], [uint8_t len@<UNASSIGNED>]]
...Changing the Pairing Code
While reverse engineering the firmware to find vulnerabilities, we sometimes found it useful to patch sections of the firmware to help confirm or develop exploits. For simplicity, we would make a copy of the firmware dump we acquired through glitching, make changes to the copy, and flash the copy to an already-spent device. However, doing so restricted our ability to experiment in parallel due to collisions between the pairing codes. It became necessary to change the pairing code.
We found a printf-style format string in the firmware which logged the pairing code. The variable being printed was a string representation of the pairing code stored in RAM, and references to that variable’s address revealed a function that converted a 2-byte little-endian integer stored at a hard-coded location in flash to an ASCII string. For our particular dump, this looked like the following (0x22FB corresponds to a pairing code of 8955):
PAIRING_CODE:
XXXXXXXX fb 22 short 22FBhSince this value is located in flash, it is unlikely to change frequently, so we concluded this was the pairing code. We used the following trivial script to change the value:
with open(FLASH_DUMP, "r+b") as f:
f.seek(0xXXXXXXXX, os.SEEK_SET)
f.write(struct.pack("<H", int(NEW_PAIRING_CODE)))Afterwards, we observed that the value was updated in flash and that we could use the new pairing code to connect to the device.
Advertising Interval Change
PMDs are designed to be small, sleek devices, but they still require a battery to power the device. Because wireless radios tend to consume a lot of power, it is common for portable devices to keep Bluetooth activity to a minimum to preserve battery life. Extending battery life is especially important for PMDs like the one we investigated since they should not be recharged or reused for safety purposes.
While this is good for end users, this made it very time-consuming to develop any exploits involving Bluetooth pairing with the device since we would need to wait 5 minutes between each Bluetooth advertising period. We were also not concerned with battery life since we connected the device to a steady source of power. It quickly became apparent we needed to find a way to change the 5-minute interval to make any meaningful progress.
At this point, we had looked through the firmware enough to know the device was running an RTOS. To find where the 5-minute recurring advertising was being set, we focused on reverse engineering the scheduler.
An item to be added to the scheduler consists of a function pointer along with some other useful information such as arguments and a delay before running the function. These items are added to a linked list and stay dormant there until they are ready to run (i.e., the delay time has elapsed), after which they are actually scheduled to run.
The Bluetooth advertising function is scheduled with a delay of 300,000 ms (5 minutes), and it is also responsible for scheduling itself to run again. This is how the delay of 300,000 ms is achieved:
...
XXXXXXXX 4f f4 7a 73 mov.w r3,#0x3e8 // store 1000 in rX
...
YYYYYYYY 6b 43 muls r3,r5 // r5 contains 300
...
// eventually calls the function to schedule BLE advertisementsWhile testing our exploits, we changed this to 10,000 ms (10 seconds):
with open(FLASH_DUMP, "r+b") as f:
f.seek(0xXXXXXXXX, os.SEEK_SET)
f.write(b"\x42\xf2\x10\x73") # Change operand of MOV to 10,000
f.seek(0xYYYYYYYY, os.SEEK_SET)
f.write(b"\x00\xbf") # Replace MUL with NOPCertificate Reconstruction
Our original firmware dump was missing a portion of flash which was causing the pairing process to fail. After analyzing the firmware, we realized the missing portions were parts of X.509 certificates used by the device to authenticate with a smartphone app.
Fortunately, we had previously used the REDACTED, an open-source app that can connect to a variety of MEDICAL devices, to connect to a brand new device, and we kept the logs from that connection. The certificates being exchanged were included in the logs, so we were able to recover the contents of the log in flash.
GDB Printing Hack
As mentioned above, we found a printf-style function, which would send print messages to the UART interface. This printf function was called frequently throughout the app, indicating interesting and useful debug information throughout the course of lifetime of the PMD, such as startup and Bluetooth events. However, we didn’t find any useful test pads or vias on the PCB that would have allowed us to access this interface easily. As such, we derived a quick-and-dirty solution to printing out these debug messages in real time during debug sessions. The following GDB script accomplished this logging mechanism:
# Will print debug messages in real-time
b *[PRINTF_ADDRESS]
commands
silent
p (char *)$sp
c
endFor reference, PRINTF_ADDRESS is an address within the printf function, which occurs after the format string has been fully processed and the final print string is ready to be output. The final print string is pointed to by $sp in this case, hence why we simply breakpoint at this address, print $sp, then continue.
One important limitation of this hack is that it is very slow due to the nature of breakpoints and GDB commands. As such, we’ve found in our experiments that using this script for extended periods of time can slow down the MCU to the extent that it may drop time-sensitive activities, such as Bluetooth connections, or even cause the device to reset.
Dead Ends
Device Commands
By reverse engineering the firmware dump in Ghidra and correlating it with the OSS MEDICAL APP source code, we could see that the MEDICAL DEVICE made multiple commands available over the Bluetooth characteristic with UUID XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX (this characteristic is referred to as the Control characteristic in the OSS MEDICAL APP source code). These commands are only accessible to the phone after the phone has authenticated with the MEDICAL DEVICE using the correct pairing code.
In Ghidra, we found that the MEDICAL DEVICE firmware handles the commands as follows.
struct Command {
int identifier;
void* function;
}
int result;
Command* command_ptr;
Command command;
// COMMAND_TABLE is a pointer to an array of Commands
command_ptr = COMMAND_TABLE;
do {
if (COMMAND_TABLE + XXX < command_table_ptr) {
result = 2;
goto processing;
}
command = *command_ptr;
command_ptr += 1;
} while (command->identifier != query[0])
if (!command_is_disabled(query[0]) {
result = (*command->function)(&query[1]);
} else {
result = 1;
}
processing:
// ...The code finds the command with the given command ID, then checks if the command is enabled or disabled, and then runs the command’s corresponding function if it is enabled. The command_is_enabled function is implemented as follows.
bool command_is_disabled(uint code) {
// command_blacklist is a global array of 16 shorts
return (bool)((byte)(command_blacklist[code >> 4] >> (code & 0xf)) & 1);
}We wrote a script to see which commands this function enables and disables, and which commands are present in COMMAND_TABLE at all. We additionally modified OSS MEDICAL APP to fuzz each of the 256 possible commands on an unexpired MEDICAL DEVICE. The two tests yielded the same results and showed us which commands were absent (not in COMMAND_TABLE at all), disabled (disabled in command_blacklist), and enabled (working correctly). We found the following commands to be enabled: 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX. We did not find any obvious security vulnerabilities associated with these commands; they do things such as starting the sensor session, reading the blacklist state, and presumably getting MEDICAL READINGS.
We did find disabled commands that, if enabled, would compromise the security of the device.
| ID | Command Format (Hexadecimal) | Command Description |
| 0xXX | REDACTED | Overwrites the certificate information (used for authentication with the phone) in the flash memory with the given bytes at the given offset. |
| 0xXX | REDACTED | Updates the MEDICAL DEVICE’s pairing code. |
| 0xXX | REDACTED | Updates the MEDICAL DEVICE’s command blacklist to either blacklist or whitelist a given command. |
Because these commands are completely disabled using the blacklist mechanism, and are thus not accessible even by a party that knows the pairing code of the MEDICAL DEVICE, we didn’t find that they compromise the security of the device at all.
Android App Firmware Search
The paper “FirmXRay: Detecting Bluetooth Link Layer Vulnerabilities From
Bare-Metal Firmware” describes a binary analysis tool to scan embedded device firmware for insecure Bluetooth configurations. To test their software, the authors needed to acquire a large number of firmware images for real-world embedded devices. The authors stated that they found that many embedded devices implement their device firmware update system by embedding the new firmware directly into the device’s mobile app. The authors stated that, by downloading millions of apps from the Google Play Store and scanning the app bundles for firmware, they were able to find 793 unique firmware images for embedded devices, 768 of which were for Nordic devices like the one used by the MEDICAL DEVICE.
Inspired by this research, we performed a cursory search of the MEDICAL DEVICE Android app for firmware to see if MEDICAL COMPANY was unintentionally leaking the MEDICAL DEVICE’s firmware in this way, but we did not find the firmware in the app.
NFC
When we initially started analyzing the firmware dump from the MEDICAL DEVICE, it was clear that there were NFC-related functions in the firmware, because of the presence of NFC-related strings present, such as the following.
- NFC is disabled
- NFC Failed to read message
- NFC message arrived
We scanned the MEDICAL DEVICE using the high frequency side of the Proxmark3 RDV as follows.
[usb] pm3 --> hf search
🕛 Searching for ISO14443-B tag...
[+] UID : XX XX XX XX
[+] ATQB : XX XX XX XX XX XX XX
[+] CHIPID : XX
[+] App Data: XX XX XX XX
[+] Protocol: XX XX XX
[+] Bit Rate: 106 kbit/s only PICC <-> PCD
[+] Max Frame Size: 32 bytes
[+] Protocol Type: Protocol is compliant with ISO/IEC 14443-4
[+] Frame Wait Integer: 8 - 8192 ETUs | 77312 us
[+] App Data Code: Application is Proprietary
[+] Frame Options: NAD is not supported
[+] Frame Options: CID is supported
[+] Tag :
[+] Max Buf Length: 0 (MBLI) chained frames not supported
[+] CID : 0Most NFC commands run with hf 14b apdu -s -d <5 bytes> yielded a response of 6a 82 – File not found, however through experimentation we found that hf 14b apdu -s -d XXXXXXXXXX (the READ BINARY instruction) yielded a success code. Understanding these commands was made more difficult by the fact that the NFC specifications are proprietary.
We wrote a custom command for the Proxmark3 firmware to fuzz the NFC interface of the MEDICAL DEVICE. The fuzzer tried APDUs with every possible combination of first and second byte (the first and second byte represent the instruction class and code respectively). Through this we found that the only other APDU that did not result in 6a 82 – File not found was XXXXXXXXXX.
Next, we wanted to see where/whether the NFC commands are handled within the firmware. We tried the following techniques to see which portions of the firmware may be running as a response of NFC interactions:
- Creating breakpoints on NFC-related functions and seeing if they run as a result of NFC scans.
- Creating a breakpoint on the SVC (ARM syscall) handler within the code to see if any syscalls run as a result of NFC scans.
- Creating a breakpoint for each of the interrupt handlers in the interrupt vector table to see if any interrupts run as a result of NFC scans.
- Creating a breakpoint for each of the app interrupt handlers (which handle events passed from the Nordic system layer up to the application) to see if any app events occur as a result of NFC scans.
Using these methods, we did not find any evidence that the firmware was influenced at all by NFC scans.
Authentication Protocol
When the patient is setting up their MEDICAL DEVICE, they first attach the PMD to their body, and then connect the MEDICAL DEVICE app on their phone to the PMD by entering their PMD’s pairing code into their phone. This pairing code is listed on the DEVICE, as shown below.
IMAGE REDACTED
We reverse engineered the protocol used to connect the phone and PMD together using the following tools and resources:
- We analyzed the source code of REDACTED, an open-source MEDICAL app that reimplements the functionality of the official MEDICAL DEVICE app, to see how the protocol works from the phone’s perspective.
- We analyzed the MEDICAL DEVICE’s firmware using Ghidra.
- We used the nRF Connect app to see which Bluetooth services and characteristics were made available by the MEDICAL DEVICE.
Bluetooth Characteristics
A Bluetooth characteristic can be thought of as analogous to an HTTP endpoint on the web, and a Bluetooth service is a set of characteristics, analogous to a web API.
The MEDICAL DEVICE makes available a Bluetooth service with UUID XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX; it is referred to as PMDService in the OSS MEDICAL APP app. Within this service, there are four characteristics:
| Characteristic name in OSS MEDICAL APP | UUID | Description |
| Authentication | XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX | Used to send authentication commands. |
| ExtraData | XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX | Used to send relatively large authentication data like certificates and elliptic curve points. |
| Control | XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX | Used to get information like MEDICAL READINGS from the device after authenticating. |
| ProbablyBackfill | XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX | Unknown purpose. |
In addition to seeing these characteristics in OSS MEDICAL APP, we could see them being advertised in the nRF Connect app:
IMAGE REDACTED
J-PAKE Exchange
The authentication protocol first confirms that both ends of the communication (the phone and the PMD) know the same pairing code. To do this it uses a cryptographic protocol called J-PAKE, which provides the following:
- It facilitates authentication of a password, without needing either party to reveal their password.
- If the passwords supplied by the two parties are identical, it facilitates the creation of a shared session key that is unknown to third parties listening in.
Certificate Exchange and Confirmation
Next, the devices exchange X.509 certificate chains. These certificates use the same format as those used for websites.
The devices first exchange their certificates via the Authentication and ExtraData characteristics as follows.
Phone -> MEDICAL DEVICE on Authentication: XX 01 <4 little-endian bytes representing the phone's certificate size>
MEDICAL DEVICE -> Phone on ExtraData: <the MEDICAL DEVICE's first certificate>
Phone -> MEDICAL DEVICE on ExtraData: <the phone's first certificate>
Phone -> MEDICAL DEVICE on Authentication: XX 02 <4 little-endian bytes representing the phone's certificate size>
MEDICAL DEVICE -> Phone on ExtraData: <the MEDICAL COMPANY's second certificate>
Phone -> MEDICAL DEVICE on ExtraData: <the phone's second certificate>The parties validate the certificate chains, and then send each other random byte sequences (i.e. cryptographic challenges) to be signed by the other party to confirm that the other party possesses the private key corresponding to the public key in the certificate chain they sent.
Phone -> MEDICAL DEVICE on Authentication: XX <16 random bytes>
MEDICAL DEVICE -> Phone on Extra Data: <64-byte ECDSA-SHA256 signature of the bytes; uses the private key corresponding to the certificate sent by the MEDICAL DEVICE>
MEDICAL DEVICE -> Phone on Authentication: XX 00 <16-byte random challenge>
Phone -> MEDICAL DEVICE on ExtraData: <the challenge, ECDSA-SHA256-signed using the private corresponding to the certificate sent by the phone>
The phone then sends a final request to the MEDICAL DEVICE for status information as follows.
Phone -> MEDICAL COMPANY on Authentication: XX 00 02
MEDICAL COMPANY -> Phone on Authentication: XX 00 00 <6 bytes containing connection status information>Bluetooth Pairing
The devices then perform Bluetooth pairing. They use the “Just Works” form of Bluetooth pairing, which provides no man-in-the-middle protection. This pairing occurs completely independently of the prior exchanges. This fact will be exploited in the next section: Man-In-The-Middle Vulnerability.
Summary
This diagram summarizes the full protocol that the phone and MEDICAL DEVICE implement to authenticate with each other:
Man-In-The-Middle Vulnerability
Introduction
As discussed in the previous section, the initial connection between our PMD and external device is followed by a J-PAKE sequence, which verifies that the external device knows the pairing code associated with the PMD. This J-PAKE sequence allows both parties to derive a shared key, which can be used to encrypt and decrypt messages between the two devices. However, the fatal flaw with this design is that the shared key from the J-PAKE sequence is never used again after the J-PAKE sequence finishes. The two devices simply pair via BLE “Just works” pairing, which is then used to protect all future data packets.
Exploit Overview
Because the pairing code and the shared key are never used during the pairing step, it is possible for the man-in-the-middle (MITM) device to simultaneously connect to the PMD and the external device, and simply forward the J-PAKE sequence blindly between the two parties, all without ever needing to know the PMD’s pairing code. Once the J-PAKE sequence is complete, the MITM still does not know the shared key, but because the shared key is never used again, it can pair to both parties without issue.
From here, the MITM has full reign over all traffic between the PMD and external device. It can read, modify and even create packets that are sent to and from each party. This has serious implications in the contexts of PMDs especially, as these fraudulent packets can even be MEDICAL READINGS. It is not farfetched to imagine the situation in which a patient who gets their MEDICAL READINGS sent to their phone can be given faulty readings, leading to the administration of an incorrect MEDICATION DOSAGE.
MITM Development
In order to demonstrate and test such an exploit, we decided to implement our own MITM as a proof of concept, making use of the Nordic nRF52 DK, which hosts an nRF52840. Because the PMD’s firmware, including its Bluetooth logic, was built around the S132 SoftDevice and NRF SDK, we decided to use the same setup on our nRF52 DK. This made it much easier to develop the Bluetooth interface on the MITM.
We used a BLE app example program which shipped with the SDK as the foundation of our firmware implementation. Because the MITM would need to connect to both the PMD and external device simultaneously, the firmware implements both BLE central and peripheral roles.
An important detail to consider at this stage is that in order for the MITM to trick a device like a phone into thinking it is a PMD, its advertising data must fit the same structure as the PMD. For example, the MITM’s name and manufacturing info were changed to match the PMD.
Once the connection is established with both parties, the MITM simply needs to listen to the correct BLE events and forward the relevant packet to the other party. The benefit of the PMD and MITM both implementing the Bluetooth interface with the same SDK is that we could simply refer to the decompiled code of the PMD in determining which events from the external device need to be handled, and what API calls should be used for forwarding data back to the external device. Pictured below are the two event flows that had to be handled for both the PMD and external device.
Event flow from the External Device

Event flow from the PMD

One important detail about the development of the MITM, is that during its Bluetooth setup, it must support the same BLE characteristics as our PMD. However, because of the nature of BLE, it is likely that a given characteristic will have a different handle value on the PMD and MITM. Therefore, when the MITM receives a characteristic write request from the external device, it must translate that MITM characteristic handle to a PMD handle, and vice versa for requests coming from the PMD.
If the J-PAKE process finishes without issue, the MITM will receive a BLE event from the external device attempting to pair. The MITM accepts and issues a pairing request to the PMD. A security parameter and Diffie-Hellman key exchange occurs with both parties and the two parties successfully pair with the MITM. The BLE event flow is pictured below.
Exploit Demonstration
As stated above, we used an nRF52 DK board loaded with our custom firmware to perform the MITM exploit. Pictured below is our setup containing the MITM and the PMD. In this setup, the only connection between the PMD and the MITM is power, as the PMD is powered from the DK’s 3.3v power line. However, this has no bearing on the exploit itself.
The PMD is censored for security purposes
For our external device, we used a cell-phone containing the first-party app associated with the PMD. In this demonstration, the first-party app will be heavily censored to protect the identity of our PMD. The MITM supports debug output over serial to our local machine. As such, we can analyze the entire connection and pairing process of the two parties in real time.
When we power up the setup and begin attempting to connect to the PMD via the phone, we receive the following messages in the debug log of the MITM device, which indicate both the PMD and phone have successfully connected to the MITM:
<info> app: Filter match - device ready to connect
<info> app: Connected
<info> app: PMD connected with handle 0.
<info> app: Connected
<info> app: Phone connected with handle 1.From here, the J-PAKE sequence will commence, resulting in a long sequence of back and forth communications from both parties:
# Round 1 Auth command
<info> app: <PHONE> Received a GATT write for handle: AUTH VALUE, 19 (0x13).
<info> app: XX XX |..
# Response from PMD
<info> app: <PMD> Got data from handle: 29 (0x1D)
<info> app: <PMD> ExtraData Notification (20 bytes):
...
<info> app: XX XX XX |...
# Round 2 Auth command
<info> app: <PHONE> Received a GATT write for handle: EXTRA DATA VALUE, 22 (0x16).
...
<info> app: <PHONE> Received a GATT write for handle: AUTH VALUE, 19 (0x13).
<info> app: XX XX |..
# Response from PMD
<info> app: <PMD> Got data from handle: 29 (0x1D)
<info> app: <PMD> ExtraData Notification (20 bytes):
...
<info> app: <PMD> Authentication Indication (3 bytes):
<info> app: XX XX XX |...
# Round 3 Auth command
<info> app: <PHONE> Received a GATT write for handle: EXTRA DATA VALUE, 22 (0x16).
...
<info> app: <PHONE> Received a GATT write for handle: AUTH VALUE, 19 (0x13).
<info> app: XX XX |..
# Response from PMD
<info> app: <PMD> Got data from handle: 29 (0x1D)
<info> app: <PMD> ExtraData Notification (20 bytes):
...
<info> app: <PMD> Authentication Indication (3 bytes):
<info> app: XX XX XX |...
# Request Auth
<info> app: <PHONE> Received a GATT write for handle: EXTRA DATA VALUE, 22 (0x16).
...
<info> app: <PHONE> Received a GATT write for handle: AUTH VALUE, 19 (0x13).
<info> app: XX ...
# Challenge from PMD
<info> app: <PMD> Got data from handle: 23 (0x17)
<info> app: <PMD> Authentication Indication (17 bytes):
<info> app: XX ...
# Challenge Reply from phone
<info> app: <PHONE> Received a GATT write for handle: AUTH VALUE, 19 (0x13).
<info> app: XX ...
# JPAKE success from PMD
<info> app: <PMD> Authentication Indication (3 bytes):
<info> app: XX XX XX
With the J-PAKE sequence complete, the certificate exchange commences, which once done, leads to the pairing sequence:
<info> app: Security parameters request from handle 1.
<info> app: Sending pairing sec_params_reply to phone
<info> app: Received a DHKey request
<info> app: Replying to phone with DHKeys
<info> app: Security parameters request from handle 0.
<info> app: Replying with sec params to 0
<info> app: Received a DHKey request
<info> app: Replying to phone with DHKeys
<info> app: Connection secured with 0
<info> app: Authentication successful
<info> app: Connection secured with 1
<info> app: Authentication successfulWith both devices paired, we can now edit packets without issue. In this example, we modify the firmware information being sent from the PMD:
<info> app: <PMD> Received sensor info response, responding with modified data.
<info> app: <PMD> Existing firmware version: xxxxxxxx.
<info> app: Existing software number: xxxxxxxx
<info> app: Existing serial number is xxxxxxxx
<info> app: New firmware version: 20.20.27.211
<info> app: New software number: 202027211
<info> app: New serial number: 202027211Here, we change the sent firmware, software and serial numbers to 202027211. Checking the app, we can see this incorrect information reflected:
Exploit Limitations
Importantly, for this exploit to work, the MITM must remain within the vicinity of the victim and PMD when the victim connects to the PMD for the first time, and while the attacker desires to read, modify and/or spoof packets.
Mitigation Strategies
To eliminate the potential for a MITM to read, edit and create packets sent between the two devices, the PMD and external devices should encrypt and authenticate all communications using the shared key derived during the PAKE sequence.
This strategy would prevent a MITM from tampering with packets, but it would still allow the MITM to pair with the PMD and external device without ever needing the pairing code, which could be unwanted. In order to prevent this, an alternate BLE pairing strategy should be used, such as an out of band solution like NFC.
Certificate Parsing Vulnerabilities
In general, code that parses lots of structured data can pose a large attack surface since it needs to be resilient against a huge number of possible malformed inputs. The PMD device we investigated exchanges certificates with a smartphone app, so naturally it contains code to parse X.509 certificates. After inspecting the parsing code, we found numerous places where we could use malformed certificates to move the cursor to places we choose. We successfully leveraged this to trigger crashes and parsing loops, both of which resulted in subsequent restarts of the device.
X.509 certificates are often stored in the ASN.1 format with some binary encoding. These encodings represent elements in type-length-value format. In other words, a parser reading a stream of this binary-encoded data knows to first look for a type (e.g., integer, string, etc.), then a length, and finally the value/contents. A parser would know where the next type-length-value is based on the length of the previous. However, if a malicious certificate contains a bad length and the parser does not carefully check it, it is possible for the malicious certificate to move the cursor and trick the parser into trying to find the next piece of data in an invalid location.
The parser in the PMD device firmware we looked at fails to check some of these lengths. The following is a high-level example of the type of failed length check we saw in the disassembly of the firmware. The C code is effectively a pseudocode example and is not indicative of the code in the firmware.
/*
* This function assumes the cursor is at the start of a
* tag-length-value sequence.
* It makes sure the type is valid, parses the length,
* and then returns the length.
* It moves the cursor to the start of the value (immediately after
* the type and the length).
*/
unsigned int parse_type_and_length(char **cursor);
/* Variables used in this example */
unsigned int parsed_length;
char *cursor;
...
/*
* Right before the following line, cursor points to a valid type in a valid
* part of the certificate.
*/
parsed_length = parse_type_and_length(&cursor);
/*
* Right before the following line, cursor correctly points to
* the data following the type and length that was just parsed.
* However, parsed_length is malicious and contains a very large length.
*
* This line tries to move the cursor to the next item using the
* parsed length, but after this line it will point to a location
* controlled by a bad actor (an invalid address).
*/
cursor += parsed_length;
/*
* This will attempt to read from an invalid address, causing a crash
*/
parse_type_and_length(&cursor);
...
Since the datasheet for the SoC contains the memory map and we know the certificates are stored in memory, we can tell roughly which address the certificates are stored at within a margin of error relative to the size of the memory. The value of parsed_length above can therefore be chosen to move the cursor to a near-arbitrary address past the end of the certificate or even before the certificate due to integer overflow.
Integer overflow occurs when the result of an arithmetic operation is not within the range of values that can be represented using the number of bits of storage. For example with an unsigned 32-bit integer, this happens when the result is greater than 2^32 – 1 (which is 4,294,967,295). When this happens the value wraps back around to 0.
In our case, the cursor pointer has a malicious parsed_length added to it. The length is not checked, so parsed_length can cause cursor to point past the end of the certificate or be large enough for it to wrap back around to 0, effectively allowing it to point anywhere. When we set the cursor to an address in the “Reserved” region starting at address 0x1000000, this caused a crash, which restarted the device.
We found other places in the firmware where the length was checked, but the check itself was vulnerable to an integer overflow. One of these locations allowed us to create a parsing loop. The following is a combined example that shows both the overflowing check and parsing loop:
/*
* This function assumes the cursor is at the start of a
* tag-length-value sequence.
* It makes sure the type is valid, parses the length,
* and then returns the length.
* It moves the cursor to the start of the value (immediately after
* the type and the length).
*/
unsigned int parse_type_and_length(char **cursor);
/* Variables used in this example */
unsigned int parsed_length;
char *cursor;
char *section_end;
...
/*
* At this point, cursor is at a section of the certificate
* where it repeatedly parses similarly structured items.
* section_end points just past the end of the section and
* is used to determine when to stop parsing.
*/
while (cursor < section_end) {
/*
* This gets the length of the first item, which is set by
* an attacker to be very large.
*/
parsed_length = parse_type_and_length(&cursor);
/*
* This check is supposed to prevent the parsed_length from
* overshooting the end of the section, but since parsed_length
* is so large, it causes cursor + parsed_length to wrap around
* back to 0 and be much lower than section_end.
* The parsed_length slips past this check.
*/
if (cursor + parsed_length >= section_end)
goto error;
/*
* Since parsed_length is assumed to be good, it is added to cursor.
* This can be used to either cause a crash like in the prior example,
* or it can be used to move the cursor back to its original position
* at the beginning of the loop.
* If this happens, then this loop runs forever.
*/
cursor += parsed_length;
}
...
After a short period of time, the parsing loop causes a reset due to a mechanism we didn’t explore further. One theory is that it could be due to real-time constraints, similar to the resets we encountered when using our printf trick in GDB.
We tested these findings using the open-source OSS MEDICAL APP app to send custom certificates to the device and verified the reset using a GDB breakpoint at the entrypoint. Additionally, we successfully exploited these vulnerabilities to interrupt the pairing process between an untampered-with device and a “good” smartphone by concurrently sending malicious certificates to crash the device from a “bad” smartphone. This is a denial of service, although it depends on the race condition between the good and bad smartphone communicating with the device and is limited in scope to pairing time. Furthermore, the bad smartphone required knowledge of the pairing code to make it far enough in the pairing process to exchange certificates with the device. However, these vulnerabilities can also be exploited by a man-in-the-middle modifying the certificates being sent to the device from a good smartphone before forwarding them.
Uninitialized Stack Memory Read Vulnerability
Discovery
In the authentication procedure, the final command sent by the phone to the MEDICAL DEVICE, 0xXX, normally yields a response as follows.
Request from phone to PMD: XX 00 02
Response from PMD to phone: XX XX XX <6 bytes containing information>In Ghidra, we could see that the code that handles this request was in the following form.
unsigned char response[9];
switch (request[0]) {
case 0xXX:
response[2] = request[1];
if (response[2] == 0) {
// normal processing that initializes response[0] through response[9]
} else {
response[0] = 0xXX;
response[1] = 0x0A;
}
response_size = 9;
send_response(response, response_size);
break;This code shows that when request[1] is 0, the MEDICAL DEVICE initializes 9 bytes of memory and responds with them. However, when request[1] is nonzero, the device responds with 9 bytes of memory, but only initializes the first 3 bytes.
This showed us that, by sending the command XX 01 02, it was possible to read 6 bytes of uninitialized stack memory.
Testing
To confirm the existence of this vulnerability, we tested it by patching OSS MEDICAL APP so that it would immediately send XX 01 02 instead of going through the normal authentication procedure. Running this, we got the following output.
D Sending auth command:
0x00000000 XX 01 02 ...
D Received Authentication 2 indication bytes: XXXXXXXXXXXXXXXXXXRunning this repeatedly yielded various responses like:
- XXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXX
- XXXXXXXXXXXXXXXXXX
Although it is tempting to believe this test demonstrates that the MEDICAL DEVICE’s response contains uninitialized stack memory in its last 6 bytes, it is weak evidence at best, because any sequence of 6 bytes could be interpreted as uninitialized memory.
Verification
To further confirm the existence of this vulnerability, we initialized the memory to a known value at the beginning of the function, and then verified that that known value was what the PMD responded with.
GDB Breakpoint
Our first approach to doing this was to create a breakpoint in GDB that would initialize response before continuing, however this changed the timing of the function call such that the PMD would not respond to the phone, or would take too long to respond.
D Sending auth command:
0x00000000 XX 01 02 ...
D Connection Disconnected/Failed: com.polidea.rxandroidble2.exceptions.BleDisconnectedException: Disconnected from MAC='XX:XX:XX:XX:XX:XX' with status 8 (GATT_INSUF_AUTHORIZATION or GATT_CONN_TIMEOUT)
D Bluetooth connection: DisconnectingBinary Patching
Our second approach was to patch the firmware such that it initializes response at the beginning of the function. The start of the function contained a call to memset as follows. (other_buffer is a buffer used in other parts of the function that don’t affect the part we are looking at.)
| ARM Assembly | Equivalent C |
; #0x24 is the offset of other_buffer ; on the stack add.w r9,sp,#0x24 movs r2,#17 movs r1,#0x0 mov r0,r9 bl memset | memset(other_buffer, 0, 17); |
Because other_buffer is not used by the code path we are looking at, we were free to patch this code to make it into the following.
| ARM Assembly | Equivalent C |
; #0x18 is the offset of response ; on the stack add.w r9,sp,#0x18 movs r2,#9 movs r1,#0xD4 mov r0,r9 bl memset | memset(response, 0xD4, 9); |
If the firmware did fail to initialize the bytes it sent out, we would expect to receive XX0A01D4D4D4D4D4D4, because the response buffer was filled with D4s before the rest of the function ran. If the firmware was initializing the bytes it sent out, we would expect to receive a different response, because the firmware probably wouldn’t initialize the buffer to contain only D4 bytes.
After applying this patch and running the exploit using OSS MEDICAL APP, the OSS MEDICAL APP logs showed us the response as follows.
D Sending auth command:
0x00000000 XX 01 02 ...
D Received Authentication 2 indication bytes: XX0A01D4D4D4D4D4D4By the aforementioned reasoning, this proves that the unpatched firmware is not initializing the final 6 bytes it sends out in response to the XX 01 02 command.
Requirements
This attack does not require the attacker to know the MEDICAL DEVICE’s pairing code, because the XX command that the attack utilizes is part of the authentication sequence, and can be sent before the phone has proven it knows the pairing code.
Additionally this attack works even if the MEDICAL DEVICE is already paired with another phone. The new phone is able to send the MEDICAL DEVICE authentication commands because, if it couldn’t, then the user would never be able to switch their MEDICAL DEVICE to a new phone.
What do the uninitialized bytes contain?
Next we wanted to see what information the uninitialized bytes of memory contain, or what information we could get put into those bytes, for example by sending other Bluetooth commands beforehand to influence the bytes on the stack.
GDB Breakpoints and Watchpoints
By repeatedly running the code with breakpoints in GDB, we could see that the response buffer was consistently stored at address 0xXXXXXXXX. We created a watchpoint (whereas a breakpoint breaks when an instruction is executed, a watchpoint breaks when a memory address is read from or written to) at this address, to see the places in the program where the bytes at 0xXXXXXXXX are updated. We additionally created a breakpoint at the send_response function that sends the Bluetooth response.
With this watchpoint and breakpoint set, we saw output as follows.
Thread 2 hit Hardware watchpoint 1: (char[9])*0xXXXXXXXX
Old value = " \377\377\377\377\377\377\377\377"
New value = " {\377\377\377\377\377\377\377"
Thread 2 hit Hardware watchpoint 1: (char[9])*0xXXXXXXXX
Old value = " {\377\377\377\377\377\377\377"
New value = " {G\377\377\377\377\377\377"
Thread 2 hit Hardware watchpoint 1: (char[9])*0xXXXXXXXX
Old value = " {G\377\377\377\377\377\377"
New value = " {GI\377\377\377\377\377"
Value of buffer to be sent out over Bluetooth:
0xXXXXXXXX: 0xXX 0xXX 0xXX 0xXX 0xXX 0xXX 0xXX 0xXX
0xYYYYYYYY: 0xXXThis example shows the buffer initially filled with 20 FF FF FF FF FF FF FF, and then the new characters {, G, and I, are added to it one by one. Confusingly, however, when the Bluetooth sending function was later called, the value in the buffer had changed, even though the watchpoint monitoring that region of memory wasn’t triggered. This suggests that the watchpoint failed to break when the memory was written to even though it should have.
We tried a variety of things to solve this problem—for example forcing the use of software watchpoints with set can-use-hw-watchpoints 0 —however we didn’t solve this problem.
Note About the Above GDB Output
In the above GDB output, the printf-like function in the code is placing the string ” {GI” into the area of memory that can be read by this uninitialized stack read exploit. This string is printed by the MEDICAL DEVICE as part of what it logs when the device starts up. We recorded this log output with GDB, as described in GDB Printing Hack, and an excerpt of it is as follows.
$1 = 0x2000a078 "\023\030\n--- SYSTEM START ---\017\n"
$2 = 0x20009e38 "\026\030UICR[130] 0xffffffff -> 0x00000000\017\n"
$3 = 0x20009e38 "\026\030UICR[131] 0xffffffff -> 0x00000000\017\n"
$4 = 0x2000a078 "Git Options : {GIT_DESCR_STR,XXXX} {GIT_COMMIT_HASH,XXXX} {GIT_AUTHOR,XXXX} {GIT_DATE,XXXX}\n"
...
$36 = 0x2000a058 "\023\030BLE starting with offset=-1\017\n"
$37 = 0x2000a028 "\022\030Pairing Code is: 8955\n\017\n"Depending on which portion of log line #4 the “ {GI” string corresponds to, it is conceivable that the portion of log line #37 with the pairing could also be placed into the portion of stack memory that can be read using the uninitialized stack read vulnerability. In this case, it could be possible for an attacker to read the pairing code from a MEDICAL DEVICE using the following steps:
- Reset the MEDICAL DEVICE using another vulnerability, or by using a magnet (there are magnets within the applicator of the MEDICAL DEVICE that induce a state change when placed near the MEDICAL DEVICE, and may reset the device entirely).
- Exploit this uninitialized stack read vulnerability to read some or all digits of the pairing code from the log lines printed as the MEDICAL DEVICE starts back up.
For numerous reasons, it seems unlikely that this attack could be successfully executed:
- The aforementioned magnet trick must cause the device to relog the pairing code. If, for instance, it merely triggers an interrupt as opposed to a full reset, it may not cause the log line with the pairing code to be reprinted.
- The printf call that prints the log line with the pairing code must place its argument on the part of the stack that can be read by this vulnerability. Our analysis shows that at least one log line is placed on this part of the stack, but we don’t know if the log line with the pairing code is.
- Between the time the pairing code is placed on the stack and the time the stack region is read via Bluetooth, the portion of the stack with the pairing code must not be overwritten. A part of the Bluetooth-handling code or a part of the logging-related code could, for instance, overwrite it between these two events. Furthermore, if there is a gap between the time the pairing code is logged and the time the device’s Bluetooth subsystem is initialized, unrelated code running between the two events could overwrite this portion of the stack.
We did not demonstrate or investigate this particular attack any further.
Ghidra Script to Calculate Stack Frames
Another approach we used to see what information was being placed into the uninitialized stack bytes that were sent out was seeing which functions in the program had these response buffer bytes within their stack frame.
The following Ghidra script generates a GDB script that breaks on every function known to Ghidra, and prints if the response is within that function’s stack frame.
# ListAllCalls.py
fm = currentProgram.getFunctionManager()
funcs = fm.getFunctions(True)
script = ""
for func in funcs:
frameSize = func.getStackFrame().getFrameSize()
code = """
break *0x{}
commands
silent
set $target_start = 0xXXXXXXXX
set $target_end = $target_start + 6
set $frame_end = $sp
set $frame_start = $frame_end - {}
if ($frame_start <= $target_end && $frame_end >= $target_start)
printf "Found a frame that overlaps with the target range:\\n"
print/x $frame_start
print/x $frame_end
where
end
continue
end
""".format(func.getEntryPoint(), frameSize)
script += code
with open("/.../script.gdb", "w") as file:
file.write(script)Therefore, the GDB script generated by this Ghidra script will narrow down the search for functions that could be modifying the response buffer, by excluding functions that don’t have the location of the response bytes within their stack frame, and therefore are unlikely to directly influence those bytes.
The generated GDB script slowed down execution of the MEDICAL DEVICE so much that, when we ran it, the MEDICAL DEVICE never ended up calling the Bluetooth sending function of interest. Therefore, although this method showed us some functions that modified the buffer of interest, it did not show us functions that modify the buffer immediately before the Bluetooth sending function ran, and therefore was not very helpful.
Conclusion
This vulnerability allows an attacker to read 6 bytes of uninitialized memory from the stack of the MEDICAL DEVICE. It does not require the attacker to know the pairing code for the device, and it works even if the patient’s phone is already connected to the PMD. Although we didn’t deduce with certainty what information is or could be compromised, this vulnerability still undermines the confidentiality guarantees of the MEDICAL DEVICE, because any such guarantees are likely predicated on the assumption that an attacker cannot read uninitialized memory from the device.







