函数调用机制(1)
这节作为同步知识的扩展,我们开始来研究。
函数可以分为Win32 API函数,面向驱动程序开发而导出的函数,SSDT里包含的有实质性作为的函数,更加底层的函数
函数还可以分为导出函数,非导出函数。
当然函数还可以分为用户程序可以调用的和内核中可以调用的这两种。
如果更细点,函数还可以分为过渡函数,非过渡函数。
函数的分类是分好了。新的问题来了:这些函数都从哪来的?
现在引入2个目标:ntdll.dll ntoskrnl.exe
在应用程序编程的时候我们经常使用的动态库有: kernel32.dll, winmm.dll, gdi32, Netapi32.dll, advapi32.dll, user32.dll 等等。他们所导出的函数就是Win32 API函数。
需要说明的是Win32 API函数都是过渡函数,这些函数总会调用Ntdll.dll库里相对应的函数。现在我们不妨来看看ntdll究竟导出了哪些函数。经过观察,它导出的函数有几种类型:Rtl, Csr, Alpc, Exp, Dbg, Etw, Ki, Ldr, Nls, Nt, Tp, Win, Zw。其中我们接触的比较多的是Rtl,Zw,NT,Ldr。
Ntdll.dll称为系统库,但是他被映射的是用户地址空间。也就是说他不是内核的一部分。他其实是通往内核的大门。应用程序是可以直接调用ntdll里的函数的。虽然这样做的人很少。
继续。现在来看ntoskrnl.exe。它运行在内核中。它导出的函数都必须在内核中使用。并且还分两类。其中一类是为驱动程序编程而准备的。其中另一类是真正有实际性作为的函数。也就是说前面绕了半天都是过渡函数,这里讲的第二类函数才是真正包含能解决问题的代码。
那么我们联系起来。有2条路线:
1,从Win32API函数到我刚才说的具有实际作为的函数
2,从驱动程序所使用的函数到具有实际作为的函数。
关于第一条路线:当用户程序调用ReadFile()函数,我们知道此函数在kernel.dll中。那么程序会运行kernel32库里的ReadFile()。kernel32库里的这个函数几乎不做任何实质性的事情,它只是调用ntdll库中的NtReadFile()函数。紧接着ntdll中的NtReadFile()函数通过中断的方式调用ntoskrnl.exe中的NtReadFile()函数。而ntoskrnl.exe中的NtReadFile()函数才是真正有作为的函数。这样,这个流程就通了
关于第二条路线:你在驱动程序中是不能调用Win32 API了,而应该调用ntoskrnl专门为驱动程序编程而导出的函数。这里应该调用ntoskrnl所导出的ZwReadFile()函数。和上面一样,这个函数也是通过中断的方式调用ntoskrnl中的NtReadFile()函数。
说到这里,读者肯定会有很多疑问。其实是这样的。
还是拿ReadFile为例。对于ntdll.dll,他导出两个名字不同的函数,分别是ZwReadFile()和NtReadFile()。但是这2个函数的地址是重合的,也就是说只是名字不同而已。继续,刚才我说过,ntdll是用户空间库,里面的函数也是运行在用户空间,而ntoskrnl里的NtReadFile()是运行在内核空间的。那么ntdll中的NtReadFile()函数是如何调用ntoskrnl里的NtReadFile()呢?我们知道,从用户空间跳入内核空间有3种方式,分别是:中断,自陷,异常。而这里用的是自陷(中断)的方式。
需要说明的是,中断和自陷其实意思差不多。中断带有被动的色彩。自陷带有主动地色彩。
我们在驱动程序编程中就会使用ntoskrnl所导出的ZwReadFile()函数,这个函数也是采用中断的方式调用ntoskrnl中的NtReadFile()函数。
说到这想必读者会对ntdll和ntoskrnl有比较深的了解。需要说明的是ntoskrnl里的Nt*函数不一定就是具有实际作用的函数。其实他还会调用更加底层的函数。而这些函数ntoskrnl并没有导出。因为微软感觉这些未导出函数我们没有必要去调用。举个例子:
比如说NtCreateThread(),和PsCreateSystemThread()这2个函数。他们其实调用的是PspCreateThread()函数。而这个函数你用Depends是看不到的。它其实是存在于ntoskrnl中的未导出函数。
虽然说有更加底层的函数存在。但是不得不承认ntoskrnl里的Nt*函数已经算是有作为的函数了。微软同样也这样认为。因此微软用一张表来记录这些函数的地址。这个表就是SSDT。也就是因为系统中存在这个表,并且这个表里存放了这些大名鼎鼎的函数的地址。那么hookSSDT表成了早期黑客以及早期杀毒软件常用的伎俩。但是现在使用这个手段的人越来越少了,原因很简单,一方面这个方法容易被发现。还有个原因就是这些函数不够底层。与其说HOOK NtCreateThread(),还不如HOOK PspCreateThread()。前提是你知道有这个函数存在。
再回过头来谈谈上面说的中断。其实是这样的:一旦发生中断,那么就会调用中断响应程序,而这个中断响应程序就会根据EAX寄存器里的值(系统调用号)到SSDT表里找,那么就会找到某个函数的地址。之后调用之。
OK,函数调用就说到这。如有不明白的地方就去看书吧。毛德操在第二章里讲的还是比较详细的。
附加,所有这些函数的前缀如下:
__e(浮点模拟),Cc(Cache管理),Csr(c/s运行时库),Dbg(调试支持),Ex(执行支持),FsRtl(文件系统运行时),Hal(硬件抽象层),Inbv(系统初试化/vga启动驱动程序bootvid.dll),Init(系统初试化),Interlocked(线程安全变量操作),Io(IO管理器),Kd(内核调试器支持),Ke(内核例程),Ki(内核中断处理),Ldr(映象装载器),Lpc(本地过程调用),Lsa(本地安全授权),Mm(内存管理),Nls(国际化语言支持),Nt(NT本机API),Ob(对象管理器),Pfx(前缀处理),Po(电源管理),Ps(进程支持),READ_REGISTER_(从寄存器地址读),Rtl(2k运行时库),Se(安全处理),WRITE_REGISTER_(写寄存器地址),Zw(本机API的替换叫法),<其它>(辅助函数和C运行时库)。
*************************下节研究代码的上下文***************************************************
驱动学习----代码上下文(1)之同步(4)扩展
(2010-05-04 23:29:46)
代码上下文(1)
我们开始研究代码上下文。这个概念很是重要。严格说这是内核研究的基础。什么叫代码的上下文?要理解这句话首先要理解什么是上下文。
那么什么是上下文呢?比如你看一篇文章,这个时候你看到了中间某个段落。这个时候你所看到的段落是属于这篇文章的,因此可以这样说:这个段落是在这个文章的环境里,也可以认为这个段落在这个文章的上下文中。同理,由于这个文章肯定在某个书中,那么可以认为这个文章是在这本书的上下文中,更加可以认为刚才那个段落也在这本书的上下文中。
现在联系下我们的进程,线程,函数,代码。首先,代码肯定在函数中(这话虽然给人的感觉是废话)。并且函数肯定运行在某个线程中,同时线程肯定在某个进程中。那么对于代码来说,他一方面在某个函数的上下文中,另一方面它处在某个线程的上下文中,再有它肯定也处在某个进程的上下文中。对于线程来说,同样也可以用上面这个方法来推导。
继续。我们都知道设备驱动程序,内核函数都是为应用程序而服务的。比如说你使用LineTo()这个函数画根线,最终这个举动会导致调用显卡驱动。再比如你使用ReadFile()函数,这个举动也会最终导致调用磁盘驱动程序。那么我还是拿ReadFile()为例。现在这个流程是这样的:
调用流程:当你运行kernel32.dll中的ReadFile()函数之后,它会调用Ntdll.dll中的NtReadFile()函数。紧接着Ntdll.dll中的NtReadFile()函数会调用一个中断0X2E(这个举动就叫自陷),其目的是运行此中断对应的中断向量函数KiSystemService。KiSystemService函数会调用内核版本的NtReadFile()。内核版的NtReadFile()函数经过一些必要的参数检查之后会封装一个IRP发给磁盘驱动程序(现阶段你可以认为上面的流程是完全正确的,其实不是很全面)如果这个流程用伪代码的形式表示出来或许更加的直观。请看:
void test() (1)
{
ReadFile() (2)
{
NtReadFile() (3)
{
KiSystemService() (4)
{
NtReadFile() (5)
{
DisspatchFun() (6)
{
}
}
...... (7)
}
}
}
.......(8)
}
分别解释这个流程:
(1)test()函数就是运行在某个应用程序中的某个线程中的一个普通的函数。(好像有点绕口)
(2)调用了一个win32 API ReadFile()。此时此刻这个线程处在用户空间。
(3)随后会调用ntdll中的NtReadFile()。此时此刻这个线程还是处在用户空间。
(4)NtReadFile()函数产生一个中断,由于这个中断的产生导致内核函数KiSystemService()被运行。也就是说NtReadFile()函数的前一部分代码运行在用户空间。直到中断产生,线程进入了内核。此时此刻这个线程暂运行在内核空间,并且使用的是线程的内核堆栈。
(5)Ntoskrnl中的NtReadFile()被调用。此时此刻的线程还是在内核空间中,还是使用的是线程内核堆栈。
(6)驱动函数的分发函数肯定是内核函数(驱动程序员的自定义内核函数),同样使用的是线程内核堆栈。
从上面可以发现几个重要的结论:
1,线程可以处在内核空间和用户空间。
2,线程包含2个堆栈,一个是针对内核空间,一个是针对用户空间
3,中断的产生直接把一个线程从用户空间拉进了内核空间。
4,上面所涉及的函数,不管是内核函数还是驱动中的分发函数,其函数上下文都是那个线程。而这个线程再普通不过,就是一个用户产生的线程。
5,上面这个流程详细的描述了你调用win32 API函数之后系统内部的调用流程,这个流程对我们应用开发程序员来说是陌生的。而你调用过无数次的win32 API函数,这个流程就发生过无数次。相对来说也是再平常不过了。
6,当函数一个个调用成功并返回之后,回到(7)这个地方。此时此刻KiSystemService()会安排线程重新回归到用户空间。
7,之后的路再熟悉不过。此时此时的伪代码就是这样的:
void test()
{
ReadFile()
...... (8)
}
**********************ok,下节我们继续研究**************************************************
驱动学习----代码上下文(2)之同步(4)扩展
(2010-05-05 02:13:28)
代码上下文(2)
上节所介绍的是某个用户线程通过调用WIN32 API函数之后系统内部的调用流程。
那么有些读者会问了,有内核线程吗?有!!在驱动中调用PsCreateSystemThread()就可以创建一个内核线程。很显然这个线程和用户线程有本质的区别,具体有2点:
1,用户线程即使进入内核空间,也是短暂的,将来还必须回到用户空间中
2,内核线程生下来就在内核中。它不会回到用户空间中。并且需要注意的是在正常的情况下内核线程的结束都是通过“自杀”的方式进行的,也就是说内核线程最终都会调用PsTerminateSystemThread()函数来结束自己。这点不像普通应用程序线程,代码运行结束之后线程也就自动结束。当然线程也是可以被“他杀”的。一般使用的函数是NtTerminateProcess。
杀毒软件和病毒比较喜欢HOOK NtTerminateProcess()。这个函数的代码将来详细分析。
当你编写一个驱动程序并且运行之后,系统会在系统进程(system)里创建(用PsCreateSystemThread)一个线程用来运行DriverEntry()函数。因此驱动程序的运行不像普通的EXE会创建一个单独的进程。这更加证明我在以前说的一句话:驱动程序只是作为内核的一个模块而存在。
内核线程调用流程的伪代码如下:
DriverEntry() (1)
{
ZwReadFile() (2)
{
KiSystemService() (3)
{
NtReadFile() (4)
{
DisspatchFun() (5)
{
}
}
...... (6)
}
}
}
刚才说了,驱动程序的入口函数DriverEntry()也是运行在某个线程中的,那么直接用DrvierEntry()来研究。
(1)当你创建一个线程的时候,你必须指定这个线程所要运行的函数。(这个本可以不解释的。编写多线程的程序员对这个问题熟悉了不能再熟悉了)和上节不一样的是,此线程为内核线程。
(2)调用微软为驱动程序编程专门导出的函数。
(3)驱动程序专用的ZW*函数也是通过调用KiSystemService()来定位记录在SSDT表中的NT*函数(ntoskrnl所导出)
(4)(5)的解释和上节一样
(6)运行此线程的其他代码。
由于此线程一直在内核,所以没有跨边界这样的举动。研究起来比较的轻松。
到这里关于代码上下文的研究已经可以告一段落。但是其实还没有完。因为KiSystemService()这个函数还没有仔细去研究。从上面可以发现,这个函数的作用非常之强大!虽然我们从理论上知道这个函数的作用。但是其内部具体如何操作的呢?有没有什么细节需要重视呢?等等等等,都必须通过代码来研究。因此我决定把整个代码好好研究一下。 下节开始分析KiSystemService()~~
*******************************************下节分析KiSystemService()******************************
驱动学习----代码上下文(3)之同步(4)扩展
(2010-05-05 03:21:13)
代码上下文(3)
现在我们来研究KiSystemService()。请把老毛子书翻到P24。
看到这段代码我又懊恼了!这段代码又涉及到新的知识:GDT(全局描述符)。关键问题是一旦介绍GDT,那么还必须介绍LDT,选择子,调用门,陷阱门,任务门,中断门。因为上面几个概念属于一个统一的知识体系。
现在读者已经感觉到驱动学习的麻烦了。这就是为什么很多人不学习驱动的原因:涉及的东西实在太多,抽象的东西太多,扩展太多,要记忆理解的太多。
我会从下节开始和大家一起讨论GDT,门机制。之后我不打算扩展下去!我会回到同步(4)中介绍后面的部分。将来遇到扩展知识再研究。不然一层一层研究下去会把读者搞晕的。
不过话又说回来。GDT相关知识还是非常非常重要的。很多地方用到相关的知识,如果不能够理解GDT,那么寸步难行!
KiSystemService()的代码分析如下:(按代码顺序)
1,由于KiSystemService()是从用户空间跨入内核空间所调用的第一个内核函数,那么代码前几个push操作的作用是把用户空间当前的寄存器的值保存到线程内核堆栈。
2,下面涉及到一些知识。比较的多。笔者5月22号有个考试要准备。因此这几天不会更新博客。。不过今天我打算把比较令人兴奋的技术提前拿出来和读者见个面。所谓一回生二回熟。这就是inline hook。 OK,这节暂停,考试好了继续。
***************************************此节暂停编写,先介绍inline hook**************************