深耕保护模式(二)

 

代码跨段执行

本质就是修改CS段寄存器

要点回顾

段寄存器:
ES,CS,SS,DS,FS,GS,LDTR,TR
段寄存器读写:
除CS外,其他的段寄存器都可以通过MOV,LES,LSS,LDS,LFS,LGS指令进行修改

CS为什么不可以直接修改呢?

CS为代码段,CS的改变意味着EIP的改变,改变CS的同时必须修改EIP,所以我们无法使用上面的指令来进行修改。

代码间的跳转(段间跳转 非调用门之类的(不能提升CPL权限))

段间跳转,有两种情况,即要跳转的段是一致代码段还是非一致代码段(参见代码段type域)

同时修改CS与EIP的指令

JMP FAR / CALL FAR / RETF / INT /IRETED

注意:只改变EIP的指令,这些不是我们要所讨论的。

JMP / CALL / JCC / RET

代码间的跳转(段间跳转 非调用门之类的) 执行流程

JMP 0x20:0x004183D7 CPU如何执行这行代码?

(1) 段选择子拆分

0x20 对应二进制形式 0000 0000 0010 0000

  • RPL = 00
  • TI = 0
  • Index = 4

(2) 查表得到段描述符

TI = 0 所以查GDT表

Index = 4 找到对应的段描述符,并不是所有的段描述符都可以跳转。

四种情况可以跳转:代码段、调用门、TSS任务段、任务门)

(3) 权限检查

如果是非一致代码段,要求:CPL == DPL 并且 RPL <= DPL。

如果是一致代码段,要求:CPL >= DPL。

简要说明什么是一致代码段什么是非一致代码段。

一致代码段又称共享代码段。假设操作系统有一段代码是提供了某些通用功能,这段代码并不会对内核产生影响,并希望这些功能能够被应用层(三环程序)直接使用,即可让一直代码段去修饰这块代码。这也是为什么一致代码段要求:CPL >= DPL(当前权限比描述权限低)就可以了。这段代码就是给低权限的应用使用的。

非一致代码段相反,严格控制权限。

(4) 加载段描述符

通过上面的权限检查后,CPU会将段描述符加载到CS段寄存器中.

(5) 代码执行

CPU将 CS.Base + Offset(本例是0x004183D7) 的值写入EIP 然后执行CS:EIP处的代码,段间跳转结束.

总结

  • 对于一致代码段:也就是共享的段。

特权级高的程序不允许访问特权级低的数据:核心态不允许访问用户态的数据。

特权级低的程序可以访问到特权级高的数据,但特权级不会改变:用户态还是用户态。

  • 对于普通代码段:也就是非一致代码段

只允许同级访问

绝对禁止不同级别的访问:核心态不是用户态,用户态也不是核心态.

直接对代码段进行JMP 或者 CALL的操作,无论目标是一致代码段还是非一致代码段,CPL都不会发生改变.如果要提升CPL的权限,只能通过调用门.

 

代码跨段执行实验

实验一

找一个非一致代码段描述符,复制一份,写入到GDT表中。

将 00cffb00`0000ffff 数据写如某个P位为0(该段描述符无效)的位置。

kd> eq 8003f048 00cffb00`0000ffff

在OD中,执行跨段跳转 JMP FAR 004B:0040126B,注意 EIP和CS。

执行前:

执行后:

是可以成功执行的,权限检查没有问题。

实验二

将00cffb00、0000ffff 改为00cf9b00、0000ffff(将DPL改为0环权限)

在OD中,执行跨段跳转 JMP FAR 004B:0040126B

执行前:

执行后:

直接进入ntdll.dll,说明遇到了异常,这里权限检查就是不通过的。

实验三

将00cf9b00、0000ffff改为00cf9f00、0000ffff(将该段描述符的属性更改为一致代码段,即共享段)

在OD中,执行跨段跳转 JMP FAR 004B:0040126B

执行前:

执行后:

执行成功,成功跳转,说明一致代码段是可以低权限访问高权限的。

总结

  1. 为了对数据进行保护,普通代码段(非一致代码段)是禁止不同级别进行访问的。用户态的代码不能访问内核的数据,同样,内核态的代码也不能访问用户态的数据.
  2. 如果想提供一些通用的功能,而且这些功能并不会破坏内核数据,那么可以选择一致代码段(共享代码段),这样低级别的程序可以在不提升CPL权限等级的情况下即可以访问.
  3. 如果想访问普通代码段,只有通过”调用门“等提升CPL权限,才能访问。

 

长调用与短调用(CALL)

通过JMP FAR可以实现段间的跳转,如果要实现跨段的调用就必须要学习CALL FAR,也就是长调用。

CALL FAR 比JMP FAR要复杂,JMP并不影响堆栈,但CALL指令会影响.

短调用

指令格式:CALL 立即数/寄存器/内存

发生改变的寄存器:ESP EIP。

长调用

跨段不提权

指令格式:CALL CS:EIP(EIP是废弃的)

发生改变的寄存器:ESP EIP CS

跨段提权(3环跳0环)

指令格式:CALL CS:EIP(EIP是废弃的)

当跨段提权时,堆栈已经不是原来的堆栈,是一个0环的堆栈,所以保留原来的堆栈(ESP)。CS和SS的权限是要保证一样的(intel定的规则),所以这里SS也需要保留。这里实际上保留什么寄存器就是什么寄存器发生变化。

发生改变的寄存器:ESP EIP CS SS

总结

1) 跨段调用时,一旦有权限切换,就会切换堆栈。

2) CS的权限一旦改变,SS的权限也要随着改变,CS与SS的等级必须一样.(intel规则)

3) JMP FAR 只能跳转到同级非一致代码段,但CALL FAR可以通过调用门提权,提升CPL的权限。

 

