内核01
Posted ltyandy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了内核01相关的知识,希望对你有一定的参考价值。
段描述符
数据段描述符
代码段描述符
系统段描述符
A - 访问 E - 向下扩展
AVL - 供程序员使用 G - 粒度
B - BIG P - 段是否有效
C - CONFORMING R - 可读
D - 默认 W - 可写
DPL - 描述符特权级
通用描述符
L - 64位代码段(IA-32E模式)
AVL - 可供系统软件使用
BASE - 段基地址
D/B - 默认操作大小(0=16位段;1=32位段)
DPL - 描述符特权级别
G - 粒度
LIMIT - 段限长
P - 段是否有效
S - 描述符类型(0=系统段 , 1=代码段或数据段)
TYPE - 段的类型
描述符概述
在实模式中(以前16位CPU所使用的模式) , 在内存中的任何地址上都能够执行代码, 所有的内存地址都是可以被读写的, 这非常不安全. 然而并没有其它手段能够限制或者说禁止代码去读写某个内存地址(例如,在保护模式下,读写地址0是错误的,会导致程序崩溃,在实模式下就不会崩溃). CPU为了提供限制/禁止的手段, 提出了保护模式. 保护模式实际上就是保护了内存, 使得内存中能够被执行代码,能够被读写的地址可以被人为得控制. 在保护模式中, 系统和用户进程是被隔离开的. 用户进程无法修改系统的内存,也无法执行系统的代码.这些也是通过保护模式达成的功能. 保护模式所实现的功能, 很大程度上依赖分段机制. 在实模式下, 分段机制很简单, 它没有限制段是否的读,写,执行属性, 也没有限制一段内存有什么权限,能够在什么权限下被读,写,执行, 只是规定了, 要使用内存, 就需要使用段基地址*16 + 段内偏移
的方式来寻址. 在保护模式下, 它兼容实模式下的寻址方式(段基地址*16 + 段内偏移
),并且在这基础之上给一个段增加了段基地址, 段的长度, 段的属性这三个属性以实现保护模式下的部分功能. 在保护模式下, 描述一段的基地址在哪里, 段有多长,段有何属性的结构被称之为段描述符.其结构如下所示:
段描述符结构
typedef struct Descriptor{
unsigned int base; // 段基址
unsigned int limit; // 段限长
unsigned short attribute; // 段属性
}
在保护模式下, 增加了很多机制, 使得段产生了不少种类:
数据段(用于存储数据,供程序读写)
代码段(用于执行代码)
系统段(用于操作系统提供特殊功能)
每个段描述既能够描述出一段内存从哪开始,到哪结束, 还能描述这个段是什么类型(代码段,数据段,系统段) ,当然, 也能够描述这个段是否可读,是否可写,是否可执行, 甚至还能描述这个段的权限是什么, 在什么权限下才能使用这一段内存.
段基址
在描述中总长度为32位(4字节) . 表示段的开始地址.
段限长
在描述中总长度为24位, 表示段的最大长度, 也就所, 此位段表示的值最大为: 0xFFFFF
, 也就是1Mb
, 此长度表示的是单位, 至于一个单位的长度到底是多少字节, 依赖于段属性的G
位(粒度位)
如果粒度位(G
位)等于0 , 段的大小范围为1字节
到1Mb
(0~0xFFFFF) , 每个单位为1字节.
如果粒度位(G
位)等于1 , 段的大小范围是1字节
到4Gb
(0~0xFFFFFFFF), 每个单位为4Kb
段属性
P位
P位用于记录当前段描述符是否有效.
p==0
: 无效, 系统不会使用该段描述符
p==1
: 段描述符有效.
S位
描述符类型(0=系统段 , 1=代码段或数据段), 这个位的值决定了Type
字段的值是何种含义.
Type
当S位等于1的时候, Type字段描述的是数据段或代码段
当Type字段的最高位(在段描述符中的11位)等于0时, 是数据段
当一段是数据段时, Type字段的(8,9,10位分别为
A
,W
,E
, 其中,
A
- 数据段是否已经被访问, 等于1表示已经被访问
W
- 数据段是否可写, 等于1表示可写.否则为只读
E
- 数据段的扩展方向, 等于1表示向下扩展(向下扩展也可称为向外扩展) , 等于0时表示向上扩展(向内扩展)
代码段/数据段
当Type字段的最高位(在段描述符中的11位)等于1时, 是代码段
当一个段是代码段时, Type字段的8,9,10位分别为
A
,W
,C
, 其中,A
,W
的作用和数据段一样.C` - 段是否是一致代码段, 等于1表示是`一致代码段`, 等于0表示是`非一致代码段
系统段
当S位等于0的时候 , Type字段的值描述的是系统段. 系统段中的描述符类型一般都是门描述符. 当这个描述符是一个段描述符之时 , Type
字段就没有像代码段或数据段中的A,W,E
标志了, Type
字段的值决定了这个描述符的作用:
D/B位
D/B位的会作用到代码段(CS
段寄存器) , 栈段(SS
段寄存器),数据段(DS
,ES
段寄存器),当使用这些段寄存器时, 将会受到不同的影响:
可执行代码段(
CS
段)此标志位被称为
D
位, 这个位会影响指令和操作数的寻址模式.D/B
== 1 - 指令默认的寻址模式是32位,操作数默认为32位或8位.D/B
== 0 - 指令默认使用16位寻址模式, 操作默认大小为16位或8位.指令前缀
67H
可以用来切换寻址模式. 例如, 当前D
==1时, 寻址模式是32位, 切换之后,寻址模式就变成16位模式指令前缀
66H
可以用来切换操作数大小, 例如, 当前D==0
时, 操作数大小默认为32,切换之后,操作数大小为16位.栈段(
SS
段)此标志位被称为
B
位,B
== 1 - 默认使用32位的ESP
寄存器操作栈B
== 0 - 默认使用16位的SP
寄存器操作栈向下展开的数据段
CPU虽然提供了这个机制, 但是操作系统并没有使用这个机制
段寄存器和段描述符
CPU提供了段描述之后, 操作系统就可以使用这种机制来做出各种各样的限制. 例如, 以下汇编代码
mov eax , dword ptr ds:[0x403000]
在这条汇编指令当中, 保护模式的分段机制在无形中产生对该指令产生了影响:
指令正在访问的地址是
段基地址*16 + 段内偏移
如果在16位的实模式下,一般就是
ds*16+0x403000
, 但是在32位的保护模式下, 16位的段寄存器并不能存储一个64位的段描述符.段寄存器保存的值被称为段选择子.
真正的段描述符存储在内存中, 由
GDTR
寄存器记录其基地址和大小. 那么 , 16位的段寄存器如果才能和64位的段描述符对应起来?
全局描述符表(GDT)
在一个系统中, 描述符的种类有多个, 分别有数据段,代码段, 系统段. 系统段又分为多种,有调用门,中断门,陷阱门,任务门. 因此, 在一个系统中, 描述符是存在多个的. 这些描述符被统一打包存储在内存中, 它们所形成的一个数组被称之为全局描述符表. 全局描述符的小标则保存于16位的段寄存器中. 一个16位的段寄存器实际由以下部分组成: 段寄存器实际的长度为96位, 16位的值, 只是寄存器的可见部分, 段寄存器还有80位是隐藏部分 , 这个隐藏部分只能被CPU所操作,无法通过任何指令来操作它. 这可见部分的16位的值也并非全部用于保存全局描述符的下标, 它被划分为以下格式: 也就是说, 只有13位是用于保存全局描述符表的下标. T1 - 用于记录,保存的下标是GDT
(全局描述符表)的还是LDT
(本地描述符表)的(windows操作系统没有LDT
) RPL - 当前请求级别 , 用作权限检查. 一共有4个值: 0~3 , 数值越小,权限越大, 0代表最高权限.
由于段寄存器用于保存段选择子, 因此, 给一个段寄存器赋值,就不单单是赋值一个数字了,例如:
mov ax,2Bh
mov ds,ax
这条指令可看成将0x2B
赋值给ds
寄存器, 实际不是. 将0x2b
的二进制展开: 0000 0000 0010 1011
, 段选择子的格式为: 13 : 1 : 3
. 那么在0x2b
这个数中, 描述符表索引,T1位,RPL分别为:
0000 0000 00101 0 11
\\_____________/ - --
| | |
| | +---> RPL = 3
| +-------> T1 = 0(GDT)
+----------------> 索引 = 5
也就说, 0x2b
这个数代表的是GDT
表中第5
个段描述符. 当前请求级别为最低权限的2
. mov ds,ax
这条指令执行之后做了什么? CPU执行这条指令后, 会将GDT
表中第5
个段描述符存储在段寄存器隐藏部分, 将段选择子存储到16位可见部分. 当然, 在做这些之前, CPU还需要做权限检查.
权限检查
段描述符中 , 有一个属性是DPL
, 这个属性总共有2个二进制位. 大小和段选择子中RPL
一样. 这二者正是用于作权限检查的. DPL
指的是描述符特权级别, 它决定了在什么特权下,才能够访问此描述符. 其值从0~3
共4个,0最表示最高权限, 3表示最低权限, 只有权限高于等于此DPL
所记录的权限,才能访问描述符. RPL
指的是请求级别, 值的是以何种权限去请求GDT表中的段描述符. 也就是说, 当指令mov ds,ax; // ax==2B
执行是, CPU会将数值2B
作为段选择子,使用这个数的低两个二进制位作为RPL
, 使用这个数的高13
为作为描述符表的索引, 去获取描述符, 但如果要获取的描述符的DPL
的值比RPL
要小(值越小权限越高) , 这条指令就无法取出这个段描述符,就完成不了赋值, CPU还会报一个异常.
除此之外, CPU还有其它检查, 权限检查是最后一项 , 这些检查依次为:
段描述符有效位检查
检查段描述符的
P
为是否为1 , 如果为0 , 说明该描述符无效,CPU会触发一个异常.段类型检查
例如, 将一个可读可写但不可执行的段加载到
CS
段寄存器是错误的,因为CS + IP
执行的是代码,如果这个段不能执行,那就没有意义. 将一个只读的段加载到SS
是错误的, 因为栈段是可以被改写的, 如果这个段加载到SS
却不能修改,也是没有意义的.段权限数据读写检查
无论当前执行的是什么指令, 都会使用
CS
段选择子中的低2
位来作为CPL
(表示当前执行级别) , 使用被加载的段选择子的低2
位作为RPL
(表示当前请求级别) , 使用被加载的段选择子的高13
位作为描述符表的下标,并从表中取出段描述符,得到该描述符表的DPL
(表示描述符特权级别). 当CPL
>DPL
或者RPL
>DPL
时, 表示权限不够, 段描述符就会加载是被 也就是说, 当CPL
或RPL
只要其中一个比DPL
要大, 操作就会失败. 这是因为操作系统不希望用户程序能够随意切换段描述符, 在32位保护模式下, 每个段描述符的基地址都是0, 段限长都是4Gb, 但其类型是不同的. 系统在创建一个用户进程的时候, 会将用户线程的CS
段寄存器的低2位置为2
,也就是最低的权限的CPL
. 这时, 这个系统就永远不能通过正常方式来获取高权限的段描述符, 也就无法访问和修改系统的内存了.段权限执行检查 在汇编中, 有一些指令是可以跨段跳转的. 例如:
jmp 33:401000
call 33:401000
上述两条指令被称为远跳转指令和远调用指令, 指令后的操作数分为两部分 : 段选择子和段内偏移. 这两条指令在执行后, 会将操作数中的段选择子对应的段描述符加载到CS
中, 此时, CPU也会做检查:
1. 检查请求的段描述符的`S`是否为1, 如果是1, 表示是请求的段是一个数据段或代码段, 再检查`Type`的高位是否 是1 , 如果是1 , 表示请求的段是代码段. 如果其中一个不是,就无法加载,指令无法执行,CPU还会报异常.
2. 继续检查,`Type`的`C`标志, 如果是一致代码段, 则要求`CPL`>=`DPL` , 也就是只能低权限转移到高权限, 如果是非一致代码段, 则要求 ` CPL==DPL` 并且, `RPL<=DPL`, 也就是平级才能转移.
但是转移之后, `CPL`和`RPL`不会改变.
3. 如果检查`S`位是0 , 则表示请求的是系统段.
下面是伪代码:
//段选择子的结构:
// [描述符下标:13 | T1:1 | RPL:2]
unsigned short segSel = 0x33;// 0x33就是要切换的段选择子,在指令jmp 33:401000 中给出。
unsigned int RPL = segSel & 0x11; // 取段选择自的低2位作为RPL
unsigned int CPL = CS & 0x11 ; // 取`CS`段寄存器的值的低2位作为CPL
if( SegDes.S == 1 && SegDes.Type & 0x1000 ){
if(SegDes.Type.E == 1){ // 一致代码段
if( CPL >= segDes.DPL ){
CS = segSel; // 可以切换。
}else{
throw "异常";
}
}else{ // 非一致代码段
if(CPL == SegDes.DPL && RPL <= SegDes.DPL){
CS = segSel;
}else{
throw "异常";
}
}
}else{
throw "异常";
}
系统段描述符 - 门描述符
很多时候, 用户层的代码需要切换到内核层执行代码。 因为有些代码执行时需要用到0环权限.
切换0环权限实际就是将CS
段寄存器的CPL
改成0(也就是0环权限).
但在3环时,CS
的CPL
是2, 是无法直接修改的(如果要修改,就需要切换段选择子, 切换段选择子,就需要使用CPL
,RPL
和段描述符中的DPL比较
)
当段描述符中的S
位等于0 , 表示这个描述符是一个门描述符。
门描述符一般用于从3环进入到0环,并能够将3环权限切换成0环权限。
门描述符的种类有:
调用门(Windows操作系统没有使用此机制)
中断门(IDT表中的中断处理函数就是这种门描述符)
陷阱门(IDT表中的陷阱处理函数就是这种门描述符)
任务门(用于任务切换)
调用门
调用门的出现是为了便于在不同的权限直接切换.
一个调用门中, 保存了以下信息:
要执行的函数的地址
要执行的函数的参数个数
段选择子 (这个段选择子用于切换权限)
字段解析:
Offset in Segment
- 函数在段内的偏移, 实际就是函数的地址, 这个地址的被拆分成高16位和低32位保存.Segment Selector
- 段选择子, 使用调用门时, 这个选择子就是被切换成CS
的选择子.Param Count
- 调用门中保存的函数的参数个数. 注意, 每个参数应当是4字节的.
如果发生了权限切换, 系统也会将用户栈切换成内核栈, 也就是权限切换时, CS
段寄存器的值会被改掉(不改掉切换不了切换段描述符) , 还会将SS
段选择子,切换为内核的段选择子, 将ESP
切换成内核的ESP
, 切换前,SS
,ESP
的值都会被保存. 在切换回来之后,才进行还原.
因为栈要进行切换, 当函数被调用时, 用户栈中的函数参数会被拷贝到内核栈, 因此, 需要在调用门描述符中指定参数的个数是多少, 否则系统将无法为函数拷贝参数到内核栈中.
调用门的设置和使用
因为在Windows中没有使用调用门, 因此, 在GDT表中是没有调用门描述符的.
想要试验一个调用门,就需要自己使用windbg
开启双击调试,并自行在GDT中设置一个.
设置的方式如下:
构造如下结构体:
typedef struct _CALLGATEDESCRIPT{
unsigned int functionAddrLow : 16; // 被调用函数地址的低16位
unsigned int SegmentSelector : 16; // 想要切换的段选择子
unsigned int paramCount : 5; // 参数个数
unsigned int none : 3; // 无
unsigned int Type : 4; // 系统段类型, 必须为12(1100b: 32位调用门)
unsigned int S : 1; // S位,必须为0
unsigned int DPL : 2; // 描述符特权级别
unsigned int P : 1; // 描述符有效位
unsigned int functionAddrHig : 16; //被调用函数地址的高16位
}CALLGATEDESCRIPT;
设置如下:
unsigned long long createCallGateDescript(unsigned short selector,/要切换的段选择子/
unsigned int functionAddr,/要通过调用门执行的函数/
int functionParamSize/函数参数所占用的字节数/)
{
CALLGATEDESCRIPT cgd = { 0 };
cgd.P = 1; // 描述符有效位, 必须设置为1
cgd.S = 0; // 系统段描述符, 必须设置0
cgd.Type = 12; // 调用门描述符,必须设置为12
cgd.DPL = 3; // 设置为3,表示此描述符可被3环所使用.
cgd.SegmentSelector = selector;// 要切换的段选择子
cgd.functionAddrHig = (functionAddr & 0xFFFF0000) >> 16;
cgd.functionAddrLow = functionAddr & 0x0000FFFF;
cgd.paramCount = functionParamSize / 4; // 参数个数,如果函数没有参数,填0,如果有参数,则填参数占用栈的字节数 / 4
return (unsigned long long)&cgd;
}
使用调用门的之前, 需要将调用门的描述符写进系统的GDT表中, 操作如下:
使用windbg双机调试, 输入指令
rgdtr
获取GDT表的地址
kd> rgdtr
gdtr=80b95000
构造一个可以在3环代码中使用的段选择子, 构造规则:
段选择子的格式为: [ 描述符在表中的下标:13 | T1:1 | RPL:2]
假如在GDT表,第9项是空闲的, 门描述符将保存在此处, 则对应的段选择子为:
十进制: 9-0-3 ==> 二进制: 1001-0-11 ==> 合并为 100 1011 ,十六进制数值为0x4B
在代码中使用此描述符:
// 前4字节是EIP,后2字节是CS
// 后2字节是0x004B, 此数值就是在第二步中构造出来的.
char buff[ ] = { 0,0,0,0,0x4b,00 };
_asm call fword ptr ds:[buff];
代码:
#include <iostream>
#include <iomanip>
#pragma pack(1)
typedef struct _CALLGATEDESCRIPT {
unsigned int functionAddrLow : 16; // 被调用函数地址的低16位
unsigned int SegmentSelector : 16; // 想要切换的段选择子
unsigned int paramCount : 5; // 参数个数
unsigned int none : 3; // 无
unsigned int Type : 4; // 系统段类型, 必须为12(1100b: 32位调用门)
unsigned int S : 1; // S位,必须为0
unsigned int DPL : 2; // 描述符特权级别
unsigned int P : 1; // 描述符有效位
unsigned int functionAddrHig : 16; //被调用函数地址的高16位
}CALLGATEDESCRIPT;
struct SELECTOR {
unsigned short index : 13;
unsigned short T1 : 1;
unsigned short RPL : 2;
};
unsigned long long createCallGateDescript(unsigned short selector,unsigned int functionAddr, int functionParamSize)
{
CALLGATEDESCRIPT cgd = { 0 };
cgd.P = 1; // 描述符有效位, 必须设置为1
cgd.S = 0; // 系统段描述符, 必须设置0
cgd.Type = 12; // 调用门描述符,必须设置为12
cgd.DPL = 3; // 设置为3,表示此描述符可被3环所使用.
cgd.SegmentSelector = selector;// 要切换的段选择子
cgd.functionAddrHig = (functionAddr & 0xFFFF0000) >> 16;
cgd.functionAddrLow = functionAddr & 0x0000FFFF;
cgd.paramCount = functionParamSize / 4; // 参数个数,如果函数没有参数,填0,如果有参数,则填参数占用栈的字节数 / 4
return *(unsigned long long*)&cgd;
}
int g_num;
short g_ss;
int g_esp;
//通过调用门调用的 函数
void _declspec(naked) GateFun()
{
g_num = 100;
_asm mov [ g_esp ] , esp;
_asm mov ax , ss;
_asm mov word ptr [g_ss],ax
_asm retf;
}
int main()
{
printf( "调用门函数地址:%08X\\n" , GateFun );
printf( "切换的段选择子:%04X\\n" , 8 );/*8是内核中的代码 段选择子*/
unsigned long long descript =
createCallGateDescript(8/*8是内核中的代码段选择子*/ , ( unsigned int )GateFun , 0 );
printf("请将这个段描述符写入到GDT[9]中: ");
std::cout << std::hex <<std::uppercase<< std::setfill(\'0\')<<std::setw(8)<< descript<<\'\\n\';
system( "pause" );
// 获取当前寄存器的值.
_asm mov[ g_esp ] , esp;
_asm mov ax , ss;
_asm mov word ptr[ g_ss ] , ax
printf( "调用前 esp=%08X, ss=%04X\\n" , g_esp , g_ss );
// 前4字节是EIP,后2字节是CS(0x004b)
char buff[ ] = { 0,0,0,0,0x4b,00 };
// 执行流程:
// 1. 从buff这块内存中取出段选择子:0x4b
//
// 解释 SEL T RPL
// 十进制 9 0 3
// 2. 将段选择子分解,100 1011 ==> 1001 0 11, 得到GDT表中的下标:9
// 3. 取出GDT表中第9项描述符, 是一个调用门描述符.
// 4. 将调用门描述符中的段选择子加载到CS段寄存器
// 5. 将调用门描述符中的函数地址设置到EIP寄存器.
_asm call fword ptr ds:[buff];
printf( "调用后 esp=%08X, ss=%04X\\n" , g_esp , g_ss );
printf( "g_num=%d\\n" , g_num );
system( "pause" );
}
windbg 输入eq 80b95068(上图右边红色框的地址).
任务门,中断门和陷阱门
中断门和陷阱门的描述符其实和调用门一模一样.
任务门描述符, 中断门描述符和陷阱门描述符都是保存在IDT
(中断描述符表)中.
其使用的过程是:
产生中断或异常后,
CPU会使用中断号找到
IDT
表中的中断描述符/陷阱描述符,取出描述符后, 得到门描述符中的段选择子.
通过此段选择子找到
GDT
表中的段描述符,从GDT表中取出的段描述符中得到段基地址
使用段基地址 +
门描述符
中的函数偏移 , 得到函数地址.调用该函数.
以上是关于内核01的主要内容,如果未能解决你的问题,请参考以下文章
19 01 11 javascript ?????????????????????(???????????????) ??????????????????????????????(代码片段