-
-
[原创][保护模式编程、四]
-
发表于: 2008-4-14 00:41 7113
-
【LDT描述符&&特权级&&门】
一、LDT(局部描述符)
GDT是全局描述符,是整个系统的描述符,描述符着所有的段!!!
在前几章我们已经熟悉了GDT的一些基本功能,与运作机制。
对GDT描述符的定义与使用也就那么几项固定的步骤,接下来再了解LDT.
LDT是局部描述符。看字面LDT与GDT很相似.它们都是描述符。
只不过GDT是全局描述符、而LDT是局部描述符。
那么LDT该如何【定义】与【使用】呢?(与GDT非常类似,不过LDT是归属于GDT的):
@【定义】:
1、在GDT的描述集合中插入一条 LABEL_DESC_LDT Descriptor 0,LdtLen - 1,DA_LDT
(这个段描述符描述的LDT数据段:是一个局部描述符段的集合,结构类似GDT.)
2、在GDT选择子集合中照样也插入一条 SelectorLdt equ ;LABEL_DESC_LDT - LABEL_GDT
(看似跟普通的段描述符没啥两样哦,不过接下来就有点小区别了!)
3、建一个LDT数据段,这个数据段里的结构与GDT描述符段类似!
{
LABEL_LDT: ;这里与GDT不同,LDT的段描述符一开始就是实际的段
LABEL_DESC_LDT_CODEA: Descriptor 0,LdtCodeALen - 1,DA_C | DA_32
LABEL_DESC_LDT_DATA: Descriptor 0,LdtDataLen - 1,DA_DRW
LdtLen equ $ - LABEL_LDT;
;在加载LDT的时候并不是靠一个绝对结构体,而是通过GDT这个桥梁索引加载的。
;LDT内部段的选择子
SelectorLdtCodeA equ LABEL_DESC_LDT_CODEA - LABEL_LDT | SA_TIL
SelectorLdtData equ LABEL_DESC_LDT_DATA - LABEL_LDT | SA_TIL
;LDT内部的选择子多了一个SA_TIL属性,他是段选择的TI位也就是第2位 如果是1代表当前选择子代表的段是LDT的内部段。 因为选择子的0-2位并不参与索引。所以选择子的索引都是8的倍数或0,
}
4、接着完成LDT里面段描述符描述的段。这里只定义了一个代码段: LABEL_DESC_LDT_CODEA
(这个跟普通的代码段一样,有执行、32位等属性. 为了测试LDT的效果那么我们还是加一个Data段吧)
那么LDT的基本定义已经完成了!!!
@【使用】:
1、LDT描述符与其内部的局部段描述符信息、,我们在进入保护模式前初始化!!!
那么我们的进入保护模式之前的段在哪还知道吗?当然就是最最开始了!在16位段实模式入口那里!!!
我们在这里做完GDT描述符的初始化工作后:
;--------------初始化LDT描述符---------
xor eax,eax
mov eax,ds
shl eax,4
add eax,LABEL_LDT
mov WORD [LABEL_DESC_LDT + 2],ax
shr eax,16
mov BYTE [LABEL_DESC_LDT + 4],al
mov BYTE [LABEL_DESC_LDT + 7],ah
;---------------初始化LDT内部的数据段描述符----------
xor eax,eax
mov eax,ds
shl eax,4
add eax,LABEL_LDT_DATA
mov WORD [LABEL_DESC_LDT_DATA + 2],ax
shr eax,16
mov BYTE [LABEL_DESC_LDT_DATA + 4],al
mov BYTE [LABEL_DESC_LDT_DATA + 7],ah
;---------------初始化LDT内部的代码段描述符----------
xor eax,eax
mov eax,ds
shl eax,4
add eax,LABEL_LDT_CODEA
mov WORD [LABEL_DESC_LDT_CODEA + 2],ax
shr eax,16
mov BYTE [LABEL_DESC_LDT_CODEA + 4],al
mov BYTE [LABEL_DESC_LDT_CODEA + 7],ah
上面几个初始化段基地址的代码相信大家已经很熟悉了!!!!!!!!
2、 先前就说过了LDT跟GDT类似都是描述符集合,那么他们在使用起来也有点类似!!!
需要用lldt指令:它的参数并不是一个结构体的指针,因为LDT是属于GDT的。不能独立的加载。必需依靠GDT这个桥梁。那么这个参数也就是LDT在GDT当中的索引值.,
在进入保护模式后我们添加如下代码进行LDT的加载:
mov ax,SelectorLdt
lldt ax ;lldt加载时是从GDT当中的ax索引位置的段做LDT段加载到LDTR寄存器的.\
jmp SelectorLdtCodeA:0 ;首先检查选择子的TI位,当是LDT选择子的时候。马上去LDTR寄存器取LDT在GDT当中的索引,找到LDT的位置。最后根据SelectorLdtCodeA在LDT局部描述符集合中查找。
------------------------------------------------------------------------------------------------------------------------
二、特权级:
我们在WIindows里使用Ring3程序也好、Ring0函数也好,在执行他们的时候或者调用他们的时候是按一定规则执行的? 当然Windows自己包装的规则不在讨论之类,.我们现在需要了解CPU最铁最硬的规则!!!
Ring0 -Ring3并不是属于WINDOWS的东西,而是属于CPU本身的!!! 那么当我们在使用程序的时候或者模块的时候cpu是如何鉴别程序特权级的呢?
1、CPL(Current Privilege Level)
CPL是指当前执行的程序或者任务的特权级,它存储在CS和SS的 0 到1位。CS、SS在这里都是段选择子了,(第2位是TI位代表LDT),这0-1位合起来可以表示Ring0-Ring3个级别.
通常情况下CPL等于代码本身所在段的特权级(DPL), 当程序跳转到不同特权级的代码段时,处理器将改变CPL, 以上指得是非一致代码段。 如果是一致代码段,一致代码段可以被同等级的或者低特权级访问,并且跳转过去后CPL也不会改变。
2、DPL(Descriptor Privilege Level)
DPL表示段或者门的特权级. 门其实也是一个段描述符来描述的。所以他们的DPL值就是段描述符属性里面的DPL位。下面来列举一些访问这些段时所需要的CPL值。
×数据段 :
一个代码段要想访问这个数据段,那么必需CPL(代码段) <= DPL(数据段)。
这样代码段才能正常访问此数据段。
×非一致代码段 :(不使用调用门的情况下)
访问非一致代码段的条件是 CPL == DPL(非一致代码段)
×调用门 :
访问调用门的条件: 跟数据一样 CPL(代码段) <= DPL(调用门段)
×一致代码段和通过调用门访问的非一致代码段:
访问一致代码段的条件: 必需CPL >= DPL
通过调用门访问非一致代码段条件: 必需CPL >= DPL
×TSS(任务状态段):
Tss是一个32位的结构体,它包含了100个字段。其中偏移量为4 -27里有3个ss和3个esp信息。它们记录着不同特权级任务切换时的堆栈信息。 由于只有特权级低到高切换任务时,新堆栈才会从TSS中取得。所以TSS里没有Ring3的SS ESP信息。
访问TSS的条件: 跟数据一样 CPL(代码段) <= DPL(TSS)
3、RPL(Request Privilege Level):
它存放在段选择子的 0-1这两位上面。 在访问目标段时CPU会做一系列检查。
其中RPL是访问目标段的关键:如果当前代码段CPL为0,要访问对方数据段的DPL值是1,而RPL值却是2。那么访问是不成功的。数据段的RPL值一定要<=DPL才能正常访问。。
------------------------------------------------------------------------------------------------------------------------
三、门(描述符 说得通俗点,门有点像函数指针):
门描述符分位4种:×调用门、×中断门、×陷阱门、×任务门。
程序从一段代码转移到另外一段代码时,目标代码的选择子会被加载到CS中,而在加载之前CPU还要对目标段描述符、界限、类型、特权级等信息做检查。检查通过后目标段选择子被加载到CS,然而EIP指向新偏移地址. 程序的转移可以由:jmp、call 、ret、sysenter、sysexit、int n、iret 这些命令来控制。
×用jmp或者Call指令进行转移的时:
直接用Call 指令转移时:
如果目标代码段是非一致代码段那么CPL必需等于目标DPL,同时要求RPL<=DPL.。
如果是一致代码段那么CPL必需>=目标DPL,RPL此时不做检查。转移过去后CPL不变。
由此看来call 、jmp 指令只适用于低特权向高特权转移。
那么怎样从高特权转移到低特权呢?继续看下面:
②、引用调用门:
门也是一种描述符,它跟Descriptor 一样大小都是8个字节。不过这个8个字节代表的意义大有不同之处。
首先看看这个Gate宏:
%macro Gate 4 ;门宏有4个参数:选择子、偏移量、调用者堆栈元素数量、属性
dw (%2 & 0FFFFh) ; 偏移 1 (2 字节)
dw %1 ; 选择子 (2 字节)
dw (%3 & 1Fh) | ((%4 << 8) & 0FF00h) ; 属性 (2 字节)
dw ((%2 >> 16) & 0FFFFh) ; 偏移 2 (2 字节)
%endmacro ; 共 8 字节
门描述符的字节意义说明:
第0、1个字节代表[偏移1]。
第2、3个字节代表段选择子。
第4、5个字节,其中第5个字节跟段描述符的第5个字节一样,TYPE(属性)、S(存储段而言,S=1代表不是系统段和门段。所以这里要变成0)、DPL(段特权级)、P(存在位)这几个位。而第4个字节是代表.调用者堆栈元素数量,这是要根据TSS完成调用者的堆栈元素复制到目标堆栈里。
第6、7个字节代表[偏移2]。与0、1[偏移1]按高高低低合并就可以得到,偏移值了。
调用门描述符的定义:
1、在GDT全局描述符里添加一个:
;门宏 、选择子 、偏移、ParamCount 、描述符属性
LABEL_CALL_GATE_TEST: Gate ,SelectorGateCode32, 0, 0 ,DA_386CGate
SelectorGateCode32只是一个普通的代码段选择子,只不过是被调用门来使用而已。
2、同样门也需要自己在GDT中的位置也就是选择子:
SelectorGateTest equ LABEL_CALL_GATE_TEST - LABEL_GDT
调用门描述符的使用:
Call SelectorGateTest:0 ;这个指令,CPU首先找到选择子在GDT的位置,然后检查选择子对应的描述符,是个什么描述符。 检查完毕以后就做对应的处理。
这里描述符是个门描述符所以CPU会进行门描述符对应的位功能检测,那么会得到:一个代码段的选择子,与偏移、等信息。 这就可以进行跳转了。将会跳转到SelectorGateCode32代码段选择子对应偏移为0的代码处。
;--------------------------------------------------------------------------------------------
四、让程序走进Ring3:
调用门的过程实际分为两部分:
1、从低特权向高特权访问, 要Call调用门指令。
2、从高特权向低特权 用Ret指令
在8086时期的Call指令短转移(段内)时,CPU会先将ip + 1 压栈。 等待ret 指令完成后才会弹出到新的ip。
而在长转移时,CPU会将 cs与ip 一起压栈.这样等待retf 指令完成后 才会弹出到对应的 ip cs。
JMP指令比较简单 只是目标操作数不同而已。
在386后不同特权级之间的堆栈是独立的,这样的话如果是远转移的话,那么就无法正确获得本身堆栈里的EIP信息了.因为都不是同一个堆栈了。那么386机制有个办法就是靠任务状态堆TSS来解决。在任务进行切换时,可以先从TSS里获得任务的重要信息。TSS是一个32位的结构体,含有100个字段。其中 4 -27字段就是3个特权级的堆栈信息。??特权级有4种,为什么确只记录3个堆栈信息呢。因为只有从低特权级到高特权级转移时,才能从TSS里获取到新堆栈信息.所以没有Ring4就无法在TSS里获Ring3的信息,
通过调用门从一个代码段跳转到另外一个代码段的整个步骤:
1、根据目标代码的DPL对应的特权级从TSS中选择SS和ESP (TSS结构体第4-27记录着3个特权级堆栈信息).如果目标代码DPL=1 那么 就选ss1 、esp1;(12 -16字段)。
2、第二步是检查所选择的ss1 esp1 或Tss是否有界限错误(ERR #TS)。
3、第三步对SS堆栈段选择子进行校验。
4、暂时性的保存当前SS和ESP的值,调用者自己的堆栈信息。
5、加载新的ss1和esp1到 ss、esp。
6、将刚才保存的当前ss、esp压栈。调用者自己的堆栈信息
7、从调用者的堆栈中将参数复制到新堆栈中,复制的数量由门描述符中的ParamCount指定。
8、将当前的cs 和下一个eip 压栈。
9、加载调用门指定的 CS EIP,开始执行新的代码段。此时的堆栈也是新的。
通过调用门方式转移时。只有低特权转高特权才能在TSS中获得新堆栈信息。如果不是低特权转高特权那么堆栈信息是不会改变的。用Call指令只能实现低特权到高特权。
通过调用门从一个代码段跳转到另外一个代码段后再返回到调用段的步骤:
1、检查保存在堆栈中的CS(也就是调用者调用时的CS值(段选择子))中的RPL ,以判断返回是否需要变换特权级,这个CS保存在新堆栈中弹出调用者eip后esp指向的地方。
2、加载新堆栈里的CS和EIP (也就是调用者的CS:EIP),此时会对加载的CS:eip进行校验。
3、如果Ret指令含有参数,则跳过Ret指定的数量。然后esp指向调用者的ss:esp信息在新堆栈中的位置。
Ret参数应该对应与门描述符的ParamCount。
4、加载堆栈里面的SS、esp到 ss:esp寄存器里,被调用者的ss esp 被覆盖。进行ss:esp校验。
5、.如果先前的Ret有参数。也就是代表是从调用者堆栈复制过去的参数,现在调用过程完成了,不需要这些参数了,同样在调用堆栈里也清理掉。 esp +参数。
6、检查ds、es、fs、gs的值,因为cs、ss都已经检验过了。要对其它的段寄存器进行检验了,如果哪一个段寄存器的段描述符DPL小于CPL(也就是段特权级大于了CPL.那么CPL不能对它进行访问了),那么一个空的描述符覆盖给此段寄存器。
Ret转移的方式跟Cal恰恰l相反,所以它适合从高特权转移到低特权。不过在转移的时候会经过一系列检查与堆栈操作。
*从Ring0 到RIng3的方法:
1、使用ret 指令(高特权到低特权):
可在32位实模式入口段( 这是个逆操作,可以看作是被Ring3 Call进来的.所以返回可以是retf):
push SelectStack3
push TpoOfStack3
push SecletorR3Code32
push 0
retf ;Retf的步骤上面有说明!
这就是从Ring0到RIng3的代码。
*从Ring3 到RIng0的方法
2、使用Call指令调用门(低特权到高特权):
从低特权到高特权需要新的堆栈段那么需要TSS数据段:首先就定义一个TSS数据段吧.
;-----------------TSS数据段-----------------
[SECTION .tss]
align 32 ;因为TSS本身是32位字段,那么就用32位对齐
[BITS 32]
LABEL_TSS:
dd 0 ;上一个任务链接
dd TopOfStack0 ;0级堆栈esp
dd SelectorStack0 ;0级堆栈ss 4-12字段
dd 0 ;1级堆栈
dd 0 ;1级堆栈
dd 0 ;2级堆栈
dd 0 ;2级堆栈 字段 r0 -r2
dd 0 ;cr3
dd 0 ;eip
dd 0 ;eflags
dd 0 ;eax
dd 0 ;ecx
dd 0 ;edx
dd 0 ;ebx
dd 0 ;esp
dd 0 ;ebp
dd 0 ;esi
dd 0 ;edi
dd 0 ;es
dd 0 ;cs
dd 0 ;ss
dd 0 ;ds
dd 0 ;fs
dd 0 ;gs
dd 0 ;LDT
dw $ - LABEL_TSS + 2 ;I/O位图基址
dw 0ffh ;I/O位图结束标志
TssLen equ $ - LABEL_TSS
;--------加载TSS------
mov ax,SelectorTss ;
ltr ax ;与加载LDT类似都是从GDT索引位置找到信息后,加载到对应的寄存器里.
下面来看看这个程序的来龙去脉吧:
1、入口段实模式:
进行GDT、等数据初始化。
然后跳转到SelectorCode32(Ring0);
2、SelectorCode32(Ring0)
首先显示一行字符串:"Hello Protect Mode".
;--------加载TSS------
mov ax,SelectorTss
ltr ax
;-------这是个逆向,这里看作是被Ring3 Call进来的.所以返回可以是retf
push SelectorStack3 ;ss
push TopOfStack3 ;esp
push SelectorR3Code32 ;cs
push 0 ;eip
retf ; Ring0 到Ring3
3、SelectorR3Code32(Ring3)
首先显示:字符"3"。
call SelectorGateTest:0 ;通过门进行低特权Ring3到高特权Ring0的转移
4、SelectorGateCode32(Ring0) 是调用门指向的代码段
首先显示:字符"0"。
mov ax,SelectorLdt
lldt ax
jmp SelectorLdtCode32:0 ;跳转到LDT代码区 ,
5、SelectorLdtCode32(Ring0)
首先显示:字符"L".
jmp SelectorCode16:0 回到实模式了。
【总结】
这个程序的整个路线就是这样了,在这里的知识点比较多。本人在学习的时候也碰到了很多问题。不过想不通的先借过,等了解到新知识后再返回来学习,效果会更好!
这里有以下几个重要知识点:
一、LDT(局部描述符)
局部描述段跟GDT的定义很相似。因为都是存放数据或者代码段的描述符集合!
LDT的加载是用 Lldt ax ;其中这个ax 是LDT描述符在GDT中的选择子。
那么LDT段的寻址方式略有不同。 jmp S:0 ;S 是LDT集合的一个选择子。其中TI位为1表示是LDT的选择子。这个时候CPU会从 LDTR寄存器指向的LDT集合中去找对应的选择子。
二、特权级(局部描述符)
特权级用在对数据访问的控制和对代码转移的控制.
对数据的访问比较简单: CPL <=目标DPL 并且 RPL<=DPL.
对代码的转移目前接触了:
1、直接用Call 、jmp指令进行转移。
当目标是非一致代码的时候,必需CPL==目标DPL,RPL<=DPL.也就是说必需是相等特权转移。
当目标是一致代码的时候,必需CPL<=目标DPL,RPL不做检查,转移后CPL不变。
所以直接用Call、Jmp 指令转移只能是低特权到高特权。如果目标是非一致那还必需等于、
2、CALL调用门:
Call 调用门 可以从低特权到高特权,并且不受非一致代码限制。 但是他要用到TSS.
3、Ret返回指令
Ret 是返回指令,Call的逆向。了解Ret的运作机制就可以 正确从高特权向底特权转移了。
特权级的标志也就是这3个: CPL、DPL、RPL。
接下来继续学习......Day Day Up
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
赞赏
- [原创][保护模式编程、四] 7114
- [原创][保护模式编程、三] 7914
- [原创]386保护模式[学习一、二] 16157
- [原创]浅谈U盘防御文件夹!!! 13288
- [原创]Kmd(kernel mode Driver)简单入门之谈!! 13884