搜索文章:

首页  |  Java技术  |  Asp.net  |  Asp编程  |  VC/C++  |  Delphi  |  VB编程

再谈Windows NT/2000内部数据结构

现在我们结合regmon(www.sysinternals.com)在nt中的实现方法再来谈谈windows nt/2000内部数据结构。

regmon是监视应用程序访问系统注册表的实用程序。大家都知道在应用程序中使用注册表一般都调用winapi regxxx,而regxxx最终会调用native api zwxxx!(参阅windows nt/2000 ddk documentation)。regmon正是通过改变这些例程以达到监视注册表的目的。zwxxx的实现方式如下:

mov eax, serviceid
lea edx, parametertable
int 2eh
ret paramtablebytes

这就是所说的nt system services,是不是与linux有点相似(只不过linux使用的是80h中断而已,它也有serviceid,如fork系统调用serviceid为2)。

system services在ddk documentation是如下定义的:

 the set of native, user-mode routines exported by the executive for use only by protected subsystems. each
 system service has a name of the form twolettersxxxyyy where:
 twoletters is the prefix for all system services.
 xxx is usually a verb, describing the operation of a given service.
 yyy is generally the object type the service operates on.

system services在系统中由两部分组成,一部分由win32k.sys导出,另一部分由ntoskrnl.exe提供服务。前者主要完成nt中win32、posix与os/2等子系统(subsystems)与内核的通信,仅能由用户态的应用程序调用,如user32!waitmessage等。由于regmon只涉及后者,所以本文将对其进行讨论,以下所有关于system service的讨论均适合两者!

上次(nsfocus magazine 10)我曾经提及keservicedescriptortable,也说过它的结构如下:

 struct _servicedescriptorentry {
unsigned int *servicetablebase;
unsigned int *servicecountertablebase;
unsigned int numberofservices;
unsigned char *paramtablebase;
 }servicedescriptortableentry

ntoskrnl.exe导出全局变量keservicedescriptortable指向servicedescriptortableentry(由win32k.sys导出的system services也有自己的servicedescriptortable,在win2000 server中其service id从1000h始,由keservicedescriptortable以下偏移50h处指向,其结构与ntosrknl.exe导出的基本一致,本文不作讨论,softice中的ntcall命令在特定情况下可以列出所有的system service)。

下面我们先用softice 4.05 for windows nt/2000来分析分析x86平台windows 2000 server build 2195的情况(以下仅摘录部分,不同版本不同时刻可能得到的数据未必一样)

:dd keservicedescriptortable l 4*4
0008:8046ab80 804704d8 00000000 000000f8 804708bc ..g...........g.
  _servicetablebase值   _paramtablebase值
  _似乎总为0
 _keservicedescriptortable地址 _numberofservice

:dd @keservicedescriptortable l byte(@(keservicedescriptortable+08))*4
 // dd servicedescriptortableentry->servicetablebase l numberofservice*4
0008:804704d8 804ab3bf 804ae86b 804bdef3 8050b034 ..j.k.j...k.4.p.

 _serviceid=0的system service入口地址(依次类推)

0008:804704e8 804c11f4 80459214 8050c2ff 8050c33f ..l...e...p.?.p.
0008:804704f8 804b581c 80508874 8049860a 804fc7e2 .xk.t.p...i...o.
0008:80470508 804955f7 8049c8a6 80448472 804a8d50 .ui...i.r.d.p.j.
0008:80470518 804b6bfb 804f0cef 804fcb95 8040189a .kk...o...o...@.
0008:80470528 804d06cb 80418f66 804f69d4 8049e0cc ..m.f.a..io...i.
 ...(略)

:db @(keservicedescriptortable+0c) l byte(@(keservicedescriptortable+08))
 // dd servicedescriptortableentry->paramtablebase
0008:804708bc 18 20 2c 2c 40 2c 40 44-0c 18 18 08 04 04 0c 10 . ,,@,@d........

 _serviceid=0的system service参数个数*4(即参数个数为18h/4=6)

0008:804708cc 18 08 08 0c 08 08 04 04-04 0c 04 20 08 0c 14 0c ........... ....
 ...(略)

要获得哪个应用程序对系统注册表有过操作,只要在对其有操作的system service中注入自己的代码,也就是改变这些system service的执行流程,先执行自己的代码(regmon中用于记录供gui部分使用),接着返回至原先处继续执行即可。通过以上分析,我们知道只要修改servicetablebase到servicetablebase+numberofservice*4范围的数据就可以改变system service的执行流程,而只要知道system service的serviceid就可以改变这一system service入口地址在这一区域的位置,那么又如何得到system service的service id呢!我们可以随便以zwopenkey作个例子:

:u zwopenkey
ntoskrnl!zwopenkey
0008:80400e2a b867000000 mov eax,00000067
  _serviceid
 _机器码(其中第二字节即zwopenkey线性地址加一处就是serviceid)
0008:80400e2f 8d542404 lea edx,[esp+04]
0008:80400e33 cd2e int 2e
0008:80400e35 c20c00 ret 000c

这样只要知道zwxxx例程名(即system service在内存中的线性地址),是不是就可以实现我们的目的了呢?来看看regmon的具体实现代码吧:

 .
 .
 .
 // 保存zwopenkey原先入口,在hookregopenkey中使用
 realregopenkey = syscall( zwopenkey );

 // 修改zwopenkey流程,指向新的入口,即调用zwopenkey时转向执行hookregopenkey
 syscall( zwopenkey ) = (pvoid) hookregopenkey;
 .
 .
 .

syscall在intel平台是如下定义的:
 #define syscall(_function) servicetable->servicetable[ *(pulong)((puchar)_function+1)]
