第33章 隐藏进程
本文最后更新于:2022年5月19日 晚上
第33章隐藏进程
本章将讲解通过修改API代码(CodePatch)实现API钩取的技术,还要讲一下有关全局钩取(Global hooking)的内容,它能钩取所有进程。此外,还讲解使用上述方法隐藏(Stealth)特定进程的技术,并通过练习示例帮助大家理解掌握。
隐藏进程(stealth process)在代码逆向分析领域中的专业术语为Rootkit,它是指 通过修改(hooking)系统内核来隐藏进程、文件、注册表等的一种技术。Rootkit的相关内容不在本章讲解范围内,为便于理解,本书中将统一使用“隐藏进程”这一名称。
33.1 技术图表
正式学习前,先看一下图33-1的技术图表。

技术图表标有下划线的部分表示的就是API代码修改技术。库文件被加载到进程内存后,在其目录映像中直接修改要钩取的API代码本身,这就是所谓的API代码修改技术。该技术广泛应用于API钩取,因为可以用它钩取大部分API,使用起来非常灵活。
前面我们讲过IAT钩取技术,如果要钩取的API不在进程的IAT中,那么就无法使用该技术。反之,“API代码修改”技术没有这一限制。
另外,为了灵活使用目标进程的内存空间,我使用了DLL注入技术。
33.2 API代码修改技术的原理
本节将具体讲解使用API代码修改技术钩取API的工作原理。与前一章学过的IAT钩取技术相比,API代码修改技术更易理解。IAT钩取通过操作进程的特定IAT值来实现API钩取,而API代码修改技术则将API代码的前5个字节修改为JMP XXXXXXXX指令来钩取API。调用执行被钩取的API时,(修改后的)JMP XXXXXXXX指令就会被执行,转而控制hooking函数。后面图33-3描述的是,向 Process Explorer 进程(procexp.exe)注入 stealth.dll 文件后钩取 ntdll.ZwQuerySystemInformation() API的整个过程(ntdll.ZwQuerySystemInformation() API是为了隐藏进程而需要钩取的API)。
33.2.1 钩取之前
首先看一下钩取之前正常调用API的进程内存。图33-2描述的是(钩取之前)正常调用API的情形。

procexp.exe代码调用ntdll.ZwQuerySystemInformation() API时,程序执行流顺序如下。
① procexp.exe 的00422CF7地址处的 CALL DWORD PTR DS:[48C69C]指令调用ntdll.ZwQuerySystemlnformation() API (48C69C地址在进程的IAT区域中,其值为7C93D92E,它是ntdll.ZwQuerySystemInformation() API的起始地址)。
②相应API执行完毕后,返回到调用代码的下一条指令的地址处。
33.2.2 钩取之后
下面看看钩取指定API后程序执行的过程。先把stealth.dir注入目标进程(procexp.exe),直接修改ntdll.ZwQuerySystemInformation() API的代码(Code Patch),如图33-3所示。

