根据前面的介绍,NT内核会把操作系统的代码和数据映射到系统中所有进程的内核空间中。这样,每个进程内的应用程序代码便可以很方便地调用内核空间中的系统服务。这里的“很方便”有多层含义,一方面是内核代码和用户代码在一个地址空间中,应用程序调用系统服务时不需要切换地址空间,另一方面是整个系统中内核空间的地址是统一的,编写内核空间的代码时会简单很多。但是,如此设计也带来一个很大的问题,那就是用户空间中的程序指针可以指向内核空间中的数据和代码,因此必须防止用户代码破坏内核空间中的操作系统。怎么做呢?答案是利用权限控制来实现对内核空间的保护。
2.6.1 访问模式
Windows定义了两种访问模式(access mode)——用户模式(user mode,也称为用户态)和内核模式(kernel mode,也称为内核态)。应用程序(代码)运行在用户模式下,操作系统代码运行在内核模式下。内核模式对应于处理器的最高权限级别(不考虑虚拟机情况),在内核模式下执行的代码可以访问所有系统资源并具有使用所有特权指令的权利。相对而言,用户模式对应于较低的处理器优先级,在用户模式下执行的代码只可以访问系统允许其访问的内存空间,并且没有使用特权指令的权利。
本书卷1介绍过,IA-32处理器定义了4种特权级别(privilege level),或者称为环(ring),分别为0、1、2、3,优先级0(环0)的特权级别最高。处理器在硬件一级保证高优先级的数据和代码不会被低优先级的代码破坏。Windows系统使用了IA-32处理器所定义的4种优先级中的两种,优先级3(环3)用于用户模式,优先级0用于内核模式。之所以只使用了其中的两种,主要是因为有些处理器只支持两种优先级,比如Compaq Alpha处理器。值得说明的是,对于x86处理器来说,并没有任何寄存器表明处理器当前处于何种模式(或优先级)下,优先级只是代码或数据所在的内存段或页的一个属性,参见卷1的2.6节和2.7节。
因为内核模式下的数据和代码具有较高的优先级,所以用户模式下的代码不可以直接访问内核空间中的数据,也不可以直接调用内核空间中的任何函数或例程。任何这样的尝试都会导致保护性错误。也就是说,即使用户空间中的代码指针正确指向了要访问的数据或代码,但一旦访问发生,那么处理器会检测到该访问是违法的,会停止该访问并产生保护性异常(#GP)。
虽然不可以直接访问,但是用户程序可以通过调用系统服务来间接访问内核空间中的数据或间接调用、执行内核空间中的代码。当调用系统服务时,主调线程会从用户模式切换到内核模式,调用结束后再返回到用户模式,也就是所谓的模式切换。在线程的KTHREAD结构中,定义了UserTime和KernelTime两个字段,分别用来记录这个线程在用户模式和内核模式的运行时间(以时钟中断次数为单位)。模式切换是通过软中断或专门的快速系统调用(fast system call)指令来实现的。下面通过一个例子来分别介绍这两种切换机制。
2.6.2 使用INT 2E切换到内核模式
图2-4展示了在Windows 2000中通过INT 2E从应用程序调用ReadFile() API的过程。因为ReadFile() API是从Kernel32.dll导出的,所以我们看到该调用首先转到Kernel32.dll中的ReadFile()函数,ReadFile()函数在对参数进行简单检查后便调用NtDll.dll中的NtReadFile()函数。

图2-4 通过INT 2E从应用程序调用ReadFile() API的过程
通过反汇编可以看到,NtDll.dll中的NtReadFile ()函数非常简短,首先将ReadFile()对应的系统服务号(0xa1,与版本有关)放入EAX寄存器中,将参数指针放入EDX寄存器中,然后便通过INT n指令发出调用。这里要说明的一点是,虽然每个系统服务都具有唯一的号码,但微软公司没有公开这些服务号,也不保证这些号码在不同的Windows版本中会保持一致。
  
   - 
    
     
    
    
     
      ntdll!NtReadFile: // Windows 
      2000
     
    
- 
    
     
    
    
     
      77f8fb5d b
      8a
      1000000    mov      eax,
      0xa
      1
     
    
- 
    
     
    
    
     
      77f8fb62 
      8d
      542404      lea      edx,[esp+
      0x
      4]
     
    
- 
    
     
    
    
     
      77f8fb66 cd
      2e          int      
      2e
     
    
- 
    
     
    
    
     
      77f8fb68 c
      22400        ret      
      0x
      24
     
    
在WinDBG下通过!idt 2e命令可以看到2e号向量对应的服务例程是KiSystemService ()。KiSystemService ()是内核态中专门用来分发系统调用的例程。
  
   - 
    
     
    
    
     
      lkd> !idt 
      2e
     
    
- 
    
     
    
    
     
      Dumping IDT:
     
    
- 
    
     
    
    
     
      2e:   
      804db
      1ed nt!KiSystemService
     
    
Windows将2e号向量专门用于系统调用,在启动早期初始化中断描述符表(Interrupt Descriptor Table,IDT)时(见第11章)便注册好了合适的服务例程。因此当NTDll.DLL中的NtReadFile()发出INT 2E指令后,CPU便会通过IDT找到KiSystemService ()函数。因为KiSystemService ()函数是位于内核空间的,所以CPU在把执行权交给KiSystemService ()函数前,会做好从用户模式切换到内核模式的各种工作,包括:
(1)权限检查,即检查源位置和目标位置所在的代码段权限,核实是否可以转移;
(2)准备内核模式使用的栈,为了保证内核安全,所有线程在内核态执行时都必须使用位于内核空间的内核栈(kernel stack),内核栈的大小一般为8KB或12KB。
KiSystemService ()会根据服务ID从系统服务分发表(System Service Dispatch Table)中查找到要调用的服务函数地址和参数描述,然后将参数从用户态栈复制到该线程的内核栈中,最后KiSystemService ()调用内核中真正的NtReadFile()函数,执行读文件的操作,操作结束后会返回到KiSystemService (),KiSystemService ()会将操作结果复制回该线程用户态栈,最后通过IRET指令将执行权交回给NtDll.dll中的NtReadFile()函数(继续执行INT 2E后面的那条指令)。
通过INT 2E进行系统调用时,CPU必须从内存中分别加载门描述符和段描述符才能得到KiSystemService ()的地址,即使门描述符和段描述符已经在高速缓存中,CPU也需要通过“内存读(memory read)”操作从高速缓存中读出这些数据,然后进行权限检查。
2.6.3 快速系统调用
因为系统调用是非常频繁的操作,所以如果能减少这些开销还是非常有意义的。可以从两个方面来降低开销:一是把系统调用服务例程的地址放到寄存器中以避免读IDT这样的内存操作,因为读寄存器的速度比读内存的速度要快很多;二是避免权限检查,也就是使用特殊的指令让CPU省去那些对系统服务调用来说根本不需要的权限检查。奔腾II处理器引入的SYSENTER/SYSEXIT指令正是按这一思路设计的。AMD K7引入的SYSCALL/SYSRETURN指令也是为这一目的而设计的。相对于INT 2E,使用这些指令可以加快系统调用的速度,因此利用这些指令进行的系统调用称为快速系统调用。
下面我们介绍Windows系统是如何利用IA-32处理器的SYSENTER/SYSEXIT指令(从奔腾II开始)实现快速系统调用的[2]。首先,Windows 2000或之前的Windows系统不支持快速系统调用,它们只能使用前面介绍的INT 2E方式进行系统调用。Windows XP和Windows Server 2003或更新的版本在启动过程中会通过CPUID指令检测CPU是否支持快速系统调用指令(EDX寄存器的SEP标志位)。如果CPU不支持这些指令,那么仍使用INT 2E方式。如果CPU支持这些指令,那么Windows系统便会决定使用新的方式进行系统调用,并做好如下准备工作。
(1)在全局描述符表(GDT)中建立4个段描述符,分别用来描述供SYSENTER指令进入内核模式时使用的代码段(CS)和栈段(SS),以及SYSEXIT指令从内核模式返回用户模式时使用的代码段和栈段。这4个段描述符在GDT中的排列应该严格按照以上顺序,只要指定一个段描述符的位置便能计算出其他的。
(2)设置表2-1中专门用于系统调用的MSR(关于MSR的详细介绍见卷1的2.4.3节),SYSENTER_EIP_MSR用于指定新的程序指针,也就是SYSENTER指令要跳转到的目标例程地址。Windows系统会将其设置为KiFastCallEntry的地址,因为KiFastCallEntry例程是Windows内核中专门用来受理快速系统调用的。SYSENTER_CS_MSR用来指定新的代码段,也就是KiFastCallEntry所在的代码段。SYSENTER_ESP_MSR用于指定新的栈指针(ESP)。新的栈段是由SYSENTER_CS_MSR的值加8得来的。
(3)将一小段名为SystemCallStub的代码复制到SharedUserData内存区,该内存区会被映射到每个Win32进程的进程空间中。这样当应用程序每次进行系统调用时,NTDll.DLL中的残根(stub)函数便调用这段SystemCallStub代码。SystemCallStub的内容因系统硬件的不同而不同,对于IA-32处理器,该代码使用SYSENTER指令,对于AMD处理器,该代码使用SYSCALL指令。
表2-1 供SYSENTER指令使用的MSR(略)
例如在配有Pentium M CPU的Windows XP系统上,以上3个寄存器的值分别为:
  
   - 
    
     
    
    
     
      lkd> rdmsr 
      174
     
    
- 
    
     
    
    
     
      msr[
      174] = 
      00000000`
      00000008
     
    
- 
    
     
    
    
     
      lkd> rdmsr 
      175
     
    
- 
    
     
    
    
     
      msr[
      175] = 
      00000000`bacd
      8000
     
    
- 
    
     
    
    
     
      lkd> rdmsr 
      176
     
    
- 
    
     
    
    
     
      msr[
      176] = 
      00000000`
      8053cad
      0
     
    
其中SYSENTER_CS_MSR的值为8,这是Windows系统的内核代码段的选择子,即常量KGDT_R0_CODE的值。WinDBG帮助文件中关于dg命令的说明中列出了这个常量。SYSENTER_EIP_MSR的值是8053cad0,检查nt内核中KiFastCallEntry函数的地址。
  
   - 
    
     
    
    
     
      lkd> x nt!
      KiFastCallEntry
     
    
- 
    
     
    
    
     
      8053cad0 nt!
      KiFastCallEntry = <no 
      type information>
     
    
可见,Windows把快速系统调用的目标指向内核代码段中的KiFastCallEntry函数。
通过反汇编Windows XP下NTDll.DLL中的NtReadFile ()函数,可以看到SystemCallStub被映射到进程的0x7ffe0300位置。与前面Windows 2000下的版本相比,容易看到该服务的系统服务号码在这两个版本间是不同的。
  
   - 
    
     
    
    
     
      kd> u ntdll...
     
    
- 
    
     
    
    
     
      ntdll!NtReadFile: // Windows XP
     
    
- 
    
     
    
    
     
      77f5bfa8 b
      8b
      7000000       mov     eax,
      0xb
      7
     
    
- 
    
     
    
    
     
      77f5bfad ba
      0003fe
      7f       mov     edx,
      0x
      7ffe
      0300
     
    
- 
    
     
    
    
     
      77f5bfb2 ffd
      2             call edx {SharedUserData!SystemCallStub (
      7ffe
      0300)}
     
    
- 
    
     
    
    
     
      77f5bfb4 c
      22400           ret     
      0x
      24
     
    
- 
    
     
    
    
     
      77f5bfb7 
      90               nop
     
    
观察本段下面反汇编SystemCallStub的结果,它只包含3条指令,分别用于将栈指针(ESP寄存器)放入EDX寄存器中、执行sysenter指令和返回。第一条指令有两个用途:一是向内核空间传递参数;二是指定从内核模式返回时的栈地址。因为笔者使用的是英特尔奔腾M处理器,所以此处是sysenter指令,对于AMD处理器,此处应该是syscall指令。
  
   - 
    
     
    
    
     
      kd> u...
     
    
- 
    
     
    
    
     
      SharedUserData!SystemCallStub:
     
    
- 
    
     
    
    
     
      7ffe0300 
      8bd
      4             mov     edx,esp
     
    
- 
    
     
    
    
     
      7ffe0302 
      0f
      34             sysenter
     
    
- 
    
     
    
    
     
      7ffe0304 c
      3               ret
     
    
下面让我们看一下KiFastCallEntry例程,其清单如下所示。
  
   - 
    
     
    
    
     
      kd> u nt!KiFastCallEntry L
      20
     
    
- 
    
     
    
    
     
      nt!KiFastCallEntry:
     
    
- 
    
     
    
    
     
      804db1bb 
      368b
      0d
      40f
      0dfff   mov      ecx,ss:[ffdff
      040]
     
    
- 
    
     
    
    
     
      804db1c2 
      368b
      6104         mov      esp,ss:[ecx+
      0x
      4]
     
    
- 
    
     
    
    
     
      804db1c6 b
      90403fe
      7f       mov      ecx,
      0x
      7ffe
      0304
     
    
- 
    
     
    
    
     
      804db1cb 
      3b
      2504f
      0dfff     cmp      esp,[ffdff
      004]
     
    
- 
    
     
    
    
     
      804db1d1 
      0f
      84cc
      030000     je       nt!KiServiceExit
      2+
      0x
      13f (
      804db
      5a
      3)
     
    
- 
    
     
    
    
     
      804db1d7 
      6a
      23             push     
      0x
      23
     
    
- 
    
     
    
    
     
      804db1d9 
      52               push     edx
     
    
- 
    
     
    
    
     
      804db1da 
      83c
      208           add      edx,
      0x
      8
     
    
- 
    
     
    
    
     
      804db1dd 
      6802020000       push     
      0x
      202
     
    
- 
    
     
    
    
     
      804db1e2 
      6a
      02             push     
      0x
      2
     
    
- 
    
     
    
    
     
      804db1e4 
      9d               popfd
     
    
- 
    
     
    
    
     
      804db1e5 
      6a
      1b             push     
      0x
      1b
     
    
- 
    
     
    
    
     
      804db1e7 
      51               push     ecx // Fall Through,自然进入KiSystemService函数
     
    
- 
    
     
    
    
     
      nt!KiSystemService:
     
    
- 
    
     
    
    
     
      804db1e8 
      90               nop
     
    
- 
    
     
    
    
     
      804db1e9 
      90               nop
     
    
- 
    
     
    
    
     
      804db1ea 
      90               nop
     
    
- 
    
     
    
    
     
      804db1eb 
      90               nop
     
    
- 
    
     
    
    
     
      804db1ec 
      90                nop
     
    
- 
    
     
    
    
     
      nt!KiSystemService:
     
    
- 
    
     
    
    
     
      804db1ed 
      6a
      00             push      
      0x
      0
     
    
- 
    
     
    
    
     
      804db1ef 
      55               push      ebp
     
    
显而易见,KiFastCallEntry在做了些简单操作后,便下落(fall through)到KiSystemService函数了,也就是说,快速系统调用和使用INT 2E进行的系统调用在内核中的处理绝大部分是一样的。另外,请注意ecx寄存器,mov ecx,0x7ffe0304将其值设为0x7ffe0304,也就是SharedUserData内存区里SystemCallStub例程中ret指令的地址(参见上文的SystemCallStub代码)。在进入nt!KiSystemService之前,ecx连同其他一些参数被压入栈中。事实上,ecx用来指定SYSEXIT返回用户模式时的目标地址。当使用INT 2E进行系统调用时,由于INT n指令会自动将中断发生时的CS和EIP寄存器压入栈中,当中断处理例程通过执行iretd返回时,iretd指令会使用栈中保存的CS和EIP值返回合适的位置。因为sysenter指令不会向栈中压入要返回的位置,所以sysexit指令必须通过其他机制知道要返回的位置。这便是压入ECX寄存器的原因。通过反汇编KiSystemCallExit2例程,我们可以看到在执行sysexit指令之前,ecx寄存器的值又从栈中恢复出来了。
  
   - 
    
     
    
    
     
      kd> u nt!KiSystemCallExit l
      20
     
    
- 
    
     
    
    
     
      nt!KiSystemCallExit:
     
    
- 
    
     
    
    
     
      804db3b4 cf            iretd
     
    
- 
    
     
    
    
     
      nt!KiSystemCallExit
      2:
     
    
- 
    
     
    
    
     
      804db3b5 
      5a            pop      edx
     
    
- 
    
     
    
    
     
      804db3b6 
      83c
      408        add      esp,
      0x
      8
     
    
- 
    
     
    
    
     
      804db3b9 
      59            pop      ecx
     
    
- 
    
     
    
    
     
      804db3ba fb            sti
     
    
- 
    
     
    
    
     
      804db3bb 
      0f
      35          sysexit
     
    
- 
    
     
    
    
     
      nt!KiSystemCallExit
      3:
     
    
- 
    
     
    
    
     
      804db3bd 
      59            pop      ecx
     
    
- 
    
     
    
    
     
      804db3be 
      83c
      408        add      esp,
      0x
      8
     
    
- 
    
     
    
    
     
      804db3c1 
      5c            pop      esp
     
    
- 
    
     
    
    
     
      804db3c2 
      0f
      07          sysret
     
    
以上代码中包含了3个从系统调用返回的例程,即KiSystemCallExit、KiSystemCallExit2和KiSystemCallExit3,它们分别对应于使用INT 2E、sysenter和syscall发起的系统调用,如表2-2所示。
表2-2 系统调用(略)
图2-5展示了使用sysenter/sysexit指令对进行系统调用的完整过程(以调用ReadFile服务为例)。

图2-5 快速系统调用(针对IA-32处理器)
格物
下面通过一个小的实验来加深大家对系统调用的理解。首先启动WinDBG程序,选择File → Open Crash Dump,然后选择本书实验文件中的dumps\w732cf4.dmp文件。在调试会话建立后,先执行.symfix c:\symbols和.reload加载模块与符号,再执行k命令,便得到清单2-4所示的完美栈回溯。
第22章将详细讲解栈回溯的原理,现在大家只要知道栈上记录着函数相互调用时的参数和返回地址等信息。栈回溯是从栈上找到这些信息,然后显示出来的过程,是追溯线程执行轨迹的一种便捷方法。
清单2-4还显示了任务管理器程序(taskmgr)调用NtTerminateProcess系统服务时的执行过程。栈回溯的结果包含4列,第一列是序号,第二列是每个函数的栈帧基地址,第三列是返回地址,第四列是使用“函数名+字节偏移量”形式表达的执行位置。以00栈帧为例,它对应的函数是著名的蓝屏函数KeBugCheckEx,它的栈帧基地址是9796fb9c,它的返回地址是82b1ab51,翻译成符号便是PspCatchCriticalBreak+0x71。
清单2-4 完美栈回溯
  
   - 
    
     
    
    
     
      # ChildEBP RetAddr 
     
    
- 
    
     
    
    
     
      00 
      9796fb
      9c 
      82b
      1ab
      51 nt!KeBugCheckEx+
      0x
      1e
     
    
- 
    
     
    
    
     
      01 
      9796fbc
      0 
      82a
      6daa
      8 nt!PspCatchCriticalBreak+
      0x
      71
     
    
- 
    
     
    
    
     
      02 
      9796fbf
      0 
      82a
      605b
      6 nt!PspTerminateAllThreads+
      0x
      2d
     
    
- 
    
     
    
    
     
      03 
      9796fc
      24 
      8287c
      87a nt!NtTerminateProcess+
      0x
      1a
      2
     
    
- 
    
     
    
    
     
      04 
      9796fc
      24 
      77da
      7094 nt!KiFastCallEntry+
      0x
      12a
     
    
- 
    
     
    
    
     
      05 
      001df
      4dc 
      77da
      68d
      4 ntdll!KiFastSystemCallRet
     
    
- 
    
     
    
    
     
      06 
      001df
      4e
      0 
      76193c
      82 ntdll!NtTerminateProcess+
      0xc
     
    
- 
    
     
    
    
     
      07 
      001df
      4f
      0 
      00bf
      57b
      9 KERNELBASE!TerminateProcess+
      0x
      2c
     
    
- 
    
     
    
    
     
      08 
      001df
      524 
      00bf
      67ec taskmgr!CProcPage::KillProcess+
      0x
      116
     
    
- 
    
     
    
    
     
      09 
      001df
      564 
      00bebc
      96 taskmgr!CProcPage::HandleWMCOMMAND+
      0x
      10f
     
    
- 
    
     
    
    
     
      0a 
      001df
      5d
      8 
      76abc
      4e
      7 taskmgr!ProcPageProc+
      0x
      275
     
    
- 
    
     
    
    
     
      0b 
      001df
      604 
      76ad
      5b
      7c USER
      32!InternalCallWinProc+
      0x
      23
     
    
- 
    
     
    
    
     
      0c 
      001df
      680 
      76ad
      59f
      3 USER
      32!UserCallDlgProcCheckWow+
      0x
      132
     
    
- 
    
     
    
    
     
      0d 
      001df
      6c
      8 
      76ad
      5be
      3 USER
      32!DefDlgProcWorker+
      0xa
      8
     
    
- 
    
     
    
    
     
      0e 
      001df
      6e
      4 
      76abc
      4e
      7 USER
      32!DefDlgProcW+
      0x
      22
     
    
- 
    
     
    
    
     
      0f 
      001df
      710 
      76abc
      5e
      7 USER
      32!InternalCallWinProc+
      0x
      23
     
    
- 
    
     
    
    
     
      10 
      001df
      788 
      76ab
      5294 USER
      32!UserCallWinProcCheckWow+
      0x
      14b
     
    
- 
    
     
    
    
     
      11 
      001df
      7c
      8 
      76ab
      5582 USER
      32!SendMessageWorker+
      0x
      4d
      0
     
    
- 
    
     
    
    
     
      12 
      001df
      7e
      8 
      74e
      94601 USER
      32!SendMessageW+
      0x
      7c
     
    
- 
    
     
    
    
     
      13 
      001df
      808 
      74e
      94663 COMCTL
      32!Button_NotifyParent+
      0x
      3d
     
    
- 
    
     
    
    
     
      14 
      001df
      824 
      74e
      944ed COMCTL
      32!Button_ReleaseCapture+
      0x
      113
     
    
- 
    
     
    
    
     
      15 
      001df
      884 
      76abc
      4e
      7 COMCTL
      32!Button_WndProc+
      0xa
      18
     
    
- 
    
     
    
    
     
      16 
      001df
      8b
      0 
      76abc
      5e
      7 USER
      32!InternalCallWinProc+
      0x
      23
     
    
- 
    
     
    
    
     
      17 
      001df
      928 
      76abcc
      19 USER
      32!UserCallWinProcCheckWow+
      0x
      14b
     
    
- 
    
     
    
    
     
      18 
      001df
      988 
      76abcc
      70 USER
      32!DispatchMessageWorker+
      0x
      35e
     
    
- 
    
     
    
    
     
      19 
      001df
      998 
      76ab
      41eb USER
      32!DispatchMessageW+
      0xf
     
    
- 
    
     
    
    
     
      1a 
      001df
      9bc 
      00be
      16fc USER
      32!IsDialogMessageW+
      0x
      588
     
    
- 
    
     
    
    
     
      1b 
      001dfdac 
      00be
      5384 taskmgr!wWinMain+
      0x
      5d
      1
     
    
- 
    
     
    
    
     
      1c 
      001dfe
      40 
      76bbed
      6c taskmgr!_initterm_e+
      0x
      1b
      1
     
    
- 
    
     
    
    
     
      1d 
      001dfe
      4c 
      77dc
      377b kernel
      32!BaseThreadInitThunk+
      0xe
     
    
- 
    
     
    
    
     
      1e 
      001dfe
      8c 
      77dc
      374e ntdll!__RtlUserThreadStart+
      0x
      70
     
    
- 
    
     
    
    
     
      1f 
      001dfea
      4 
      00000000 ntdll!_RtlUserThreadStart+
      0x
      1b
     
    
仔细观察清单2-4中的地址部分,很容易看出用户空间和内核空间的分界,也就是在栈帧04和栈帧05之间。栈帧05中的KiFastSystemCallRet函数属于ntdll模块,位于用户空间。栈帧04中的KiFastCallEntry函数属于nt模块,位于内核空间。栈帧04的基地址是9796fc24,属于内核空间;栈帧05的基地址是001df4dc,属于用户空间。它们分别来自这个线程的内核态栈和用户态栈。WinDBG的k命令穿越两个空间,遍历两个栈,显示出线程在用户空间和内核空间执行的完整过程,能产生如此完美的栈回溯显示了WinDBG的强大。
2.6.4 逆向调用
前文介绍了从用户模式进入内核模式的两种方法,通过这两种方法,用户模式的代码可以“调用”位于内核模式的系统服务。那么内核模式的代码是否可以主动调用用户模式的代码呢?答案是肯定的,这种调用通常称为逆向调用(reverse call)。
简单来说,逆向调用的过程是这样的。首先内核代码使用内核函数KiCallUserMode发起调用。接下来的执行过程与从系统调用返回(KiServiceExit)类似,不过进入用户模式时执行的是NTDll.DLL中的KiUserCallbackDispatcher。而后KiUserCallbackDispatcher会调用内核希望调用的用户态函数。当用户模式的工作完成后,执行返回动作的函数会执行INT 2B指令,也就是触发一个0x2B异常。这个异常的处理函数是内核模式的KiCallbackReturn函数。于是,通过INT 2B异常,CPU又跳回内核模式继续执行了。
  
   - 
    
     
    
    
     
      lkd> !idt 
      2b
     
    
- 
    
     
    
    
     
      Dumping IDT:
     
    
- 
    
     
    
    
     
      2b:   
      8053d
      070 nt!KiCallbackReturn
     
    
以上是使用WinDBG的!idt命令观察到的0x2B异常的处理函数。
2.6.5 实例分析
下面通过一个实际例子来进一步展示系统调用和逆向调用的执行过程。清单2-5显示了使用WinDBG的内核调试会话捕捉到的记事本进程发起系统调用进入内核和内核函数执行逆向调用的全过程(栈回溯)。
清单2-5 记事本进程从发起系统调用进入内核和内核函数逆向调用的全过程
  
   - 
    
     
    
    
     
      kd> kn
     
    
- 
    
     
    
    
      
      # ChildEBP RetAddr 
     
    
- 
    
     
    
    
     
      00 
      0006fe
      94 
      77fb
      4da
      6 USER
      32!XyCallbackReturn
     
    
- 
    
     
    
    
     
      01 
      0006fe
      94 
      8050f
      8ae ntdll!KiUserCallbackDispatcher+
      0x
      13
     
    
- 
    
     
    
    
     
      02 f
      4fc
      19b
      4 
      80595d
      2c nt!KiCallUserMode+
      0x
      4
     
    
- 
    
     
    
    
     
      03 f
      4fc
      1a
      10 bf
      871e
      98 nt!KeUserModeCallback+
      0x
      87
     
    
- 
    
     
    
    
     
      04 f
      4fc
      1a
      90 bf
      8748d
      4 win
      32k!SfnDWORD+
      0xa
      0
     
    
- 
    
     
    
    
     
      05 f
      4fc
      1ad
      8 bf
      87148d win
      32k!xxxSendMessageToClient+
      0x
      174
     
    
- 
    
     
    
    
     
      06 f
      4fc
      1b
      24 bf
      8714d
      3 win
      32k!xxxSendMessageTimeout+
      0x
      1a
      6
     
    
- 
    
     
    
    
     
      07 f
      4fc
      1b
      44 bf
      8635f
      6 win
      32k!xxxSendMessage+
      0x
      1a
     
    
- 
    
     
    
    
     
      08 f
      4fc
      1b
      74 bf
      84a
      620 win
      32k!xxxMouseActivate+
      0x
      22d
     
    
- 
    
     
    
    
     
      09 f
      4fc
      1c
      98 bf
      87a
      0c
      1 win
      32k!xxxScanSysQueue+
      0x
      828
     
    
- 
    
     
    
    
     
      0a f
      4fc
      1cec bf
      87a
      8ad win
      32k!xxxRealInternalGetMessage+
      0x
      32c
     
    
- 
    
     
    
    
     
      0b f
      4fc
      1d
      4c 
      804da
      140 win
      32k!NtUserGetMessage+
      0x
      27
     
    
- 
    
     
    
    
     
      0c f
      4fc
      1d
      4c 
      7ffe
      0304 nt!KiSystemService+
      0xc
      4
     
    
- 
    
     
    
    
     
      0d 
      0006feb
      8 
      77d
      43a
      21 SharedUserData!SystemCallStub+
      0x
      2
     
    
- 
    
     
    
    
     
      0e 
      0006febc 
      77d
      43c
      95 USER
      32!NtUserGetMessage+
      0xc
     
    
- 
    
     
    
    
     
      0f 
      0006fed
      8 
      010028e
      4 USER
      32!GetMessageW+
      0x
      31
     
    
- 
    
     
    
    
     
      10 
      0006ff
      1c 
      01006c
      54 notepad!WinMain+
      0xe
      3
     
    
- 
    
     
    
    
     
      11 
      0006ffc
      0 
      77e
      814c
      7 notepad!WinMainCRTStartup+
      0x
      174
     
    
- 
    
     
    
    
     
      12 
      0006fff
      0 
      00000000 kernel
      32!BaseProcessStart+
      0x
      23
     
    
根据执行的先后顺序,最下面一行(帧#12)对应的是进程的启动函数BaseProcessStart,而后是编译器生成的进程启动函数WinMainCRTStartup,以及记事本程序自己的入口函数WinMain。帧#0f表示记事本程序在调用GetMessage API进入消息循环。接下来GetMessage API调用Windows子系统服务的残根函数NtUserGetMessage。从第2列的栈帧基地址都小于0x800000000可以看出,帧#12~#0d都是在用户模式执行的。帧#0d执行我们前面分析过的SystemCallStub,而后(帧#0c)便进入了内核模式的KiSystemService。KiSystemService根据系统服务号码,将调用分发给Windows子系统内核模块win32k中的NtUserGetMessage函数。
帧#0a~#05表示内核模式的窗口消息函数在工作。帧#07~#05表示要把一个窗口消息发送到用户态。帧#04的SfnDWORD表示在将消息组织好后调用KeUserModeCallback函数,发起逆向调用。帧#02表明在执行KiCallUserMode函数,帧#01表明已经在用户模式下执行,这两行之间的部分过程没有显示出来。同样,帧#01 和帧#00 之间执行用户模式函数的过程没有完全体现出来。XyCallbackReturn函数是用于返回内核模式的,它的代码很简单,只有如下几条指令。
  
   - 
    
     
    
    
     
      USER32!
      XyCallbackReturn:
     
    
- 
    
     
    
    
     
      001
      b:
      77d44168 
      8b442404     mov   eax,dword ptr [esp+
      4] 
      ss:
      0023:
      0006fe84=
      00000000
     
    
- 
    
     
    
    
     
      001
      b:
      77d4416c cd2b          
      int   
      2Bh
     
    
- 
    
     
    
    
     
      001
      b:
      77d4416e c20400        ret   
      4
     
    
第1行把用户模式函数的执行结果赋给EAX寄存器,第2行执行INT 2B指令。执行过INT 2B后,CPU便转去执行异常处理程序KiCallbackReturn,回到了内核模式。
本文摘自《软件调试(第2版)卷2:Windows平台调试(上、下册)》

本书是国内当前集中介绍软件调试主题的权威著作。本书第2卷分为5篇,共30章,主要围绕Windows系统展开介绍。第一篇(第1~4章)介绍Windows系统简史、进程和线程、架构和系统部件,以及Windows系统的启动过程,既从空间角度讲述Windows的软件世界,也从时间角度描述Windows世界的搭建过程。第二篇(第5~8章)描述特殊的过程调用、垫片、托管世界和Linux子系统。第三篇(第9~19章)深入探讨用户态调试模型、用户态调试过程、中断和异常管理、未处理异常和JIT调试、硬错误和蓝屏、错误报告、日志、事件追踪、WHEA、内核调试引擎和验证机制。第四篇(第20~25章)从编译和编译期检查、运行时库和运行期检查、栈和函数调用、堆和堆检查、异常处理代码的编译、调试符号等方面概括编译器的调试支持。第五篇(第26~30章)首先纵览调试器的发展历史、工作模型和经典架构,然后分别讨论集成在Visual Studio和Visual Studio(VS)Code中的调试器,最后深度解析WinDBG调试器的历史、结构和用法。
本书理论与实践结合,不仅涵盖了相关的技术背景知识,还深入研讨了大量具有代表性的技术细节,是学习软件调试技术的珍贵资料。
转载:https://blog.csdn.net/epubit17/article/details/109895116
