Now Reading
Compromising Garmin’s Sport Watches: A Deep Dive into GarminOS and its MonkeyC Digital Machine

Compromising Garmin’s Sport Watches: A Deep Dive into GarminOS and its MonkeyC Digital Machine

2023-04-22 07:39:56

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.


As a teaser, beneath is the listing of vulnerabilities that I discovered throughout my undertaking and disclosed to Garmin:




No CVE requested


TVM doesn’t be certain that toString returns a String object

GRMN-02 CVE-2023-23301 1.0.0

Out-of-bound learn when loading string sources


No CVE requested


Inconsistent measurement when loading string sources

GRMN-04 CVE-2023-23298 2.3.0

Integer overflows in BufferedBitmap initialization

GRMN-05 CVE-2023-23304 2.3.0

SensorHistory permission bypass

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 Toybox.GenericChannel.setDeviceConfig

GRMN-08 CVE-2023-23303 3.2.0

Buffer overflows in Toybox.Ant.GenericChannel.enableEncryption

GRMN-09 CVE-2023-23306 2.2.0

Relative out-of-bound write in Toybox.Ant.BurstPayload

GRMN-10 CVE-2023-23300 3.0.0

Buffer overflows in Toybox.Cryptography.Cipher.initialize


Similar as GRMN-09


Kind confusion in Toybox.Ant.BurstPayload


No CVE requested


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.


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:

Binwalk entropy evaluation of the extracted FW_ALL_BIN file.

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
  • TVM converts these objects right into a 5-byte construction that’s pushed onto the stack:
    • The primary byte represents the information kind (0x01 for int, 0x02 for float, 0x05 for Array, 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.)
  • 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) or getm (to resolve modules when utilizing the import or utilizing 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.

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:

doc: A bit
  - id: section_type
    kind: u4
  - id: size
    kind: u4
  - id: information
    measurement: size
      switch-on: section_type
      # [...]
      section_magic::section_magic_head: section_head
      # [...]
    # [...]
    0xd000d000: section_magic_head
    # [...]



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:

    doc: Developer signature block
      - 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.


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.

String definition inside a PRG file.

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. between 0x00000000 and 0x10000000 excluded), we’re pointing contained in the PRG information part
  • MSB 0x10 (i.e. between 0x10000000 and 0x20000000 excluded), we’re pointing contained in the PRG code part
  • MSB 0x20 (i.e. between 0x20000000 and 0x30000000 excluded), we’re pointing contained in the API information part (saved within the firmware)
  • MSB 0x30 (i.e. between 0x30000000 and 0x40000000 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).

Putting a string definition close to the tip of a bit.

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.

API information part embedded within the firmware.

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:

Class definition with a local perform within the second discipline definition.

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 ID 0x800003
  • The primary discipline definition is a technique (kind 0x6), whose image is 0x800018 and worth is 0x300055D9
  • The second discipline definition can be a way (kind 0x6), whose image is 0x800446 and worth is 0x4002015F

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.

Record of native capabilities renamed with their symbols.

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 specify AES128 or AES256.
  • mode, which is an enum to specify ECB or CBC.
  • key, which is a ByteArray of the key key.
  • iv, which is a ByteArray of the initialization vector.

This initialize technique is applied natively within the firmware:

See Also

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(
var ivBytes = StringUtil.convertEncodedString(

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() {
        self.measurement = 0xdeadbeef;
// [...]

var burst = new MyBurstPayload();

var information = new[8];
for (var j = 0; j < 8; j++) {
    information[j] = 0x44;


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() {
        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,

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.


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:

Permissions part in PRG file.

These permissions are then listed on the Join IQ retailer for every utility. For instance, the Spotify CIQ app lists the next permission:

Permissions of the Spotify CIQ app.

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 ANDed 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.

Class definition in our PRG file.

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:

Up to date class definition with its technique pointing to a local perform.

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.


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

Source Link

What's Your Reaction?
In Love
Not Sure
View Comments (0)

Leave a Reply

Your email address will not be published.

2022 Blinking Robots.
WordPress by Doejo

Scroll To Top