图33-3看上去相当复杂,下面逐一分析说明。
首先把stealth.dll注入目标进程,钩取ntdll.ZwQuerySystemInformation() API。ntdll.ZwQuerySystemInformation()API起始地址(7C93D92E)的5个字节代码被修改为JMP 10001120 (仅修改5个字节代码)。10001120是stealth.MyZwQuerySystemInformation()函数的地址。此时,在procexp.exe
代码中调用ntdll.ZwQuerySystemInformation()API,程序将按如下顺序执行。
①在422CF7地址处调用ntdll.ZwQuerySystemlnformation() API (7C93D92E)。
②位于7C93D92E地址处的(修改后的)JMP 10001120指令将执行流转到10001120地址处(hooking函数)。1000116A地址处的CALL unhook()指令用来将ntdll.ZwQuerySystemInformation() API的起始5个字节恢复原值。
③位于1000119B地址处的CALL EAX(7C93D92E)指令将调用原来的函数(ntdll.ZwQuerySystemInformation() API)(由于前面已经“脱钩”,所以可以正常调用执行)。
④ ntdll.ZwQuerySystemInformation()执行完毕后,由7C93D93A地址处的RETN 10指令返回到stealth.dll代码区域(调用自身的位置)。然后10001212地址处的CALL hook()指令再次钩取ntdll.ZwQuerySystemlnformation() API (即将开始的5字节修改为JMP 10001120指令)。
⑤ stealth.MyZwQuerySystemInformation()函数执行完毕后,由 10001233地址处的RETN 10命 令返回到procexp.exe进程的代码区域,继续执行。
上述过程刚开始看似很难,多看几遍,慢慢就会明白的。
使用API代码修改技术的好处是可以钩取进程中使用的任意API。前面讲过的IAT钩取技术仅适用于可钩取的API,而API代码修改技术无此限制,(虽然代码会更复杂一些)使用起来要自由得多。使用API代码修改技术的唯一限制是,要钩取的API代码长度要大于5个字节,但是由于所有API代码长度都大于5个字节,所以事实上这个限制是不存在的。
顾名思义,API代码修改就是指直接修改映射到目标进程内存空间的系统DLL的代码。进程的其他线程正在读(read)某个函数时,尝试修改其代码会怎么样呢?这样做会引发非法访问(Access Violation)异常(后面会讲解该问题的解决方法)。
接下来继续讲解进程隐藏的工作原理。
33.3 进程隐藏
进程隐藏的相关内容信息已经得到大量公开,其中用户模式下最常用的是ntdll.ZwQuerySystemInformation() API钩取技术,下面对其进行讲解。
33.3.1 进程隐藏工作原理
隐形战机是为了防止雷达探测追踪而运用各种先进科学技术研制的全新战机(与现有战斗机完全不同)。隐形战斗机的隐形对象就是其本身。®而隐形进程的概念与此恰好相反。为了隐藏某个特定进程,要潜入其他所有进程内存,钩取相关API。也就是说,实现进程隐藏的关键不是进程自身,而是其他进程。仍以战斗机为例子,实现进程隐藏的工作原理大致如下:
普通战斗机起飞升空后,通过某种方法使追踪雷达发生故障(人为操作、破坏), 这样雷达就无法正常工作,普通战斗机就变为隐形战机。
虽然例子举得有些牵强,但描述的工作原理与隐藏进程是完全一样的。
33.3.2 相关 API
由于进程是内核对象,所以(用户模式下的程序)只要通过相关API就能检测到它们。用户模式下检测进程的相关API通常分为如下2类(出处:MSDN)。
- CreateToolhelp32Snapshot() & EnumProcess()
1 | |
上面2个API均在其内部调用了ntdll.ZwQuerySystemInformation() API。
- ZwQuerySystemlnformation()
1 | |
借助ZwQuerySystemInformation() API可以获取运行中的所有进程信息(结构体),形成一个链表(Linkedlist)。操作该链表(从链表中删除)即可隐藏相关进程。所以在用户模式下不需要分别钩取CreateToolhelp32Snapshot()与EnumProcess(),只需钩取ZwQuerySystemInformation() API就可隐藏指定进程。请大家注意,我们要钩取的目标进程不是要隐藏的进程,而是其他进程(操作的不是“飞机”,而是“雷达”)。
33.3.3 隐藏技术的问题
假如我们要隐藏的进程为test.exe,如果钩取运行中的ProcExp.exe (或者taskmgr.exe)进程的ZwQuerySystemInfoimation()API,那么ProcExp.exe就无法查找到test.exe。
ProcExp.exe =进程查看器
taskmgr.exe =任务管理器
使用上述方法后,test.exe就对ProxExp.exe (或者taskmgr.exe)进程隐藏了。但是,这种方法存在以下两个问题。
问题一:要钩取的进程个数
检索进程的实用工具真的只有上面2种吗?不是的,除了上面提到的ProxExp.exe与taskmgr.exe)之外,还有众多其他的进程检索实用工具,甚至包含许多用户自己编写的进程查看工具。要想把某个进程隐藏起来,需要钩取系统中运行的所有进程。
问题二:新创建的进程
如果用户再运行一个ProcExp.exe (或者taskmgr.exe)会怎么样呢?由于第一个ProcExp.exe 进程已经被钩取了,所以它查找不到testexe进程。第二个ProcExp.exe进程由于尚未被钩取,所以仍然能正常查找到test.exe进程。
解决方法:使用全局钩取
为了解决以上2个问题,我们隐藏test.exe进程时需要钩取系统中运行的所有进程的ZwQuerySystemInformation() API,并且对后面将要启动运行的所有进程也做相同的钩取操作(当然操作是自动进行的)。这就是全局钩取的概念。由于需要在整个系统范围内进行钩取操作,所 以才用了 “全局”(Global)这个词。
全局API钩取相关内容将在本章后半部分与下一章详细讲解。
下面通过练习示例进一步理解、掌握通过修改API代码的方法钩取API的技术。
33.4 练习 #1 (HideProc.exe,stealth.dll)
HideProc.exe负责将stealth.dll文件注入所有运行中的进程。Stealth.dll负责钩取(注入stealth.dll 文件的)进程的ntdll.ZwQuerySystemInformation() API。接下来我们使用上面2个文件隐藏notepad.exe进程。
上面两个练习文件不能用来解决“全局钩取-新进程”的问题。也就是说,运行HideProc.exe后,新建的进程不会自动钩取,因此这是一种不完全隐藏技术。本练习示例在Windows XP SP3 & Windows 7 (32位)环境中通过测试。
33.4.1 运行 notepad.exe、procexp.exe、taskmgr.exe
首先分别运行notepad.exe (要隐藏的进程)、procexp.exe (钩取对象1)、taskmgr.exe进程(钩取对象2)。
33.4.2 运行 HideProc.exe
运行HideProc.exe,将stealth.dll文件注入当前运行的所有进程,如图33-4所示。

