9.2.2 代码注入

9.2.2 代码注入

代码注入技术是软件破解技术中非常重要的一项技术。在Windows平台,代码注入是指在一个进程中向另外一个进程注入代码的技术。代码注入的目的和含义就是,通过在一个进程中向另外一个进程添加一段目标程序设计之外的代码并执行,以实现目标程序设计之外的目的,而且这个过程一般是在目标程序运行周期内完成的。

根据需要实现的目的和注入的时机不同,代码注入技术在实现技术上会存在不同,但一般基本原理是一样的。下面我们来看看代码注入的一般原理,如图9.32所示。

图9.32 代码注入一般原理

实现代码注入技术的一般过程如下:

(1)向目标进程写入额外代码;

(2)执行目标进程中已经写入的额外代码。

向目标进程写入额外代码是非常容易实现的。下面给出一段代码,可以简单实现向目标进程写入额外代码的功能。

当然,很多保护系统会破坏WriteProcess Memory进程。在这种情况下,我们可以采用MapSection的方式,这里不再详细阐述。

向目标进程写入代码是第一步。代码注入的关键其实在于第二步——执行注入的额外代码。Windows系统提供了不少执行注入代码的方式,包括常用的CreateRemote Thread()函数、QueueUser APC()函数等。但是,这些函数一般不让我们选择注入代码的执行时机,而是由系统控制,这对破解技术中的代码注入来说是很不利的。因为在软件破解技术中,时机是非常重要的,所以,如果需要完全掌控注入代码的执行时间,就需要控制启动注入的代码的执行方式。

假定我们需要在程序启动之前向程序注入一段代码并执行,思路如下。

(1)以暂停的方式启动进程,这样可以保证程序的入口代码尚未被执行。

(2)注入需要在目标进程中执行的额外代码。

(3)使用设置线程上下文的方式修改主模块入口到额外代码入口。这里使用了Windows系统的一个特性,即当以暂停方式启动一个进程后,系统会把主模块入口放在线程上下文的EAX成员中,修改此成员即可修改主模块的入口地址。

(4)恢复目标进程并执行。

首先,构建如下代码实现暂停启动目标进程的目的。

接着,向启动的进程注入需要执行的额外代码,示例如下。

然后,通过修改线程上下文的方式实现中转主模块入口的目的,代码如下:

接下来,我们就可以恢复目标进程的运行了,代码如下:

至此,我们就完成了一个代码注入的所有过程。但是,通过上面的步骤我们可能会发现,我们所注入的代码还是无法按照预定的目标运行,这是之前所说的executeProc()函数的代码规则问题所致。

当我们将一段代码注入一个进程空间中时,要想使得代码能够正常运行,就要在设计这些代码的时候考虑一系列问题。首先,要考虑这些被注入进程的代码在目标进程中的位置可能是随机的,这就要求代码拥有自我重定位能力,或者在注入之前要先重定位再注入。其次,注入的代码要能自行解决函数解析的问题,因为在不同的进程中,任何一个模块的基址都有可能是不一样的,尽管许多系统模块看起来在不同的进程之间基址是一样的,但是我们不能永远做这样的假设,所以,最好的办法就是自己解决模块解析的问题。最后,就是代码出口的问题,这个问题与在代码HOOK中的考虑方式是一样的。在上面这个例子中,由于我们使用修改线程上下文的方式实现HOOK,所以只需要再将真实的主模块入口地址写回线程上下文EAX成员就可以了。

下面将详细介绍如何设计具有这些特性的代码,并尽量避免使用纯汇编指令,而使用高级语言来实现绝大部分代码,这对于实现复杂的功能是非常有必要的,而且也可以避开繁杂的汇编指令设计以节省时间。

