第12章 注入技术

久违的 开新章节咯~~~~

DLL 是Windows 平台提供的一种模块共享和重用机制,本身不能被独立执行,但可以被加载到其他进程中间接执行。

Windows操作系统中,各个进程内存空间是相互独立的,

操作其他进程的函数:

VirtualQueryEx/VirtualProtextEx 查询,设置目标进程的内存信息和页属性

ReadProcessMemory/WriteProcessMemory 对目标进程的内存空间进行读写

缺点:操作繁琐而且不能执行自己的代码。

所以我们就可以使用DLL,避免了繁琐的过程,而且可以直接执行自己的代码,更方便的进行Patch Hook等操作。

12.1 DLL注入方法

程序加载DLL的时机:(同时也是DLL注入的方法)

  • 进程创建阶段加载输入表中的DLL (静态输入)
  • 通过调用LoadLibrary主动加载(动态加载)
  • 系统机制要求,加载系统预设的一些基础服务模块,如Shell 扩展模块等

12.1.1 通过干预输入表处理过程加载目标DLL

当一个进程被创建后,不会直接到EXE本身的入口处执行,首先被执行的是ntdll.dll中的LdrInitializeThunk 函数

(ntdll.dll在进程创建阶段就已经被映射到新进程中了)

原理:

LdrInitializeThunk会调用LdrpInitializeProcess对进程一些必要内容进行初始化,LdrpInitializeProcess 会继续调用LdrpWalkImportDescriptor对输入表进行处理,即加载输入表中的模块,并填充IAT。

所以在输入表被处理之进行干预,为输入表增加一个项目,使其指向要加载的目标DLL,或者替换原输入表中的DLL并对调用进行转发即可达成目的。

1.静态修改PE输入表法

首先整理一下读取输入表的流程

从Data_Directory中的ImportTable读取 RVA以及大小,根据RVA,确定存储IID的范围,并逐个进行读取。

以XP SP3的notepad 为例子

ImportTable的位置位于0x160,值为

RVA为 00007604,大小为C8,我们需要增加一个IID,所以大小要增加0x14(20字节)

新的大小为C8+0x14 和为0xDC

为了插入IID,我们需要找到一段空间,把原来的所有IID移动到这个位置,然后在结尾处补上我们的IID。

在寻找合适的空隙时,既不能超出本节的RawSize,也不能超出对齐后的VirutualSize,要从两者中取出最小值。

从图中可见各节的RawSize已经按照FileAlignment对齐了,所以文件中节之间是没有空隙的,只能期待某个节的VirtualSize小于RawSize。从上表看只有.text节符合条件

但是它的空隙大小为 0x7800-0x7748 小于我们需要的DC。

这时有两种方法,一是增加一个section,另一个是扩充section

这里采用扩充节的方法。

扩充节我们需要,扩充,更改节头内储存的大小。

我们应该扩充多大呢,这个要按照FileAlignment来,这里扩充0x200。

然后要修改表头内储存的大小,可以看到原大小为00008000,我们要修改为00008200

倒序后为00 82 00 00. 同时也要确定该节具有读写权限,(似乎是用于IAT?)

然后我们把IID复制到我们扩充的位置,上面我们说过了RVA 为7604,这里转换为文件偏移,为6A04,大小为C8,把这部分数据复制到扩充部分,并且在结尾填充新的IID


插播一小段,这里的方法可能不适合所有文件。目前我试出来最合适的方法是:

新的SizeOfRawData = (max{Misc,SizeOfRawData}÷FileAlignment)向上取整 × FileAlignment + 增加的大小

新的VritualSize = ( max{Misc,SizeOfRawData}÷SectionAlignment)向上取整 × SectionAlignment + 增加的大小

然后修改SizeOfImage,新的SizeOfImage +=增加的大小

不这么做有些程序会直接报错。


由于我们更改了IID的位置以及大小,所以也要将ImportTable内的数值更改,

10400是文件偏移,转换为RVA后为13000,倒序后为 00 30 01 00 大小为 DC

更改后为

做完这些,还剩下dll的信息没有加载,可以把它记录到原IID的位置

此时所有修改完成,但是由于notepad 是有预加载的,预加载生效时,系统会直接读取预加载的dll。所以我们要取消一下预加载。将0x1B0到0x1B8的内容清零,双击更改过后的程序

2.进程创建期修改PE输入表法

原理与PE输入表法完全相同,可以在R3/R0的各个阶段进行干预。

区别:静态修改PE输入表法直接修改文件,而进程创建期修改PE输入表法修改映射后的PE内存。

