第34章 高级全局API钩取:旧连接控制
本文最后更新于:2022年5月27日 下午
第34章高级全局API钩取:旧连接控制
本章将学习更高级的全局API钩取技术,在示例中我们将钩取IE,使其试图连接到指定网站时转而连接到我的博客。
练习目标是钩取IE进程的API,在它连接到特定网站的过程中,将其连接到其他网站。无论是在IE地址栏中直接输入地址还是点击某个链接,IE都无法连接到被阻止的网站(把这当作拦恶意网页功能就比较容易理解)。
在防火墙层面实现恶意网页拦截功能会更有效果。本章示例仅供学习之用,在实际产品开发中实现恶意网页拦截功能时要充分考虑这一点
34.1 目标 API
API钩取的核心就是选择目标API,即要钩取的API,每个入在这一过程中都有各自不同的绝招。程序开发经验越丰富、API钩取经验越多,对选择目标API就越有利(当然,通过强大的网络检索功能也能解决大部分问题)。开始前先大致“猜测” 一下,只要钩取套接字库(ws2_32.dll)或微软提供的网络访问相关库(wininet.dll、 winhttp.dll)就可以了(钩取后者更容易)。
下面运行IE进行分析。首先使用Process Explorer查看IE加载了哪些DLL。
从图34-1可以看到,IE不仅加载了ws2_32.dll,还加载了wininet.dll库。Wininet.dll提供的API中有个名为InternetConnect()的API (出处:MSDN ),顾名思义,该API用来连接某个网站。
1 |
|
接下来验证Wininet.InternetConnect() API是否就是要钩取的API。
验证:调试旧进程
首先使用OllyDbg附加IE进程,然后在wininet!InternetConnectW() API处设置好断点(InternetConnectW() API是InternetConnect()的宽字符版本),如图34-2所示。
然后在IE地址栏中输入要连接的网站地址,如图34-3所示。
调试器暂停在设置的断点处,此时查看进程栈,如图34-4所示。
从栈信息中可以看到,连接地址(IpszServerName )就是前面在正地址栏中输入的www.baidu.com地址。下面修改连接地址进行测试。
如图34-5所示,将 “www.baidu.com” 修改为 “www.reversecore.com” 字符串。
上面地址全为Unicode字符串,最后以2个字节的NULL (0000)结束(在HEX 窗口中向字符串末尾输入00 00即可)。
修改连接地址后运行调试器,它会在设置断点的wininet!ImernetConnectW()处反复暂停,这是因为一个网站往往由多个链接地址组成。删除断点后继续运行(仅在第一次调用InternetConnectW()时操作一次栈即可)。最终,IE浏览器会连接到修改后的www.reversecore.com网站,而不是先前的www.google.com网站,如图34-6所示。
因此,钩取wininet!InternetConnectW()后,修改IpszServerName参数即可控制IE要连接的网站。看来,钩取!InternetConnectW()API是个非常好的选择。原理相当简单,因为IE使用wininet.dll库,所以很容易钩取API。以上这些就是常用的AM钩取方法,但具体实现时有一点需要考虑,IE8具 有独特的进程结构,钩取时要使用全局API钩取技术。
34.2 旧进程结构
重新运行IE浏览器,打开多个选项卡(tab ),分别连接到不同网站,如图34-7所示。
使用Process Explorer查看IE进程结构,如图34-8所示。
从图34-7与图34-8中可以看到,IE中有2个选项卡,共有3个IE进程(iexplore.exe )在运行。并且PID为4184的iexplore.exe进程与其他iexplore.exe进程形成了父子关系。从IE进程结构来看,IE应用程序为父进程(PID: 3784),它管理着各选项卡对应的子进程。
IE 7开始引入“选项卡”这一概念,进程结构也发生了如上所示的变化。这种新进程结构下,每个选项卡都是一个独立运行的进程,其中一个选项卡发生错误,不会影响到其他选项卡或父进程(IE本身)(最新的网页浏览器中都使用了这项技术)。
像这样,IE应用程序中每个选项卡对应的子iexplore.exe进程实际负载网络连接,创建选项卡进程时,(相关进程的)API就会被执行钩取操作,即采用全局API钩取技术钩取。否则,在新选项卡中连接网站时将无法钩取。前面我们介绍了通过钩取kernel32!CreateProcess() API实现全局API钩取的方法,并且说明了使用CreateProcess() API钩取这一方法的限制条件。
本章将介绍一种更安全、更方便的全局API钩取方法,采用该方法可有效消除因使用CreateProcess() API钩取技术实现全局API钩取而产生的不便。这种新方法是,钩取ntdll!ZwResumeThread()API,创建进程之后,主线程被Resume (恢复运行)时,可以钩取目标API。
34.3关于全局API钩取的概念
下面对全局API钩取进行简单整理。通过前面的学习,我们已经能对特定进程的指定API进
行简单的钩取操作。
34.3.1 常规API钩取
使用常规API钩取方法时,每当(要钩取的)目标进程被创建时,都要钩取指定API。图34-9描述了通过DLL注入技术实施常规API钩取操作的情形。
图34-9中要钩取的目标进程为Test.exe(PID: 2492 )。使用InjDll.exe程序将Hook.dll注入Test.exe进程,然后钩取指定API①。若后面生成了另外一个Test.exe进程(PID: 3796),则必须先向它注入Hook.dll才能(对PID为3796的进程)实现正常的API钩取操作②。也就是说,每当要钩取的目标进程生成时都要手动钩取API。
34.3.2全局API钩取
接下来看一下全局API钩取的操作过程,如图34-10所示。
InjDll.exe负责将gHook.dll注入Explorer.exe进程(Windows操作系统的基本Shell )。请注意,我们要钩取的进程不是Test.exe,而是启动运行Test.exe的Explorer.exe进程,这是最核心的部分。gHook.dll扩展了图34-9中Hook.dll的功能,它钩取创建子进程的API,每当子进程被创建时,它都会将自身(gHook.dll)注入新创建的进程(参考图34-10 )。所以向Explorer.exe进程(Windows Shell)注入1次gHook.dll后,随后Explorer.exe创建的所有子进程都会自动注入gHook.dll。这就是自动API钩取的基本概念,将这一概念扩展应用到系统中运行的所有进程,就形成了全局API钩取。
除Explorer.exe之外,其他进程也可以创建子进程。所以要完美实现全局API钩取,必须钩取当前运行的所有进程。但是基于系统稳定性与减少不必要系统开销的考虑,常常(根据实际要求)仅钩取特定进程(示例中对IE的钩取就是典型例子)。
到此我们已经梳理了全局API钩取的概念。下面分析钩取哪些API才能使全局API钩取实现起来更容易。
34.4 ntdll!ZwResumeThread() API
首先,想想创建子进程的API有哪些,创建进程的API中最具代表性的绝对是kernel32!CreateProcess() API。下面编写一个简单的程序来测试CreateProcess() API,代码如下所示。
所有源代码均使用MS Visual C++Express Edition 2010工具编写而成,在Windows 7 32位& IE 8中通过测试。
编译代码34-1,生成cptest.exe可执行文件。调试这个文件可以把握与进程创建相关的API调用流程。
1 |
|
CreateProcessW是CreateProcess 的宽字符(UNICODE)版本。
图34-11是调用cptest.exe的kernel32!CreateProcessW()的代码。
跟踪进入kernel32!CreateProcessW(StepInto(F7)),可以看到在其内部又调用了kernel32!CreateProcessInternelW(),如图34-12所示。
在图34-12中查看下方栈内存,可以看到它与图34-11中的栈(函数参数)几乎是一样的。继续跟踪进入kernel32!CreateProcessInternelW(),如图34-13所示。
kernel32!CreateProcessInternelW()是一个相当大的函数。在代码窗口中向下拖动滑动条,就会岀现调用ntdll!ZwCreateUserProcess()的代码,如图34-14所示。
这里没找到Zw,找的是Nt
在图34-14中查看下方的栈,可以看到它与图34-12中的栈有着非常大的不同。第二个参数(Arg2)是一个结构体,查看左侧的Hex dump窗口可以发现,结构体成员中,地址12F954存储的12FD3C是字符串(“notepad”)的地址(参考图34-11 中的栈)。调用Ntdll!ZwCreateUserProcess()时子进程就会被挂起(Suspend ),暂停运行,如图34-15所示。
notepad.exe进程已经生成,但是其EP代码尚未运行。在图34-14代码中继续执行,就会出现调用ntdll!ZwResumeThread() API 的代码,如图34-16所示。
顾名思义,ntdlllZwResumeThread()函数就是用来恢复运行线程的。该线程即是子进程(notepad.exe)的主线程。所以调用执行该API时,子进程的EP代码才会执行,如图34-17所示。
综上所述,CreateProcessW() API的调用流程整理如下:
1 |
|
创建子进程的过程中最后被调用的API是ntdll!ZwResumeThread()。所以钩取该API,在子进程的EP代码运行之前,拦截获取控制权,然后钩取指定API。ntdll!ZwResumeThread()是尚未公开的API,函数定义(出处:MSDN)如下:
1 |
|
用户模式中 ntdll!ZwResumeThread() API 与 ntdll!NtResumeThread() API 虽然名称不同,但其实是同一函数。
前面介绍的4个API(CreateProcessW、CreateProcessInternalW、ZwCreateUserProcess、ZwResumeThread)中,无论钩取哪个API,都能实现我们的目标—全局API钩取(下面练习中将钩取ntdll.ZwResumeThread() API )。
若钩取位于上层的CreateProcessW()函数,则在某个特定情形下(如直接调用CreateProcessInternelW时)可能导致无法正常钩取。所以最好钩取CreateProcessInternelW()下层的函数(各有优缺点,建议各位都尝试一下)。
34.5练习示例:控制旧网络连接
下面做个练习,目标是控制IE的网络连接。钩取IE进程的特定API,用IE连接指定网站时,使之连接到另外一个网站(www.reversecore.com)。此外,在IE中添加新选项卡,同时比较新添加的进程的情形,进一步了解全局API钩取技术。
本练习示例在Windows XP SP3、Windows 7 32位操作系统及IE 8中通过测试。
示例练习中,我们将向目标进程注入redirect.dll来实现API钩取。redirect.dll钩取下面2个API。
1 |
|
34.5.1 运行IE
首先运行IE浏览器,然后使用Process Explorer查看运行中的IE进程的结构。
从图34-18中可以看到,IE进程以父子进程的形式运行。只要钩取父进程ntdinZwResumeThread()API,那么后面生成的所有子IE进程都会自动钩取。
34.5.2 注入 DLL
首先在命令行窗口中使用InjDll.exe命令,将redirect.dll文件注入IE进程(iexplore.exe),如图34-19所示。
使用Process Explorer工具查看redirect.dll文件是否正常注入IE进程,如图34-20所示。
InjDll.exe是专门用于注入的程序。更多相关说明请参考第44章“DLL注入专用工具”。
34.5.3 创建新选项卡
在IE浏览器中创建新选项卡,如图34-21所示。
使用Process Explorer工具可以看到,redirect.dll已经成功注入新选项卡进程(PID:3472),如图34-22所示。
由此可见,通过钩取ntdll!ZwResumeThread()API成功实现了全局API钩取。
34.5.4尝试连接网站
在IE任意一个选项卡中尝试连接下列网站。
www.naver.com
www.daum.net
www.nate.com
www.yahoo.com
如图34-23所示,虽然地址栏中输入的是yahoo,但是浏览器实际跳转到了网站ReverseCore。
34.5.5 卸载 DLL
下面从IE进程卸载(Unloading) redirect.dll文件,如图34-24所示。
使用Process Explorer工具可以看到redirect.dll已成功卸载(参考图34-25 )。
现在使用IE浏览器重新连接Naver,可以看到IE正常连接,如图34-26所示。
34.5.6 课外练习
请各位使用前面介绍的InjDll.exe与redirect.dll文件再多做些课外练习。练习做得多了,才能 真正理解全局API钩取技术的原理与含义。
钩取所有进程。
仅钩取explorer.exe (以后运行IE )。
34.6 示例源代码
本节讲解主要函数(为讲解方便,代码中省略了异常处理部分)。
34.6.1 DIIMain()
首先看看DllMain()函数。
1 |
|
DllMain()函数的核心功能是ntdll!ZwResumeThread()与wininet!InternetConnectW() API的“挂钩/脱钩”功能。其中有条语句显得比较特别,若运行的进程名为iexplorer.exe时,则加载wininet.dll文件。iexplorer.exe进程正常运行时,自然会加载wininet.dll,为什么还要特意增加一条语句加载它呢?这与全局API钩取的特性有关。钩取ntdll!ZwResumeThread()API时,需要在相关进程的主线程开始之前拦截控制权,此时,我们要钩取的wininetdll模块可能尚未加载。若模块未加载就钩取其内部API,将导致失败。为防止出现这类问题,进程为iexplore.exe时,钩取wininet! InternetConnectW() AJPI之前必须加载wininet.dll文件。
示例代码中,我们使用5字节修改技术钩取Wininet.InternetConnectW() API,当然使用7字节修改技术也是可以的。但是钩取ntdll.ZwResumeThread()API只能使用5字节修改技术(由于没有足够的空间,所以无法使用7字节修改技术)。关于7字节修改技术(“热补丁”)请参考第33章。
34.6.2 NewlnternetConnectW()
wininet!InternetConnectW()的钩取函数为NewInternetConnectW()函数,它负责监视IE的连接地址,IE尝试连接到特定网站时,将其转到我们指定的网站。以下是NewInternetConnectW()函数的代码。
1 |
|
从以上代码可以看到,NewInternetConnectW()函数代码并不复杂。函数的第二个参数IpszServerName字符串即是要连接的网站地址。监视该连接地址,IE连接的是特定网站(Naver、Daum、Nate、Yahoo)时,就将连接地址修改为我的博客地址(ReverseCore )。
34.6.3 NewZwResumeThread()
NewZwResumeThread()函数用来对ntdll!ZwResumeThread() API进行全局钩取,代码如下:
1 |
|
NewZwResumeThread()函数的第一个参数是要恢复运行的线程句柄(ThreadHandle)。前面说明中已经指出,该线程即是子进程的主线程。在NewZwResumeThread()函数的前半部分调用ZwQueryInformationThread() API,就是为了获取线程句柄所指线程(子进程的线程)所属的子进程的PID。像这样,通过线程句柄参数即可获得(刚刚创建的)子进程的PID,然后使用该PID就可以注入redirect.dll (钩取DLL)文件。相关子进程在主线程运行前就已经注入redirect.dll文件,自动实现了API钩取。最后正常调用ntdll!ZwResumeThread()API,将子进程的主线程恢复运行。这样,子进程就在API被钩取的状态下得以正常运行。
钩取 ntdll!ZwResumeThread() API 比构取 kernel32!CreateProcess() API 更强大、更方便。因为CreateProcess()在内部调用了 CreateProcessInteraal()0如果在程序中直接调用CreateProcessInternel(),则无法正常钩取(此时直接钩取CreateProcessInternel()反而更好)。像这样,越钩取低级API (ntdll.dll中提供的API),效果越好。但是大部分低级API尚未文档化,根据OS版本不同可能变化。相反,高级API (kernel32.dll级 别-公开的)一般不会变化,文档化做得也非常好,用来钩取是比较稳定的,但是钩取性能要差一些。所以,高级API钩取与低级API钩取各有长短,使用时要根据具体情况选择,这才是明智的做法。
34.7小结
本章分析了练习示例的源代码,进一步学习了全局API钩取的实现原理,并借此掌握了有关API钩取的所有知识。若想成为API钩取专家就要不断尝试,经历大量失败来积累丰富的实战经验。接触并解决各类问题才能逐渐提高代码逆向分析水平,相信大家会对此深有体会。