第12章 隐蔽的恶意代码启动
12x1 启动器(Launcher)
启动器(也称为加载器)是一种设置自身或其他恶意代码片段以达到即时或将来秘密运行的恶意代码。启动器的目的是安装一些东西,以使恶意行为对用户隐藏。
启动器经常包含它要加载的恶意代码。最常见的情况是在它的资源节中包含一个可执行文件或者DLL。正常情况下,Windows PE文件格式中的资源节是供可执行程序使用的,但并不应该是可执行程序的组成代码。正常资源节的内容包括图标、图片、菜单以及字符串。启动器通常在资源节存储恶意代码,当启动器运行时,它在运行嵌入的可执行程序或者DLL程序之前,从资源节将恶意代码提取出来。
恶意代码启动器通常需要以管理员权限运行,或者通过提权拥有这些权限。普通的用户进程不能执行本章讨论的技术。事实上,启动器可能会包含一些提权代码,这提供了识别启动器的另一种方法。
12x2 进程注入
隐藏启动的最流行技术是进程注入。顾名思义,这种技术是将代码注入到另外一个正在运行的进程中,而被注入的进程会不知不觉地运行注入的代码。恶意代码编写者试图通过进程注入技术隐藏代码的行为,有时他们也试图使用这种技术绕过基于主机的防火墙和那些针对进程的安全机制。
VirtualAllocEx函数用来在另外一个进程中分配一块内存空间。WriteProcessMemory函数用来向VirtualAllocEx函数分配的地址空间写数据。
1. DLL注入
DLL注入将代码注入到一个远程进程,并让远程进程调用LoadLibrary,从而强制远程进程加载一个DLL程序到它的进程上下文。一旦被感染的进程加载了恶意DLL程序,OS会自动地调用DLLMain函数,DLLMain函数由这个DLL程序的作者编写。这个函数包含的代码拥有与被注入进程访问系统的相同权限。通常情况下,恶意DLL程序除了DLLMain函数之外,拥有很少的内容,并且它所做的任何操作看起来似乎都来自于被感染的进程。
为了将恶意DLL注入到一个主机进程,启动器恶意代码必须获取受害进程的句柄。最常用的方法是使用Windows API函数CreateToolhelp32Snapshot、Process32First和Process32Next,来查找进程列表中的目标进程。一旦发现目标进程,启动器会提取目标进程的进程标识(PID),然后用提取的PID调用OpenProcess,以获取目标进程的句柄。
DLL注入使用CreateRemoteThread函数,这个函数可以让启动器恶意代码在远程进程中创建并运行一个新的线程。使用CreateRemoteThread时,需要传入三个比较重要的参数:OpenProcess函数获得的进程句柄(hProcess),注入线程的入口点(lpStartAddress)以及线程的参数(hpParameter)。例如,线程的入口点可能被设置为Loadlibrary函数的地址,并且恶意DLL名字作为它的参数。这会触发受害进程用恶意DLL名字作为参数调用LoadLibrary,因此恶意DLL就可以加载到受害进程中(假设LoadLibrary函数在受害进程的内存空间中有效,并且恶意DLL的名字字符串存在于同一个进程空间)。
恶意代码编写者通常使用函数VirtualAllocEx,为恶意动态库的名字字符串创建内存空间。如果提供远程进程的句柄,VirtualAllocEx函数将在远程进程中分配内存空间。
调用CreateRemoteThread之前,调用的最后一个函数是WriteProcessMemory。这个函数将恶意DLL程序的名字字符串写入到VirtualAllocEx分配的内存空间中。
2. 直接注入
同DLL注入一样,直接注入也涉及在远程进程的内存空间中分配和插入代码。直接注入同DLL注入类似,它们都使用了许多相同的WindowsAPI函数。不同的是,它并不用单独编写一个DLL并且强制远程进程载入它,而是直接将恶意代码注入到远程进程中。
直接注入比DLL注入更加灵活,但是要想注入的代码在不对宿主进程产生副作用的前提下成功运行,直接注入需要大量的定制代码。这种技术可以被用来注入编译过的代码,但更多的时候,它用来注入shellcode.
直接注入比DLL注入更加灵活,但是要想注入的代码在不对宿主进程产生副作用的前提下成功运行,直接注入需要大量的定制代码。这种技术可以被用来注入编译过的代码,但更多的时候,它用来注入shellcode.
在应用直接注入技术的恶意代码中,经常会发现如下三个函数:VirtualAllocEx、writeProcessMenory和CreateRemoteThread。通常会有两次virtualAllocEx和WriteProcessMemory调用。第一次调用是分配内存空间并写入远程线程使用的数据。第二次调用分配内存空间并且写入远程线程代码。CreateRemoteThread调用包含远程线程代码的位置(lpStartAddress)和数据(lpParameter)。
由于远程线程使用的数据和函数都必须位于受害进程空间内,所以正常编译的程序都无法工作。例如,字符串不在正常的.data段中,需要调用函数LoadLibrary/GetProcAddress来访问未载入的函数。另外,还有一些其他的限制,但我们在这里不做深入探究。基本上,直接注入技术要求恶意代码编写者或者精通汇编语言代码,或者是仅仅注入相对简单的shellcode。
要分析远程线程的代码,你可能需要调试恶意代码,并且在反汇编器中,转储WriteprocessMemory调用发生前所有的内存缓存区,以便进行分析。由于这些缓存区经常包含shellcode,因此你需要掌握shellcode分析技巧.
12x3 进程替换
除了注入代码到一个宿主程序外,一些恶意代码还会使用一种被称为进程替换方法,将一个可执行文件重写到一个运行进程的内存空间。当恶意代码编写者想要将恶意代码伪装成一个合法进程,并且不会产生DLL注入让进程崩溃的危险时,他们会使用进程替换技术。
这种技术让恶意代码与被替换进程拥有相同的特权级。例如,如果一段恶意代码执行了一次针对进程svchost.exe的进程替换攻击,用户会看到一个名为svchost.exe的进程从C:Windows\System32目录中启动,并很可能认为它没有什么风险(这是一种常见的恶意代码攻击方式)。
进程替换的关键是以挂起状态创建一个进程。这也就意味着这个进程将会被载入内存,但是它的主线程被挂起。在外部的程序恢复主线程之前,这个程序将不做任何事情,恢复主线程后,才开始执行。
一旦进程被创建,接下来就要用恶意的可执行文件替换受害进程的内存空间,通常会使用函数ZwUnmapViewofSection来释放由参数指向的所有内存。解除内存映射之后,加载器通常执行函数VirtualAllocEx为恶意代码分配新的内存,并且用函数WriteProcessMemory将恶意代码的每个段写入到受害进程的内存空间,这些操作常在一个循环中进行。
最后一步,恶意代码恢复受害进程的环境,通过调用SetThreadContext函数,让入口点指向恶意的代码,让其获得运行。最后,调用ResumeThread函数,初始化恶意代码并进行执行,而此时它已经替换了受害进程。
进程替换是恶意代码伪装成非恶意程序的一种有效方法。通过伪装成受害进程,恶意代码可以绕过防火墙和入侵防御系统(IPS),并且使它看起来像正常的Windows进程,从而让它逃避探测。
此外,通过使用原始的二进制路径,恶意代码还可以欺骗精明的用户,当查看进程列表时,他们只能看到执行是已知、有效的二进制程序,而并不会意识到它已经被解除了映射,并被替换成了恶意程序。
12x4 钩子(Hook)注入
钩子注入是一种利用Windows钩子(Hook)加载恶意代码的方法,恶意代码用它拦截发往某个应用程序的消息。恶意代码编写者可以用挂钩注入,来完成以下两种事情。
- 保证无论何时拦截到一个特殊消息,恶意代码都会被运行。
- 保证一个特殊的DLL被载入到受害进程的内存空间。
1. 本地和远程钩子(Hook)
有两种类型的Windows钩子:
- 本地钩子被用来观察和操纵发往进程内部的消息。
- 远程钩子被用来观察和操纵发往一个远程进程的消息(系统中的另一个进程)。
远程钩子有两种形式:上层和底层。上层的远程挂钩要求钩子例程是DLL程序的一个导出函数。它被操作系统映射到被挂钩线程或者系统所有线程的进程地址空间。底层远程钩子则要求钩子例程被保护在安装钩子的进程中。这个例程在操作系统获得处理事件的机会前被通知。
2. 使用钩子的击键记录器
钩子注入常被一种叫做击键记录器的恶意程序所使用,被用来记录击键。击键可以分别使用WH_KEYBOARD和WH_KEYBOARD_LL钩子例程类型,来注册上层和底层的钩子。
对于WH_KEYBOARD例程,钩子通常运行在远程进程的上下文空间中,也可以运行在安装钩子的进程空间中。对于WH_KEYBOARD_LL例程,事件直接发送到安装钩子的进程,所以钩子运行在创建钩子的进程中。无论使用哪种钩子类型,击键记录器都可以截获击键,并且在传递到进程或者系统之前,把它们记录到文件或是修改。
3. 使用SetWindowsHookEx
用来执行远程Windows挂钩的主要函数是SetwindowsHookEx,它拥有如下参数:
- idHook 指定要安装的钩子例程的类型。
- lpfn钩子例程指针。
- hMod对于上层的钩子,它来标识包含lpfn定义的钩子例程的DLL句柄。对于底层钩子,它来标识包含1pfn例程的本地模块句柄。
- dwThreadId 指定与钩子例程关联的线程标识,如果这个参数为0,则挂钩例程将绑定与调用线程同在一个桌面的所有线程。当为底层钩子时必须被设置为0。
钩子例程可以包含处理消息(当它们从系统产生后)的代码,也可以什么也不做。无论如何,钩子例程都必须调用CallNextHookEx,这可以保证调用链中下一个钩子例程能够得到消息,并且保证系统继续正常运行。
4. 目标线程
当制定dwThreadId时,恶意代码通常决定载入到dw某个系统线程,或者载入到所有线程。也就是说,仅当它是击键记录器或者类似的程序时,恶意代码才载入到所有线程(目的是拦截消息)。然而,载入到所有的线程会降低系统的运行速度,并且可能触发入侵防护系统。因此,如果是简单载入一个DLL到远程进程,则注入单个线程会保持恶意代码的隐蔽性。
指定单线程为目标,要求查找进程列表中的目标进程,如果碰到目标进程没有运行,恶意代码要先启动它。如果一个恶意的应用程序挂钩了一个经常使用的Windows消息,它很有可能会触发入侵防御系统,所以恶意代码通常会挂钩一个不常使用的消息,如WH_CBT(一个用于计算机训练的消息)。
12x5 Detours
Detours是微软研究院1999年开发的一个代码库。它的初衷是作为一个来扩展已有操作系统和应用程序功能的简单工具。Detours开发库让开发人员对二进制应用程序进行修改变得简单可行。
同样,恶意代码编写者也喜欢Detours库,他们使用Detours库执行对导入表的修改,挂载DLL到已有程序文件,并且向运行的进程添加函数钩子等。
恶意代码编写者最常使用Detours库,来添加一个新的DLL到硬盘上的二进制文件。恶意代码修改PE结构,并且创建一个名为.detour的段,它通常位于导出表和调试符号之间。.detour段在新的导入地址表中包含了原始的PE头部。使用Detours库提供的setdll工具,恶意代码编写者修改PE头部,使其指向新的导入表。
12x6 APC注入
在本章的前面,我们看到通过CreateRemoteThread函数创建一个线程就可以使用远程进程中的一个函数。然而,线程创建需要系统开销,所以调用一个现有的线程会更加高效。Windows的异步过程调用(APC)可以满足这种要求。
APC可以让一个线程在它正常的执行路径运行之前执行一些其他的代码。每一个线程都有一个附加的APC队列,它们在线程处于可警告的等待状态时被处理。例如它们调用如WaitForSingleObjectEx、waitForMultipleobjectsEx和SleepEx函数等。实质上,这些函数给了线程一个处理等待APC的机会。
如果应用程序在线程可警告等待状态时(未运行之前)排入一个APC队列,那么线程将从调用APC函数开始。线程逐个调用APC队列中的所有APC。当APC队列完成时,线程才继续沿着它规定的路径执行。恶意代码编写者为了让他们的代码立即获得执行,他们用APC抢占可警告等待状态的线程。
APC有两种存在形式:
- 为系统或者驱动生成的APC,被称为内核模式APC。
- 为应用程序生成的APC,被称为用户模式APC。
恶意代码可以使用APC注入技术,让内核空间或者用户空间中生成用户模式的APC.
1. 用户模式下APC注入
线程可以使用API函数QueueUserAPC排入一个让远程线程调用的函数。运行用户模式的APC要求线程必须处于可警告等待状态,因此恶意代码会查看进程中是否有可能进入这个状态的目标线程。幸运的是,对恶意代码分析师来说,WaitForSingleObjectEx是最常使用的Windows API调用,并且有很多处于可警告等待状态的线程。
让我们来检查一下QueueUserAPC的参数:pfnAPC、hThread以及dwData。QueueUserAPC要求句柄为hThread的线程使用参数dwData运行pfnAPC定义的函数。
注意:分析过程中,你可以通过查找恶意代码查询目标进程的API调用,如Create Toolhelp32Snapshot、Process32First和Process32Next 来发现目标线程的代码。在这些代码之后的经常是Thread32First和Thread32Next调用,它们通常被包含在一个在目标进程中查找目标线程的循环中。另外,恶意代码也会利用参数SYSTEM_PROCESS_INFORMATION调用Nt/ZwQuerySystemlnformation来发现目标进程。
2. 内核模式的APC注入
恶意代码驱动和Rootkit也常常希望在用户空间中执行代码,但是对它们来说这样做并不容易。一种方法是在内核空间执行APC注入。恶意的驱动可创建一个APC,然后分配用户模式进程中的一个线程(最常见的是suchost.exe)运行它。这种类型APC通常由shellcode组成。
设备驱动利用两个主要的函数来使用APC: KeInitializeApc和KeInsertQueueApc.