关于作者
Tao Sauvage 是 Anvil Secure 的首席安全工程师,拥有超过 8 年的经验。他喜欢在他接触到的任何东西中寻找漏洞,尤其是当它涉及嵌入式系统、逆向工程和代码审计时。
他之前的研究项目涵盖移动操作系统安全,导致 Android 和风电类设备的多个 CVE,并创建了针对 Antaira 系统的概念“蠕虫”poc。
曾任攻击性Web测试框架OWASP OWTF项目核心开发人员,维护CANToolz黑盒CAN总线python框架分析。
开始
我在2022年逆向了我的Garmin Forerunner 245 Music的固件,并在其对Connect IQ应用程序的支持中发现了十几个漏洞。可以利用它们绕过权限并危及手表。我已将各种脚本和概念验证应用程序发布到GitHub 存储库github.com/anvilsecure/garmin-ciq-app-research。与Garmin协调披露,其中一些漏洞自 2015 年以来一直存在,影响了一百多种型号,包括健身手表、户外手持设备和自行车GPS。
为什么选择Garmin的运动手表?
Garmin 是全球健身设备市场的主要参与者。根据Counterpoint Research 的数据,2020年,它在全球智能手表市场排名第二,仅次于苹果。关于他们设备的安全性,我在网上没有找到太多资料。因此,我有兴趣进一步挖掘,因为这个未知领域可能会影响大量最终用户,包括我自己。
2022 年初,我能在网上找到的唯一信息是 Atredis 的以下有趣博客文章:Dionysus Blazakis 的“A Watch、A Virtual Machine 和 Broken Abstractions”https://www.atredis.com/blog/2020/11/4/garmin-forerunner-235-dion-blazakis(2020 年)。它提供了对 Garmin Forerunner 235 如何工作以及如何实施名为 Connect IQ (CIQ) 应用程序的应用程序的深入了解。Blazakis 的博文开启了我的整个旅程,我正在他们的研究之上进行建设。
漏洞列表
作为预告,以下是我在项目期间发现并向 Garmin 披露的漏洞列表:
砧 ID | 漏洞编号 |
CIQ API |
概括 |
GRMN-01 |
没有请求 CVE |
1.0.0 |
TVM 不保证 |
GRMN-02 | CVE-2023-23301 | 1.0.0 |
加载字符串资源时越界读取 |
GRMN-03 |
没有请求 CVE |
1.0.0 |
加载字符串资源时大小不一致 |
GRMN-04 | CVE-2023-23298 | 2.3.0 |
|
GRMN-05 | CVE-2023-23304 | 2.3.0 |
|
GRMN-06 | CVE-2023-23305 | 1.0.0 |
加载字体资源时缓冲区溢出 |
GRMN-07 | CVE-2023-23302 | 1.2.0 |
缓冲区溢出 |
GRMN-08 | CVE-2023-23303 | 3.2.0 |
缓冲区溢出 |
GRMN-09 | CVE-2023-23306 | 2.2.0 |
相对越界写入 |
GRMN-10 | CVE-2023-23300 | 3.0.0 |
缓冲区溢出 |
GRMN-11 |
与 GRMN-09 相同 |
2.2.0 |
输入混淆 |
GRMN-12 |
没有请求 CVE |
1.0.0 |
本机函数不检查参数的数量 |
GRMN-13 | CVE-2023-23299 | 1.0.0 |
通过字段定义操作绕过权限 |
我们与 Garmin 协调了 2022 年和 2023 年的披露(请参阅负责任的披露时间表部分https://www.anvilsecure.com/#responsible-disclosure-timeline)。他们澄清说,自 2015 年 1 月发布的 1.0.0 版本以来就存在多个漏洞。
他们还澄清说,根据 Garmin 指定的Connect IQ 兼容设备列表并在 CIQ API 版本 3.1.x 中修复,这些漏洞影响了一百多台设备。
预研究
CIQ 应用程序在其 Garmin 操作系统(恰如其分地命名为 GarminOS)中实现的虚拟机(在固件中称为 TVM,我将其称为“虚拟机”)内执行。TVM 主要用于稳定性,但它也增加了一个安全层:
-
如果应用程序执行时间过长,VM 会中止它。
-
VM 负责分配和释放内存,以防止内存泄漏。
-
如果应用程序没有正确的权限(例如访问 GPS 位置),VM 会阻止应用程序访问敏感的 API。
Atredis 的博文重点介绍了本地实现的 TVM 操作代码 (opcode) 的安全性。它强调了几个关键问题,恶意汇编代码可以利用这些问题来破坏虚拟化层并在手表上获得本机代码执行,从而实现完全控制。
攻击场景是用户安装恶意 CIQ 应用程序(手动或从Connect IQ Store)。我们可以将其与 Android 应用程序进行比较,用户可以从 Play 商店或通过旁加载在他们的移动设备上安装恶意 APK。
如果您有兴趣,我建议阅读 Atredis 的博客文章。虽然他们在他们的公告中只列出了 Forerunner 235 型号,但我强烈怀疑他们发现的漏洞影响了更广泛的设备。
在我的旅程中,我有兴趣分析 Garmin 应用程序的三个额外方面,这些方面可能代表潜在的攻击向量:
-
GarminOS 如何加载 CIQ 应用程序?
-
Atredis 的博文中简要提到的原生功能有哪些?
-
应用权限是如何实现的?
GarminOS和TVM
GarminOS 是 Garmin 内部开发的完全定制的操作系统,至少可以说,现在并不常见。它实现了线程和内存管理,但没有用户模式与内核模式的概念,也不支持多进程等。它主要是用 C 编写的,在过去的几年里,UI 框架开始转向 C++(基于我通过幸运发现的这个随机 Garmin 论坛消息中链接的播客https://cppcast.com/brad-larson-cpp-watch/)。
他们操作系统的公开文档有限,但我们知道他们的手表使用 ARM Cortex M 系列处理器,这有助于以后进行逆向工程。在这里,我们将对Garmin Forerunner 245 Music 模型https://www.garmin.com/en-US/p/646690进行分析和测试。
有趣的是,Garmin 开发了他们自己的名为MonkeyC的编程语言,用于编写可以在手表上运行的应用程序。他们提供开发人员可以依赖的SDK和API 文档来开发 CIQ 应用程序。
MonkeyC 语言是 Java 和 JavaScript 等语言的混合体。它编译成由 Garmin 的 TVM 解释的字节代码。
下面是一个输出“Hello Monkey C!”的简单 MonkeyC 程序示例。到应用程序的日志文件:
import Toybox.Application as App;
import Toybox.System;
class MyProjectApp extends App.AppBase {
function onStart(state) {
System.println("Hello Monkey C!");
}
}
固件分析
我最初尝试分析手表提示您更新时临时存储在手表上的固件更新。然而,我很快意识到这是一个增量构建,并不包含整个固件。
幸运的是,Garmin在其网站上提供了测试版固件映像,其中包含所有内容。它们被构造为 GCD 文件,这是一种由 Herbert Oppmann 非正式记录的文件格式。
解析 GCD 固件更新后,我提取了FW_ALL_BIN
包含手表原始图像的记录:
然后我可以使用Ghidra直接将固件映像加载为 ARM:LE:32:Cortex ,经过一些试验和错误后使用以下内存映射:
0x3000
您会注意到闪存的起始地址。我提到 beta 固件映像包含所有内容,但它并不准确,因为它们缺少最有可能位于地址0x0
和0x3000
.
我在逆向工程期间收集的各种杂项信息:
-
MonkeyC 有 21 种数据类型:
// MonkeyC data types
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 将这些对象转换为一个 5 字节的结构,然后压入堆栈:
-
第一个字节表示数据类型(
0x01
forint
、0x02
for float、0x05
forArray
、0x09
boolean 等) -
剩余的 4 个字节表示直接值(例如,
0x11223344
对于使用 32 位编码的整数)或指向位于堆上的另一个结构的 ID,用于更复杂的类型(Hash
,Array
,Resource
等) -
TVM 总共支持 53 个操作码(完整列表在这里)
-
包括常见的,例如
add
,sub
,return
,nop
。 -
以及更专业的,例如
newba
(分配ByteArray对象)或(在使用or语句getm
时解析模块)。import
using
-
如前所述,这些操作码以 C 语言的本机代码实现,是 Atredis 研究的重点。
CIQ申请
在编译 CIQ 应用程序时,SDK 会生成一个 PRG 文件(我将其称为“程序”),其中包含代码、数据、签名和权限等多个部分。
PRG 部分使用Type-Length-Value (TLV) 编码https://en.wikipedia.org/wiki/Type%E2%80%93length%E2%80%93value定义,其中:
-
4 字节:段类型,使用特殊值(例如代码段为 0xc0debabe)
-
4字节:段长
-
n 字节:段数据,在段长度中指定
当我需要交互式分析二进制 blob 时,我非常喜欢Kaitai Struct 。我为PRG文件写了一个Kaitai结构,支持反汇编(但不支持资源;我认为我的Kaitai技能不够好)。它可以在我们的GitHub https://github.com/anvilsecure/garmin-ciq-app-research/blob/main/ciq.ksy上找到。
例如,反汇编 TLV 部分可以按如下方式完成:
section:
doc: A section
seq:
id: section_type
type: u4
id: length
type: u4
id: data
size: length
type:
section_type :
cases:
# [...]
section_magic::section_magic_head: section_head
# [...]
enums:
section_magic:
# [...]
0xd000d000: section_magic_head
# [...]
签名
PRG 文件使用 RSA 和带有 SHA1 的 PKCS #1 v1.5 标准进行签名。他们可以持有以下任一签名部分:
-
应用商店签名
-
开发者签名
在第一种情况下,只包含 512 字节的签名。在第二种情况下,包含了 512 字节的签名和公钥。手表上似乎没有拒绝开发者签名应用程序的选项。
在我们的开泰结构中添加对开发者签名的支持很简单:
section_developer_signature_block:
doc: Developer signature block
seq:
id: signature
size: 512
id: modulus
size: 512
id: exponent
type: u4
当编译器创建 PRG 文件时,它首先生成并附加所有节(头、入口点、数据、代码、资源等)。然后计算 RSA 签名并附加签名部分。最后,它附加结束部分,其中包含所有零(魔术值为 0,长度为 0,总共 8 个字节)。
我只是对签名验证过程进行了粗略的审查,只是足以让我可以签署我自己的补丁 PRG 文件 https://github.com/anvilsecure/garmin-ciq-app-research/blob/main/ciqpy/main.py#L90-L93。
如果有人有兴趣进一步了解固件执行的签名验证,请告诉我。我很想组队。你可以在这篇文章的底部找到我的联系方式。
攻击面
由于解析 PRG 文件是在本机代码中执行的,因此它是一个有趣的攻击面:
-
文件格式包含多个偏移量,如果未正确验证,可能会导致整数溢出/下溢。
-
它指定应用程序需要的权限,以及用于验证的签名。
-
它包含链接表和其他用于在执行期间解析符号或处理异常的信息。
-
可以在 PRG 文件中嵌入复杂的数据结构,包括图像、动画和字体等。
幸运的是,Garmin 正确处理了部分长度(据我所知)。这些部分中的其他长度属性通常使用 2 个字节进行编码,但在代码中存储在 4 个字节的整数中,以防止出现许多整数溢出情况。
但仍有许多因素需要检查。让我们回顾一下我在逆向 PRG 加载时发现的几个问题。
资源
MonkeyC 支持多种类型的资源。他们的文档https://developer.garmin.com/connect-iq/core-topics/resources/提到了字符串、位图、字体、JSON 数据和动画。
字符串定义
字符串定义(如下所示)由news
操作码处理。调用时news
,您将符号传递给您的字符串定义,它通常指向您的 PRG 的数据部分。字符串定义以 sentinel value 开头0x1
,后跟使用 2 个字节编码的字符串长度,然后是字符串字节。
Atredis 的CVE-2020-27486公告https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-27486解释说,news
操作码根据字符串定义中指定的长度分配字符串缓冲区,然后继续调用strcpy
以复制字符串字节。这可能导致内存损坏,因为strcpy
不使用指定的长度并且只会在第一个空字节处停止。
查看news
操作码,我确认这是通过使用strncpy
now 修复的。然而,进一步挖掘我注意到另一个问题,尽管影响较小。
加载定义时,TVM 首先将符号解析为它的值,该值代表一个部分中的“物理”偏移量。符号的最高有效字节 (MSB) 指定哪个部分:
-
MSB
0x00
(即介于0x00000000
和0x10000000
排除),我们指向 PRG 数据部分内部 -
MSB
0x10
(即介于0x10000000
和0x20000000
排除),我们指向 PRG 代码部分内部 -
MSB
0x20
(即介于0x20000000
和0x30000000
排除),我们指向 API 数据部分(存储在固件中) -
MSB
0x30
(即介于0x30000000
和0x40000000
排除),我们指向 API 代码部分(也存储在固件中)
然后 TVM 使用较低的 6 个字节作为这些部分中的偏移量。(也有0x40
用于本机函数的 MSB,但我稍后会回来讨论它们。)
通过 API 数据和代码部分,我的意思是固件嵌入了从 MonkeyC 编译的 SDK 的副本。虽然它不像我们要开发的应用程序那样是 PRG 文件,但它们包含相同的数据结构。API 代码部分包含 MonkeyC 字节代码,API 数据部分包含类和字符串定义。
TVM 检查根据符号计算的偏移量是否在预期部分的范围内。例如,如果您的 PRG 数据部分是bytes 并且您指定值为 的0x1000
符号,它将失败,因为它超出了 PRG 数据部分的末尾 ( )。0xdeadbeef
0x00aabbcc
0xaabbcc
0xaabbcc > 0x1000
但是,字符串存在问题。字符串定义指定要读取的数据的长度,并且 TVM 不会检查它是否超出了该部分的末尾。因此可以将字符串定义放置在部分的边界处,具有较大的大小,并且 TVM 将读取超出部分末尾的数据(直到下一个空字节)。
事实上,由于字符串定义的标记值只是0x01
,我们还可以轻松地在 API 数据和代码部分中找到可被视为有效字符串定义的偏移量。所以我们在 PRG 部分中放置无效字符串定义不受限制,我们也可以在 API 部分中找到它们。
字体资源
我分析的固件支持两种类型的字体:非Unicode(哨兵值0xf047
)和Unicode(哨兵值0xf23b
)。编译 PRG 文件时不再支持前者,但处理它们的代码仍然存在于固件中(很可能是出于追溯兼容性的原因)。
不再支持的非 Unicode 格式更短更易于描述:
指数 | 字节大小 | 姓名 |
---|---|---|
0x00 | 4 | 哨兵值 |
0x04 | 4 | 高度 |
0x08 | 4 | 字形计数 |
0x0c | 4 | 分钟 |
0x10 | 2 | 数据大小 |
0x12 | 3 * 字形数 | 字形表缓冲区 |
n | 4 | 字形哨兵 |
n + 4 | 1 * 数据大小 | 额外的数据缓冲区 |
加载字体时,由于第 7 行整数溢出,本机代码错误地计算加载数据所需的缓冲区大小:
e_tvm_error _tvm_app_load_resource(s_tvm_ctx *ctx,int fd,uint app_type,s_tvm_object *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);
}
// [...]
}
可以制作一个会导致越界写入操作的字体标头。例如,选择以下值:
- 字形数:0x4000001A
- 字体数据大小:0x108
计算出的缓冲区大小将为:(0x108 & 0xffff) + 0x4000001A * 4 + 0x34 = 0x1000001a4
. 由于寄存器只能保存 32 位值,因此它被截断为0x1000001a4 & 0xffffffff = 0x1a4
. 然后固件将尝试将0x4000001A
字形复制到字节缓冲区0x1a4
。
解析 Unicode 字体和位图资源时会发现类似的问题。然而,试图覆盖小型嵌入式设备上的大型缓冲区可能很棘手。我决定继续逆向固件以识别可能更容易被利用的漏洞。
本机函数
当从固件中提取 API 数据和代码部分时,我注意到虽然很多功能是在 MonkeyC 中实现的,但其他功能实际上是本地实现的(如前所述,它们的符号以开头)0x40
。
调用方法时,以 开头的符号0x40
被视为回调表中的索引:
// [...]
if ((field_value[0].value & 0xff000000) == 0x40000000) {
// `i * 4` is checked earlier in the function to be within 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);
// [...]
在我的固件中,我注意到 460 个本机函数!这是一个相当大的攻击面,因为其中任何一个中的错误都可能危及操作系统。
关于以 开头的符号需要注意的事项0x40
:
- 它们的第二个 MSB 表示参数的数量
- 剩余的 2 个字节表示回调表中的偏移量
例如,该符号0x40050123
指向一个本机函数(MSB 为0x40
),该函数需要 5 个参数(第二个 MSB 为0x05
)并且其在表中的索引为0x123
。
解析本机函数符号
我想解析那些原生函数的符号来加速逆向。我根据它的0xc1a55def
神奇值定位并提取了 API 数据部分。
然后我解析并搜索了所有以0x40开头的方法。为此,我将我的 Kaitai 结构编译为 Python 以自动化该过程。以下是来自开泰 web IDE 的此类方法的示例:
在上面的截图中,我们发现了以下信息:
-
我们在模块ID的类定义里面
0x800490
,继承自模块ID0x800003
-
第一个字段定义是一个方法(type
0x6
),它的符号是0x800018
,值是0x300055D9
-
第二个字段定义也是一个方法(type
0x6
),其符号为0x800446
,值为0x4002015F
现在,让我们关注第二个字段定义。由于其值的 MSB 为0x40
,因此它是一个本机函数,它有 2 个参数并且位于回调0x15F
表中的偏移量处。
0x800446
我们可以在提供给终端用户的SDK中找到调试符号:
monkeybrains.jar.src$ grep $((16#800446)) ./com/garmin/monkeybrains/api.db
getHeartRateHistory 8389702
getHeartRateHistory
但是根据他们的文档有两个。哪一个?这是我们使用模块 ID 的地方:
monkeybrains.jar.src$ grep $((16#800490)) ./com/garmin/monkeybrains/api.db
Toybox_SensorHistory 8389776
因此,偏移处的本机回调0x15F
是Toybox.SensorHistory.getHeartRateHistory。您可能已经猜到了:父模块 ID0x800003
是Toybox
.
此方法似乎只采用一个参数 ( options
) 但 TVM 是面向对象的,因此在幕后,getHeartRateHistory
确实采用两个参数:this
和options
。(出于好奇,第一个字段定义是<init>
类的方法。)
我们可以为所有本机函数自动执行此过程(Kaitai 到 Python,加上一些额外的 Python 代码来解析调试符号),并使用 Ghidra 中的 Python 脚本 API 重命名这些函数。
现在反转原生函数要容易得多,因为我们已经可以根据官方文档知道它们的参数。
Toybox.Cryptography.Cipher.initialize
缓冲区溢出
查看文档,Toybox.Cryptography.Cipher.initialize方法需要 4 个参数:
-
algorithm
, 这是一个枚举来指定AES128
orAES256
。 -
mode
, 这是一个枚举来指定ECB
orCBC
。 -
key
,这是ByteArray
秘密密钥的一个。 -
iv
,这是一个ByteArray
初始化向量。
此initialize
方法在固件中本地实现:
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 key parameter and store it into `key`.
// [...]
// Anvil: Retrieve the underlying byte array data
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 length
key_data_length = *(ushort *)&bytearray_data->length;
// Anvil: Copy the byte array data to the static buffer
memcpy(static_key_buffer,bytearray_data + 1,(uint)key_data_length);
// [...]
// Anvil: if CIPHER_AES128 then expected size is 16
if (*(int *)(local_78 + 0x18) == 0) {
expected_key_size = 0x10;
}
else {
// Anvil: if CIPHER_AES256, then expected size is 32
if (*(int *)(local_78 + 0x18) == 1) {
expected_key_size = 0x20;
}
// [...]
}
// Anvil: If the key size is unexpected, 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;
// [...]
在上面的代码片段中,本机函数检索关键数据并调用memcpy
将其复制到位于堆栈上的静态缓冲区。复制完成后,它才会检查密钥的大小,如果它有无效值则抛出错误。
然而,此时我们已经破坏了堆栈,包括程序计数器 (PC) 寄存器的值。
同样的逻辑适用于函数后面的初始化向量initialize
,尽管这次缓冲区位于堆上而不是堆栈上:
// [...]
// Anvil: Retrieves the IV byte array data
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->length;
// Anvil: Assigns its length to a structure at offset 0x16
*(short *)(local_78 + 0x16) = (short)iv_length;
// Anvil: Copy the byte array data to the buffer on the heap
memcpy(local_78 + 6,bytearray_data + 1,iv_length & 0xffff);
// [...]
// Anvil: If the IV size is not 16, throw an exception
if (*(short *)(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);
}
// [...]
key
以下 MonkeyC 应用程序可以在复制参数时触发崩溃:
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
相对越界写入
查看文档,该Toybox.Ant.BurstPayload.add
方法只需要一个参数:message
作为数组或字节数组。该方法将消息对象添加到内部缓冲区。它是本地实现的:
e_tvm_error native:Toybox.Ant.BurstPayload.add(s_tvm_ctx *ctx,uint nb_args)
{
// [...]
// Anvil: Retrieves our current BurstPayload instance object
object = (s_tvm_object *)(ctx->frame_ptr + 5);
field_size = 0;
// Anvil: Retrieves its `size` field
eVar1 = tvm_get_field_size_as_int(ctx,object,&field_size);
uVar2 = (uint)eVar1;
if (uVar2 == 0) {
// Anvil: If the `size` field 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 instance's `burstDataBlob` field
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.value,(undefined *)&blob_data), uVar2 == 0)) {
// Anvil: We write our `message` data to the internal buffer.
*(undefined4 *)(blob_data + field_size + 0xc) = payload_data._0_4_;
*(undefined4 *)(blob_data + field_size + 0x10) = payload_data._4_4_;
// [...]
首先突出的是size
现场验证。虽然该函数会检查其值的上限,但不会检查负值。
我们如何控制物体size
的场BurstPayload
?MonkeyC 支持继承,因此我们可以简单地从对象继承并在调用其构造函数后覆盖其值。
例如,以下代码片段在调用其父项的方法后覆盖了该size
字段。调用时,本机函数将尝试写入 8 个字节,从.0xdeadbeef
initialize
add
data
blob_data + 0xdeadbeef + 0xc
class MyBurstPayload extends Ant.BurstPayload {
function initialize() {
Ant.BurstPayload.initialize();
self.size = 0xdeadbeef;
}
}
// [...]
var burst = new MyBurstPayload();
var data = new[8];
for (var j = 0; j < 8; j++) {
data[j] = 0x44;
}
burst.add(data);
Toybox.Ant.BurstPayload
类型混淆
除了size
验证不当之外,代码中还有另一个问题。它假定burstDataPayload
是某种类型的对象(看 的initialize
方法BurstPayload
,它似乎是一个Resource
对象)。
但是,使用我们用于重新定义size
字段的相同技术,我们可以将burstDataPayload
字段更改为另一种类型的对象。
例如,以下代码将burstDataBlob
字段更改为Array
对象:
class MyBurstPayload extends Ant.BurstPayload {
function initialize() {
Ant.BurstPayload.initialize();
self.size = 0;
// Both objects are INT
self.burstDataBlob = [0, 0];
}
}
// [...]
var burst = new MyBurstPayload();
var data = [
// First object, changing from INT to FLOAT
0x02, 0x42, 0x42, 0x43, 0x43,
// Second object, changing from INT to FLOAT
0x02, 0x45, 0x45,
];
burst.add(data);
调用该函数时add
,本机函数将覆盖数组数据的前 8 个字节。这些字节表示存储的前 2 个对象(第一个对象的 5 个字节和第二个对象的 3 个字节),它们是INT
. 我们用我们自己的 type 对象覆盖它们FLOAT
。
在其他原生函数中也可以看到相同的模式,它们假定对象的字段与 SDK 中定义的相同。他们不考虑那些通过继承修改的情况。
现在一些易受攻击的本机功能需要权限。对于与 相关的漏洞Toybox.Ant.BurstPayload
,我们的 CIQ 应用程序必须将Toybox.Ant
模块添加到其权限列表(与Toybox.Background
模块一起)。
我有兴趣了解固件如何强制执行权限。
权限
模块定义有一个标志,指定它们是否需要使用权限。这个标志是为各种核心模块设置的,例如:
-
Toybox.Ant
用于Ant相关通信 -
Toybox.Positioning
检索 GPS 坐标 -
Toybox.UserProfile
检索与用户相关的信息,例如出生日期、体重等。 -
完整列表在这里(https://developer.garmin.com/connect-iq/core-topics/manifest-and-permissions/)
然后,PRG 文件在其权限部分中包含它需要访问的模块 ID。例如,如果您的应用程序需要访问Toybox.UserProfile
模块,它将0x800012
在其权限部分包含其 ID ( ),如下所示:
然后,这些权限会在每个应用程序的 Connect IQ 商店中列出。例如,Spotify CIQ 应用列出了以下权限:
对应于Toybox.Communications模块。
检查权限
在固件中,我发现了以下检查权限的函数。它的伪代码如下所示:
uint prg_tvm_has_permission(s_tvm_ctx *ctx, int module_id, byte *out_bool) {
// For each module ID in the permissions section
// Is it equal to requested module ID?
// If yes, then we return true as in authorized
// If no, we check the next ID in the section
// No match found, we return false as in unauthorized
}
在此功能中脱颖而出的第一件事是早期处理的以下边缘情况:
// [...]
bVar1 = module_id == module_Toybox_SensorHistory;
*out_bool = 0
if ((bVar1) && (ctx->version < VERSION_2.3.0)) {
*out_bool = 1;
return 0;
}
// [...]
跟踪version
属性,我意识到它来自 PRG 的头部部分中指定的版本。我们可以篡改 head 部分以指定低于 2.3.0 的版本,并自动授予对Toybox.SensorHistory模块的访问权限。该模块提供对心率、海拔、压力、压力水平等信息的访问。
到目前为止,我不确定该prg_tvm_has_permission
函数何时被调用。进一步挖掘,我注意到它被以下操作码引用:
-
getm
解决一个模块 -
getv
从模块中检索属性 -
putv
从模块更新属性
接收prg_tvm_has_permission
正在解析(使用getm
)或在读取/写入属性(使用getv
/ putv
)时引用的模块的模块 ID。
不幸的是,我们无法篡改该模块 ID,因为它是直接从存储在固件中的 SDK 数据部分的类定义中解析出来的。根据测试,尝试从特权模块继承也不会起作用。
类和字段定义
如果您还记得前面在解析符号时突出显示的类定义,它包含高级信息,例如父模块 ID(如果有)和应用程序类型等。它还包含一个字段定义列表,对应于类定义的每个字段。
模块被定义为数据部分中的类定义。对于 SDK 提供的模块和编写 PRG 应用程序时(在后台)创建的模块都是这种情况。
字段定义可以是任何 MonkeyC 类型(如本博文前面所列),最多为类型 15(双精度),这取决于TVM 解析它时类型的AND
编辑方式。0xf
这包括整数、字符串、其他类定义和方法等。例如,它不能是原始模块(类型 17)或系统指针(类型 18)。
在上面显示的类定义中,我们可以看到第一个字段定义是一个方法(类型 6),它的符号是0xD
和它的值0x100000D5
。如果你回忆起之前的字符串定义,你就会明白这0x100000D5
意味着它位于0xD5
PRG 文件代码部分的偏移处。
调用方法时0xD
,TVM 将解析类定义,然后是其字段定义,直到找到该符号值的匹配项。在我们的例子中,它将找到0x100000D5
,将其转换为正确部分(此处为 PRG 代码部分)中的偏移量,并在那里重定向执行。我正在简化,但这就是它的要点。
现在您可能想知道:如果我们将字段定义值更新为指向 SDK 部分内部会怎么样?例如,如果我们要执行以下操作:
在更新的字段定义中,我们更改0x100000D5
为0x40040033
. 如果您还记得的话,这应该表示一个本机函数 ( 0x40
),它采用 4 个参数 ( ) 并且在回调表中0x04
处于偏移量(该值特定于我的固件版本)。这个本机函数实际上是Toybox.Communications.openWebPage,它应该需要权限,因为它在特权模块内。0x33
0x40040033
Toybox.Communications
现在,当 TVM 检查权限时,它最终会检查我们的模块 ID,这意味着检查我们模块的类定义是否需要权限。因为它没有,它会很乐意让你调用方法0xD
,最终调用openWebPage
本机函数。
这可以进一步推广:我们可以在我们的 PRG 文件中嵌入 SDK 的完整副本!我们需要修复各种偏移量并清除权限标志。然后,我们可以使用任何和所有模块,即使权限部分为空。
这有效地完全绕过了 Garmin 的权限检查。
结论
在这篇博文中,我回顾了我在 Garmin Forerunner 245手表上的分析测试步骤。我们专注于可以使用 MonkeyC 开发并在设备上运行的 Garmin 应用程序。
我们分析了 GarminOS TVM 如何运行应用程序,重点关注 PRG 解析、原生功能和权限。我们在旅途中发现了漏洞,这些漏洞允许逃脱 VM 层并危及手表。我们还发现了如何绕过 Garmin 权限并调用任何功能,而不管我们应用程序的权限如何。我已经将各种脚本和概念验证编译到GitHub 存储库中。https://github.com/anvilsecure/garmin-ciq-app-research
2015 年发布的第一版 CIQ API (1.0.0) 中引入了一些漏洞,例如CVE -2023-23299。它们影响了超过一百种 Garmin 设备,包括健身手表、户外手持设备和自行车 GPS。https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-23299
未来的研究思路
Garmin 手表上有很多地方没有被关注(就公共研究显示而言)。我们已经在这篇博文的前面提到了签名验证,但我们还有:
-
手表使用 Ant 和 Ant+ 堆栈与外部传感器(例如心率监测器或跑步吊舱)通信
-
蓝牙低功耗 (BLE) 堆栈,也用于传感器,以及手表与智能手机通信时(例如将数据发送到 Garmin Connect 移动应用程序)
-
某些设备还具有 Wi-Fi 模块
-
USB 堆栈,当将设备连接到计算机以复制文件时
-
设备通过 USB 公开的文件系统
了解协议栈中是否存在错误,例如是否有可能劫持 Ant(或 BLE 或 Wi-Fi)连接并从那里提供恶意数据将是一件很有趣的事情。如果固件还不正确地处理从这些协议接收到的数据。
此外,手表可以显示手机收到的通知。了解这些通知是如何处理的,以及恶意通知是否可以利用手表(例如,显示通知消息时的字符串格式漏洞)可能会很有趣。
我没有涉及的一个方面是每个应用程序存储。应用程序有一个专门的存储空间来保存首选项和其他选项。了解它是如何实现的,以及恶意应用程序是否有可能访问和操纵另一个应用程序存储的数据可能会很有趣。
Atredis 提到尝试修补 QEMU 以运行手表固件。我个人并没有尝试执行动态分析,例如模糊测试,但这很可能有助于揭示更多错误。
我认为有一件事是肯定的:我们只是略读了表面。
负责任的披露时间表
-
2022-07-25:Anvil 通过他们的网络表单向 Garmin 提交了技术报告以及我们的 90 天披露政策。
-
2022-09-11:Garmin 承认漏洞并请求延期至 2022 年 12 月 3 日。我们同意。
-
2022-10-14:Anvil 提交了关于权限绕过的第二份技术报告。
-
2022-11-09:Garmin 表示他们有望在 2022 年 12 月 3 日获得初步调查结果。Garmin 承认权限绕过并请求延期至 2023 年 2 月 28 日。我们同意。
-
2022-12-01:Garmin 表示他们发现了其他受影响的产品,并要求将所有漏洞的新延期至 2023 年 3 月 14 日。
-
2022-12-06:Anvil 同意新的截止日期并要求提供受影响产品的清单。
-
2022-12-13:Garmin 提供受影响设备的列表,由 Connect IQ API 版本识别。
-
2023-01-09:Anvil 请求 CVE ID。
-
2023-01-26:MITER 分配 CVE ID(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 与 Garmin 共享 CVE ID 并询问他们是否计划发布安全公告。
-
2023-02-01:Garmin 表示他们不打算发布列出 CVE 的咨询。
-
2023-03-14:Anvil 询问 Garmin 他们是否已发布受影响设备的新固件映像。
-
2023-03-16:Garmin 表示大部分更新已经发布。他们指定三款设备已延迟,目标是 2023 年 3 月 22 日
原文始发于微信公众号(军机故阁):Garmin的智能运动手表漏洞:深入了解 GarminOS 及其 MonkeyC 虚拟机
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论