inVariant18/ry_upgrade
GitHub: inVariant18/ry_upgrade
对RongYuan RY5088键盘固件升级工具进行逆向分析,揭示了x1文件的包装去除和deflate解压流程,并提供了提取脚本。
Stars: 0 | Forks: 0
# 基于 RY5088 Monsgeek M1V5 TMR (v408) ry_upgrade.exe
- 文档中使用的 `ry_upgrade.exe` 来自 Monsgeek M1V5 TMR `ID_2949_RY5088_AKKO_M1V5 TMR_RY1033_ARGB_KB_V408`
- 来自 Core68HE 的 `ry_upgrade.exe` 使用完全相同的逻辑。
- 用于测试和文档的 `x1` 来自 Core68HE `ID_3513_RY5088_ML_Core68HE_1M_8K_ARGB_RY1049ES_KB_V504L`
## 摘要
`x1` 文件不是直接的最终固件镜像。
相反,升级程序执行以下步骤:
1. 从磁盘加载 `x1` 文件。
2. 拒绝空文件或过小的文件。
3. 通过以下方式重建新流:
- 保留字节 `0`
- 跳过字节 `1..0xCA`
- 从 `0xCB` 开始复制字节
4. 将重建的流作为原始 deflate 解压。
5. 返回完全解码的数据。
6. 实际刷入的固件是该解码数据中的一个子区域。
对于本工作期间分析的样本,解码后的容器布局如下:
| 区域 | 偏移量 | 大小 | 含义 |
|---|---|---|
| 前缀 | `0x0000` | `0x5000` (`20480`) | 独立的启动/IAP 风格镜像加上填充(引导加载程序)|
| 固件有效载荷 | `0x5000` | `114420` | 与使用 [DUMMY USB 方法](https://github.com/echtzeit-solutions/monsgeek-akko-linux/blob/f16083ae12550e64ddbd55689ac38a92da3d9e04/scripts/uhid_dummy_device.py) 捕获的固件完全匹配 |
| 后缀 | `0x20EF4` | `0x2BC` (`700`) | 结构化描述符/配置类数据 |
## 主要函数
这些是新版 `ry_upgrade.exe` 构建中的重要函数。
| 地址 | 名称(重命名后)| 角色 |
|---|---|---|
| `0x005f2b50` | `FindUpgradeChannelAndLoad` | 选择升级通道,加载 `x1`,并分派到解码器 |
| `0x005f1f90` | `RemoveWrapper` | 读取文件,移除自定义包装器,并准备包装流 |
| `0x0061ba00` | `InflateFUN` | 流式 inflate 驱动程序,用于扩展重建的流 |
| `0x0066fd90` | `FUN_0066fd90` | 低级别 deflate/inflate 步骤;引用 `corrupt deflate stream` |
| `0x00b73200` | `FUN_00b73200` | `RemoveWrapper` 使用的文件读取辅助函数 |
| `0x005ec920` | `FUN_005ec920` | 选择通道文件前使用的资源路径构建器 |
## 在 Ghidra 中查找解码器
新版构建没有暴露到合并的 `x1x2` 字符串的普通自动代码交叉引用,因此分阶段重建了解码器的路径。
### 阶段 1:查找合并的通道字符串
重要的字符串是:
- 地址:`0x015c1b88`
- 值:
`Unable to find the upgrade channel for the current devicex4x5x6x7x3RFx1x2`
这很有用,因为它包含 `x1x2` 标记,并标识了选择更新二进制文件的函数。
### 阶段 2:手动恢复代码引用
此构建中没有自动交叉引用,因此直接搜索了字符串地址字节:
- `0x015c1b88` 的小端字节:
`88 1B 5C 01`
- 匹配代码位置:
`0x005f2baf`
从该指令向上检查,显示在 `0x005f2b50` 处有正常的函数序言,因此手动在那里创建了一个函数并重命名:
- `0x005f2b50` -> `FindUpgradeChannelAndLoad`
### 阶段 3:跟随调用到解码器
反编译 `FindUpgradeChannelAndLoad` 显示了普通 USB 路径的相关分支:
- 它选择 `"x1x2"`
- 加载关联的文件内容
- 调用:
`0x005f1f90`
该函数后来被重命名:
- `0x005f1f90` -> `RemoveWrapper`
### 阶段 4:跟随 inflate 路径
在 `RemoveWrapper` 中,重建的流被传递到:
- `0x0061ba00` -> `InflateFUN`
在 `InflateFUN` 中,低级别解压步骤是:
- `0x0066fd90` -> `FUN_0066fd90`
该低级函数引用了字符串:
- `0x015c7280` -> `corrupt deflate stream`
这确认了路径是基于原始 deflate 的。
## `FindUpgradeChannelAndLoad` 在 `0x005f2b50`
此函数是更新包的外部分派器。
### 它的功能
- 构建或解析到通道资源集的路径
- 根据设备类型选择正确的更新通道
- 使用如下通道名称:
- `"x1x2"`
- `"x2"`
- `"x3RFx1x2"`
- `"x4x5x6x7x3RFx1x2"`
- 以及相关变体
- 加载所选文件内容
- 将加载的缓冲区传递给 `RemoveWrapper`
### 为什么它重要
这个函数告诉我们:
- 升级程序在普通路径中使用 `x1x2`
- `x1` 最初不是单独处理的
- 解码器入口点是 `RemoveWrapper`
### 重要证据
在反编译中,`"x1x2"` 分支通过调用结束:
- `RemoveWrapper(&sStack_6c);`
这是进入包装器移除逻辑的交接。
## `RemoveWrapper` 在 `0x005f1f90`
这是整个 `x1` 解码链中最重要的函数。
### 它的功能,按步骤进行
1. 从磁盘读取所选的 `x1` 文件。
2. 处理文件读取错误。
3. 拒绝空文件。
4. 拒绝非常小的文件。
5. 为重建流分配新的输出向量。
6. 将原始文件的第一个字节复制到重建的向量中。
7. 从原始文件跳过 `0xCB` 字节。
8. 复制跳过后的文件剩余部分。
9. 将重建的向量复制到稳定的自有缓冲区。
10. 分配 inflate 暂存区/状态。
11. 调用 deflate 膨胀器。
12. 通过 `outDecodedResult` 返回解码后的数据。
13. 释放临时资源。
### 按分支的详细行为
#### 文件读取
文件读取辅助函数在此处调用:
- 指令:`0x005f1ffe`
- 辅助函数:`FUN_00b73200`
该辅助函数填充一个结果类结构,其中编码了:
- 成功或失败
- 加载的文件指针
- 加载的文件长度
#### 错误路径:文件读取失败
如果文件辅助函数返回带有错误标记的对象,`RemoveWrapper`:
- 构造一个错误对象
- 将其存储在输出结果中
- 释放临时对象
- 提前返回
#### 空文件路径
如果长度为零,`RemoveWrapper` 创建一个错误状态,指示当前升级文件为空。
#### 小文件路径
如果加载的文件小于 `0x194`,`RemoveWrapper` 也会返回错误。
这是一个保护措施,防止明显无效的输入到达 inflate 代码。
### 包装器移除核心
包装器移除块从以下位置开始:
- `0x005f2310` 到 `0x005f235e`
关键逻辑是:
1. 分配一个新向量。
2. 追加原始文件的第一个字节。
3. 计算:
`loaded_len - 0xCB`
4. 从以下位置复制:
`loaded_ptr + 0xCB`
5. 结果包装长度变为:
`1 + (loaded_len - 0xCB)`,等于 `loaded_len - 0xCA`
在以下位置添加了反编译器注释:
- `0x005f2346`
### 伪代码中的等效逻辑
```
rebuilt[0] = input[0];
memcpy(rebuilt + 1, input + 0xCB, input_len - 0xCB);
wrapped_len = input_len - 0xCA;
```
### 简化的 Python 等效代码
```
wrapped = data[:1] + data[0xCB:]
```
### 为什么这证明了自定义包装器的存在
因为升级程序不将原始文件直接输入到膨胀器中。
相反,它明确地:
-保留字节 `0`
- 丢弃字节 `1..0xCA`
- 保留其余部分
这意味着 `x1` 包含一个自定义前端包装器或头块,实际的压缩流不使用它。
### 重建的包装流所有权
重建包装向量后,函数将其复制到稳定的自有分配中:
- 这是稍后传递给 inflate 逻辑的缓冲区
- 这是脚本保存为 `x1_wrapped_deflate.bin` 的内容
### Inflate 调用
构建包装流后,`RemoveWrapper` 分配暂存缓冲区并调用:
- `0x0061ba00` 处的 `InflateFUN`
在以下位置添加了反编译器注释:
- `0x005f24ce`
## `InflateFUN` 在 `0x0061ba00`
此函数是流式 inflate 驱动程序。
### 它的功能
- 将包装流作为输入
- 根据需要增长输出缓冲区
- 重复调用低级解压例程
- 累积解码输出
- 返回状态和输出缓冲区信息
### 为什么它重要
这是重建流变成完全解码容器的地方。
它似乎不执行加密解密。
相反,它的行为像一个普通的流式解压器。
## `FUN_0066fd90` 在 `0x0066fd90`
这是由 `InflateFUN` 调用的低级 deflate 步骤。
### 重要证据
该函数引用了:
- `corrupt deflate stream`
引用链是:
- `0x015c7280` 处的字符串
- `FUN_0066fd90` 内部 `0x0066fede` 处的交叉引用
### 这意味着什么
这确认了升级程序正在解压 deflate 压缩数据,而不是在此路径中应用 AES 或其他分组密码。
## 精确解码规则
升级程序实现的通用解码规则是:
```
wrapped = x1[:1] + x1[0xCB:]
inflated = raw_deflate_inflate(wrapped)
```
## 提取脚本
本工作期间创建的辅助脚本是:
- [extract_x1.py](extract_x1.py)
### 它写入的内容
默认情况下,脚本写入:
- `x1_wrapped_deflate.bin`
- `x1_inflated.bin`
- `x1_assumed_firmware.bin`
### 通用解码
这部分是通用的,与程序逻辑匹配:
```
wrapped = data[:1] + data[0xCB:]
inflated = zlib.decompressobj(-15).decompress(wrapped)
```
### 默认假设的切片
对于分析的样本,脚本还使用以下方式导出假设的固件切片:
- 前缀:`0x5000`
- 后缀:`0x2BC`
即:
```
firmware = inflated[0x5000 : len(inflated) - 0x2BC]
```
重要注意事项:
- 包装器移除和 inflate 逻辑被强烈确立为通用
- 最终的 `0x5000` / `0x2BC` 切片规则在分析的样本中得到验证,但尚未保证适用于所有键盘型号
## 样本产物大小
这些值来自分析的 `x1` 样本,来自 `ID_3513_RY5088_ML_Core68HE_1M_8K_ARGB_RY1049ES_KB_V504L`
| 产物 | 大小 |
|---|---:|
| 原始 `x1` | `55863` |
| 重建的包装流 | `55661` |
| 解压的解码容器 | `135600` |
| 捕获的固件 (`firmware.bin`) | `114420` |
| 前缀区域 | `20480` |
| 后缀区域 | `700` |
## 最终固件是如何被验证的
精确的刷入固件不是猜测的。
它是通过将解码输出与捕获的已知良好固件镜像进行比较来验证的。
### 结果
捕获的固件与此精确切片匹配:
```
x1_inflated.bin[0x5000 : 0x5000 + 114420]
```
该匹配是字节对字节的精确匹配。
## 前缀和后缀是什么样的
两个区域也都进行了分析。
### 前缀:前 `0x5000` 字节
观察到的特征:
- 以有效的 Cortex-M 向量表开头
- 初始堆栈指针看起来像:
`0x20004EB0`
- 复位向量编码了一个 ARM Thumb 条目,接近:
`0x080002C1`
- 包含字符串:
- `" HID IAP Config"` 在 `0x000010fb`
- `" HID IAP Interface"` 在 `0x0000113b`
- 以靠近 `0x5000` 边界的大量 `FF` 填充区域结束
解释:
- 这非常类似于引导加载程序或 IAP 相关的固件区域
- 它似乎是一个填充到整齐边界的独立 ARM 镜像
### 后缀:最后 `0x2BC` 字节
观察到的特征:
- 不被识别为代码
- 没有有意义的代码交叉引用指向其开头
- 包含重复的 HID 风格项字节,例如:
- `05`
- `09`
- `19`
- `29`
- `75`
- `95`
- `81`
- `91`
- `C0`
- 包含 `06 FF FF`,这是 HID 报告中经典的供应商定义用法页面标记符
解释:
- 这看起来像结构化描述符/配置数据
- 它可能是靠近主固件区域末尾开始的描述符表的延续
- 它看起来不像加密或随机的尾部数据
## 尾部继续结构化数据的证据
模式搜索显示 HID 风格描述符序列在固件/尾部分割之前开始,并继续到最后的 `700` 字节。
这意味着尾部的 `0x2BC` 区域可能不是一个完全独立的 blob。
它更可能是解码容器内较大的描述符/配置结构的尾部。
## 什么是强烈验证的 vs 什么是仍然假设的
### 强烈验证的
- `x1` 通过 `RemoveWrapper` 加载和处理
- 包装器移除规则是:
`byte0 + bytes[0xCB:]`
- 重建的流作为原始 deflate 膨胀
- 该路径使用 deflate,而不是 AES,在分析的代码路径中
- 解码结果是一个较大的容器,而不仅仅是最终固件
- 对于分析的样本,真正的固件从 `0x5000` 开始,与 `firmware.bin` 匹配
### 仍然是样本特定的或尚未通用的
- 所有键盘型号是否使用相同的 `0x5000` 前缀长度
- 所有键盘型号是否使用相同的 `0x2BC` 后缀长度
- 每个 `x1` 是否包含相同风格的前缀和尾部布局
## 参考资料
- https://github.com/echtzeit-solutions/monsgeek-akko-linux/
- Codex 编写文档和 python 脚本
**免责声明:本研究仅用于教育目的**
标签:DAST, deflate解压, IoT安全, Monsgeek, RY5088, USB分析, 二进制分析, 云安全运维, 云资产清单, 固件分析, 固件安全, 固件提取, 固件逆向, 嵌入式安全, 恶意软件分析, 硬件安全, 系统运维工具, 逆向工具, 逆向工程, 键盘固件