BOOL DetourCreateProcessWithDll(
    LPCTSTR lpApplicationName,
    LPTSTR lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes,
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    BOOL bInheritHandles,
    DWORD dwCreationFlags,
    LPVOID lpEnvironment,
    LPCTSTR lpCurrentDirectory,
    LPSTARTUPINFOW lpStartupInfo,
    LPPROCESS_INFORMATION lpProcessInformation,
    LPCSTR lpDllName,
    PDETOUR_CREATE_PROCESS_ROUTINEW pfCreateProcessW
    );
(1)以挂起的方式创建目标进程

如果在原始调用中dwCreationFlags不包含 CREATE_SUSPENDED标志,那么在调用真正的创建进程函数CreateProcess之前要把这个标志加上。

(2)获取目标进程的PE结构信息

目标进程中的PE结构信息需要通过读取目标进程的EXE的实际加载位置的内存来获取,可以自行搜索目标进程内存,比对属性为MEM_IMAGE的页映射文件是不是目标EXE。此时内存只映射了EXE本身和ntdll.dll,而ntdll.dll的家在位置一般比较靠后,所以找到的第一个有MEM_IMAGE属性的页地址就是EXE的实际加载地址。

代码如下

ULONG_PTR FindImageBase(HANDLE hProc,LPSTR lpCommandLine)
{
	ULONG_PTR uResult = 0 ;
	TCHAR szBuf[1024]={0};
	SIZE_T dwSize = 0 ;
	PBYTE pAddress = NULL ;
	
	MEMORY_BASIC_INFORMATION mbi = {0};
	BOOL bFoundMemImage = FALSE ;
	char szImageFilePath[MAX_PATH]={0};
	char *pFileNameToCheck = strrchr(lpCommandLine,'\\');

	//获取页的大小
	SYSTEM_INFO sysinfo;
	ZeroMemory(&sysinfo,sizeof(SYSTEM_INFO));
	GetSystemInfo(&sysinfo);
	
	//Found First MEM_IMAGE Page
	pAddress = (PBYTE)sysinfo.lpMinimumApplicationAddress;
	while (pAddress < (PBYTE)sysinfo.lpMaximumApplicationAddress)
	{
		ZeroMemory(&mbi,sizeof(MEMORY_BASIC_INFORMATION));
		dwSize = VirtualQueryEx(hProc,pAddress,&mbi,sizeof(MEMORY_BASIC_INFORMATION));
		if (dwSize == 0)
		{
			pAddress += sysinfo.dwPageSize ;
			continue;
		}

		switch(mbi.State)
		{
		case MEM_FREE:
		case MEM_RESERVE:
			pAddress = (PBYTE)mbi.BaseAddress + mbi.RegionSize;
			break;
		case MEM_COMMIT:
			if (mbi.Type == MEM_IMAGE)
			{
				if (GetMappedFileName(hProc,pAddress,szImageFilePath,MAX_PATH) != 0)
				{
					//printf("Address = 0x%p ImageFileName = %s\n",pAddress,szImageFilePath);
					char *pCompare = strrchr(szImageFilePath,'\\');
					if (stricmp(pCompare,pFileNameToCheck) == 0)
					{
						bFoundMemImage = TRUE;
						uResult = (ULONG_PTR)pAddress;
						break;
					}
				}
			}
			pAddress = (PBYTE)mbi.BaseAddress + mbi.RegionSize;
		    break;
		default:
		    break;
		}

		if (bFoundMemImage)
		{
			break;
		}
	}
	return uResult ;
}

另一种办法是使用未公开的APIZwQueryInformationProcess 查询ProcessBasic Information,获取PEB的地址,PEB的偏移0x8处就是ImageBase了

