欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创]调用门的使用以及调试调用门的注意事项

[原创]调用门的使用以及调试调用门的注意事项

目前在科锐的学习已经进入了安卓阶段。温故而知新,故把在学习Windows内核阶段时比较感兴趣的调用门进行了整理。在这里与大家分享一下。

由于论坛的版面问题,很多图片看似不清晰。建议大家下载附件中的Word文档。Word文档中的图片是我对角拉伸缩小过的。可以对角拉伸放大,防止图片变形。

纲要:

   CPU手册中调用门相关章节的解释以及名词解释(因为还涉及到多方面的知识结构,这里就只包含了和调用门紧密相关的部分资料)。

   使用内核调试器向GDT中安装一个调用门描述符。

   利用调用门来切换特权级,从Ring3到Ring0的切换。

   在调用门中使用参数、全局变量和测试特权指令。

   调试调用门时的要注意的事项。

调用门可以理解为一个可以安全的切换特权级的使用起来稍微麻烦些的工具。调用门通过CALL FAR和RET FAR来调用和返回。

强调:在Windows XP中并没有使用调用门;听说在Windows 7 64位版本中操作系统利用调用门进行32位向64位的转换,也就是当一个运行在64位操作系统上的32位程序通过调用门转换之后最终调用了64位的内核代码。调用门内部进行了对参数的转换。


以下内容摘自CPU手册综合卷

   


名词解释:

   Privilege Levels(特权级) - Protection Rings(保护环)

      在卷一的6.3.5节和卷三的5.5节都有相关解释。大意:提供4种特权级是为了处理器的段保护机制,编号从0到3。编号越大,特权级越小,相对的可以使用的指令也就越少。保护环就是特权级,特权级就是保护环。手册中规定Ring0用于操作系统内核非常重要的代码和数据,一般都被操作系统控制。Ring1、2用于操作系统的服务、设备、驱动等等。Ring3用于普通的应用程序。Windows操作系统只使用到了4种特权级中的两种,Ring0和Ring3。CPU手册中也说明,是可以只使用4种特权级中的两种,Ring0和Ring3。

      

   Procedure

      该词汇在手册卷三的2-4页底部有注解:procedure是对逻辑单元或代码块的总称。可以理解为程序、过程、函数或者是例程都可以。本文解释为例程,比较符合我的个人观点。

      

   Current privilege level (CPL): 当前特权级,表示当前正在被执行的任务或程序所在的特权级。存储在CS和SS段寄存器的第0位和第1位,当程序流程切换到不同的特权级时,处理器会改变CPL。

   Descriptor privilege level (DPL):描述符特权级,表示段或门的特权级。存储在描述符的DPL字段。

   Requested privilege level (RPL):请求的特权级,存储在段选择子的第0位和第1位。

   GDT:全局描述符表,存储了所有段描述符、数据描述符或者是系统描述符。GDT的地址存放在GDTR寄存器中。

   LDT:局部描述符表,和GDT一样可以存储各种描述符。LDT的地址存放在LDTR寄存器中。Windows没有使用LDT。

   IDT:中断描述符表。IDT的地址存放在IDTR寄存器中。


