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分析, 二进制分析, 云安全运维, 云资产清单, 固件分析, 固件安全, 固件提取, 固件逆向, 嵌入式安全, 恶意软件分析, 硬件安全, 系统运维工具, 逆向工具, 逆向工程, 键盘固件