★我要吧★

 找回密码
 注册[Register]
搜索
qq空间相册密码查看为什么登陆后需要激活无法注册?

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

[复制链接]
发表于 2011-12-22 22:47:43 | 显示全部楼层 |阅读模式
作者:WebCrazy(tsu00@263.net)  
日期:2000-7-14  



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


Regmon是监视应用程序访问系统注册表的实用程序。大家都知道在应用程序中使用注册表
一般都调用WinAPIRegxxx,而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.  
     
SystemServices在系统中由两部分组成,一部分由win32k.sys导出,另一部分由ntoskrnl.
exe提供服务。前者主要完成NT中win32、Posix与Os/2等子系统(subsystems)与内核的通信
,仅能由用户态的应用程序调用,如user32!WaitMessage等。由于Regmon只涉及后者,所
以本文将对其进行讨论,以下所有关于SystemService的讨论均适合两者!
                     
    上次(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导出的SystemServices也有自己的ServiceDescriptorTable,在Win2000  
Server中其ServiceID从1000h始,由KeServiceDescriptorTable以下偏移50h处指向,其结
构与ntosrknl.exe导出的基本一致,本文不作讨论,SoftICE中的ntcall命令在特定情况下
可以列出所有的SystemService)。

    下面我们先用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呢!如果你有兴趣的话可以参阅<<Undocumented
NT>>。这书我也没见过,只知道网上它名声在外。哦,还要感谢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

    这个NativeAPI很有典型性,它的第一条指令取得当前线程的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部分不能简单的调
用WIN32API,而NT执行体提供的NtQuerySystemInformation主要针对所有进程、线程或其
他NT内部信息等,所以我们必须寻找其它方法(一般方法是跟踪相应的Win32API用Debugger
对其进行艰苦但充满挑战充满乐趣的逆向工程,然后找出其在NT执行体中的具体实现过程
,你也可以使用此方法对本文所提及的进行验证)。


Regmon中这两个函数通过查找KPEB取得进程名,GetProcessNameOffset主要是调用
PsGetCurrentProcess取得KPEB基址,然后搜索KPEB,得到ProcessName相对KPEB的偏移量
,存放在全局变量ProcessNameOffset中。在NT/2000DDK中如下定义PsGetCurrentProcess:
    #define PsGetCurrentProcess() IoGetCurrentProcess()     
    而IoGetCurrentProcess已经在前面讨论过了。

作者在3页内存区域(x86中一页为4k)查找,从程序中注释可知他也不知道是否会超出此范
围,还有程序段中SYSNAME被定义为system,因为调用Driver中DriverEntry入口正是由
system进程调度(GetProcessNameOffset在DriverEntry中调用)。你也可以使用SoftICE查
出特定WindowsNT/2000版本中ProcessNameOffset的值。在x86平台Windows 2000 Server  
Build2195中它为1fch(NT 4.0与3.51中为1dch),然后根据这个值找几个进程核对核对。


GetProcess将当前进程的KPEB基址加上ProcessNameOffset值取得当前进程(Regmon中即调
用操作Registry的NativeAPI进程)的名称。
     
  至于KPEB/KTEB等的具体结构,各字节的具体含义,由于其所谓的Undocument,我查
MSDN,到各新闻组,追踪NT内核,也没找到其中的一小部分,这也是我着手写此篇的用意,
希望懂得的高手,朋友能互相交流交流,还有本文有误之处,还望您能指出并与我说说,
谢谢!

    参考资料:
      1.Regmon 4.22源代码
      2.Windows 2000 DDK Documentation
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

QQ|手机版|小黑屋|☆我要吧☆ ( 豫ICP备13016831号-1 )

GMT+8, 2024-4-25 08:04 , Processed in 0.065654 second(s), 22 queries .

Powered by abc369 X3.4

© 2001-2023 abc369.

快速回复 返回顶部 返回列表