5.8.5 - 栈切换

   每当使用一个调用门转移程序控制到一个高特权级的非一致代码段(也就是非一致目标代码段的DPL比CPL小)时,处理器自动切换到目标代码段特权级的栈。栈切换是为了预防高特权级的例程由于栈空间不足而导致的崩溃。它也阻止了低特权级例程通过共享栈有意或无意的干扰高特权级例程(例如通过修改高特权级例程的返回地址来提权等等…)。

   每个任务必须定义最多4个栈:一个给运行在特权级3的应用程序代码使用并且特权级2、1和0也都有一个栈。也就是每个特权级都有自己的栈。如果只使用了特权级3和0,那么必须定义两个栈。每个栈的位于单独的段并且用一个段选择子(栈段SS)和偏移(栈指针ESP)来确定栈。

   Windows操作系统只使用了特权级3和0。

   特权级0、1和2的栈指针存储在当前正在运行任务的TSS中。图7-2。这些指针由一个段选择子和栈指针(ESP)组成。这些指针初始化为完全只读的值(也就是静态的)。当任务运行时处理器不会改变他们。他们只是用来在调用到更高特权级时创建新栈。当从被调用例程返回时将处理这些栈。在之后例程被调用使用初始化栈指针创建一个新栈。TSS没有指定特权级3的栈,因为处理器不允许程序控制从运行在CPL0、1或2的例程转移到一个运行在CPL3的例程,除了返回(ret)之外。

   TSS中的SSx、ESPx(x=0,1,2)字段是静态的,除非软件修改了它们。假设操作系统为用户任务的TSS填写了ESP0其值为0x800;当通过调用门进入特权级0的代码段时,会切换到特权级0的栈。栈指针ESP初始值就是0x800;返回时假设ESP变成了0x808,处理器并不会把0x808写回到ESP0字段;下次再使用调用门进入0特权级的代码段时,使用的还是0x800来初始化特权级0的栈指针。

   

   操作系统负责为所有被使用的特权级创建栈和创建栈段描述符。并且保存这些栈指针的初始值到TSS中。每个栈必须是可读写访问(在段描述符的type字段指定)并且必须有足够的空间(在段描述符的limit字段指定)来保存以下信息:

      1.  调用者例程的SS、ESP、CS和EIP寄存器内容。

      2.  被调用例程所需要的参数和临时变量(也就是局部变量)需要的空间。

      3.  当对异常或中断处理例程进行隐式调用时,还需要EFLAGS寄存器和错误码的空间。

   由于一个例程可以调用其他例程,因此每个栈必须有足够的空间来保存很多帧的数据(每一帧都表示上述的三个信息)。

   如果操作系统没有使用处理器的多任务机制,它还是必须为栈的相关操作创建至少一个TSS。

   当通过调用门调用一个例程导致特权级改变时,处理器执行以下步骤去切换栈并且在新特权级上开始执行被调用的例程: 

      1.  使用目标代码段的DPL(也就是新的CPL)从TSS选择一个指向新栈指针。也就是说使用调用门的Segment Selector字段作为目标代码段,如果目标代码段的DPL为0那么在TSS中选择SS0和ESP0来作为被调用例程栈的栈段选择子和栈指针。

      2.  从当前TSS中为将要被切换的栈读取栈段选择子和栈指针。当读取栈段选择子、栈指针或栈段描述符时发现任何违规都会产生一个无效TSS(#TS)异常。

      3.  检查目标特权的栈段描述符和类型如果检测到违例将产生无效TSS(#TS)异常。

      4.  临时保存当前(调用者例程)的SS和ESP寄存器的值。保存在处理器内部。

      5.  为新栈(被调用例程)加载段选择子和栈指针到SS和ESP寄存器。

      6.  push临时保存(调用者例程)的SS和ESP寄存器的值到新栈(被调用例程)中。图5-13

      7.  拷贝调用门Param.Count字段指定的参数数量到新栈中。也就是说从调用者的栈中拷贝调用门的Param.Count字段指定个数的参数到被调用例程的栈中。

      8.  push返回指令指针(调用者例程的CS和下一条指令的指针)到新栈中。

      9.  从调用门分别加载新代码段的段选择子和新指令指针到CS和EIP寄存器中。并且被调用的例程开始执行。代码段的段选择子在Segment Selector字段指定,新指令指针在Offset in Segment字段指定。

      

   在调用门的Param.Count字段指定了处理器应当从调用者的栈拷贝到被调用者的栈数据的数量(最多31个,因为Param.Count字段只有5个2进制位)。如果需要传递给被调用者的数据超过31个,那么参数可以是指向数据结构的指针或使用已经保存的SS和ESP寄存器来访问调用者的栈空间。(这里我并不能理解如何通过已经保存的SS和ESP来访问调用者的栈空间,是通过段超越前缀的这种方式吗???好像又不对。。我是直接通过DS段寄存器来访问调用者的栈空间的。)


5.8.6 - 从被调用例程中返回

可以使用RET指令在相同的特权级上执行近返回、远返回;在不同的特权级上执行远返回。预期是为了从一个使用CALL指令从被调用的例程中返回的指令。不支持从一个JMP指令返回,因为JMP指令不在栈中保存要返回的指令指针。

   近返回只在当前代码段中进行程序控制转移;因此处理器只做界限检查。当处理器从栈中pop指令指针到EIP中时,处理器检查指针是否超出了当前代码段的界限。

   在相同的特权级远返回,处理器从栈中pop要返回的代码段的段选择子和返回指令指针。正常情况下,这些指针应当是有效的,因为是由CALL指令push到栈上的。然而,处理器依然会进行特权级检查来检测是否改变了指针或者对栈维护的不当。

   只有当返回到低特权级(要返回的代码段的DPL在数值上大于CPL)时才允许改变特权级。处理器使用调用者例程保存的CS寄存器中的RPL字段决定是否可以返回到数值上较高的特权级。如果RPL的特权比CPL低。触发一个return acorss privilege levels

   当远返回到调用者例程时,处理器执行以下步骤: 

      1.  检查保存的CS寄存器值(调用者的CS寄存器)的RPL字段决定是否需要再返回时改变特权级。

      2.  在被调用者例程的栈上加载CS和EIP寄存器的值。

      3.  (如果RET指令包含一个Param.Count操作数并且返回需要改变特权级)在pop出CS和EIP值之前,当前ESP寄存器的值加上Param.Count(从RET指令获得,字节为单位),在被调用调用例程的栈上略过这些参数。ESP寄存器的结果指向保存的被调用者例程的栈的SS和ESP值(注意RET指令中的字节数必须与调用者例程中引用的调用门中Param.Count相匹配)。

      4.  (如果返回需要改变特权级)用保存的SS和ESP值加载到SS和ESP寄存器并且切换回调用者例程的栈。丢弃被调用者例程的栈的SS和ESP的值。当加载栈段选择子或栈指针时任何界限违规被检测到,将触发一个通用保护异常(General-Protection Exception #GP)。还要检查新栈段描述符的类型和特权违规。

      5.  (如果RET指令包含一个Param.Count操作数)当前ESP寄存器值加上Param.Count(从RET指令获得,字节为单位),在调用者例程的栈上略过这些参数。ESP寄存器的结果是不检查栈段的界限。如果ESP的值超过了界限,直到下一次栈操作才会被发现。

      6.  (如果返回需要改变特权级)检查DS、ES、FS和GS段寄存器的内容。如果哪个段的DPL比新CPL小(除了一致代码段),那么这个段寄存器被置为0(Null Segment Selector)。

5.8.3  调用门

   调用门帮助在不同特权级之间的程序控制转移。通常只在操作系统或执行者使用特权级保护机制时使用。调用门也可以使用在16位和32位代码段之间转移程序控制,详见本卷21.4节。

   图5-8显示了调用门描述符的格式。调用门描述符可以安装在GDT或LDT,但不能安装在中断描述符表(IDT)。它有6个功能:

 

      1.  它指定要访问的代码段。

      2.  它定义了在指定代码段例程的入口点。

      3.  它指定了调用者尝试去访问例程所需要的特权级。

      4.  如果发生了栈切换,它指定了拷贝到新栈的可选参数的数量。

      5.  它定义了push到目标栈的值的大小:16位门强制16位push,32位门强制32位push。

      6.  它指定了调用门描述符是否有效。

  在调用门中的Segment Selector字段指定了将要访问的代码段。Offset字段指定了代码段的入口点。通常入口点是指定例程的第一条指令。DPL字段指定了调用门的特权级,反之通过调用门访问指定例程所需要的特权级。P标志字段指定了调用门描述符是否有效。如果栈切换发生,Param.Count字段指定了从调用者例程的栈拷贝到新栈的参数数量。Param.Count指定16位调用门使用Word为单位,32位调用门使用DoubleWord为单位的数量。 

   注意:通常门描述符的P标志总是置为1。如果置为0,当程序试图去访问时,将产生一个不存在(#NP)异常。操作系统可以使用P标志做特殊用途。例如:它可以用来检查门的使用次数。在这里P标志初始化为0,因为这样会导致不存在异常处理的Trap(陷阱)从而使程序进入异常处理程序中。异常处理程序然后递增计数器并将P标志置为1,以便从处理程序返回时,门描述符是有效的。


调用门的应用篇

   本篇所有内容都在Windows XP SP3的虚拟机环境下进行调试。

   通过内核调试器向GDT中安装一个有4个参数的调用门并使用Ring3的程序来调用它,从而改变特权级到Ring0。

      首先编写Ring3程序。这里我是用了VC++6.0 SP6编译器、链接器版本和操作系统版本如下图所示

         

      测试程序只有两个函数

         main函数的大致功能: 

            Ring3的程序先在使用调用门之前获取了各个段寄存器的值并打印到屏幕。

            call 0x48段选择子所指向的安装在GDT中的调用门,而调用门所指向的例程用来再次获取各个段寄存器的值并返回到Ring3。

            在调用门返回之后再次打印通过调用门获取到的各个段寄存器的值。

            再次获取Ring3各个段寄存器的值并打印。 

         调用门例程的大致功能:

            测试特权指令sgdt获取GDTR寄存器的值,并保存在全局变量中。

            Ring3传递的第四个参数是一个指针,将调用门中各个段寄存器的内容保存在这个指针指向的内存空间中。

            累加Ring3传递给调用门的前三个参数作为测试。

      编译Ring3的程序后,获取调用门例程的例程地址。将调用门例程Gate()的函数名拖到反汇编窗口即可看到该例程的地址。

         

      根据调用门例程地址0x00401020构造一个调用门描述符,图5-8中描述了调用门描述符各个字段的意义。 

         结果:0040EC04`00081020

         Offset in segment 15:00 = 0x1020(调用门例程的低16位)

         Segment Selector = 0x0008(该字段的第0和第1位指定了RPL,第2位指定了目标代码段的描述符是在GDT还是LDT中,,而0x0008的低三位均为0,则表示目标代码段描述符在GDT中,并且RPL是特权级0。段描述符刚好是Ring0的代码段描述符,从而使特权从3切换到0。)

         Param.Count = 00100b = 4(表示调用门有四个参数)

         Type = 1100b = 0xC(表示这个描述符是一个调用门描述符)

         DPL = 11b = 3(表示Ring3的例程可以使用这个调用门描述符)

         P = 1(必须为1,否则在使用时会产生#NP不存在异常)

         Offset in segment 31:16 = 0x0040(调用门例程的高16位)

         为什么要将Offset in segment拆成两部分是为了兼容老处理器,具体就不再叙述,总之好多地方都是为了兼容老处理器而设计的。属于历史遗留问题。

      然后在内核调试器WinDbg中通过r命令查看GDTR寄存器的值。

      安装调用门:以64位修改内存的命令eq来修改GDT基址+0x48的位置(因为我们的Ring3程序调用了0x0048段选择子的调用门,+0x48的位置刚好是一个空段描述符)的值为:0040EC04`00081020

         

      在Ring3测试调用门,因为微软编译器并不支持__asm { call 0x0048:0 }这种代码所以需要将call 0x0048:0的字节码直接写入到程序中。为了调试方便还需要加入一条int 3指令。

         

      直接运行Ring3的程序,因为代码中有int 3指令,所以会被内核调试器直接接收到。如果使用Ring3的调试器则会被Ring3的调试器收到。

         

         要注意的是调用门最好不要步入(Step In),步过(Step Over)最好。需要解释一下:

            步入之后会修改FS寄存器的值为0x30

            如果调用门中使用了参数,那么在RETF指令之后会直接蓝屏

            如果没有使用参数,那么RETF指令返回到Ring3之后虽然不会蓝屏但是也会把FS寄存器的值置为0。。。

            原因我也不清楚,貌似是和中断有关。困扰了我好久也依然不知道原因。但是步过的话就一切正常。

            这里我就直接步入了。注意:重启之后需要重新安装调用门。

         

         可以看到在调用门例程中我使用了Ring3的一个局部变量和全局变量地址0x427C48,其实不必大惊小怪,因为我并没有使用栈段SS来访问这些局部变量,而是使用了DS来访问的,而调用门中的DS和Ring3的DS是相同的。注意CS和SS的值已经切换到Ring0了。 

      因为蓝屏,重启并重新安装调用门,步过调用门,我们再看看Ring3的栈变化。因为调用门的RETF在栈切换时会自动帮我们把Ring3的栈平衡,所以不需要担心。

         

      最后我们在控制台中来观察一下各个段寄存器的变化,发现CS、SS寄存器在调用门中被修改为目标特权级的CS和SS,返回之后又还原了。而其他段寄存器的值并未发生变化。

         

      至此调用门的安装,使用和调试全部讲完。最需要注意的是,最好不要步入调用门内部。

      我们也可以使用驱动来安装一个可以指定参数个数的调用门,由于篇幅过长,就不再叙述。代码详见附件。

上传的附件 src.rar
Call Gate.docx

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功