servicetable->servicetable就是我们上面所述的 servicedescriptortableentry->servicetablebase(为了便于描述)。_function+1即serviceid所在地址。整个表达式即取得_function对应的system service的入口地址在线性内存中的位置。其它定义请参阅regsys.c与regsys.h!

可以使用softice对比一下regsys.sys装载前后servicetable中system service入口地址的变化,加深对system service拦截的理解.

好了现在我们知道regmon的基本实现方法了(当然真正要实现此功能还要考虑很多问题,如保护态应用程序与内核驱动程序之间的通信、线程同步等等)。

让我们再来看看keservicedescriptortable的另一个应用吧!如果我们重新分配段内存池,构造自己的servicetable与paramtable数组(必须复制系统原有的system services,否则...),然后修改结构中servicetablebase与paramtablebase,使其指向自己的servicetable与paramtable,再修改一下numberofservices,是不是可以增加自个儿的system service呢!如果你有兴趣的话可以参阅<>。这书我也没见过,只知道网上它名声在外。哦,还要感谢james shatlyk给我提供随书配套例子代码。如果您见过此书(不知道有没有chinese 版,e文也可),能不能与我联系联系?

谈完system service后,再让我们来看看regmon是如何在driver中取得系统进程名的。
首先谈谈kteb(kernel thread environment block)与kpeb(kernel process environment block),与teb(其实应该是user-teb)一样,kpeb/kteb则纪录着系统内核进程/线程信息。要了解kteb、kpeb,首先要知道如何得到当前进程/线程中它们的基址,可以先看看native api iogetcurrentprocess。在windows 2000 ddk document中它是如下定义的:
peprocess iogetcurrentprocess();
使用ida pro或softice,可知其在ntoskrnl.exe仅是由几条汇编指令实现的:

 mov eax,fs:[00000124]
 mov eax,[eax+00000044] //nt 4.0以下这个值应为[eax+40]
 ret

这个native api很有典型性,它的第一条指令取得当前线程的kteb,而整个api刚好将相对于kteb始68(即16进制44)字节处取得当前进程的kpeb返回给使用者。你可以使用softice验证一下。

让我们再来看看其具体是如何实现的:

//----------------------------------------------------------------------
//
// getprocessnameoffset
//
// in an effort to remain version-independent, rather than using a
// hard-coded into the kpeb (kernel process environment block), we
// scan the kpeb looking for the name, which should match that
// of the gui process
//
//----------------------------------------------------------------------
ulong getprocessnameoffset()
{
 peprocess curproc;
 int i;
 dbgprint(("getprocessnameoffset\n"));
 curproc = psgetcurrentprocess();

 //
 // scan for 12kb, hopping the kpeb never grows that big!
 //
 for( i = 0; i < 3*page_size; i++ ) {

 if( !strncmp( sysname, (pchar) curproc + i, strlen(sysname) )) {

 return i;
 }
 }

 //
 // name not found - oh, well
 //
 return 0;
}

//----------------------------------------------------------------------
//
// getprocess
//
// uses undocumented data structure offsets to obtain the name of the
// currently executing process.
//
//----------------------------------------------------------------------
filterstatus getprocess( pchar name )
{
 peprocess curproc;
 char *nameptr;
 ulong i;

 //
 // we only try and get the name if we located the name offset
 //
 if( processnameoffset ) {

 curproc = psgetcurrentprocess();
 nameptr = (pchar) curproc + processnameoffset;
 strncpy( name, nameptr, 16 );

 } else {

 strcpy( name, "???");
 }
 .
 .
 .,
 }

这段代码从regmon中nt driver部分摘录,详细可参阅regsys.c。
这两函数主要功能是取得进程名称,供程序使用。大家都知道在driver部分不能简单的调用win32 api,而nt执行体提供的ntquerysysteminformation主要针对所有进程、线程或其他nt内部信息等,所以我们必须寻找其它方法(一般方法是跟踪相应的win32 api用debugger对其进行艰苦但充满挑战充满乐趣的逆向工程,然后找出其在nt执行体中的具体实现过程,你也可以使用此方法对本文所提及的进行验证)。

regmon中这两个函数通过查找kpeb取得进程名,getprocessnameoffset主要是调用psgetcurrentprocess取得kpeb基址,然后搜索kpeb,得到processname相对kpeb的偏移量,存放在全局变量processnameoffset中。在nt/2000 ddk中如下定义psgetcurrentprocess:
#define psgetcurrentprocess() iogetcurrentprocess()
而iogetcurrentprocess已经在前面讨论过了。
作者在3页内存区域(x86中一页为4k)查找,从程序中注释可知他也不知道是否会超出此范围,还有程序段中sysname被定义为system,因为调用driver中driverentry入口正是由system进程调度(getprocessnameoffset在driverentry中调用)。你也可以使用softice查出特定windows nt/2000版本中processnameoffset的值。在x86平台windows 2000 server build 2195中它为1fch(nt 4.0与3.51中为1dch),然后根据这个值找几个进程核对核对。

getprocess将当前进程的kpeb基址加上processnameoffset值取得当前进程(regmon中即调用操作registry的native api进程)的名称。

  至于kpeb/kteb等的具体结构,各字节的具体含义,由于其所谓的undocument,我查msdn,到各新闻组,追踪nt内核,也没找到其中的一小部分,这也是我着手写此篇的用意,希望懂得的高手,朋友能互相交流交流,还有本文有误之处,还望您能指出并与我说说,谢谢!

参考资料:
 1.regmon 4.22源代码
 2.windows 2000 ddk documentation

()

相关文章:
© 2006   www.java-asp.net