后面的参数(dll path) 一定要使用绝对路径
简要介绍一下HideProc.exe命令的几个参数:
□ -hide/-show: -hide用于隐藏,-show用于取消隐藏。
□ process name:要隐藏的进程名称。
□ dll path:要注入的DLL文件路径。
33.4.3 确认stealth.dll注入成功
使用Process Explorer查看所有成功注入stealth.dll文件的进程,如图33-5所示。

请注意,鉴于系统安全性的考虑,系统进程(PID0&PID4)禁止进行注入操作。
33.4.4查看notepad.exe进程是否隐藏成功
在procexp.exe与taskmgr.exe中可以看到,原来存在的notepad.exe进程消失了(参考图33-6、 图33-7)。


虽然notepad.exe进程的确在运行,但procexp.exe与taskmgr.exe中确实看不到notepad.exe进程,如图33-6与图33-7所示。
由于仍然能看到记事本窗口,所以这种隐藏进程的方法并不算完美。但是请记得,我们的目标只是隐藏线程本身,可以暂时不管程序窗口。
33.4.5 取消notepad.exe进程隐藏
以-show模式运行HideProc.exe命令,将stealth.dll文件从所有进程中卸载,如图33-8所示。

在procexp.exe与taskmgr.exe中查看notepad.exe进程,可以看到它又正常显示。
这里好像需要重启procexp才能看到正常显示的notepad
33.5 源代码分析
下面分析练习示例的源代码,进一步了解通过修改API代码来实现API钩取的技术原理。
所有源代码均使用VC++2010 Express Edition工具开发而成,并在Widows XP SP3 & Windows 7 (32位)系统环境中通过测试。
33.5.1 HideProc.cpp
HideProc.exe程序负责向运行中的所有进程注入/卸载指定DLL文件,它在原有InjectDll.exe程序基础上添加了向所有进程注入DLL的功能,可以认为是InjectDll.exe程序的加强版。
lnjectAIIProcess()
InjectAllProcess()是hideproc.exe程序的核心函数,它首先检索运行中的所有进程,然后分别将指定DLL注入各进程或从各进程卸载。下面分析InjectAllProcess()函数,源代码如下:
1 | |
首先使用CreateToolhelp32Snapshot() API获取系统中运行的所有进程的列表,然后使用Process32First()与Process32Next() API将获得的进程信息存放到PROCESSENTRY32结构体变量pe中,进而获取进程的PID。
以下是CreateToolhelp32Snapshot()、Process32First()、Process32Next() API的函数定义(出处:MSDN):
1 | |
请注意,只有先提升HideProc.exe进程的权限(特权),才能准确获取所有进程的列表。在 HideProc.cpp 中,main()函数中调用了 SetPrivilege()函数,而 SetPrivilege()函数内部又调用了 AdjustTokenPrivileges() API 为 HideProc.exe 提升权限。
获取了进程的PID后,要根据所用的命令选项(-show/-hide)来选择调用InjectDllO函数还是EjectDll()函数。还需要注意的一点是,某进程的PID小于100时,则忽略它,不进行操作。原因在于,系统进程的PID (PID=0, 4,8,…) —般都小于100, 为保证系统安全性,不会对这些进程注入DLL文件(这些PID值来自对Windows XP/Vista/7 OS的分析使用经验,其他Windows版本中,系统进程的PID值可能不同)。
33.5.2 stealth.cpp
实际的API钩取操作由Stealth.dll文件负责,下面分析其源代码(Stealth.cpp)。
SetProcName()
首先看导出函数SepProcName()。
1 | |
以上代码先创建名为“.SHARE”的共享内存节区,然后创缓冲区,最后再由导出函数SetProcName()将要隐藏的进程名称保存到(SetProcName()函数在HideProc.exe中被调用执行)。
在共享内存节区创建g_szProcName缓冲区的好处在于,stealth.dll被注入所有进程时,可以彼此共享隐藏进程的名称(随着程序不断改进,甚至也可以做到动态修改隐藏进程)。
DIIMain()
下面看DllMain()函数。
1 | |
如上所见,DllMain()函数的代码非常简单。首先比较字符串,若进程名为“HideProc.exe”,则进行异常处理,不钩取API。发生DLL_PROCESS_ATTACH事件时,调用hook_by_code()函数钩取API;发生DLL_PROCESS_DETACH事件时,调用unhook_by_code()函数取消API钩取。
hook_ by_code()
该hook_by_code()函数通过修改代码实现API钩取操作。
1 | |
hook_by_code()函数参数介绍如下:
LPCTSTR szDllName: [IN]包含要钩取的API的DLL文件名称。
LPCTSTR szFuncName: [IN]要钩取的API名称。
PROC pfnNew: [IN]用户提供的钩取函数地址。
PBYTE pOrgBytes: [OUT]存储原来5个字节的缓冲区-后面“脱钩”时使用。
正如在工作原理中提到的一样,hook_by_code()函数用于将原来API代码的前5个字节更改为“JMP XXXXXXXX”指令。函数源代码比较简单,结合代码注释很容易理解,中间跳转地址的换算部分是代码逆向分析中相当重要的内容,下面仔细看看。根据Intel x86 (IA-32)指令格式,JMP指令对应的操作码为E9,后面跟着4个字节的地址。
也就是说,JMP指令的Instruction实际形式为“E9 XXXXXXXX”。需要注意的是,XXXXXXXX 地址值不是要跳转的绝对地址值,而是从当前JMP命令到跳转位置的相对距离。通过下述关系式可求得XXXXXXXX地址值。
XXXXXXXX=要跳转的地址-当前指令地址-当前指令长度(5)
最后又减去5个字节是因为JMP指令本身长度就是5个字节。例如,当前JMP指令的地址为402000,若想跳转到401000地址处,写成“E9 00104000” 是不对的,XXXXXXXX地址值要使用上面的等式换算才行。
XXXXXXXX=401000-402000-5=FFFFEFFB
所以JMP指令的Instruction应为“E9FFFFEFFB”,通过OllyDbg的汇编或编辑功能可以确认这一点,如图33-9所示。

