一种隐藏注入模块的新方法*

2015-03-19 00:35建,刘
计算机工程与科学 2015年8期
关键词:内核线程进程

吴 建,刘 新

(湘潭大学信息工程学院“智能计算与信息处理”教育部重点实验室,湖南 湘潭411105)

1 引言

在信息安全领域,对借助各类工具对恶意软件进行动态跟踪分析是安全专家的日常工作。但是,越来越多的恶意软件会检测自身进程空间中是否存在其他模块来发现安全分析工具的存在。为对抗这种反监控,安全分析工具需要通过Rootkits[1]和Anti Rootkits技术来隐藏注入模块[2,3]。

注入是指让目标进程非自愿地加载指定的模块,进而执行特定的代码。Windows操作系统下的注入方法非常多,我们依据目标进程是否会被动地载入模块,将注入方式划分为“有模块注入”和“无模块注入”两类。

“有模块注入”的方法常用的有:消息钩子注入、DLL劫持注入、注册表注入、输入法注入、内核回调注入、依赖可信进程注入等等[2,4]。“有模块注入”的优点是注入手段多,并且可以做到在不向目标进程空间写入任何数据的情况下,把DLL 模块注入进去。由于这些注入方式是利用操作系统提供的一些特殊接口来实现的,而正常软件也经常利用这些注入方法来实现某些功能,所以恶意软件很难区分出哪些注入是由安全分析工具发起的,也就无法抵抗这些注入行为。但是,这些注入方法会在目标进程里多出一个模块,增加了暴露的可能性。

由于注入模块在进程的整个生命周期中都存在(在任何时候使用ARK 工具对目标进程进行人工检测,都很容易发现这个注入模块),因此这种注入方式尽管可能暂时骗过恶意软件的防御,但无法逃避经验丰富的恶意软件编制者的检测。恶意软件如果想逃避监控的话,可以通过定时扫描自身进程空间的方式来发现是否增加了监控模块,以便采取反监控措施。

为了让注入模块更加隐蔽,安全技术人员提出了一种被称为“无模块注入”的技术,该技术的基本思想是去掉DLL模块这个代码的载体,直接把代码写入到目标进程。目前常用的“无模块注入”方法有SHELLCODE[5]注入、自加载DLL 等。但 是,这些注入方法实现起来比较麻烦,并且要向目标进程写入大量的数据。对于有内存读写保护的进程,必须要把握住很好的时机(进程已经创建,但保护还没生效的时候),这种注入手段才能奏效。由于这些“无模块注入”的方式一定要向目标进程写入或者映射数据,而正常软件一般是不会有这种行为的,所以注入行为本身很容易被检测到并且被阻止运行。

“无模块注入”虽然在注入完成后可以很好地隐藏注入到目标进程的代码,但它一定会有跨进程操作内存的行为,而且这种行为是很危险的,所以安全软件通常采用的注入方法依然是“有模块注入”。为了更好地隐藏自己,在注入完成之后有必要把这个多出的DLL模块给隐藏起来。

比较常见的隐藏模块的方法有断开链表的节点、Hook 枚 举 模 块 的API 函 数、抹 去PE 头等[4,6],但这些隐藏方法都有各种各样的缺点。

Hook枚举模块的函数,修改函数返回值是一种比较“古老”的技术,目前已经有了通用的方法来对抗这种隐藏,该方法被称为“模块重载技术”。因为无论是用户态的DLL 模块,还是内核态的内核模块都是可以重载的,只需要重新加载一份DLL模块或者内核模块,然后调用重新加载的模块里的函数,就可以绕过被安装了钩子的函数。所以,依靠Hook技术来隐藏自己是有局限性的。并且,在Windows 7 X64 操作系统的内核里已经有了PatchGuard[7],如果Hook内核代码,则会导致系统崩溃。所以,利用常规的Hook技术来实现对模块的隐藏,作用越来越来小,限制越来越大。

抹去PE头是用来对抗特征码搜索的一种技术,这种方法通过清除模块的结构特征来实现隐藏的效果,所以一般只能作为一种隐藏的辅助手段。

由于上面两种方法存在上述缺陷,要想很好地隐藏自己的模块,有必要从操作系统维护模块信息的数据结构下手来处理。在RING3层,维护模块信息的数据主要在_PEB_LDR_DATA 里,这个结构包含三个链表。下面是用WINDBG 在Windows XP 和Windows 7X86 下 查 看PEB_LDR_DATA 显示的部分结构:

