Liberating glucose data from the Freestyle Libre 3

The Abbott Freestyle Libre 3 v3.4.2 iOS and Android apps do not provide a way to export blood glucose data without syncing to a cloud account. The data is stored on device in an encrypted RealmDB with a wrapped encryption key also stored on the device. Frida is used to hook the Android APK and unwrap the key to decrypt the RealmDB.


Background

The Freestyle Libre 3 is a Continuous Glucose Monitor (CGM) produced by Abbott. The sensor records glucose readings every minute from interstitial fluid for 14-days before being replaced with a new one.

Conceptual view of how a modern Continuous Glucose Monitor (CGM) works

The actual sensor is a small channel filled with glucose oxidase which releases electrons when it interacts with glucose. The probe of the sensor has a few small wells to collect interstitial fluid which is then routed over the glucose oxidase pad.

Sensor probe showing wells and glucose oxidase pad (gold region)

The first iteration of the sensor had a small community of enthusiasts who thoroughly reversed engineered the device to use it as a wireless ADC after the 14-day period. Unfortunately, the 3rd generation of the sensor doesn't seem to have a teardown outside of the blurry images in the FCC filing.

An encryption key is generated and shared with the Libre 3 via NFC to "initialize" the sensor. From then on, glucose readings are communicated via BLE to Android and iOS apps. The glucose readings are stored in an encrypted RealmDB which is then synced to the cloud. The apps display the glucose readings on a graph.  A CSV of glucose data can be downloaded from the LibreView web interface by agreeing to the Terms & Conditions of a LibreView account and living in a geo-fenced region.

Data flow Sketch

However, the graph is not zoomable and the timeseries data cannot be exported without using the cloud sync. The motivation behind these terrible product decisions is unknown. It is extremely useful to compare blood glucose readings across various confounding variables (heart rate, temperature, food intake, etc.).

Freestyle Libre 3 Screenshot

Let's get those blood glucose readings out of this horrendous UI!

Prior Work

xDrip+ is an Android app to collect health data from a variety of sources but does not interface directly with the Libre 3. A variety of methods exist to migrate data between the Libre 3 app and xDrip but many of them require a rooted device or an active internet connection. Juggulco is an "offline" non-root method but requires a LibreView account to be associated with the sensors. The majority of the methods do not disclose the actual mechanism behind the data bridge and some require sideloading apks of unknown origin.

The BLE data is encrypted but the DiaBLE and Juggluco projects seems to have made some progress on decrypting the readings. However, Abbott will swing the DMCA hammer if too much is published despite the reverse engineering being done to "achieve interoperability" of the CGM user's own glucose data.

To date, there is nothing published for accessing the data for iOS users.

iOS

Since a feature of the app is to see non-interactive plots of historical glucose data, it seemed reasonable that there must be a local database of glucose readings stored on the device filesystem. The idevicebackup2 utility from libimobiledevice is a straightforward way to access the iOS filesystem on a non-jailbroken device, albeit a space consuming one since the entire phone is backed up.

$ mkdir iPhone-backup 
$ idevicebackup2 backup iPhone-backup

The iOS device backup is a collection of files that are enumerated in the SQLite Manifest.db  located inside iPhone-backup.

$ sqlitebrowser Manifest.db

As expected, there is a database associated with the Libre 3 app on the filesystem! Realm is a "fast, scalable alternative to SQLite with mobile to cloud data sync that makes building real-time, reactive mobile apps easy." Additionally there are "elog" files (which are likely "encrypted" logs).

trident.realm database inside the iOS backup

Unfortunately, trying to open trident.realm with Realm Studio results in a very disappointing message: "The Realm might be encrypted".

However, another interesting file exists in the backup: com.abbott.libre3.us.plist. It is by default a binary plist but can be converted to XML quickly:

plistutil -i com.abbott.libre3.us.plist > libre.xml.plist