除了 JMP指令外,还有一种short JMP命令,顾名思义,它是用来进行短距离跳转的指令,对应的IA-32指令为“EBXX”(指令长度为2字节)。希望各位在OllyDbg 中自己测试一下EB指令
像上面这样每次使用JMP指令都要计算相对地址,显得不太方便。当然,也可以 使用其他指令直接用绝对地址跳转,但是这样的指令长度往往较为复杂。
例(1) PUSH+RET
68 00104000 PUSH 00401000
C3 RETN
例(2) MOV+JMP
B8 00104000 MOV EAX, 00401000
FFE0 JMP EAX
计算32位地址时,使用Windows的计算器显得有些不方便。推荐大家试试32位 的Calculator v1.7 by cybult,它是一款实用性超强的计算器。
关于解析Op代码映射的方法请参考第49章。
实际的ZwQuerySystemInformation() API钩取操作由hook_by_code()函数完成,下面使用OllyDbg对ZwQuerySystemInformation() API钩取前/后进行调试,进一步了解钩取技术原理(相应进程为procexp.exe)。
钩取之前
首先看看钩取前的ZwQuerySystemInformation() API代码。ZwQuerySystemInformation()的地址为77F06238,指令代码如图33-10所示。

钩取之后
注入stealth.dll文件,由hook_by_code()函数钩取API后,代码如图33-11所示。