我们了解一下代码是如何进行自我重定位的。在普通的汇编指令中,可以通过cal1、pop这样的指令组合来取得代码当前的位置,并通过计算代码与设计代码之间的偏移而实现重定位。但是,这种方式在代码指令设计非常复杂或者面临大量的重定位指令时是相当困难的。因此,可以采用一种预先调整的办法。我们不釆用上面示例代码中的这种直接复制executeProc()函数代码指令到目标进程的方式,而是将整个注入程序的PE映像复本都复制到目标进程中。这样做会带来两个好处:一方面,如果复制注入程序的整个PE映像,由于是一个完整的映像文件,我们就可以通过获取PE映像的重定位信息来对所有的代码进行重定位,这样就避免了到目标空间中进行代码重定位的问题;另一方面,通过直接从注入程序的内存中复制数据,可以复制许多处于代码区段的变量数据到目标进程,这样就可以很方便地向目标进程传递参数信息。

需要注入目标进程的代码如下:

以上代码的基本思路是开辟一块内存空间,将程序自身的PE头和各个区段的数据复制到这个内存空间中。这样做的目的是方便接下来对代码进行重定位,同时也将那些不是以连续内存空间映射到内存中的PE变成一个连续的内存空间,从而方便地写入目标进程。

代码中的关键函数是Load VReloc()。这个函数是我们处理代码重定位的核心,示例如下:

该段代码是处理一般PE程序文件重定位的代码。通过上面的代码,我们可以将PE程序文件定位到想要的新基址,通过build RemoteData()函数将注入程序的整个映像中的代码复制到目标进程空间当中,并做好相应的重定位工作。这样,目标进程当中的executeProc()函数即便有代码需要重定位,也可以正常运行。但是,仅重定位代码还不够,如果我们在executeProc()函数中调用了外部函数,由于代码是直接通过WriteProcess Memory()函数复制到目标进程中的,所以代码中对外部函数的引用同样是无效的。可以通过动态函数载入技术或者重新载入模块的导入表来实现外部函数的调用。

动态载入技术的基本原理就是通过在代码中使用Load Library()、Get Proc Address()等函数动态定位我们需要使用的函数地址。但是在上面的情况中,代码执行时连Load Library()和GetProc Address()这两个函数的地址都不知道,且不能在注入程式中以参数的方式传递给目标进程(因为在不同的进程中,这两个函数所在的kenel32.dll模块的基址可能不同,所以Load Library()和Get Pro Address()函数的地址也有可能不同)。因此,在目标进程的executeProc()函数中,如果要通过动态函数载入技术载入外部函数,就需要找到一种不使用外部函数的办法来定位Load Library(),或者至少是GetProc Address()的函数地址。实现这个目的的关键在于,如果能够通过不调用外部函数的方式定位某些系统模块的基址,就可以通过查找这些模块的导出表来找到Load Library()等关键函数的位置。

有两种方式可以方便地获得模块基址。一种方式是以参数的方式传递NTDLL模块的基址到目标进程,然后通过NTDLL导出的Ldr Load Dll()等函数定位其他函数的基址。对系统内核比较了解的读者应该知道,尽管NTDLL的基址每次开机时可能不同,但是在同一个会话的各个进程中是相同的,所以在注入程序时获取的NTDLL基址对目标进程同样可用。另外一种方式是通过PEB块定位模块列表的位置,并从模块列表中取出有用的模块基址,再通过导出表查找。程序的PEB可以方便地通过线程的TEB获取,而TEB可以通过FS段的数据定位。因此,我们可以在不借助任何外部力量的情况下实现函数的定位。这里给出使用这种方式获取kenel32模块基址(在NT6下为kenelbase模块)的代码,具体如下:

获取模块的基址后,就可以通过模拟一个简单的Get Proc Address()函数来获取系统函数GetProc Address()的地址了。一旦得到正确的GetProc Address()函数地址,就可以得到所有外部函数的地址。下面再给出一段简单的MiniGetproc Address()函数的代码,该函数并未处理导出表的中转函数,因此只用于获取关键函数的位置。

在充分理解上面的技术以后,我们就可以完善要注入的代码了。