The file contains some interesting but not terribly useful information like the current sensor UUID, the last time the sensor was read, etc. But right at the end of the file there is a juicy field!

        <key>RealmEncryptionKey</key>
        <array>
                <integer>96</integer>
                <integer>10</integer>
				...
                <integer>157</integer>
        </array>

Could this actually be the RealmEncryptionKey? It turns out this array is 92-bytes while a real Realm key is 64-bytes (or 128-characters when hex encoded). The first and last 64 byte windows of the key did not unlock the Realm, so the odds were good the key was wrapped. In theory the wrap/unwrap functions should be obvious from disassembling and decompiling the iOS app.

Searching for the RealmEncryptionKey in ghidra after loading the Libre 3 ipa confirmed that it was indeed a key used for decrypting the Realm database. The key is generated via a call to SecRandomCopyBytes and is unique for each installation. But, the candidate function for unwrapping the key (arguments were the 92-byte RealmEncryptionKey data and 0x5c (92 in decimal)) was full of obtuse references due to Swift/Objective-C calling conventions. Additionally, Ghidra did not fully disassemble the entire function. Hopper teased apart many of the Objective-C and Swift calling conventions but also did not successfully disassemble the function - there is a decent chance obfuscation methods were used for this section of code.

Likely candidate function for unwrapping the 92-byte key
Partial disassembly

Without a jailbroken iOS device, performing dynamic analysis to understand this function was out of the question.

But, with some luck, the Android app may behave in the same way as the iOS app.

Android

Static / jadx

Acquiring the Android apk is straightforward from the usual apk mirrors and decompiling it with jadx is just as easy.

After searching for "Realm" in the jadx decompilation it looks like the Android app behaves in a very similar manner -  AppDetailsForRealmDatabase is loaded from the App's Shared Preferences.

Chasing down the unWrapDBEncryptionKey turns up an interesting development: the Libre 3 developers hid the interesting functionality of the libreSKBCryptoLib class behind a JNI/NDK interface.

Libre3SKBCryptoLib.class

At first this seemed pretty promising - the apk contains a liblibre3extension.so for four different CPU architectures. However, ghidra failed to disassemble the unwrap functions for each of the architectures.

With a failed static analysis across two platforms (and five architectures) it was time to look into dynamic analysis of the key wrapping.

Dynamic / Frida

Getting a rooted Android device running on Linux is very straightforward with waydroid (it also runs on X11).

# waydroid init -f
# waydroid container start
$ waydroid show-full-ui
$ adb devices
List of devices attached
192.168.17.132:5555    device

After acquiring an apk, it can be installed into the waydroid session and launched via adb:

$ adb install libre3.apk
$ adb shell am start -n com.freestylelibre3.app.us/com.adc.trident.app.startup.SplashActivity

Everything looks good until the app refuses to move past the Splash Screen with "Please install this App from Google Play." Somehow the app is detecting the sideload and refusing to run. Fortunately, it loads enough!

Also, since the app has been launched once, the trident.realm has been created within waydroid:

$ adb shell ls /data/user/0/com.freestylelibre3.app.us/files
... trident.realm ...

And lo-and-behold a key for the database stored in XML!. The Android app stores the wrapped key as base64 encoded text while the iOS app does not; the base64 encoding is clear from the jadx decompilation. Note: this key is different than the key used to encrypt the iOS realm database - the keys are unique per installation.

$ adb shell cat /data/user/0/com.freestylelibre3.app.us/shared_prefs/AppDetailsForRealmDatabase.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="DBIdentifier">CoPojFQfHHI+Ls5nwRChIkqZ+bq0aYUL5S7RPSt3jLWfu0e8yCTaqFT7HY+JdE6Qoy+5bWgutGtk&#10;dyqZJFsGtJyhqxVJAqw8jvLQC5xEgltQ75iN+5vRObBLykc=&#10;    </string>
</map>

Using Frida it is possible to call the native library functions with our own arguments. Installation is well-documented.