ZwQuerySystemInformation()函数起始代码做了如下更改(前5个字节):
1 | |
地址 35A1100就是钩取数stealth.NewZwQuerySystemInformation()的地址。并且E9后面的4个 字节(8B69AEC3)就是使用前面的公式计算得到的(希望各位自己算一算)。
示例环境中,Stealth.dll加载到ProcExp.exe进程的350000地址。
unhook_by_code()
unhook_by_code()函数是用来取消钩取的函数,如代码33-5所示。
1 | |
其实,“脱钩”的工作原理非常简单,就是将函数代码开始的前5个字节恢复原值(代码很简单,请参考注释理解)。
NewZwQuerySystemlnformation()
最后,分析钩取函数NewZwQuerySystemInformation()。在此之前,先看看ntdll.ZwQuerySysteminformation() API。
1 | |
简单讲解:将 SystemlnformationClass 参数设置为 SystemProcessInformation(即5)后调用
ZwQuerySystemInformation() API,Systemlnformation [in/out]参数中存储的是SYSTEM_PROCESS_INFORMATION结构体单向链表(single linked list)的起始地址。该结构体链表中存储着运行中的所有进程的信息。所以,隐藏某进程前,先要查找与之对应的链表成员,然后断开其与链表的链接。接下来看看NewZwQuerySystemInformation()函数的代码,了解具体实现方式。
1 | |
对NewZwQuerySystemInformation()函数的结构简要说明如下:
□ “脱钩” ZwQuerySystemInformation()函数;
□调用 ZwQuerySystemInformation();
□检查SYSTEM_PROCESS_INFORMATION结构体链表,查找要隐藏的进程;
□查找到要隐藏的进程后,从链表中移除;
□挂钩(hook) ZwQuerySystemInformation()。
NewZwQuerySystemInformation()函数代码的中间部分有一个while()语句,它用来检PROCESS_INFORMATION结构体链表,比较进程名称(pCur->Reserved2[1])(进程名称为Unicode字符串)。如果掌握了函数的工作原理,再结合代码注释,相信大家在理解上应该没什么困难。
33.6 全局API钩取
本节正式开始讲解全局API钩取的概念及具体实现方法。全局API钩取实质也是一种API钩取技术,它针对的进程为:①当前运行的所有进程;②将来要运行的所有进程。
请注意,前面讲解过的示例程序(HidePmc.exe、stealth.dll)并不是全局API钩取的例子,因为它并不满足全局API钩取定义中的第②个条件。也就是说,虽然运行HideProc.exe将notepad.exe 进程隐藏起来,但是若重新运行新的Process Exploer (或者task manager), notepad.exe进程在它们之中仍然可见。原因在于,运行HideProc.exe后未对新创建的进程(自动)注入stealth.dll文件。有多种方法可以解决这一问题,全局API钩取就是其中一种,下面讲解该技术的具体实现方法。
33.6.1 Kernel32.CreateProcess() API
Kernel32.CreateProcess() API用来创建新进程。其他启动运行进程的API (WinExec()、ShellExecute()、system())在其内部调用的也是该CreateProcess()函数(出处:MSDN)。
1 | |
因此,向当前运行的所有进程注入stealth.dll后,如果在stealth.dll中将CreateProcess() API他一起钩取,那么以后运行的进程也会自动注入stealth.dll文件。进一步说明如下:由于所有进程都是由父进程(使用CreateProcess())创建的,所以,钩取父进程的CreateProcess() API就可以将stealth.dll文件注入所有子进程(父进程通常都是explorer.exe)。怎么样?这个想法不错吧?全局API钩取的 实现方法没有想得那么难,但钩CreateProcess() API时,要充分考虑以下几个方面。
(1) 钩取CreateProcess() API时,还要分别钩取kernel32.CreateProcessA()、kernel32.CreateProcessW()
这2个API (ASCII版本与Unicode版本)。
(2) CreateProcessA()、CreateProcessW()函数内部又分别调用了CreateProcessInternalA()、
CreateProcessInternelW()函数。常规编程中会大量使用CreateProcess()函数,但是微软的部分软件
产品中会直接调用CreateProcessInternelA/W这2个函数。所以具体实现全局API钩取时,为了准确
起见,还要同时钩取上面2个函数(若可能,尽量钩取低级API)。
(3) 钩取函数(NewCreateProcess)要钩取调用原函数(CreateProcess)而创建的子进程的API。
因此,极短时间内,子进程可能在未钩取的状态下运行。
我们进行全局API钩取时必须解决上面这些问题。幸运的是,很多代码逆向分析高手通过努力发现了比kernel32.CreateProcess()更低级的API,钩取它效果会更好(能够一次性解决上面所有问题)。这个API就是ntdll.ZwResumeThread() API。
33.6.2 Ntdll.ZwResumeThread() API
1 | |
用户模式下,NtXXX系列与ZwXXX系列仅是名称不同,它们其实是相同的API。
ZwResumeThread()函数(出处:MSDN)在进程创建后、主线程运行前被调用执行(在CreateProcess()API内部调用执行)。所以只要钩取这个函数,即可在不运行子进程代码的状态下钩取API。但需要注意的是,ZwResumeThread()是一个尚未公开的API,将来的某个时候可能会被改变,这就无法保障安全性。所以,钩取类似ZwResumeThread()的尚未公开API时,要时刻记得,随着OS补丁升级,该API可能更改,这可能使在低版本中正常运行的钩取操作到了新版本中突然无法正常运行。
33.7 练习#2 (HideProc2.exe,Stealth2.dll)
stealth2.dll 用来钩取 CreateProcess,钩取 ZwResumeThread 请参考第34章。本练习示例在Windows XP SP3 & Windows 7 (32位)环境下通过测试。
请注意,为操作简单,本练习中我们将只隐藏notepad.exe。
33.7.1 复制 stealth2.dll 文件到%SYSTEM%文件夹中
为了把stealth2.dll文件注入所有运行进程,首先要把stealth2.dll文件复制到%SYSTEM%文件夹,所有进程都能识别该路径,如图33-12所示。

33.7.2 运行 HideProc2.exe -hide
与以前的HideProc.exe相比,HideProc2.exe只是运行参数发生了改变。由于要隐藏的进程名
称被硬编码为notepad.exe,所以运行隐藏程序时不需要再输入。使用-hide选项运行HideProc2.exe
后,全局API钩取就开始了(请各位使用Process Explorer查看c:\windows\system32\stealth2.dll文件
是否正常注入运行进程),如图33-13所示。

33.7.3 运行 ProcExp.exe¬epad.exe
请运行多个Process Explorer (或者任务管理器)与notepad程序,如图33-14所示。

从图33-14中可以看到,分别运行了2个ProcExp.exe与notepad.exe进程。但是ProcExp.exe中却看不到notepad.exe进程,它被隐藏起来了。大家可以尝试多运行几个ProcExp.exe,最终结果都是一样的,新创建的ProcExp.exe进程中,notepad.exe进程都被隐藏起来、都是不可见的。这就是全局API钩取要实现的效果。
33.7.4 运行 HideProc2.exe -show
运行HideProc2.exe-show命令,撤销全局API钩取操作,如图33-15所示。

