Contents

windows下的注入技术(上)

DLL注入方法

程序加载DLL的时机主要有以下3种:

  1. 在进程创建阶段加载输入表中的DLL,即俗称的静态加载,在C++中可以通过#pragma comment 实现
  2. 通过调用LoadLibrary(Ex)主动加载,称为动态加载
  3. 由于系统机制的要求,必须加载系统预设的一些基础服务模块,例如 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个部分:

  1. 打开进程,获取进程句柄
  2. 在内存空间开辟一段内存空间
  3. 向开辟的内存空间写入需要注入的DLL的路径
  4. 利用GetProcAddress()获取LoadLibrary的地址
  5. 调用远程线程,利用LoadLibrary加载DLL

要实现这些,还需要目标进程的四个权限:

  1. PROCESS_CREATE_THREAD
  2. PROCESS_QUERY_INFORMATION
  3. PROCESS_VM_OPERATION
  4. 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

CreateRemoteThreadRtlCreateUserThread的作用都是创建远线程,所以具体流程都是一样的。

那么以上三种远线程注入函数注入的区别是:

  1. RtlCreateUserThread不需要csrss验证登记,需要自己结束;而CreateRemoteThread不需要自己结束自己
  2. CreateRemoteThreadRtlCreateUserThread都调用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做了些什么。

通过代码中提前定义好的相应函数地址变量可以发现和之前的注入方法是差不多的,但区别是这些函数不会直接去获得,后面再看。

https://s1.ax1x.com/2022/08/31/v42otg.png

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的基址,不是则地址减一,也就是一直向后寻找。

https://s1.ax1x.com/2022/08/31/v4fpl9.png

从PEB中找到相应函数地址

需要寻找的函数为:

  1. LoadLibraryA
  2. GetProcAddress
  3. VirtualAlloc
  4. NtFlushInstructoinCache - 用于修复重定向后刷新指令缓存

首先获取PEB基址

https://s1.ax1x.com/2022/08/31/v44hFK.png

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

https://s1.ax1x.com/2022/08/31/v4ItU0.png

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

https://s1.ax1x.com/2022/08/31/v4ouZR.png

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

https://s1.ax1x.com/2022/08/31/v4oxfK.png

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

https://s1.ax1x.com/2022/08/31/v4T96e.png

https://s1.ax1x.com/2022/08/31/v4TEkt.png

载入镜像到新分配的内存

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

https://s1.ax1x.com/2022/08/31/v4qn5n.png

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

https://s1.ax1x.com/2022/08/31/v4qcad.png

处理导入表

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

https://s1.ax1x.com/2022/08/31/v4L4YR.png

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

https://s1.ax1x.com/2022/08/31/v4OCX8.png

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

https://s1.ax1x.com/2022/08/31/v4OBHe.png

修复重定向

当自己手动将一个PE文件加入到内存时需要修正重定位表,步骤如下:

  1. 知道要修正定位表的地址
  2. 计算PE文件默认基址和加载后的基址相差的偏移量
  3. 将要修正的地址加上这个偏移量

https://s1.ax1x.com/2022/08/31/v4XVbD.png

https://s1.ax1x.com/2022/08/31/v4vn1I.png

调用DLLMain

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

https://s1.ax1x.com/2022/08/31/v4v8Ag.png

运行效果

https://s1.ax1x.com/2022/08/31/v4vRjx.png