typedef struct _PROCESS_BASIC_INFORMATION
{
    NTSTATUS ExitStatus;
    PVOID PebBaseAddress; /* contains the PEB address! */
    ULONG_PTR AffinityMask;
    DWORD BasePriority;
    HANDLE UniqueProcessId;
    HANDLE InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;
(3)获取原IID大小,增加一项,搜索可用的空隙

只要直接从PE映射的最后一个节的结束位置开始申请内存来放新的IID就可以了,由于此时进程状态的特殊性,在申请内存时需要加上MEM_REVERSE标志,以便在后续的操作中使用PAGE_EXECUTE_READWRITE属性

(4)构造新的IID及相关的OriginalFirstThunk, Name,FirstThunk 结构
(5)修正PE映像头

与直接修改文件时一样,需要修改输入表目录的虚拟偏移和大小,使其指向新申请的内存中真正的IID,同时要清空BoundImport 数据目录

(6)更新目标进程的内存

将修改后的PE头及新节的数据写入目标进程,在修改PE头时,要先使用VirtualProtectEx将页属性修改为可写。

(7)继续运行主线程

如果在调用CreateProcessWithDLLL函数的时候没有指定CREATE_SUSPEND标志,就需要调用ResumeThread函数让主线程继续运行。

以上过程也可以在内核中实现,时机是注册一个LoadImageNotify,当发现加载的是指定EXE时,就修改其输入表。而且,在内核中完成这项工作使目标程序本身更难防范,因为在运行到程序OEP时,目标DLL就已经加载成功了。

3.输入表项DLL替换法(DLL劫持法)

利用了DLL的加载机制

加载DLL时,系统的加载顺序:

  1. \KnownDLLs
  2. 程序目录
  3. 系统目录
  4. 当前目录
  5. Path变量

可以将同名DLL复制到程序目录,来提前加载。

12.1.2改变程序运行流程使其主动加载目标DLL

1.CreateRemoteThread法

基本思路:在目标进程中申请一块内存并向其中写DLL路径,然后调用CreateRemoteThread,在目标进程中创建一个线程。线程函数地址就是LoadLibraryA(W),参数就是存放DLL路径的内存指针。

这时需要目标进程的4个权限(Win7中需要更多),分别为

  • PROCESS_CREATE_THREAD
  • PROCESS_QUERY_INFORMATION
  • PROCESS_VM_OPERATION
  • PROCESS_VM_WRITE

WinVista以上由于有会话隔离,需要Patch掉KernalBase.dll中的检查,使它不能调用真正的CsrClientCallServer

2.RtlCreateUserThraed法

3.QueueUserApc/NtQueueAPCThreadAPC注入法

APC是“Asynchronous Procedure Call”异步过程调用的缩写,它是一种软中断机制,当一个线程从等待状态中苏醒,它会检测有没有APC交付给自己,如果有,就会执行这些APC过程。

由系统产生的APC称为内核模式APC,由应用程序产生的称为用户模式APC,在用户层,我们可以像创建远程线程一样,使用QueueUserAPC把APC过程添加到目标线程的APC队列里,等这个线程恢复执行时,就会恢复我们插入的APC过程了。

为了增加调用机会,应像所有线程插入APC

4.SetThreadContext法

正在执行的线程可以被SuspendThread暂停执行,然后系统就会把此线程的上下文环境保存下来,当使用ResumeThread 恢复线程的执行时,系统会把之前保存的上下文环境恢复,使线程之前保存的eip开始执行。

注入DLL时,可以将目标进程线程暂停,然后向其写入ShellCode,把线程的Context的eip设置为Shellcode的地址,这样线程恢复执行时就会先执行我们的ShellCode了

5.内核中通过Hook/Notify干预执行流程法

与方法4类似,不过在内核中,更方便进行这类干预操作。

6.内核KeUserModeCallBack法

Windows系统在加载全局钩子DLL时,由Win32k.sys回调user32.dll中的函数,并最终调用LoadLibraryExW实现的,所以也可以用同样的方法为运行中的进程注入自己的DLL

具体实现方法有两种

(1)回调user32.dll!——ClientLoadLibrary加载DLL

这种方法完全依照系统加载全局钩子DLL的过程,会自行填充回调所需的数据结构,然后调用keUserModeCallBack,其前提是系统中已经加载了user32.dll

(2)回调自己的ShellCode

7.纯WriteProcessMemory法

原理:在线程要执行的地方预先设下陷阱,当线程执行到这里时,先执行DLL的ShellCode,执行完毕再把陷阱填平

(1)在创建进程时注入DLL

(2)将DLL注入运行中的进程

12.1.3利用系统机制加载DLL

操作系统提供的的某些系统机制是依赖一些基础服务模块实现的,当进程主动或被动触发了这些系统机制时,就会在适当的时候去主动加载这些模块。因此,可以定制一个符合该规范的DLL,将其注册为系统服务模块,这样就可以“合法”的进入目标进程了。

1.SetWindowHookEx 消息钩子注入

消息钩子是windows提供的一种消息过滤和预处理机制,可以通过APIS SetWindowHookEx 按照一个用于过滤特定类型消息的钩子函数,其原型如下

HHOOK SetWindowsHookExA(
  int       idHook,
  HOOKPROC  lpfn,
  HINSTANCE hmod,
  DWORD     dwThreadId
);

第一个参数指定了Hook的类型,也决定了HOOKPROC被调用的时机,可选参数可参考官方文档

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowshookexa

关键在于最后一个参数,如果这个参数设置为0 ,安装的就是一个全局消息钩子,这时要求HOOKPROC必须在DLL中,并且要指定第三个参数hMod,这样,系统在其他进程中调用HOOKPROC时,如果发现目标DLL尚未加载,就会使用KeUserModeCallBack函数回调User32.dll 的_ClientLoadLibrary()函数,由User32.dll把这个DLL加载到目标进程中

使用这种方法很容易被安全软件拦截,而且对没有消息循环的纯后台程序无能为力。

2.AppInit_DLLs注册表注入

在加载 user32.dll时,会调用一个LoadAppDLLs()函数,它会读取下面这个注册表项

HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs

如果发现这个注册表项下面登记了DLL(在Win7中,只有将LoadAppInit_DLLs 设置为1,AppInit_DLLs才会启用)就会主动加载它。所以只要把加载的DLL登记在这里,就可以将其注入那些加载了user32.dll的进程。其缺点与SetWindowHook相同,通常只能注入GUI程序,所以要根据注入的目标进程是否可能加载User32.dll来确定。

3.输入法注入

输入法是一类特殊程序,一般有外挂式,和输入法接口式(Input Method Editor IME) 两种实现形式。

外挂式注入比较简单,通过模拟一些Windows输入消息向当前处于活动状态的编辑窗口输入文字,优点是之哟啊输入法启动,就可以在所有进程中使用。

缺点是兼容性不好,一个windows版本需要一个对应的输入法版本,此外,这类输入法为了截获用户输入的内容,通常会挂键盘钩子,这容易造成系统不稳定或者效率不高。所以大部分输入法还是采用IME实现的。

IME的实质,是一个符合Windows 平台输入法接口规范的DLL,当目标进程切换到这个输入法时,负责管理输入法的imm32就会加载这个IME模块。

4.SPI网络过滤器注入

SPI(Service Provider Interface, 服务提供者接口) 式Winsock2 提供的一项新特性,通过它可以借助实现一个分层服务提供者对现有的传输服务进行扩展。因为Win98以后的Windows操作系统都对SPI提供了很好的支持,所以SPI有很好的跨平台和兼容性,用户无需了解复杂的网络驱动程序编写细节,也无需考虑API Hook,进程注入等细节,一旦安装,系统就会自行加载。

Winsock2 SPI支持用户提供传输者(transport)和名称空间(namespace)两种类型的服务提供者,同时支持用户开发基础服务提供者和分从服务提供者两种类型的传输服务提供者,我们一般编写的是分层服务提供者(Layered Transport Provider, LSP)

LSP的安装方法是把要安装的SPI模块的信息写到注册表的如下位置。

  • HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/WinSock2/Parameters/Protocol_Catalog9\Catalog_Entries\000000000001
  • HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/WinSock2/Parameters/Protocol_Catalog9\Catalog_Entries\000000000002
  • HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/WinSock2/Parameters/Protocol_Catalog9\Catalog_Entries\000000000003

安装完毕,每个需要加载网络模块的进程都会加载我们的SPI模块

5.ShimEngine注入

Windows兼容性模式实现引擎(Windows Shim Engine),Windows会尝试模拟旧的系统环境来运行程序,方式是通过修复可能有问题的API调用,默认的兼容性引擎DLL是ShimEng.dll,我们也可以自行编写兼容性引擎来代替他。

利用该方法时,先以挂起方式启动进程,然后向pShimData写入我们的模拟 ShimEng DLL 文件的完整路径(WCHAR形式) ntdll 会向载入正常的ShimEngDLL 那样在如他,该方法在x64位同样可用,只是pShimData的偏移量有所不同

6.ExplorerShell 扩展注入

ExplorerShell 扩展注入也是比较常见的注入方式,例如安装WinRARA之后,在右键快捷菜单中会出现 RAR相关的选项,实际上这个菜单是由WinRAR注册的一个Shell扩展生成的,扩展模块一般是一个Com DLL,注册为Shell扩展后,所有调用了Shell接口的进程都会加载他。

ExplorerShell只是众多Shell扩展中的一种,而且要在弹出右键菜单时才能触发。

更简单的方法是通过ShellconOverlayIdentifier这个扩展点来注入,它的作用是控制Explorer中的文件对象的图标。使用这个扩展点的好处是:只要Explorer的对话框显示出来,这些扩展注册的模块立即被夹在。

12.2 DLL注入的应用

  1. 实现精确,复杂的内存补丁
  2. 实现增强的PEDIY
  3. 与HOOK技术相结合

12.3 DLL注入的防范

12.3.1 驱动层防范

因为驱动层位于系统底层,拥有极高的权限,而且防护是全局的,所以方式效果是最好的,驱动层也成为安全防护类软件必须抢占的制高点,但是如果攻击者也能通过加载驱动对系统保护手段进行破坏的话,攻防双方就重新站到了统一起跑线上,。

1.KeUserModeCallBack防全局消息钩子注入

前面讲到,系统家在全局钩子DLL是由win32k.sys通过内核函数KeUserModeCallBack回调user32.dll的_Client

LoadLibrary 函数实现的,所以可以为Win32k.sys安装IAT Hook,从而对将要加载的模块进行判断,原型如下

NTSTAUS,原型如下。

NTSTATUS
KeUserModeCallback(
  IN  ULONG ApiNumber,
  IN  PVOID InputBuffer,
  IN  ULONG InputLength,
  OUT PVOID *OutputBuffer,
  IN  PULONG OutputLength
  );

当发现ApiNumber对应的是user32.dll 回调函数表 apfnDispatch中_ClientLoadLibrary的索引时,获取InputBuffer 中的DLL路径,并对其合法性进行验证,对非法模块则拒绝此次调用。

2.NtMapViewOfSection/LoadImageNotify 对模块进行验证

正常的DLL加载过程一般是通过LoadLibraryA(W)或者LoadLibraryExA(W)实现的,也可以通过ntdll!Ldr

LoadDLL) 实现 (因为他们都是系统DLL导出的公开接口,最终都会调用ntdll!LdrLoadDLL) 在函数内部,会调用ntdll!NtMapViewOfSection 将目标DLL映射到当前进程中,所以,可以对系统服务进行SSDT Hook,其原型如下

