motebaya/hmsc-re

GitHub: motebaya/hmsc-re

该项目通过逆向工程技术完整还原了 HMSC 2.0.0 Zend 扩展的加密机制,并提供了一个独立的 PHP 解密器将受保护的文件还原为原始源码。

Stars: 0 | Forks: 0

# HMSC 逆向工程与解密器 本项目记录并重现了 [HMSC 2.0.0](https://github.com/EddieKidiw/HMSC) 的解密过程 Zend 扩展(用于 PHP 7.4 NTS)。它可以将受 HMSC 保护的 PHP 文件转换为 原始 PHP 源码,而无需加载 `hmsc.so`。 此项工作旨在对授权检查的文件进行逆向工程、互操作和分析。 ## 使用方法 环境要求: - PHP 7.4 CLI - OpenSSL 扩展 - zlib 扩展 - PCRE 扩展 解密单个文件: ``` php7.4 main.php samples/sample1.php ``` 结果会自动写入: ``` decrypted/deo_sample1.php ``` CLI 会向 STDERR 打印每个解析、完整性、转换、解压缩以及 加密阶段的记录。最终的输出路径会打印到 STDOUT。 运行完整的 fixture 测试: ``` php7.4 tests/run.php ``` ## 项目结构 ``` main.php CLI entry point hmsc_dec.php non-executable compatibility bootstrap src/ Console/VerboseLogger.php diagnostic output Crypto/AesGcmDecryptor.php AES-GCM and inner zlib stage Crypto/ContainerDecoder.php binary format and integrity validation Exception/ project exception type IO/FileProcessor.php input and output handling Model/ parsed wrapper and container value objects Parser/WrapperParser.php protected PHP wrapper extraction Service/HmscDecryptor.php high-level orchestration tests/ dependency-free integration tests samples/ local protected fixtures, ignored by Git decrypted/ generated PHP output, ignored by Git ``` ## 初步观察 受 HMSC 保护的文件由两个可见部分组成: 1. 一个正常的 PHP 包装器,用于检查是否存在 `hmsc` Zend 扩展。 2. 放置在 `exit;?>` 之后的类 Base64 文本。 典型的包装器包含: ``` return hmsc_init('...') ? hmsc('...') : false; exit;?> BASE64_BODY ``` 在没有该扩展的情况下执行文件会打印出安装提示信息。如果移除开头的 PHP 标签,PHP 就会打印出 Base64 主体,因为它位于 PHP 块之外。该主体不是 PHP 源码,也不是简单的 Base64 编码 源码。它是一个经过 Base64 编码的二进制加密容器。 样本 4、5 和 6 使用了 `hmsc('')`。它们在 `exit;?>` 之后仍然携带有加密容器; 空参数仅在扩展内部选择不同的容器长度分支。 ## 定位编译 Hook 通过符号名称搜索 Ghidra 输出,而不是枚举超过 9000 个 生成的文件。有效的路径为: ``` FUN_0019a7f0 -> start_zend_hmsc_module -> saves zend_compile_file -> replaces zend_compile_file with FUN_00199592 ``` `FUN_0019a7f0` 安装了自定义编译器回调。原始的 `zend_compile_file` 指针保存在 `DAT_003f3568` 中。关闭时在 `zend_hmsc_shutdown` 中恢复。 这是 HMSC 的核心设计。公开的 PHP 函数并不直接 负责编译返回的源码。Zend 扩展拦截了每一次 文件编译,识别出 HMSC 包装器,解密其隐藏主体,并 将恢复的字节传递给正常的 Zend 扫描器。 不匹配 HMSC 包装器的文件 会被委托给保存的原始编译器处理。 ## 包装器解析 主 hook 反编译为 `FUN_00199592`,它通过 `zend_stream_fixup` 获取完整流。它通过 `hmsc_initialize` 构建正则表达式,该函数被识别为一种 uuencode 风格的字符串解码器。 解码嵌入的值得到: ``` /hmsc_init\('|'\)\?hmsc\('|'\):|\;\?\>\n/ ``` 扩展将受保护的文件拆分为以下几个相关字段: ``` index 1: Base64 argument supplied to hmsc_init() index 2: Base64 argument supplied to hmsc() index 4: Base64 body following exit;?> ``` 它将 `hmsc_init` 字段进行 Base64 解码并重新编码,作为有效性检查。 格式错误的 Base64 会产生: ``` Hmsc init failed, Update the module hmsc. ``` ## `hmsc_init` 的作用 `hmsc_init` 不是密文,也不提供 AES 密钥。它在 编译 hook 中有三个可观察到的用途: 1. 它赋予包装器可识别的形状。 2. 其 Base64 表示会经过验证。 3. 当 `hmsc()` 带有非空参数时,其解码后的字节长度用于移除伪装。 解码后的数据本身看起来是刻意设为不透明的。在恢复的路径中,这些字节 并未被用作 AES 密钥、IV 或身份验证标签。 ## `hmsc` JSON 的作用 对于样本 1、2 和 3,`hmsc()` 参数的 Base64 解码结果是包含 编码日期和看似随机字段的 JSON。编译 hook 会解析该日期, 并可在解密前拒绝已过期的文件。 JSON 也是已签名 PHP 包装器的一部分。在移除附加的 伪装字节时会用到其 Base64 字符串长度。它不是 AES 密钥。 对于样本 4、5 和 6,`hmsc('')` 会选择此分支: ``` container = Base64Decode(trailing body) ``` 对于非空 JSON 参数,分支为: ``` camouflage_length = strlen(hmsc Base64 argument) + strlen(Base64Decode(hmsc_init argument)) container = decoded_body without the final camouflage_length bytes ``` 这个条件判断正是支持全部六个样本所需的更改。 ## 二进制容器格式 在可选的伪装移除后,解码后的主体具有以下布局: ``` +----------------------+--------------------------------+----------------------+ | 16-byte prefix | transformed encrypted payload | 41-byte footer | +----------------------+--------------------------------+----------------------+ ``` 页脚包含: ``` offset -41, length 5: version mask offset -36, length 5: version XOR material offset -20, length 20: raw SHA-1 wrapper signature ``` 对两个五字节版本字段进行异或运算得到: ``` 2.0.0 ``` 签名计算如下: ``` SHA1(wrapper through the newline after exit;?>) ``` 在哈希之前,使用扩展内置的正则表达式移除换行符、回车符和制表符序列: ``` /\n|\r|\t+/ ``` 这可以检测对可见加载器的任何修改,包括其 `hmsc_init` 和 `hmsc` 参数。 ## Payload 转换 中间部分的解码按以下顺序进行: 1. 移除 16 字节的容器前缀。 2. 移除 41 字节的页脚。 3. 对 ASCII 字母应用 ROT13。 4. 反转整个字节字符串。 5. 提取前 12 个字节作为 AES-GCM IV。 6. 对剩余字节进行 zlib 解压缩。 7. 将解压后的结果拆分为密文和最后的 16 字节 GCM 标签。 ROT13 操作在 Ghidra 中表现为向量化范围检查和字节 调整,随后是一个反向复制循环。反编译的 C 语言代码看起来比 等效的 PHP 代码要庞大得多: ``` $decoded = strrev(str_rot13($encoded)); ``` ## AES-GCM 解密 扩展调用: ``` EVP_aes_128_gcm EVP_DecryptInit_ex EVP_CIPHER_CTX_ctrl EVP_DecryptUpdate EVP_DecryptFinal_ex ``` 从调用中重建的参数如下: ``` cipher: AES-128-GCM key: cfcd208495d565ef66e7dff9f98764da IV: first 12 bytes after ROT13 and reversal tag: final 16 bytes of the outer zlib result AAD: none ``` 密钥是嵌入在 `.rodata` 中 `DAT_00333179` 处的固定 16 字节值。 `EVP_DecryptFinal_ex` 验证 GCM 标签。因此,错误的密钥、IV、密文或 标签会导致身份验证失败,而不会产生未经校验的明文。 经过身份验证的明文是另一个 zlib 流。对其进行解压缩即可得到 原始 PHP 源码。 ## 恢复源码的执行 该扩展不会将解密后的 PHP 写入磁盘。在第二阶段 zlib 之后,它调用: ``` hmsc_zend_compile_files -> hmsc_scanning_files -> Zend parser/compiler ``` `hmsc_scanning_files` 将恢复的内存缓冲区准备为扫描器输入。 Zend 编译该缓冲区,就像它从受保护的文件名中读取了普通的 PHP 源码一样。 这就解释了为什么代码能正常执行,而加密主体 仍然处于可见的 PHP 块之外。 独立项目在执行前停止,并将恢复的源码 写入 `decrypted/deo_.php`。 ## 重构算法 紧凑形式如下: ``` parse wrapper decode hmsc_init decode trailing Base64 body if hmsc argument is non-empty: remove strlen(hmsc_base64) + strlen(decoded_hmsc_init) bytes verify footer version == "2.0.0" verify SHA1(normalized visible wrapper) middle = container[16 : -41] middle = reverse(rot13(middle)) iv = middle[0:12] cipher_and_tag = zlib_decompress(middle[12:]) ciphertext = cipher_and_tag[0:-16] tag = cipher_and_tag[-16:] compressed_php = AES_128_GCM_DECRYPT(ciphertext, key, iv, tag) php_source = zlib_decompress(compressed_php) ``` ## 验证 测试套件涵盖了两种包装器变体: | 样本 | `hmsc()` 参数 | 恢复大小 | | ----------- | ----------------: | -------------: | | sample1.php | JSON | 3,379 bytes | | sample2.php | JSON | 350,713 bytes | | sample3.php | JSON | 216,430 bytes | | sample4.php | empty | 186,173 bytes | | sample5.php | empty | 28,149 bytes | | sample6.php | empty | 327,275 bytes | 每个恢复的结果均以 `
标签:ffuf, OpenVAS, PHP, Zend扩展, 云资产清单, 代码分析, 凭证管理, 安全测试工具, 密码学, 手动系统调用, 解密工具, 逆向工程