除此之外,在NTDLL 中还保存着一个叫做HashTable的链表,这个链表也保存着进程的模块信息。所以,在RING3 层总共会有四个LDR_MODULE链保存着DLL模块的信息。

容易想到,如果把保存模块的信息的节点从这四个链表中摘除,就可以在RING3层实现对相应模块的隐藏。但是,在内核中还有一棵管理进程内存空间的平衡二叉树(VAD 树),在这棵平衡二叉树中,有一类节点保存着内存映射(SECTION)的相关信息。并且在这类节点中,保存着一个与SECTION 关联 的 文 件 对 象(FILE_OBJECT)[8]。而DLL模块是以内存映射(SECTION)的方式被加载的,所以通过在内核态遍历VAD 树还是可以发现被隐藏模块的。要想隐藏得更加彻底,还需要编写驱动程序将VAD 树中对应节点的文件对象中的路径清零。由于这种隐藏模块的方式需要编写驱动程序,实现起来比较麻烦;并且在比较新的操作系统中,加载驱动程序越来越困难[9]。再加上在Windows 7X64系统中还有PatchGuard,修改这棵VAD 树会导致系统崩溃。因此,通过断开操作系统中用来维护模块信息的链表来隐藏模块,也越来越不适应新环境。

2 隐藏注入模块的新方法

为了达到隐藏模块的目的,需要寻找一个比较通用的、不在上述四条链表和VAD 树中留下痕迹的加载模块的方法。

一种思路是不调用操作系统的加载模块的API函数,直接把DLL 模块中的代码和数据写入到目标进程,然后再自己处理导入表和重定位表。实验表明,在恶意软件没有采取防御的情形下,这种方式是有效的。但是,因为这种方式有跨进程向内存空间写入数据的行为,所以很容易被恶意软件防范。

还有一种思路是先让DLL 模块正常加载,然后复制一份完整的DLL 模块的内存映象,把原先的DLL模块释放掉,再以DLL模块正常加载的基址为地址申请与原DLL模块内存映象相同大小的内存,然后再把备份的映象复制回去。这样该DLL模块就在操作系统中消失了,并且免去了修复导入表和重定位的麻烦。但是,这种方式同样需要操作目标进程的内存,所以也会被恶意软件防范。

上面的两种注入方法可以归类为“无模块注入”,可以比较好地隐藏被注入的代码。但是,它有个致命的危险行为:操作不属于自己的内存空间,进而被恶意软件防范。

既然“无模块注入”的致命缺点是有跨进程操作内存空间的行为,如果能够在目标进程内部进行代码的去模块化操作,由于恶意软件无法察觉这种进程内部操作内存的行为,那么这种去模块化的方法从理论上来看应该能够绕过恶意软件的防御。

系统中任何一个软件为了能在系统中生存和正常运行下去,总会允许一些“有模块注入”行为注入到自身(比如输入法的调用,就是一种“有模块注入”)。对于“有模块注入”,恶意软件很难鉴别出哪些注入是系统必须的,哪些注入是由安全软件发起的,所以不可能阻止所有的注入行为。亦即任何软件都无法有效地防御所有的“有模块注入”,因此可以把“有模块注入”和代码的去模块化操作结合起来。并且通过在无模块化的代码中释放被注入的模块,可以解决“有模块注入”在目标进程中会多出一个模块的缺点,从而达到很好的隐藏效果。

根据以上分析,形成了以下思路:先通过“有模块注入”的方式在目标进程中注入一个类似“加载器”的模块,然后在“加载器”中实现无模块化加载主功能模块,最后把“加载器”模块释放掉,实现去模块化操作。

2.1 隐藏模块的实现步骤

本方法需要用到两个DLL,第一个DLL 是包含监控功能的DLL,为了描述方便,将其命名为main.dll;第二个DLL 用来加载主main.dll,并实现对main.dll的隐藏,这个DLL 命名为load.dll。以下是实现步骤:

步骤1采取一种恶意软件无法进行有效防御的“有模块注入”方法(比如输入法注入)注入load.dll到目标进程。

步骤2在load.dll中加载main.dll,此时main.dll会完成对全局变量以及堆的初始化。