NtMapViewOfSection(
  IN HANDLE               SectionHandle,
  IN HANDLE               ProcessHandle,
  IN OUT PVOID            *BaseAddress OPTIONAL,
  IN ULONG                ZeroBits OPTIONAL,
  IN ULONG                CommitSize,
  IN OUT PLARGE_INTEGER   SectionOffset OPTIONAL,
  IN OUT PULONG           ViewSize,
  IN                      InheritDisposition,
  IN ULONG                AllocationType OPTIONAL,
  IN ULONG                Protect );

具体实现中,可以先通过SectionHandle 查询要映射的Section 对象,并获取与它县关联的FILE_OBJECT,再对文件信息进行判断,如果是非法模块,则拒绝本次调用

在x64系统中,不在允许进行SSDT Hook,因此可以采用注册LoadImageNotify的方法。但是在调用ImageNotify时,目标DLL已经完成了映射,但是DLLMain尚未被调用,可以直接对模块的入口进行Patch,使它直接返回0

3.拦截进程打开、读、写,以及创建远线程,发送APC等操作

对目标进程动态注入DLL的过程中,少不了要打开目标进程,创建或打开线程、申请和写入内存并更改线程的上下文等操作,所以,只需要拦截这些关键的API,就可以达到保护目标进程的目的。