现在Process Explorer (或者任务管理器)中又能看到notepad.exe进程了。
33.8 源代码分析
33.8.1 HideProc2.cpp
与前面的HideProc.cpp相比,HideProc2.cpp只是减少了运行参数的个数,相关讲解请参考前面的内容。
33.8.2 stealth2.cpp
与前面的stealth.cpp相比,stealth2.cpp的不同之处在于将要隐藏的进程名称硬编码为notepad.exe,并且添加了钩取CreateProcessA()API与CreateProcessW() API的代码,以便实现全局钩取操作。
DIIMain()
1 | |
从以上DIIMain()函数代码中可以看到,新增了对CreateProcessA()、CreateProcessW() API进行钩取/ “脱钩”的代码。
NewCreateProcessA()
下面看看NewCreateProcessA()函数代码,它是钩取CreateProcessA() API的函数(代码与NewCreateProcessW()几乎一样)。
1 | |
NewCreateProcessA()函数代码比较简单。先执行“脱钩”操作(unhook_by_code),调用执行原函数,再将stealth2.dll注入(InjectDlI2)生成的子进程,最后再钩取(hook_by_code),为下次运行做准备。其中需要注意的是,注入stealth2.dll文件用的函数为InjectDll2()。以前的InjectDll()函数通过PID获取进程句柄进行注入(调用OpenProcess() API),但在上述示例中调用CreateProcessA() API时,能自然而然获得子进程的句柄(lpProcessInformation->hProcess),请留意这一点。
到此我们学习了有关全局API钩取的内容。由于它是一种钩取系统全部进程的技术,所以有时会引发意料之外的错误。使用这项技术前必须仔细测试。另外,钩取尚未公开的API时,一定要检查它在当前OS版本中能否正常运行。
33.9 利用“热补丁”技术钩取API
33.9.1 API代码修改技术的问题
对代码33-8中NewCreateProcessA()函数的结构简单梳理如下:
1 | |
为正常调用原API,需要先①“脱钩”(若不“脱钩”,调用②原始API就会陷入无限循环)。然后在钩取函数(NewCreateProcessA)返回前再次④挂钩,使之进人钩取状态。
也就是说,每当在程序内部调用CreateProcessA()API时,NewCreateProcessA()就会被调用执行,不断重复“脱钩” /挂钩。这种反复进行的“脱钩”准钩操作不仅会造成整体性能低下,更 严重的是在多线程环境下还会产生运行时错误,这是由“脱钩” /挂钩操作要对原API的前5个字节进行修改(覆写)引起的。
一个线程尝试运行某段代码时,若另一进程正在对该段代码进行“写”操作,这时就会出现冲突,最终引发运行时错误。所以我们需要一种更安全的API钩取技术。
《Windows核心编程》一书中曾指出,运用代码修改技术钩取API会对系统安全造成威胁。
33.9.2 “热补丁”(修改7个字节代码)
使用“热补丁”(HotPatch)技术比修改5个字节代码的方法更稳定,本小节将讲解有关“热补丁”技术的内容。
“热补丁”对应的英文为Hot Patch或Hot Fix,与修改5个字节代码的技术不同,使用“热补丁”技术时将修改7个字节代码,所以该技术又称为7字节代码修改技术。
普通API起始代码的形态
讲解“热补丁”技术前,先看看常用API的起始代码部分(参考图33-16至图33-19)。




以上列出的API起始代码有如下2个明显的相似点:
(1) API代码以”MOV EDI,EDI”指令开始(IA-32指令: 0x8BFF)。
(2) API代码上方有5个NOP指令(IA-32指令: 0x90)。
MOV EDI,EDI指令大小为2个字节,用于将EDI寄存器的值再次传送到EDI寄存器,这没有什么实际意义。NOP指令为1个字节大小,不进行任何操作(NOPeration)(该NOP指令存在于函数与函数之间,甚至都不会被执行)。也就是说,API起始代码的MOV指令(2个字节)与其上方的5个NOP指令(5个字节)合起来共7个字节的指令没有任何意义。
很显然,kernel32.dll、user32.dll、gdi32.dll是Windows OS相当重要的库。那么微软到底为什么要使用这种方式来制作系统库呢?原因是为了方便“打热补丁”。“热补丁”由API钩取组成,在进程处于运行状态时临时更改进程内存中的库文件(重启系统时,修改的目标库文件会被完全取代)。
工作原理及特征
要理解“热补丁”钩取方法的核心原理,需要先了解该方法的2种特征。下面使用“热补丁”方法钩取图33-16中的kernel32.CreateProcessA()API,借此理解学习“热补丁”钩取的技术原理。
A.二次跳转
首先将API起始代码之前的5个字节修改为FAR JMP指令(E9 XXXXXXXX),跳转到用户钩取函数处(10001000)。然后将API起始代码的2个字节修改为SHORT JMP指令(EB F9)。该SHORT JMP指令用来跳转到前面的FAR JMP指令处(参考图33-20)。