步骤3复制一份main.dll到其他区域作为备份。

步骤4直接修改main.dll的入口函数为返回,再释放main.dll。此时在目标进程内已经不存在main.dll,也就检测不到main.dll了。

步骤5在main.dll原基址申请同样大小的地址空间,把备份数据复制到原地址空间。

步骤6执行main.dll的一个导出函数,在这个函数内释放load.dll,并执行一些初始化操作。至此,load.dll和main.dll都已从目标进程内释放,但main.dll内的代码还可以正常执行。

步骤7为了达到更加隐蔽的效果,还需要把main.dll的整个PE头部清零。

2.2 抹掉PE头,对抗暴力枚举

注入到目标进程的DLL 模块,会有比较明显的结构特征。恶意软件通过遍历整个内存空间,然后搜索特征码的方式,可以发现被隐藏的模块。这种检测方式可以称之为“暴力枚举”。为了对抗这种暴力枚举,需要将模块的结构特征清除掉。

PE 文件是Windows操作系统上的程序文件(Portable Executable)的 统 称,DLL 模 块 就 是 按PE文件格式[10]组织的,常见的EXE、DLL、OCX、SYS、COM 都是PE文件。图1描述了PE 文件的基本结构。

Figure 1 Structure of PE file图1 PE文件结构

经 过 实 验,把“DOS HEADER”和“PE HEADER”清零后,DLL 模块中的代码是可以正常工作的。下面是清零的关键代码。

2.3 释放DLL同时保留DLL的资源

通过调用释放模块的API函数,可以把DLL模块的信息从目标进程和系统的相关数据结构内删除。这个时候,无论是在用户态还是内核态,都无法通过常规方法发现被隐藏的模块。但同时,模块占用的内存区域和一些资源也会被释放掉,这样会导致程序不能正常执行。

为解决内存被释放的问题,可以备份整个模块的数据和重新分配内存。现在就只剩下一个关键问题:在调用释放模块函数的时候,如何阻止操作系统释放DLL模块的资源。

操作系统提供的释放资源的API函数freelibrary(),会执行下面两个操作:

(1)把DLL模块所占用的由系统分配的资源释放掉(比如堆heap)。为了阻止这种资源释放,可以在调用释放模块的API函数之前,把DLL 模块的入口点的指令修改为RET 指令。代码如下:

(2)减少依赖的其他DLL模块的计数,进而可能会导致这些DLL模块被释放。为了使注入的模块更加隐蔽,应该在这个DLL 模块中尽可能少依赖其它DLL。在用VS 编译DLL 时,选择“多线程/MT”选项,可以做到编译出来的DLL 模块只有KERNEL32.DLL 和USER32.DLL 两个依赖项。而在被注入的进程里,一般也会有这两个DLL,从而可以解决依赖的DLL 模块被释放的问题。

2.4 保持模块“活性”

把DLL模块注入到目标进程后,如果得不到运行机会,这块代码就是“死的”,是一种无效注入。必须要让DLL 模块里的代码不断地得到进入CPU 的机会去执行一些操作,这种注入才是有效的。我们把这种争取CPU 控制权的行为称为“保持活性”。

要保持模块的活性,一种思路是Hook被注入进程经常会调用的一些函数(比如消息响应函数WINPROC),这样可以让注入的代码经常有机会得到执行。但是,这种方法不能准确地确定注入的代码被执行的时机,并且有时还会触发被注入进程的一些保护机制。

还有一种方法就是在DLL 模块被初始化时,创建一个线程。在这个线程里,定时地执行注入的代码;并且还可以使用同步对象(EVENT、MUTEX 等)和内存映射等机制与外部进程进行实时的通信和交互,就相当于在目标进程中插入了一个间谍。采用这种方式注入代码的执行时间可控,而且可以主动运行,比第一种方法更好。但是,这种方法也有个比较明显的缺点:在目标进程中会多出一个线程,并且这个线程是无模块的,恶意软件作者可以发现这个线程。因此,还需要进一步地改进,使得该注入线程看上去更像一个普通线程。

2.5 线程伪装

线程是调度的基本单位,要完全隐藏线程,基本上不可能真正实现。不过,目前绝大多数程序都是多线程的,在目标进程多出一个线程即便经验丰富的专业技术人员也很难辨别其是否为注入,因此没有必要把线程给彻底隐藏。只需要把“线程没有对应的模块”这个问题给解决了,就能达到很好的隐蔽效果。

