跟踪Native API函数调用.docx
《跟踪Native API函数调用.docx》由会员分享,可在线阅读,更多相关《跟踪Native API函数调用.docx(18页珍藏版)》请在冰豆网上搜索。
跟踪NativeAPI函数调用
跟踪Native API函数调用
跟踪NativeAPI函数调用
序言
我们来研究一样非常有用的东西。
我甚至要说,在某些情况下,离了它是寸步难行的,而且它能让我们对Windows的内部机制有个很好的认识。
是的,本文的题目已经是不新鲜的已经被讲滥了的东西了。
在这方面有众多的文章甚至是专著,作者也都是著名的专家,像JeferryRitcher,MattPetriek,SvenSchreiber,MarkRussinovich等等。
是的,值得注意的是这些内容都是针对WindowsNT产品线的,对9x来说就不成了。
为了更有效地掌握文章的内容,您应该具有以下知识:
*C语言
*使用WindowsNT 2K/XP/2K3操作系统
*了解进程,线程和WindowsNT系统的基本结构。
*知道x86处理器如何在保护模式下工作
*DDK(我们这里用的是DDK2KBuild2195)
*SoftIce.我用的是DriverStudiov2.7。
一定要安装完整的Studio版,而不要用那种分离出来的Ice,否则会有兼容性问题。
然而这个问题在论坛上讨论了不是一两次了。
我们有什么?
就我从Ritcher写的书和文章(我为什么要把Ritcher搬出来说事儿呢?
对,因为它的书和文章都翻译成了俄语且广泛流行,更重要的是他的方法是比较标准的)里学到的来看,作者从事于直接在用户模式下拦截API函数的处理与研究。
对我们的意义何在呢?
这里有个对比:
方法1:
直接修改进程的import/exporttable,将原始函数的指针改为指向我们的stub函数。
优点:
1.实现起来非常简单(因此在JeferryRitcher的“ProgrammingApplicationsforMicrosoftWindows”一书中对其进行了详尽的讲解)。
此方法保证了进程地址空间不会受到破坏。
2.出现问题的时候只会宕掉试验用的进程,不会使整个系统崩溃。
缺点:
1.必须用某些方法向其它进程的地址空间中注入自己的代码(设置全局HOOK(SetWindowsHookEx()),通过注册标加载DLL,直接向进程地址空间注入代码(WriteProcessMemory(),CreateRemoteThread()…))。
2.不能对任意进程进行此种操作,因为WinNT为系统中所有的对象都赋予了一个securitydescriptor,这个securitydescripter定义了一个对象能否拥有另一对象以及拥有的程度。
3.如果被拦截的系统函数A本身基于另一个系统函数B而进程突然直接调用了B,这时该怎么办?
这时又得找到函数B并对其进行拦截。
4.所跟踪到的函数调用情况只是针对于“被处理过”的进程,而不是所有的进程。
方法2:
修改包含所要跟踪的函数的那部分库代码(splicing法,例如在函数起始部分注入指向我们处理程序的机器指令Jmp0xXXXXXXXX)。
优点:
不要需要修改进程模块的import表!
在Win9X中此法可以跟踪到所有进程对系统库函数的调用。
缺点:
1.有让系统宕掉的危险,因为在修改库代码的时候可能会发生上下文的切换,如果修改尚未完成而被修改的函数被另一个进程或线程调用的话,则发生异常的可能性非常大,而可能更坏的是,系统会挂起或者是发生BSOD。
如果此函数的拦截代码位于非调用进程的进程上下文中,也会有同样的问题。
2.要实现这种方法必须修改库代码的segments,而这些segments却有着“ReadExecute”属性,但修改还是可以的。
最后还有一种办法——特别是用在Windows的调试机制中(DebugAPI)。
讲到这里,我想大家都能明白。
顺便说一句,在wasm.ru有相当多的文章是讲前面这些拦截方法的。
特别值得注意的是90210/HI-TECH的文章,所有这些方法在那篇文章里都有。
现在我们假设我们需要监视所有的进程来取得某些资源使用情况和对这些资源的操作(创建/打开/读取/写入某个文件、注册表、修改某个进程页的保护属性等等)。
准备好了么?
现在请再看一下前面的内容并告诉我,借助于前面讲的方法这可能实现吗?
当然,在读者当中一定会有乐观主义者认为是可以的,但这说起来容易做起来难。
但是有一种办法更为简单,最主要的是它更为有效。
NATIVEAPI
我们知道,Windows2000的设计理念是它不止能运行Win32应用程序,还能运行旧的Win16,MsDos,Os/2v1.x程序和POSIX程序。
简单的说,系统里内建了三个子系统,每个子系统执行自己的程序。
实际上,没人能影响我们向系统中添加对其它操作系统的支持,所要做的只是编写程序并将他绑定到相应的子系统模块上就行了。
然而,程序是怎么工作的呢?
为了回答这个问题我们来看下面这幅图。
(图欠奉)
如果所启动的程序不是Win32应用程序,而是其它操作系统的程序,此时Win2k会寻找与其相对应的能识别并启动此程序的子系统。
如果没找到,则会返回错误。
我们来详细讲一下。
任何调用,比如说OS/2应用程序的,会由此应用程序的支持模块转换为Win32子系统库中的相应的函数,这个Win32子系统库就是KERNEL32.DLL。
KERNEL32.DLL会将调用转到NTDLL.DLL中的一个函数。
(斜体加粗)尽管如此,Win32应用程序可以直接调用ntdll.dll模块中的函数,甚至连这一步都可以省掉,直接调用int0x2e。
(斜体加粗小号字体)只是像这样做的应该都是些特殊的应用程序,显然它们应该知道作些什么。
然而,在WinNT中这样的程序并不少见,而且它们都被成为服务,确切讲叫服务应用程序。
其中的一个例子就是众所周知的Svchost.exe。
这样的应用程序只能存在于WinNT中,在Win9x中则不能工作。
现在我们开始讲最有意思的东西。
实际上ntdll.dll这个库中的所有函数都是某种stub,看上去是下面这个样子:
.text:
77F84F40 publicZwWriteFile ;NtDll!
ZwWriteFile
.text:
77F84F40ZwWriteFile procnear
.text:
77F84F40
.text:
77F84F40arg_0 =byteptr 4
.text:
77F84F40
.text:
77F84F40 mov eax,0EDh ;NtWriteFile
.text:
77F84F45 lea edx,[esp+arg_0]
.text:
77F84F49 int 2Eh
.text:
77F84F4B retn 24h
.text:
77F84F4BZwWriteFile endp
我们来分析一下这段代码。
函数ZwWriteFile()在堆栈中接收9个参数,每个都是4个字节长。
(斜体加粗)几乎所有关于NativeApi函数的信息都能在GarryNebetta的《WindowsNT/2000NativeAPI参考》一书中找到。
但是要强调的是,本书是在Win2K时代写成的,所以在书中只讲了Win2K中存在的函数,而在新版的5.1(WinXP)和5.2(Win.Net)内核中添加了数量未知的新函数,遗憾的是对这些函数的描述我还没有找到。
(斜体加粗)还有一个地方值得一提——函数的名称。
正如您看到的,前缀分别为Ntxxx和Zwxxx的函数是一样的。
如果看一下NTDLL.DLL模块的反汇编代码的话,就会发现除了前缀不一样外这两种函数没有区别——Nt与相应的Zw函数指向的是同一处代码。
在模块NTOSKRNL.EXE中情形又有些不同。
前缀为Zw的函数,实际上是由此模块导出的函数,并且是直接调用的。
而前缀为Nt的函数则需要在相应的Zw函数取得控制之前做一系列的安全验证。
然而,我建议不要为这个问题太费脑筋。
我们将看到,这里直接使用到两个寄存器。
在EAX寄存器中放的是函数的系统调用号(index,我们后面会讲到)。
在EDX寄存器中放的是函数参数堆栈的指针,这个指针的值减去4个字节就是函数返回点的地址。
之后就是使用中断调用指令INT0x2E进行处理器模式的切换,这个中断被保留用作系统调用handlers的入口点。
这正是我们所需要的。
实际上,所有用户模式(UserMode)应用程序发出的系统函数调用最终都要发向内核并通过INT0x2E,而通过这个INT0x2E处理器从ring3切换到了ring0,即KernelMode。
好,到这里系统调用门儿外的情况我们已经分析了,现在我们看一下门儿里面的东西。
KiSystemService&ServiceDescriptorTable
;_KiSystemService:
.text:
00464FCD push 0 ;被M$称为ENTER_SYSCALLmacro的代码块,不只在这个函数的prolog中能见到。
;最有意思的是从3.51版的WindowsNT开始,这个函数就没再发生过变化。
在堆栈中保存主要的寄存器。
.text:
00464FCF push ebp
.text:
00464FD0 push ebx
.text:
00464FD1 push esi
.text:
00464FD2 push edi
.text:
00464FD3 push fs
;向FS中加载PCR指针
.text:
00464FD5 mov ebx,30h
.text:
00464FDA db 66h
.text:
00464FDA mov fs,bx
;在堆栈中保存前exceptionhandler
.text:
00464FDD push dwordptrds:
0FFDFF000h
;初始化新的exceptionhandler链表,就是叫做SEHframe的东西。
.text:
00464FE3 mov dwordptrds:
0FFDFF000h,0FFFFFFFFh
;取得当前线程结构体的地址
.text:
00464FED mov esi,ds:
0FFDFF124h
;在堆栈中保存前User/Kernel模式以及相关的地址
.text:
00464FF3 push dwordptr[esi+134h]
.text:
00464FF9 sub esp,48h
.text:
00464FFC mov ebx,[esp+6Ch]
.text:
00465000 and ebx,1
.text:
00465003 mov [esi+134h],bl
; 修正当前的stackframe
.text:
00465009 mov ebp,esp
;保存当前的stackframe
.text:
0046500B mov ebx,[esi+128h]
;建立新的stackframe
.text:
00465011 mov [ebp+3Ch],ebx
.text:
00465014 mov [esi+128h],ebp
.text:
0046501A cld
;到这儿就有意思了,验证当前线程是否正被调试,如果是的话,可以自己通过地址看。
.text:
0046501B test byteptr[esi+2Ch],0FFh
.text:
0046501F jnz loc_464F49
.text:
00465025
.text:
00465025loc_465025:
.text:
00465025
.text:
00465025 sti
.text:
00465026
.text:
00465026loc_465026:
.text:
00465026
_KiSystemServiceRepeat:
;向edi寄存器拷贝服务号以进行下面的操作
.text:
00465026 mov edi,eax
.text:
00465028 shr edi,8
.text:
0046502B and edi,30h
.text:
0046502E mov ecx,edi
;取得线程descriptortable指针(每个线程都有一个自己的SERVICE_DESCRIPTOR_TABLE结构体,因为_KTHREAD中就保存有指向它的指针)
.text:
00465030 add edi,[esi+0DCh]
.text:
00465036 mov ebx,eax
;还有一项验证,突然此调用进入了驱动程序win32k.sys,抑或者这个服务根本就不存在?
.text:
00465038 and eax,0FFFh
.text:
0046503D cmp eax,[edi+8]
.text:
00465040 jnb loc_464E02
.text:
00465046 cmp ecx,10h
.text:
00465049 jnz shortloc_465065
;取得当前TEB的地址,之后又有一个问题,这个调用是GDI类型的吗?
.text:
0046504B mov ecx,ds:
0FFDFF018h
.text:
00465051 xor ebx,ebx
.text:
00465053 or ebx,[ecx+0F70h]
.text:
00465059 jz shortloc_465065
.text:
0046505B push edx
.text:
0046505C push eax
.text:
0046505D call dword_482220
.text:
00465063 pop eax
.text:
00465064 pop edx
.text:
00465065
.text:
00465065loc_465065:
.text:
00465065
;增加系统调用计数器。
这个计数器是性能计数器中的一个,供PerformanceMonitor之类的程序使用
.text:
00465065 inc dwordptrds:
0FFDFF5DCh
;在esi中放入用户参数堆栈的指针。
还记着ntdll.dll库中的ZwWriteFile函数的stub中的edx寄存器吗?
我希望您还记着为什么这里使用堆栈
;指针的参数传递机制,而不是通常的堆栈方式。
因为在调用了0x2e并进行了模式切换之后,处理器向SS中加载了一个值。
这个值之前保存在
;TSS段(0x28)里,现在它将指向GDT表中的另一个descriptor。
因为这个结构体所有线程都有一个(对于DoubleFault(_KiTrap08)的处理
;用的是自己的TSS段,不用考虑得太周全),这样在模式切换后会对当前线程堆栈进行调整。
线程的用户模式下的堆栈和内核模式下的是不一
;样的,因为如果一样的话,则我们只会在屏幕上看见一样东西——BSOD。
除此之外,具体线程间的堆栈也是不一样的。
对此您可一定要有个
;清晰的认识。
我们继续来看系统服务函数的代码。
.text:
0046506B mov esi,edx
取得服务参数表KiArgumentTable的指针。
.text:
0046506D mov ebx,[edi+0Ch]
为了文章的完整性,我们来对servicetable及其参数进行一点研究。
来看下图:
(图欠奉)
这就是我们的descriptortable。
请注意灰白色的区域。
实际上,我只着重区分出了我们真正感兴趣的域。
剩下的域一般都是NULL指针或是零值。
现在我们来看这些东西用C语言如何描述。
typedefstruct_SERVICE_DESCRIPTOR_TABLE
{
SYSTEM_SERVICE_TABLENtoskrnlTable; //ntoskrnl.exe(nativeapi)
SYSTEM_SERVICE_TABLETable2; //空闲
SYSTEM_SERVICE_TABLETable3; //用于InternetInformationServices
SYSTEM_SERVICE_TABLETable4; //空闲
}
SERVICE_DESCRIPTOR_TABLE,
*PSERVICE_DESCRIPTOR_TABLE,
**PPSERVICE_DESCRIPTOR_TABLE;
typedefstruct_SYSTEM_SERVICE_TABLE
{
PNTPROCServiceTable; //指向handler入口点数组的指针
PDWORD CounterTable; //服务调用计数器(未使用)
DWORD ServiceLimit; //所支持的服务的数目
PBYTE ArgumentTable; //服务参数数组指针
}
SYSTEM_SERVICE_TABLE,
*PSYSTEM_SERVICE_TABLE,
**PPSYSTEM_SERVICE_TABLE;
(加粗)除了这个由Ntoskrnl.exe公开导出的结构体外,在WinNT中还有一个这样的结构体,名为ServiceDescriptorTableShadow,这个结构体未被导出,只在模块内部使用。
(斜体加粗)如果内部的函数名没有被导出,则就意味着这个函数我们无需了解。
我们有非常简单的办法可以对它们进行访问。
总之一定要使用一种调试器,或是DDK中的KernelDebugger(i386kd.exe)、WinDbg(windbg.exe),或是我的最爱——SoftIce,但是使用前一定要设置好调试符号,这些调试符号可以从微软的站点上下载。
除此之外,如果您已经弄好了调试符号(参见PDBFile或是Ctrl+F12)可以轻松地搞定相关的.PDB文件。
(加粗)在Windows2000里它就在在ServiceDescriptorTable后面,可以很快地找到。
但是并非在所有版本的WindowsNT中都是这样。
这个结构体的有趣之处在于,除了NtoskrnlTable域,第二个域的值并不为零,而是一个SystemServiceTable结构体指针,指向GDI函数入口点的数组,而这些GDI函数都位于驱动程序Win32k.sys中。
我们知道,在WinNT中图形部分被封装进了内核,这是为了相应的进程能运行得更快。
但是,大多数情况下,人们对这一问题都不太在意,因为很少有人对拦截图形函数感兴趣。
现在我们来更详细地讲一下这个结构体。
我们将看到,KeServiceDescriptorTable结构体中有四个完全相同的SystemServiceTable结构体。
其中只有第一个结构体被赋值,我把这个结构体叫做NtoskrnlTable。
SystemServiceTable结构体由四个域构成。
我们来逐一讲解。
ServiceTable–指向系统服务handler入口点数组的指针。
让我们感兴趣的是,有一些入口点指向的函数是存在的,并且被Ntoskrnl.exe导出了,而另一些则不是这样。
(斜体加粗)例如,函数NtCreateProcess并未被导出,然而入口点却指向了某段代码,这段代码实现了相应的功能。
所以在内部,比如说在驱动程序里,实现创建进程这样的功能的时候工作就变得复杂,因为我们不可能使用已经准备好的导出函数,而且除了KiSystemService函数之外,没有一个通用的接口。
但是NtCreateProcess只是冰山的一角,与进程创建有关的大部分准备工作都封装在了Kernel32.dll和Ntdll.dll之中。
这样的函数可不止这一个。
如果您急切地想在驱动中实现类似的操作,请先坐下来,然后分析WinNT的实现方法。
这个活儿可不是那么轻松的,而且要求要有大量的相关知识和相当的耐性,除此之外更为复杂的是,这段代码可能广泛分布于不同的模块中,而不止是局限在某个函数的内部。
您得好好想想您到底要干什么,但是在Windows9x中在内核模式下创建进程是可以轻松完成的,这靠的是驱动程序Shell.vxd的“导出”函数_ShellExecute。
对于为什么Microsoft在WindowsNT中采取了这样的措施,我脑子里只有一个答案,那就是为了系统的安全。
CounterTable–一个内核变量的指针,这个内核变量被用作KiSystemService使用次数的计数器,只在checkedbuild版本里才有值。
在普通版本里其值为NULL。
ServiceLimit–此版本WindowsNT实现的服务函数的数目。
例如在Windows2000build2195,此值为248(0xF8),而在WindowsXPbuild2600里则为284(0x11C)。
显然这个值增大了。
ArgumentTable–指向参数数目字节数组的指针。
这些参数都是以字节为单位传给函数的。
也就是说,如果函数NtAcceptConnectPort在堆栈中接收0x18个字节,那就是6个双字。
双字在ServiceTable数组中的序号对应着ArgumentTable表中有此相同序号的成员。
除了上面所讲的,还有一个有意思的细节值得我们注意。
您可能已经注意到,在结构体ServiceDescriptorTable中最好的位置上只占据了两个SystemServiceTable结构体(除了Ntoskrnl.exe外,第二个可能会被IIS服务占用)。
自然还剩下了两个,用户可以用其来想系统中添加自己的服务。
为此需要调用KeAddSystemServiceTable函数,而这个函数在DDK中有说明。
我想,到这里已经全都讲清了。
我们继续拆解_KiSystemService的代码。
;向CL加载相应的ArgumentTable数组成员
;来确切地知道要向内核堆栈传递多少个字节
.text:
00465070 xor ecx,ecx
.text:
00465072 mov cl,[eax+ebx]
.text:
00465075 mov edi,[edi]
;向EBX寄存器中加载相应服务函数的入口点
.