调用CreateProcessA() API时,遇到API起始地址(7C80236B)处的JMP SHORT 7C802366指 令,就会跳转到紧接在其上方的指令地址(7C802366)。然后遇到JMP 10001000指令,跳转到实际钩取的函数地址(10001000)。像这样经过2次连续跳转,就完成了对指定API的钩取操作(我将这种技术称为“二次跳转”,其优点稍后介绍)。这一过程中需要注意的是,我们修改的7个字节的指令(NOP*5、MOV EDI,EDI)原来都是毫无意义的。
从图33-20中的7C802366、7C80236B地址可以看到,虽然都是JMP指令,但指 令形态不同。7C802366地址处的指令形式为E9 XXXXXXXX,大小为5个字节,被称 为FAR JMP,用来实现远程跳转(可以跳转到进程内存用户区域中的任意位置);而 7C80236B地址处的指令形式为EB YY,大小为2个字节,被称为SHORT JMP,它只能以当前EIP为基准,在-128〜127范围内跳转。IA-32指令中有些相同指令拥有不同指令形态,IA-32指令的解析方法请参考第49章。
B.不需要在钩取函数内部进行“脱钩” /挂钩操作
前面讲解过修改代码的前5个字节进行钩取的技术,使用时需要在钩取函数NewCreateProcessA()内部反复“脱钩” /挂钩,这可能导致系统稳定性下降。
而使用“热补丁”技术钩取API时,不需要在钩取函数内部进行“脱钩” /挂钩操作。在5字节代码修改技术中“脱钩” /挂钩是为了 “调用原函数”,而使用“热补丁”技术钩取API时,在API代码遭到修改的状态下也能正常调用原API。这是因为,从API角度看只是修改了其起始代码的MOV EDI,EDI指令(无意义的2个字节),从[API起始地址+2]地址开始,仍然能正常调用原API,且执行的动作也完全一样。 以Kernel32.CreateProcessA()为例,从图33-16所示的原API起始地址(7C80236B)开始执行,与从图33-20中的[API起始地址+2]地址(7C80236B)开始执行,结果完全一样。由于钩取函数中去除了 “脱钩” /挂钩操作,在多线程环境下使API钩取变得稳定。这正是二次跳转的优势所在。
33.10 练习 #3: stealth3.dll
stealth3.dll文件中使用了 “热补丁” API钩取技术,下面用它练习,如图33-22所示

练习方法与stealth2.dll—样。先把stealth3.dll文件复制到%SYSTEM%文件夹,然后在命令行窗口运行HideProc2.exe命令,如图33-22所示(操作步骤与练习2的步骤1-4相同)。由于HideProc2.exe命令未做改动,像之前一样使用就可以了(隐藏notepad.exe进程的行为是相同的)。
练习示例在Windows XP SP3& Windows 7 (32位)系统环境中通过测试。
33.11 源代码分析
下面分析stealth3.cpp源代码,内容大致与stealth2.cpp类似,主要看与实施“热补丁”技术相关的代码。
stealth3.cpp
hook_by_hotpatch()
首先5析hook_by_hotpatch()函数,它运用“热补丁”技术钩取API。
1 | |
使用“热补丁”技术钩取API时,操作顺序非常重要。首先要将API起始地址上方的NOP*5指令修改为JMP XXXXXXXX。通过下面公式很容易求出XXXXXXXX值(即上述代码中的dwAddress变量),计算公式如下所示:
1 | |
上述公式与前面讲解hook_by_code()函数时介绍的地址计算公式实际是一样的。
XXXXXXXX =要跳转的地址-当前指令地址-当前指令长度(5)
当前指令(NOP*5)地址=pFunc-5,所以上述公式可做如下修改:
XXXXXXXX = (DWORD)pfnNew - ((DWORD)pFunc - 5) -5
=(DWORD)pfnNew - (DWORD)pFunc
*pfnNew =用户钩取函数
*pFunc =原 API 地址
求得XXXXXXXX值后,使用下述代码将NOP *5指令(5个字节大小)替换为JMP XXXXXXXX指令。
1 | |
接下来,将位于API起始地址处的MOV EDI,EDI指令(2个字节大小)替换为JMP YY指令。
1 | |
使用JMP YY指令时,要先计算出YY值,计算公式与前面相同。
YY=要跳转的地址-当前指令地址-当前指令长度(2)
要跳转的地址是pFunc - 5,当前指令地址为pFunc, YY值计算如下:
YY = (pFunc - 5)- pFunc - 2 = -7 =0xF9
“热补丁”技术中,YY值总为0xF9,将其硬编码到源代码就可以了(0xF9是-7 的 “2的补码”形式)。
unhook_by_hotpatch()
接下来分析unhook_by_hotpatch()函数,它在“热补丁”技术中用来取消API钩取操作。
1 | |
上述代码用来将修改后的指令恢复为原来的NOP *5与MOV EDI,EDI指令。“热补丁”技术中这些指令都是固定不变的,所以可以将它们硬编码到源代码。
NewCreateProcessA()
下面分析用户钩取函数NewCreateProcessA()。
1 | |
从上述代码中可以看到,不再调用unhook_by_code()与hook_by_code函数,且与已有函数根本的不同在于添加了计算pFunc的语句,如下所示:
1 | |
该代码语句用于跳过位于API起始地址处的JMP YY指令{ 2个字节,原指令为MOV EDI,EDI),从紧接的下一条指令开始执行,与调用原API的效果一样。
33.12 使用“热补丁” API钩取技术时需要考虑的问题
令人遗憾的是,这么优越的“热补丁” API钩取技术也不是万能的,使用时目标API必须满足它的适用条件(NOP *5指令+MOV EDI,EDI指令),但是有些API却不能满足这些条件(参考图33-23、图33-24)。