把线程从一个模块转移到另一个模块的方法被称为线程伪装,用这种方法可以很好地解决无主线程的问题。系统判断线程所在的模块,是通过先取得线程的开始地址,然后再用这个开始地址去定位相应的模块。因此,只要伪造一个假的线程开始地址,就可以让这个线程“属于相应的模块”。具体方法如下:在目标进程已经存在的模块中寻找一个地址(比如模块入口函数),在这个地址处构造一个跳转指令,直接跳到线程函数的起始地址。然后,在创建线程的时候,用找到的这个“跳板地址”作为创建线程的参数。等这个线程运行起来以后,再恢复被修改的指令。这样,被隐藏的模块的线程就不再是“无主线程”。

通过这种方法,可以很好地解决驻留线程被检测的问题。下面这份代码让“无主线程”thread-Start成功转嫁到了kernel32.DLL上。

2.6 两个特殊问题的解决

把注入的DLL模块给隐藏后,系统的API函数GetmoduleHandle、GetModuleFileName 等 需要枚举模块的函数均无法定位到这个模块,可能会导致注入的代码出现一些异常。下面是在实验过程中遇到过的两种问题:

(1)如果DLL模块中是有资源的,系统在加载资源前会要求先定位到这个DLL 模块,然后才会加载资源。但是,这个DLL模块已经被隐藏,会导致程序出错。

(2)操作系统在进行异常分发时,也需要定位异常指令所在的模块。但是,由于该模块已被隐藏,所以异常处理代码不能发挥作用。

我们的解决方法是在定位模块的函数中安装钩子,然后进行堆栈回溯检测,如果发现是自己的模块在定位自己时,就返回正确的信息。但是,这个方法需要安装钩子,处理不够隐蔽。

更隐蔽一点的方法就是在这个被隐藏的DLL模块中既不携带资源,也不进行异常处理。由于注入的代码一般不是很多,因此要做到既无资源,又无异常,还是比较容易实现的。

3 实验结果与分析

XueTr是一款广受好评的ARK 工具,它对模块和线程有很强大的检测能力。本实验就用XueTr来观察隐藏模块的效果。

前面已经介绍了Hook枚举模块的函数、断开进程的LDR_MODULE 链、修改SECTION 类型的内存区域节点等实现隐藏模块的方法。有的方法达不到隐藏效果,有的方法在低版本的Windows操作系统(比如Windows XP)下可以实现模块隐藏,但在一些高版本的操作系统(比如Windows 7X64)中却无法实现。本次实验采用了“断开进程的LDR_MODULE链”这种方法,与我们设计的方法进行对比。

在不隐藏模块的情况下,实验结果如图2 所示,main.dll会被检测出(图中的第一行)。

采用“断开进程的LDR_MODULE链”来隐藏模块,如图3所示。第一行是被隐藏的模块,并且被XueTr标记为危险状态(危险状态显示为红色,但在黑白印刷中,看不出红色,下同)。

而用我们的方法来隐藏被注入的模块,可以达到很好的隐藏效果。从图4可以看出,注入的模块已经完全被隐藏。

Figure 2 Effect of not hiding the injected modules图2 不隐藏模块的效果

Figure 3 Effect of hiding the injected modules by disconnecting chains图3 断链隐藏模块的效果

Figure 4 Effect of hiding the injected modules by our method图4 我们的方法隐藏模块的效果

再来看线程伪装的效果。图5是在没有进行线程伪装之前,XueTr检测到的线程信息。可以看到,第一行显示出驻留线程是“无主线程”并且被标记为危险状态。

Figure 5 Effect before camouflaging thread图5 没有线程伪装的效果

线程伪装之后的效果如图6所示。驻留线程已经“属于了”kernel32.DLL这个模块。

Figure 6 Effect after camouflaging thread图6 线程伪装后的效果

从实验结果可以看出,本文中的方法比目前的主流方法隐蔽性更好。并且这种方法在Windows XP及其以后的Windows系列平台都适用。

4 结束语

本文将“有模块注入”和“无模块注入”结合起来,形成了一种突破性强、隐藏性好、兼容性好的隐藏注入模块的方法,并且对这种隐藏方法进行了实验。用这种方法实现对被注入模块的隐藏,突破防御的能力强,能够兼容各种版本的Windows操作系统,并且隐蔽性比目前的通用方法都要好。