调用门(无参)

调用门执行流程

指令格式:CALL CS:EIP(EIP是废弃的)

执行步骤:

  • 1) 根据CS的值 查GDT表,找到对应的段描述符 这个描述符是一个调用门。
  • 2) 在调用门描述符中存储另一个代码段段的选择子。(具体看下图低四字节(16到31位))
  • 3) 选择子指向的段 段.Base + 偏移地址 就是真正要执行的地址。

门描述符的结构

可以看到高四字节的第8到11位是写死了的,S位(12位)是0,表明是一个系统段描述符,所有门都属于系统段描述符。

P位必须为1,表示该段描述符有效。

低四字节的16到31位是真正的段选择子,该段选择子对应的段描述符的base+门描述符的高16位到31位和低0到15位的偏移才是真正的执行地址。

windows中没有使用调用门,需要自己去构造调用门。

需要注意的几点:

  1. 是DPL要为3,如果不为3那么我们将无法访问到门描述符,敲门的资格都没有。
  2. ParamCount是传参用的,这里并不需要传参,都写成0。
  3. Segenment Selector是段选择子,这里要将权限设置为0环。

综上所述,设计的门描述符可以是:0x0000EC00`00080000。

修改gdt表,找一个操作系统并没有使用的段描述符更改,这样并不会蓝屏。

实验一

测试代码:

#include "stdafx.h"
#include <windows.h>
void _declspec(naked) GetGdtRegister()
{
    _asm
    {
        int 3

        retf  //不能是RET
    }
}
int main(int argc, char* argv[])
{
    char Buffer[6];

    *(DWORD*)&Buffer[0] = 0x12345678;     //eip随便填
    *(WORD*)&Buffer[4] = 0x004B;
    _asm
    {
        call fword ptr[Buffer]   //call cs:eip   
    }
    getchar();
    return 0;
}

这里不能直接运行,原因是门描述符给的limit是0,而base算下来(怎么算看上面门描述符结构图)也是0,加起来地址就是0。

那么这里比如我们想执行的是自己写的GetGdtRegister()函数,那么就要把limit写为该函数的地址(因为这里base是0)。

通过下断点,进入反汇编的方法可以找到地址,这里为0x00401010

那么将这个值写入limit,新的门描述符就应该是:0x0040EC00`00081010

修改后:

现在就可以执行了,这里还有一个需要注意的地方:

我们的代码虽然写的是三环程序的int3,但是由于这里权限已经提升,断点异常已经不再是三环程序处理(内核相比应用层具有优先处理权),应有内核层处理。这里直观的感受就是,0环调试器(windbg)断点了,vc6无法断点。

再观察执行前和执行后的堆栈变化。(参考跨段提权(3环跳0环))

注意ESP CS SS三个寄存器的值。

在windbg断点后,可以看到寄存器的值,其中ESP已经变成0环的堆栈(大于0x80000000),cs正是我们指定的值8。

再看堆栈内存,压栈的分别是:返回地址,CS,ESP,SS。这也更上面子标题“跨段提权(3环跳0环)”中的图是一样的。

实验二

我们既然已经提权到0环权限,那么我们就可以写只能在驱动开发中才能写的代码,比如:打印gdt表。(三环权限是不能够读取高两G内存的,属于内核管理)

测试代码如下:

#include "stdafx.h"
#include <windows.h>


BYTE GDT[6] = {0};
DWORD dwH2GValue;
void _declspec(naked) GetGdtRegister()
{
    _asm
    {
        pushad
        pushfd

            mov eax,0x8003f00C
            mov ebx,[eax]        //需要0环权限,高地址
            mov dwH2GValue,ebx
            sgdt GDT;          //读取GDT寄存器,这个指令3环也可以执行

        popfd
        popad

        retf
    }

}
void printRegister()
{
    DWORD GDT_ADDR = *(PDWORD)(&GDT[2]);
    WORD GDT_LIMIT = *(PDWORD)(&GDT[0]);
    printf("%x  %x   %x\n",dwH2GValue,GDT_ADDR,GDT_LIMIT);
}
int main(int argc, char* argv[])
{
    char Buffer[6];
    *(DWORD*)&Buffer[0] = 0x12345678;     //eip随便填
    *(WORD*)&Buffer[4] = 0x004B;
    _asm
    {
        call fword ptr[Buffer]   //call cs:eip   
    }
    printRegister();
    getchar();
    return 0;
}

可以看到我们做了驱动开发才能做的事,读取到了GDT表的base和limit,还有GDT表中某个位置的值。

 

调用门(有参)

在高四字节的0到4位表明参数有几个。

调用门描述符:0040EC03 00081030

kd> eq 8003f048 0040EC03`00081030

论证代码:

// T.cpp : Defines the entry point for the console application.
//


#include "stdafx.h"
#include <windows.h>

DWORD x;
DWORD y;
DWORD z;

void _declspec(naked) GetProc()
{
    _asm
    {
        pushad
        pushfd

        mov eax,[esp+0x24+0x8+0x8]
        mov dword ptr ds:[x],eax
        mov eax,[esp+0x24+0x8+0x4]
        mov dword ptr ds:[y],eax
        mov eax,[esp+0x24+0x8+0x0]
        mov dword ptr ds:[z],eax

        popfd
        popad

        retf 0xC
    }
}

void PrintArgs()
{
    printf("%x %x %x",x,y,z);
}
int main(int argc, char* argv[])
{
    char buff[6];
    *(DWORD*)&buff[0] = 0x12345678;
    *(DWORD*)&buff[4] = 0x4B;
    _asm
    {
        push 1
        push 2
        push 3
        call fword ptr[buff]
    }
    PrintArgs();
    getchar();
    return 0;
}

可以读取到参数,那么自然也可以对参数进行其他操作。

(完)