并非所有API都能使用“热补丁” API钩取技术,所以使用前先确认要钩取的API是否支持它。若不支持,则要使用前面介绍过的5字节代码修改技术。
ntdll.dll中提供的API代码都较短,钩取这些API时有一种非常好的方法,使用这种方法时先将原API备份到用户内存区域,然后使用5字节代码修改技术修改原API的起始部分。在用户钩取函数内部调用原API时,只需调用备份的API即可,这样实 现的API钩取既简单又稳定。由于ntdll.dll API代码较短,且代码内部地址无依赖性,所以它们非常适合用该技术钩取API。
33.13 小结
通过修改API代码钩取API技术的讲解到此结束。讲解技术的核心内容时用的篇幅较多。各位阅读学习时不要死记硬背,要把重点放在对技术原理的理解上。学完本章以及下一章要介绍的全局API钩取内容,就能够完全掌握API钩取技术。
Q.运行hideproc.exe程序0.5秒后自动终止,为什么会这样?
A. hideproc.exe进程完成所有工作后会自动终止退出,程序就是这样编写的。若任务管理器中看不到notepad.exe进程,就表示执行成功。
Q.执行HideProc.exe -hide abc.exe d:\stealth.dll命令,结果出现如下注入失败信息:
OpenProcess3976 failed!!! OpenProcess4040 failed!!!为什么注入会失败呢?
A. Windows Vista/7中使用了会话隔离技术,这可能导致DLL注入失败。出现这个问题时,不要使用kernel32.CreateRemoteThread(),而使用ntdll.NtCreateThreadEx()就可以了。相关内容请参考Session in Windows 7中的说明。有时开启杀毒软件自身的进程保护功能也会导致DLL注入失败。此外,尝试向PE32+格式的进程注入PE32格式的DLL时,也会失败(反之亦然)。注入时必须保证要注入的DLL文件与目标进程的PE格式一致(PE32+格式是Windows 64位OS使用的可执行文件格式)。
Q.要隐藏的进程在任务管理器的“进程”选项卡中消失了,但在“应用程序”选项卡中仍然可见,且程序窗口也依然可见。如何把程序窗口也一起隐藏起来呢?
A.是的,若只在进程列表中实现了进程隐藏,则程序窗口仍然可见。若将程序窗口也一起隐藏起来,则在任务管理器的“应用程序”选项卡中也消失不见。正文示例的代码只是为了钩取API,对程序的窗口并未作任何处理(程序窗口确实存在,但是进程却消失不见了,我也有意让各位看看这个现象)。就像隐形战斗机,隐形并不是指用肉眼看不到它,而是仅指用雷达探测不到它。DLL文件注入目标进程后,只要调用与窗口隐藏相关的API即可轻松隐藏程序窗 口(SetWindowPos(),MoveWindow()等)。
Q.除了前面介绍过的5字节修改方法之外,还有其他钩取API的方法吗?在钩取函数中反复进行“脱钩” /挂钩操作显得相当麻烦啊!
A.我介绍的5字节修改方法适用范围较广,一般情况下也运行得非常好。除此之外,还有7字节修改方法,在钩取函数中也不需要进行“脱钩” /挂钩操作。但并不适用于所有API, 特别是ntdll.dll提供的原生(native) API就无法使用7字节修改技术。关于7字节“热补丁”技术的内容请参看前面正文。此外还有一种方法是,将API代码全部拷贝到其他地方,但是这需要处理好重定位的问题(该方法非常适用于ntdll的原生API,因为这些API的代码都比较简短)。总之,从应用范围以及简便性方面考虑,5字节修改技术是首选。
Q.使用全局钩取技术注入dll文件时会不会给系统带来很大负担呢?所有进程在创建的时候都要注入dll,那么内存使用量会大幅飙升吧?
A.首先,任何钩取操作都会给系统带来一定负担。编写程序时若能巧妙运用一些手法,则可以将这种对系统的影响降到最低,不会有什么问题,但一定要充分考虑好系统稳定性与资源利用问题。向所有进程注入DLL时,内存使用量也会随之增加,但并不是以“DLL尺寸*注入进程的个数增加。Windows中,相同DLL只要加载到内存中1次即可,进程通过映射技术使用它。简言之,通过映射技术将代码映射到相同内存,即代码区对所有进程都是一样的,而数据区则要根据相应进程重新创建。
参考
《逆向工程核心原理》 第33章