不过,本文的方法还存在一些小的不足:用这种方法实现模块的隐藏时,在目标进程会多出一块内存。要处理得更加隐蔽,可以考虑把这块内存也隐藏起来。

另外,如果被注入进程有检测机制,并且在常用的函数处安装了钩子,然后在堆栈中回溯出调用链,也能定位到被隐藏的模块。但是,这种自我检测会对程序的运行效率造成比较大的影响,所以一般很少有程序采用。同时,为了对抗这种检测,可以考虑在调用常用的系统函数前构造一条假的调用链[4]。另外,还有可以从根本上绕过这种堆栈回溯检测的方法:重载有所依赖的系统DLL 模块。为了更加隐蔽,在重载系统DLL的时候,也可以把重载的系统DLL做无模块化处理。

以上这些问题,将在后续的研究中逐步解决。

[1] Gong Guang,Li Zhou-jun,Hu Chao-jian,et al.Research on stealth technology of Windows kernel level rootkits[J].Computer Science,2010,37(4):59-62.(in Chinese)

[2] Xu Ming,Yang Tong,Zheng Lian-qing,et al.Concealing technology of Trojan horses and prevention[J].Computer Engineering and Design,2011,32(2):489-492.(in Chinese)

[3] He Zhi,Fan Ming-yu,Luo Bin-jie.Research on remote-thread injection based hidden process technology[J].Computer Applications,2008,28(6):92-94.(in Chinese)

[4] Xu Sheng.Game plugin art[M].Beijing:Electronic Industry Press,2013.(in Chinese)

[5] Wang Ying,Li Xiang-he,Guan Long,et al.Attack and defending technology of shellcode [J].Computer Engineering,2010,36(18):163-168.(in Chinese)

[6] Doug W.Methods for detecting kernel rootkits[D].University of Louisville,2007.

[7] Han Zhuo,Ran Xiao-min,LüWen-gao.Kernel integrity verification of Windows 7[J].Journal of Information Engineering University,2011,12(6):764-768.(in Chinese)

[8] Mao De-cao.Windows kernal scenario analysis—using open source code ReactOS[M].Beijing:Electronic Industry Press,2009.(in Chinese)

[9] Zhang Zhi,Cai Wan-dong.Excution method of unsigned driver on Windows x64[J].Microelectronics &Computer,2014,31(2):101-105.(in Chinese)

[10] Qi Li.Windwos PE authoritative guide[M].Beijing:Mechanical Industry Press,2011.(in Chinese)

附中文参考文献:

[1] 龚广,李舟军,忽朝俭,等.Windows内核级Rootkits隐藏技术的研究[J].计算机科学,2010,37(4):59-62.

[2] 许名,杨仝,郑连清,等.木马隐藏技术与防范方法[J].计算机工程与设计,2011,32(2):489-492.

[3] 何志,范明钰,罗彬杰.基于远程线程注入的进程隐藏技术研究[J].计算机应用,2008,28(6):92-94.

[4] 徐胜.游戏外挂攻防艺术[M].北京:电子工业出版社,2013.

[5] 王颖,李祥和,关龙,等.Shellcode攻击与防范技术[J].计算机工程,2010,36(18):163-168.

[7] 韩卓,冉晓旻,吕文高.Windows7内核完整性验证机制研究[J].信息工程大学学报,2011,12(6):764-768.

[8] 毛德操.Windows内核情景分析——采用开源代码ReactOS(上、下册)[M].北京:电子工业出版社,2009.

[9] 张智,蔡皖东.Windows x64无签名驱动程序运行方法[J].微电子学与计算机,2014,31(2):101-105.

[10] 戚利.Windows PE权威指南[M].北京:机械工业出版社,2011.

猜你喜欢
内核线程进程
强化『高新』内核 打造农业『硅谷』
基于国产化环境的线程池模型研究与实现
债券市场对外开放的进程与展望
基于嵌入式Linux内核的自恢复设计
Linux内核mmap保护机制研究
浅谈linux多线程协作
微生物内核 生态型农资
线程池技术在B/S网络管理软件架构中的应用
社会进程中的新闻学探寻
我国高等教育改革进程与反思