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扩展, 云资产清单, 代码分析, 凭证管理, 安全测试工具, 密码学, 手动系统调用, 解密工具, 逆向工程