宋晓斌 穆 源 朱 涛 马陈城
(中国人民解放军61660部队 北京 100093)
随着近年来网络攻击事件呈爆发增长趋势,网络安全问题已经成为业界关注和讨论的热点,正逐步引起各方重视.木马、僵尸网络、钓鱼网站等传统网络安全威胁有增无减,分布式拒绝服务攻击、高级持续性威胁(advanced persistent threat, APT)攻击等新型网络攻击手段应用愈发普遍.从传统的感染型病毒、远控木马,到近些年常用的社会工程、0day漏洞、勒索软件等发起的有针对性的APT攻击,攻击特征更趋向于高级化、隐蔽化及持久化,并且能够轻易绕过传统的安全防护体系.其中DLL(dynamic link library)注入技术作为实现隐蔽渗透的一项主流技术已经给传统安全防护体系带来极大的挑战.
DLL注入最初被用于改进程序、修复Bug,后来由于其隐蔽及难以检测的特性逐步被应用于渗透攻击.DLL注入目前作为恶意攻击者或红队人员经常使用的一门技术,经过多年的发展已十分成熟.DLL注入的本质就是将DLL放进某个目标进程的地址空间里,使其成为目标进程的一部分.DLL被加载到进程后会自动运行DllMain函数,攻击者可以把具有攻击行为的代码放到DllMain函数中,当通过某种手段加载该DLL时,添加的代码就会被执行.与一般的DLL加载的区别在于目标进程在正常运行过程中并不调用该DLL中的任何函数,属于被动加载行为.
图1 导入表逻辑结构
在通常情况下,程序加载DLL的时机主要有以下3个:一是在进程创建阶段加载输入表中的DLL,即静态输入;二是通过调用系统API函数LoadLibrary主动加载,即动态加载;三是由于系统机制的要求,必须加载系统预设的一些基础服务模块,如网络服务接口模块、输入法模块等.因此,DLL注入也通过上述3种手段进行.
1) 干预PE加载过程注入技术.通过在完全加载PE文件之前对程序进行人工干预,为PE文件中的输入表增加1个指向待注入的DLL项,当程序主线程在输入表初始化阶段时就会主动加载目标DLL.主要包括导入表注入、篡改原始DLL注入等.
2) 动态注入技术.通过改变程序执行流程使其主动加载目标DLL.主要包括通过创建远程线程注入、APC注入、依赖可信任进程注入、反射式DLL注入等.
3) 利用系统机制加载注入技术.基于操作系统提供的某些系统机制需要依赖基础服务模块来实现,当进程主动或被动触发这些系统机制时,就会主动加载这些模块.因此,可以定制符合相关规范的DLL,将其注册为系统服务模块达到注入的目的.主要包括使用注册表注入、输入法注入、消息钩取注入、LSP劫持注入等.
当程序被加载时,系统根据程序导入表信息加载需要的DLL,导入表结构如图1所示.导入表注入的原理就是修改程序的导入表,将自定义的DLL添加到程序的导入表中,程序运行时可以将自定义的DLL加载到程序的进程空间.
具体过程为:①将需要注入DLL的程序写入到内存中,并新增1个节;②复制原来的导入表到新节中;③在新节复制的导入表后新增1个导入表项IMAGE_IMPORT_DESCRIPTOR;④增加8 B的INT(import name table)表和8 B的IAT(import address table)表;⑤存储需要注入DLL的名称;⑥增加1个IMAGE_IMPORT_BY_NAME结构,并将函数名称存入结构体第1个变量后的内存中;⑦将IMAGE_IMPORT_BY_NAME结构地址的相对虚拟地址(relative virtual address, RVA)赋值给INT表和IAT表第1项;⑧将DLL名称所在位置首地址的RVA赋值给新增导入表的Name项;⑨修改PE文件中IMAGE_DATA_DIRECTORY结构的VirtualAddress和Size;⑩重新保存为新的PE文件.
篡改原始DLL注入即通过伪造恶意DLL文件对程序中原始的DLL文件进行替换.当进程在正常执行过程中调用相应的DLL时,恶意DLL即可被调用执行.其中最为典型的例子为ComRes注入.ComRes注入的原理是当目标进程使用CoCreateInstance这个系统API时,COM服务器会加载C:Windowssystem32目录下的ComRes.dll文件到目标进程中,利用这个加载过程,攻击者可以用伪造的恶意ComRes.dll替换系统本身的ComRes.dll,然后利用LoadLibrary将伪造的DLL加载到目标EXE中.
完整的实现过程如图2所示.首先使用进程PID打开进程,获得进程句柄;然后利用进程句柄申请内存空间,将DLL路径写入内存;最后创建远程线程,调用LoadLibrary执行DLL.
图2 创建远程线程实现DLL注入流程
APC即异步过程调用.异步过程调用是一种能在特定线程环境中异步执行的系统机制.Windows系统中每个线程都会维护1个线程APC队列,用户可以利用系统API在线程APC队列添加APC函数,系统会产生1个软中断来执行这些APC函数.APC有2种形式,由系统产生的APC称为内核模式APC,由应用程序产生的APC称为用户模式APC.同时还需要借助特定函数才能触发,如SleepEx,SignalObjectAndWait等.因此APC注入的场景应为:1)必须是多线程环境;2)注入的进程必须调用特定的函数.APC注入的原理是利用当线程被唤醒时,APC中的注册函数会被执行,并以此去执行恶意DLL加载代码,进而完成DLL注入的目的.其实现流程为:1)当进程中某个线程执行到SleepEx或者WaitForSingleObjectEx时,系统就会产生1个软中断;2)利用QueueUserAPC这个API可以在软中断时向线程的APC队列插入1个函数指针;3)当线程再次被唤醒时,此线程会首先执行APC队列中被注册的函数.如果攻击者插入的是LoadLibrary执行函数,就能达到注入DLL的目的.
其原理是利用Windows系统中高权限的可信进程通过2次注入实现攻击,以图3为例进行说明:在第1次注入过程中,将a.dll注入到Services.exe中,再利用a.dll将b.dll注入到目标进程中.依赖可信进程注入本质上仍属于远程线程注入的一种,区别在于它利用了系统可信进程进行远程注入,极大提高了注入的成功率,并在注入结束后将自身释放,这种注入方式可以有效降低被查杀的可能.
图3 依赖可信进程的注入过程
大多数DLL注入通常需要将目标DLL存储在磁盘中,而文件“落地”就存在着被查杀的风险.因此反射式DLL注入技术应运而生,该技术不需要在文件系统中存储目标DLL文件.减少了文件“落地”被删的风险.同时它并不具备传统DLL注入方式的基本模式,因此更难以被杀毒软件检测.其核心思路是可以通过网络或在本地存放1份DLL的加密版本,然后将其解密之后存储在内存.利用VirtualAlloc和WriteProcessMemory将DLL文件写入目标进程的虚拟地址空间.DLL文件中包含1个导出函数,该函数的功能就是装载其自身,即实现了PE Loader功能.接下来只需要通过DLL的导出表找到该导出函数并调用它即可实现DLL注入.要实现反射式DLL注入需要注射器与待注入的DLL.其中,被注入的DLL除了需要导出1个函数来实现对自身的加载之外,其余部分可根据具体功能需求进行开发.而注射器只需要将待注入的DLL文件写入目标进程,然后将控制权转交给上述导出函数即可.
使用注册表注入主要依赖于注册表中的2个表项:AppInit_DLLs和LoadAppInit_DLLs,如图4所示.当注册表项AppInit_DLLs中存在DLL文件路径时,会跟随进程的启动加载指定的DLL文件,同时LoadAppInit_DLLs置为1.注册表注入的原理是当User32.dll被加载到进程时,会读取AppInit_DLLs表项.若有值,将调用LoadLibrary载入这个字符串指定的每个DLL.所以注册表注入只对加载User32.dll的进程有效.
图4 相关注册表项
输入法注入的原理是通过篡改IME文件实现,目前主流的输入法都是通过IME实现.IME是在Windows平台上使用的标准输入法接口规范,其本质是一个DLL文件,系统为这个DLL定义了一系列接口以满足不同功能需求.Windows系统在切换输入法时,会把这个输入法需要的IME文件装到当前进程中,利用该特性,在IME文件中使用LoadLibrary函数注入恶意DLL文件.但输入法注入的实现需要对输入法IME文件的实现过程有一定了解,实现起来相对困难.
钩子(Hook)是一种Windows消息拦截机制[1].消息钩子注入原理是利用SetWindowsHookEx系统API拦截目标进程的消息到指定DLL中的导出函数,利用该特性,可以将DLL注入到指定的进程中.操作系统会将消息钩子加载到所有进程空间,当指定消息发生时,则优先调用相应的处理函数,通过该过程即可实现DLL注入攻击.图5所示即为SetWindowsHookEx安装钩子后的消息处理流程,在进入系统消息处理函数WinProc前进入自定义的响应消息操作流程中.
分层服务提供者(layered service provider, LSP)是一个安装在Winsock目录中的DLL程序.应用程序通过Winsock2进行网络通信时,会调用ws2_32.dll的导出函数,如connect,accept等.而后端通过LSP实现这些函数的底层.简单来说,就是调用Winsock2提供的函数时会调用对应的LSP提供的服务器提供者接口(service provider interface, SPI)函数.SPI是由LSP导出的供ws2_32.dll调用的系列函数,是Winsock2提供的一项新特性,通过它可以借助LSP对现有的传输服务提供者进行扩展.LSP注入的原理如图6所示,可以概括为只要将自定义的LSP DLL安装到系统网络协议链中,那么所有基于Winsock实现的程序都会主动加载该LSP DLL.可以利用这个特点实现对网络功能的进程注入.
图5 SetWindowsHookEx插入消息拦截后的消息处理流程
图6 LSP劫持注入原理
DLL注入检测技术目前主要分为2大类:静态检测与动态检测.其中静态检测主要通过提取PE文件特征[2-4]采用比对的方式进行判别;动态检测以API监控技术、回溯分析技术为主.
文献[5]提出了基于Detours技术Hook[6-8]装载DLL文件API函数的防御方法.文献[9]提出对CreateRemoteThread函数进行Hook来防止创建远程线程的DLL注入,该方法能够在一定程度上避免远程线程注入,文献[10]提出一种DLL抢占式注入技术,通过对NtCreateThread,NtResumeThreadNt,MapViewOfSection进行Hook实现一种内核Hook引擎,同时结合用户态检测模块进行行为分析,但也都存在一定的局限性,需要Hook到每一个进程空间中,增加进程开销.同时攻击者也可以通过反Hook技术进行绕过.
文献[11]提出一种模块比对的方法,首先在正常PE文件没有被非法注入DLL时[12],建立合法模块列表.当PE文件扫描时,枚举当前PE文件IDT表,并与合法模块进行对比,将没有在合法模块列表中的模块初步确定为可疑模块,并通过进一步判断和分析验证这些可疑模块是否为非法模块.
文献[13]提出建立线程“白名单”机制,即只允许指定进程执行合法线程,阻断非法线程访问.首先明确软件正常运行时的可信线程列表,线程“白名单”的建立是一个长期训练的过程,大部分软件所产生的线程相对稳定,但随着运行环境的变化,所加载的线程也可能发生一定的变化,例如引入了新功能,这些也属于可信线程的范畴.在建立了稳定的线程“白名单”后,便可以通过实时监测,确保在执行过程中只允许可信线程执行,并拦截非法远程线程的注入执行.
文献[14]提出一种通过比对VERSIONINFO资源的方式进行判别,该技术是基于恶意攻击者在创建DLL文件时通常不关注一些额外的属性信息,例如版本信息、公司名称等,这将导致微软构建的合法DLL与恶意DLL在格式上存在细微差别,通过比对这些信息判断是否存在恶意DLL文件.除此之外,还可以采取提取字符串、函数调用[15-16]或利用卷积神经网络进行特征提取[17]等方法.这些方法均需要解析DLL获取具体数据与正常的DLL进行对比,判断是否为恶意DLL.但DLL很容易被处理或加壳导致无法提取上述信息.
文献[18]提出一种基于合法范围的检测技术,该技术通过分析合法DLL与注入DLL区别,发现所有合法DLL的IAT与INT数据均按顺序排列,符合一定规则.而手动注入的DLL的相关数据通常位于节空隙、节扩展部分或新增节,导致与合法数据存在不一致性.基于该思想检测各模块IAT与INT是否位于范围内来判断是否存在注入行为.但如果攻击者将数据伪造于合法范围内,则该方法将失效.
为解决静态比对技术存在的局限性,文献[18]还提出了一种基于异常回溯的深度检测方法,具体原理为程序输入表中相应的DLL均是程序自身所需的,在运行期间需要调用其中的库函数,因此与这些DLL具有主动关联性.而注入的DLL只是在加载时执行DllMain中的代码,程序自身不调用其库函数.因此程序与注入的DLL具有被动关联性.若清除了程序所需的DLL,程序原有的调用该DLL函数的代码就会因访问无效的地址而导致进程出现访问异常;若清除了注入的DLL,由于程序没有任何主动调用其库函数的代码,所以不会导致进程出现访问异常.以此实现对注入行为的检测.但该方法需要耗费大量时间进行异常捕获与进程重启,不适用于常规场景下的检测.
目前,常见的DLL注入技术由于经过长期发展,手段工具相对完善,攻击过程逐步趋向自动化,与之相对应的检测技术也已十分成熟,因此面临难以实现隐蔽攻击的问题.DLL注入技术已逐渐向无文件落地及文件加密处理方式转变[19-20],以反射式DLL注入技术为例,目前主流的团队渗透工具Cobalt Strike便采用该技术实现远程控制,通过网络传输的方式接收恶意DLL文件,然后对文件进行解密,通过该方式可以躲避绝大多数防护软件的监测.同时,DLL注入技术的隐蔽性也取决于DLL实现的复杂性,例如API监控技术,DLL文件中可以通过手动实现各类API功能的方式进行绕过,此类攻击方式相对罕见,原因在于实现系统API过程十分复杂,需要对系统API的实现原理较为清晰,因此这也对攻击者提出了更高的要求.
总体上,现有的动静态检测技术各有其优缺点,静态检测的优势在于低开销,检测效率高,但检测方式相对简单,容易被攻击者绕过.而动态检测则准确率较高,但开销较大.现有的检测技术在应对常规模式下的DLL注入均可以做到有效识别,但在应对高级DLL注入攻击时均存在一定的检测障碍.在静态检测方面,目前的方法均通过有限特征进行判别,下一步可以尝试通过总结非法DLL特征,构建1维或多维特征向量,结合深度学习技术,对非法DLL进行识别.在动态检测方面,可以结合恶意代码分析技术,根据程序行为进一步分析注入的DLL合法性.同时,是否可以结合动静态检测各自的优势,进一步提升准确性也是后续值得研究的重点.从另一个角度来看,目前的检测技术大部分关注用户态的行为,但在用户态下实现注入的方式在变换上并不复杂,攻击者可以采取多种变形方式绕过,而对于内核态相对较难,因此开展内核态DLL注入检测技术研究也将成为未来的研究方向之一.
本文主要对当前较为常见的10种DLL注入技术的原理进行了阐述分析,并总结了6类DLL注入检测技术.同时分析了当前2类技术各自存在的问题,并对各自未来的研究方向与发展趋势进行了展望,为后续的研究提供参考借鉴.