With the Libre 3 app launched and in the "Please install this App from Google Play." state, we can launch the frida-server running inside waydroid and connect to it:

$ adb root
$ adb shell "/data/local/tmp/frida-server &"
$ frida -U "Libre 3"

Once frida has "hooked" into the Libre 3 app it is possible to call the native functions hidden in liblibre3extension.so.

Frida-> Java.perform(function(){}); // Seems necessary to use Java.use
Frida-> var crypto_lib_def = Java.use("com.adc.trident.app.frameworks.mobileservices.libre3.security.Libre3SKBCryptoLib");
Frida-> var crypto_lib = crypto_lib_def.$new()
Frida-> unwrapped = crypto_lib.unWrapDBEncryptionKey([96, 10, ..., -99])
[
    93,
    -81,
    ...
    -122,
    26
]
Frida -> unwrapped.length
64

Success! A 64-byte Realm encryption key. But first, some information on massaging the data from the plist for the call to succeed.

The unWrapDBEncryptionKey function requires an array of Int8 while the iOS plist has an array of UInt8. The following snippet of python converts a UInt8 iOS wrapped key stored as one array entry per-line in realm.key to Int8 with numpy:

with open('realm.key', 'r') as f:
	data = f.readlines()
u = np.array(data, dtype=np.uint8)
i = u.astype(np.int8)
for b in i:
	print(str(b)+', ', end='')

[96, 10, ..., -99]

The printed array can then be copy/pasted into the Frida call to unWrapDBEncryptionKey.

Similarly, we can use python to convert the Int8 results of unWrapDBEncryptionKey into the 128-character key required by Realm.

import struct
with open ('unwrapped.txt', 'r') as f:
	data = f.readlines()
key = ''.join([struct.pack('b', int(i)).hex() for i in data])
print(key)
5daf8d031405bfeafffc64659bcffb692f186164e3d1899d628ed16de32eaaccb0c66662360d772a92749eeda7eb53fe29096fc4cb34fa2b04eb14f3a7af861a

Note: Since it is 2023, it is worth noting all Python code in this post was generated directly via perplexity.ai from human-language prompts.

And finally, after jumping through many hoops, the Realm database from the iOS installation is revealed!

Several pieces of the puzzle aren't solved despite having the glucose readings:

  • What do unWrapDB and wrapDB actually do? Are they RFC 3394?
  • How are the *.elog files encrypted and what do they contain?

Both answers lie somewhere in (the likely obfuscated) liblibre3extension.so.

Summary

At a high level, the steps to reproduce this are:

  1. Backup iOS device and isolate trident.realm and com.abbott.libre3.us.plist files
  2. Extract the RealmEncryptionKey from the plist file and convert it to a Int8 array
  3. Install waydroid, sideload Libre 3 apk, and install frida server
  4. Launch Libre 3 app and hook Frida
  5. Use Frida to unwrap the Int8 array from Step 2
  6. Use the unwrapped key to unlock trident.realm using Realm Studio

It is important to note that the iOS backup and trident.realm isolation steps need to be performed every time a copy of the data is desired.


Bonus: The FDA 510(k) for the Libre 3 contains a snippet on Cybersecurity:

ADC has provided cybersecurity risk management documentation for the System that includes analysis of confidentiality, integrity, and availability for data, information and software related to the System accordance with FDA Draft Guidance “Content of Premarket Submissions for Management of Cybersecurity in Medical Devices.” For each identified threat and vulnerability risk event scenario, risk assessment of impact to confidentiality integrity, and availability was performed and documented within the cybersecurity risk management documentation. Appropriate risk mitigation controls have been implemented and tested.

It's a shame that the risk assessment was not published. It does not seem like "The RealmDB gets decrypted" was in the assessment considering it contains API keys to third party services.


Was this an interesting read? Send your thanks to Sundar Pichai for refocusing Everyday Robots and enabling a summer sabbatical! If you're interested in discussing employment opportunities reach out to <author>@gmail.com.