1、注入技术
1.1注入技术的背景
在Windows操作系统中,各个进程的内存空间是相互独立的,虽然能通过函数VirtualQueryEx/VirtualProtectEx查询、设置目标进程的内存信息和页属性,通过函数ReadProcessMemory/WriteProcessMemory对目标进程的内存空间进行读写,但是这样操作繁琐(例如获取一个二级指针的内容就需要2次跨进程读内存),更重要的是不能跨进程执行自己的代码。因此使用DLL注入目标进程再执行相关的操作是优先使用的一种手段,不仅避免了快进场操作带来的繁琐过程及安全限制上的问题,更重要的是能够直接执行我们的代码,从而更方便的执行Hook、Patch操作等。通常情况下,程序加载DLL的时机主要有以下3个:1)进程创建阶段加载导入表中的DLL,俗称“静态输入”;2)通过调用LoadLibrary(Ex)主动加载,称为动态加载;3)由于系统机制的要求,必须加载系统预设的一些基础服务模块(如shell扩展模块、网络服务接口模块或者输入法模块等);
1.2DLL注入方法
1.2.1、改变程序运行流程使其主动加载目标DLL
程序运行的容器是进程,真正的活动是进程中的线程。因此改变程序流程的通常做法是改变线程EIP、创建新线程或修改目标进程内的某些代码,使其执行LoadLibrary(Ex)来加载目标DLL。
1、CreateRemoteThread法:其基本思路是在目标进程中申请一块内存并向其中写入DLL路径,然后调用CreateRemoteThread函数在目标进程中创建一个线程,线程函数的地址就是LoadLibraryA(W),参数就是存放DLL的内存指针。之所以把LoadLibraryA(W)作为线程函数,是因为它刚好只有一个参数。
2、RtlCreateUserThread法:RtCreateUserThread函数是Rtl执行体的一部分,与CreateRemoteThread类似,最终都要调用NtCreateThreadEx来创建线程实体。但是它一般只用来创建一些特殊的线程。此法暂时不熟悉,待熟悉后再回来填这个坑。
3、QueueUserApc/NtQueueAPCThread注入法:APC是异步过程调用的缩写,它是一种软中断机制,当一个线程从等待状态(线程调用SleepEx、SignalObjectAndWait、MsgWaitForMultiple、ObjectsEx、WaitForMultipleObjectsEx、WaitForSingleObjectEx函数时会进入可唤醒状态)中苏醒时,它会检测有没有APC交付给自己,如果有它就会执行这些APC过程。APC有两种形式:由系统产生的APC称为内核APC,由应用程序产生的APC称为用户模式APC。在用户层,我们可以像创建远线程一样,利用QueueUserApc把过程添加到目标线程的APC队列里面,等这个线程恢复运行时,就会执行我们插入的APC过程了。在用户模式添加APC之后,线程不会直接调用APC函数,只有当线程被唤醒时才会调用。
4、SetThreadContext法:正在执行的线程可以被SuspendThread函数暂停执行,然后系统就会把此时线程的上下文环境保存下来。当使用ResumeThread恢复线程的执行时,系统就会把之前保存的上下文环境回复使线程从之前保存的eip开始执行。在注入DLL时,可以将目标进程中的线程暂停,然后向其写入shellcode,把线程的CONTEXT的eip设置为Shellcode的地址,这样线程恢复时就会先执行我们的shellcode了。在shellcode中加载目标DLL,然后再跳回原始的eip执行。对于原来的线程而言,好像什么都没发生一样。
1.2.2、利用系统机制加载DLL
操作系统提供的某些系统机制是依赖一些基础服务模块(可能是操作系统提供也可能是第三方提供的)实现,当进程主动或者被动触发了这些系统机制时,就会在适当的时候主动加载这些模块。因此,可以制定一个符合该规范的DLL,将其注册为系统服务模块,这样就可以合法的进入目标进程了。
1、SetWindowHookEx消息钩子注入:消息钩子是Windows提供的一种消息过滤和预处理机制,可以通过API SetWindowHookEx安装一个用于过滤特定类型消息的钩子函数。
2、输入法注入:输入法是一类特殊的程序,一般有外挂式和输入法接口式IME两种实现形式。外挂式注入比较简单,它通常是一个exe文件,通过模拟一些Windows输入消息向当前处于活动状态的编辑窗口输入文字,一个显著的优点是输入法只要启动,就可以在所有进程中使用。但是其缺点包括实现困难、兼容性差。IME实际上是一个符合Windows平台输入法接口规范的DLL,Windows为这个DLL定义了一系列标准接口。
2、Hook技术
Hook中文译为挂钩或者钩子其关键就是通过一定的手段埋下钩子,钩住我们关心的重要流程,然后对执行过程进行干预。“插入特定代码以干预程序的执行流程”就是Hook的奥义。从流程上看,原来的直接调用过程被修改后绕了个弯,经过我们自定义的处理过程才达到真正的目标函数处,因此这一过程也称为“Detour”。插入的这个与函数接口完全相同的函数称为Detour函数。Hook分为两类:一类是通过修改数据(通常是引用的函数地址)进行hook,另一类则直接修改函数内的指令进行hook。类似于C语言中的指针,第一种hook是修改指针的指向,第二种hook直接是修改指针指向的内容的值。各种名目繁多的hook,总结起来其实就只有两种,Address Hook和Inline Hook。
2.1、Address Hook
是指通过修改数据进行hook的方法。当然并非随意修改“数据”都可以达到hook的目的。此处的“数据”通常是指一些函数的地址(也可能是偏移量),这就是称其为“Address Hook”的原因。它们通常存放在各类表或者结构中或者某个指定的地址处,亦或特定的寄存器,它们有一个共同点就是在某个时刻会成为程序执行过程中的eip。只要把这些地址替换成我们的Detour函数地址,就可以轻松拿到程序的控制权了。
2.1.1 PE中的IAT hook:IAT hook具体指某个模块的IAT,所以它的作用范围只针对被Hook的模块,且必须在以静态链接的方式调用API时才生效,使用LoadLibrary或者GetProcAddress进行动态调用时无效(在其他模块调用被hook的API时也没有影响)。
2.1.2 PE中的EAT:EAT输出表与IAT不同,它存放的不是函数地址,而是函数地址的偏移,使用时需要加上模块基址。所以在进行hook的时候,也要用Detour函数的地址减去被hook模块的基址。在hook之后,所有试图通过输出表获取函数地址的行为都会受到影响(主程序及其导入表中所有模块的IAT在程序初始化时就已经被填充了,因此,之后通过IAT进行调用不受EAT hook的影响)。
2.1.3 IDT:系统的中断描述符表IDT是操作系统在处理中断机制时使用的一张表,当中断发生时,操作系统通过IDT查询中断的处理函数。IDT的表基址存放在idtr寄存器中,表内项目数存放在idtl中,每个中断项的中断处理函数(或者叫中断处理例程)称为ISR。
2.2、Inline Hook
Inline hook是指直接修改指令的hook,与Address hook相比更容易理解,也没有那么多形式。因此其关键是转移程序的执行流程,所以一般使用jmp、call、retn之类的转移指令,主要包括以下几种形式:1)jmp xxxx (5字节),直接跳转到某地址;2)push xxxx/retn (6字节),通过压栈返回实现跳转;3)mov eax, xxxxxx/jmp eax(7字节),先将转移地址放入寄存器,再实现跳转,之所以使用eax来临时保存转移地址,是因为eax寄存器通常用于保存函数的返回值,在函数入口处修改它不会影响函数的执行结果;4)call hook(更换指令或者输入表);50HotPatch Hook,一个短跳加一个长跳,暂时不熟悉。
3、堆栈溢出
3.1、栈溢出的原理
栈是向低地址方向生长的,而变量在栈中是向高地址方向生长的,因此,当栈里面的变量被赋予的值超过其最大缓冲区的大小时,就会覆盖前面push到栈里面的返回地址,导致函数在返回时发生错误,这就是栈溢出。如下图所示,栈变量按照生命先后顺序逐渐从高地址向低地址扩展(图中变量a、b、c的相对地址),但是数组的地址却是相反的,数组是按照低地址向高地址扩展的,即数组中索引大的元素位于高地址(更接近于栈帧基址ebp),索引越小的元素位于低地址(离栈帧基址ebp越远),如果向数组写入超过数组长度的内容,则会导致数组之前的变量甚至函数返回地址被覆盖掉。
4、shellcode
shellcode实际上是一段可以独立执行的代码(也可以认为是一段填充数据),在触发了缓冲区溢出漏洞并获取eip指针的控制权后,通常会将eip指针指向Shellcode以完成漏洞利用过程。Shellcode的工作流程如下图:
Shellcode在漏洞样本中的存在形式一般为一段可以自主运行的汇编代码,它不依赖于任何编译环境,也不能调用API函数名来实现功能,它只能通过主动查找DLL基址并获取API地址的方式来实现API调用,然后根据功能调用相应的API函数来完成其自身的功能,Shellcode一般分为两个模块:基本模块和功能模块,如下图所示:
基本模块用于实现shellcode初始运行、获取kernel基址以及获取API地址的过程。获取kernel基址的常见方法包括暴力搜索、异常处理链表搜索和TEB搜索。TEB查找的原理如下:在NT内核系统中,fs寄存器指向TEB结构。TEB+0x30指向PEB结构,PEB+0xc指向PEB_LDR_DATA结构,PEB_LDR_DATA+0x1c处存放着程序加载的动态库链接地址,第1个指向ntdll.dll,第2个就是Kernel32.dll的基址,如下图所示:
从DLL文件中获取API地址的方法如下:1)在DLL基址+0x3c处获得e_lfname的地址,即可获得PE文件头;2)在PE文件头0x78偏移处得到函数导出表的地址;3)在导出表的0x1c偏移处获得AddressOfFunctions的地址,在导出表的0x20偏移处获得AddressOfNames的地址,在导出表0x24处获得AddressOfNameOrdinalse的地址;4)AddressOfFunctions函数地址数组和AddressOfNames函数名数组通过AddressOfNameOrdinalse一一对应。