4.Call Stack检测非法模块

12.3.2 应用层防范

当不能通过加载驱动进行全局防范时,可以在应用层对一些常见情况进行防范。

1.通过Hook LoadLibraryEx函数防范全局钩子、输入法注入等

2.在DllMain中防御远程线程

当进程中产生一个新线程的时候,在线程初始化阶段,也就是 ntdll!LdrpInitializeThread 会向进程中所有已经加载的模块发送DLL_THREAD_ATTACH 通知,而此时,线程的实际入口要在_LdrpInitialize执行完毕,继续调用ZwContinue 时才会执行所以我们可以在被保护进程中加在自己的安全模块,当它得到PROCESS_THREAD_ATTACH通知时,会对当前线程的合法性进行判断。如果是非法线程,就直接退出当前线程。调试器附加的原理也是远程创建一个线程,因此该方法可以用来防止被调试。

3.枚举并查找当前进程中的非法模块和可疑内存

除了对加载过程进行监控,程序还可以对进程内的模块家在情况进行扫描(也可以发现可以模块的存在)

GetMappedFileName 就是这样一个API,它实际调用了NtQueryVirtualMemory,只要某个地址映射了一个PE文件,就能在结果中反映出来。

4.Hook ntdll中的底层函数进行CallStack检测

这种检测方式,与在驱动中的做法相同,但因为从应用层不可能达到系统底层,所以只能对应用底层模块ntdll中的一些函数进行hook

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