代码跨段执行
本质就是修改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
执行前:
执行后:
执行成功,成功跳转,说明一致代码段是可以低权限访问高权限的。
总结
- 为了对数据进行保护,普通代码段(非一致代码段)是禁止不同级别进行访问的。用户态的代码不能访问内核的数据,同样,内核态的代码也不能访问用户态的数据.
- 如果想提供一些通用的功能,而且这些功能并不会破坏内核数据,那么可以选择一致代码段(共享代码段),这样低级别的程序可以在不提升CPL权限等级的情况下即可以访问.
- 如果想访问普通代码段,只有通过”调用门“等提升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中没有使用调用门,需要自己去构造调用门。
需要注意的几点:
- 是DPL要为3,如果不为3那么我们将无法访问到门描述符,敲门的资格都没有。
- ParamCount是传参用的,这里并不需要传参,都写成0。
- 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;
}
可以读取到参数,那么自然也可以对参数进行其他操作。