保护模式内存访问检测

使用选择子访问段描述符表时 , 索引值的合法性检测
当索引值越界时 , 引发异常
判断规则 : 索引值 * 8 + 7 <= 段描述表界限值

内存段类型合法性检测

  • 具备可执行属性的段( 代码段 ) 只能加载到CS寄存器
  • 具备可写属性的段( 数据段 ) 才能加载到SS寄存器
  • 具备可读属性的段才能加载到DS,ES,FS,GS寄存器

代码段和数据段的保护
处理器每访问地址都要确认该祖不超过界限值
判断规则 :
代码段 : IP + 指令长度 <= 代码段界限
数据段 : 访问起始地址 + 访问数据长度 <= 数据段界限

image-20220515162035057

保护模式的特权级

  • x86 架构中的保护模式提供了 4 个特权级( 0,1,2,3 )
  • 特权级从高到底分別是 0,1,2,3 ( 数字越大特权级越低 )

image-20220515113659126

为了安全,让应用程序运行在低特权级,不能访问高特权级的数据代码段

特权级的表现形式

  • CPL(CurrentPrivilege Level)当前可执行代码段的特权级,由 CS寄存器最低2位定义
  • DPL(Descriptor Privilege Level)内存段的特权级,在段描述符表中定义
  • RPL(Request Privilege Level)选择子的特权级,由选择子最低2位定义

DPL和CPL

image-20220515114131125

段描述符中的 DPL 用于标识内存段的特权级 ; 可执行代码访问内存段时必须满足一定特权级(CPL)即CPL<=DPL,否则,处理器将产生异常

CPL 和 DPL 的关系

  • 保护模式中,每一个代码段都定义了一个DPL

  • 当处理器从A代码段成功跳转到B代码段执行
    跳转之前: CPL = DPLA
    跳转之后: CPL = DPLB

  • 保护模式中 , 每一个数据段都定义了一个 DPL

  • 当处理器执行过程中需要访问数据段时 :CPL < = DPLdata

inc.asm添加以下标识符以便设置相应特权级

image-20220515114605108

结论:

  • 处理器进入保护模式后CPL= 0(最高特权级)是无法直接跳转到DPL=3的代码段的
  • 处理器不能直接从高特权级转换到低特权级执行
  • 选择子RPL大于对应段描述符的 DPL时 , 产生异常

引出的问题

  1. 如何在不同特权级的代码段之间跳转执行 ?
  2. 高特权级代码为什么不能使用低特权级栈段 ?
  3. 选择子的 RPL 具体有什么用?

门描述符

通过门描述符在不同特权级的代码间进行跳转

根据应用场景的不同,门描述符分为:

  • 调用门( Call Gates )
  • 中断门( Interrupt Gate )
  • 陷阱门 ( Trap Gate )
  • 任务门( Task Gate )

门描述符的内存结构

  • 每一个门描述符占用 8 字节内存
  • 不同类型门描述的内存含义不同

image-20220515162457984

内存结构和段描述符内存结构是相似的,8-15位各个字段和段描述符完全一致。

调用门call Gates描述符的定义

image-20220515162612315

1
2
3
4
5
6
7
8
9
10
11
12
; 门
; usage: Gate Selector, Offset, DCount, Attr
; Selector: dw
; Offset: dd
; DCount: db
; Attr: db
%macro Gate 4
dw (%2 & 0xFFFF) ; 偏移地址1
dw %1 ; 段选择子
dw (%3 & 0x1F) | ((%4 << 8) & 0xFF00) ; 属性
dw ((%2 >> 16) & 0xFFFF) ; 偏移地址2
%endmacro

调用门描述符的工作原理

通过调用门选择子就可以访问到门描述符,这又可以根据门描述符内的段描述符的选择子去访问一个段描述符,拿到段基址和段界限,再和门描述符内的偏移地址组合就可以得到确定的内存地址

省略中间过程就成了通过调用门选择子就可以直接跳转到某个固定的地址执行

image-20220515162911435

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[section .gdt]
; GDT definition
; 段基址, 段界限, 段属性
GDT_ENTRY : Descriptor 0, 0, 0
CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32
VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32
STACK32_DESC : Descriptor 0, TopOfStack32, DA_DRW + DA_32
FUNCTION_DESC : Descriptor 0, FunctionSegLen - 1, DA_C + DA_32
; Gate Descriptor
; Call Gate 选择子, 偏移, 参数个数, 属性
FUNC_CG_ADD_DESC Gate FunctionSelector, CG_Add, 0, DA_386CGate
FUNC_CG_SUB_DESC Gate FunctionSelector, CG_Sub, 0, DA_386CGate
; 使用上和函数指针一样的
; GDT end

GdtLen equ $ - GDT_ENTRY

GdtPtr:
dw GdtLen - 1
dd 0

; GDT Selector
Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0
VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0
Stack32Selector equ (0x0003 << 3) + SA_TIG + SA_RPL0
FunctionSelector equ (0x0004 << 3) + SA_TIG + SA_RPL0
FuncCGAddSelector equ (0x0005 << 3) + SA_TIG + SA_RPL0
FuncCGSubSelector equ (0x0006 << 3) + SA_TIG + SA_RPL0

; end of [section .gdt]

主函数中

1
2
3
4
5
6
7
8
   mov ax, 2
mov bx, 1

call FuncCGAddSelector : 0 ;调用门选择子,这个0是没有意义的语法需要占位,换成其他的也可以
call FuncCGSubSelector : 0 ;调用门选择子,本质是个函数指针
;调用门选择子跨段调用函数等价于以下,段基址+偏移地址
call FunctionSelector : CG_Add
call FunctionSelector : CG_Sub

那么问题 来了: 既然,想调用某个函数借助门选择子:0 改为 借助目标段选择子:函数偏移地址,执行效果相同,那为什么我们还需要调用门呢?

  • 1)我们需要用门来实现不同特权级的代码间的转移, 因为单纯地通过CPL、RPL和RPL进行比较 来进行不同特权级代码段间的转移的话,有诸多限制;
  • 2)有特权级变换的转移的复杂之处, 不但在于严格的特权级检验,还在于特权级变换的时候,堆栈也要发生变化;处理器利用调用门的机制避免了高特权级的过程由于栈空间不足而崩溃;

汇编语言中的跳转方式

  • 段内 : call,jmp
    参数为相对地址,函数调用时只需要保存当前偏移地址
  • 段间跳转: call far, jmp far
    参数为选择子和偏移地址
    函数调用时需要同时保存段基地址和地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[section .func]
[bits 32]
FUNCTION_SEGMENT:

; ax --> a
; bx --> b
;
; return:
; cx --> a + b
AddFunc:
mov cx, ax
add cx, bx
retf ;retf这个f是指far的意思

CG_Add equ AddFunc - $$ ;获取add函数的偏移地址

; ax --> a
; bx --> b
;
; return:
; cx --> a - b
SubFunc:
mov cx, ax
sub cx, bx
retf ;retf这个f是指far的意思

CG_Sub equ SubFunc - $$ ;获取sub函数的偏移地址

FunctionSegLen equ $ - FUNCTION_SEGMENT

单步调试,寄存器出现了我们的预期结果

image-20220515165758486

结论:

  • 门描述符是一种特殊的描述符 , 需要注册于段描述表
  • 调用门可以看作一个函数指针( 保存具体函数的入口地址 )
  • 通过调用门选择子对相应的函数进行远调用( callfar )
  • 可以直接使用选择子 : 偏移地址的方式调用其它段的函数
  • 使用调用门时偏移地址无意义 , 仅仅是语法需要( 为什么? )

历史遗留问题:那么保护模式下的不同段之间如何进行代码复用( 如 : 调用同一个函数 ) ?

  • 将不同代码段需要复用的函数定义到独立的段中( retf )
  • 计算每一个可复用函数的偏移量( FuncName - $$ )
  • 通过段选择子 : 偏移地址的方式对目标函数进行远调用

小结:

  • 门描述符是一种特殊的描述符 , 需要注册于段描述符表
  • 门描述符分为 : 调用门 , 中断门 , 陷阱门 , 任务门
  • 调用门可以看作一个函数指针( 保存具体函数的入口地址 )
  • 调用门选择子对应的函数调用方式为远调用( call far )

特权级跳转retf

  • 调用门只支持从低特权级跳转到高特权级执行
  • 无法利用调用门从高特权级跳转到低特权级执行

image-20220515172353146

ret指令用于函数返回,返回到函数调用的地方,而ret的本质用法是跳转指令,跳转到调用函数的地方。

retf的本质就是 调用门当发生权限切换的时候.堆栈会保存 SS ESP CS EIP(返回地址)
retf本质就是将这些堆栈值进行恢复. 并不是说调用门非要使用retf才可以. 你如果自己进行POP也是可以的.

调用门的特级跳转

  1. 通过远调用( call far ) : 低恃权级->高特权级
  2. 通过远返回( retf ) : 高特权级->低特权级

远返回(retf)能够实现高恃权级到低特权级的代码跳转,那么,考虑如何利用其机制完成这个跳转?

函数调用的过程( 近调用 )

image-20220515172922977

再论函数调用的过程( 远调用 )

image-20220515173042908

因此retf跳转关键在于这个栈,然而栈必须要知道的事实:

  • x86 处理器对于不同的特权级需要使用不同的栈
  • 每一特权级对应一个私有的栈(最多4个栈)
  • 特权级跳转变化之前必须指定好相应的栈

高特权级->低特权级的解决方案:指定栈和目的地址

  1. 指定目标栈段选择子( push )
  2. 指定栈顶指针位置( push )
  3. 指定目标代码段选择子( push )
  4. 指定目标代码段偏移( push )
  5. 跳转( retf )

高特权级->低特权级显然是远跳转,因此需要retf

测试实验:

设置code32代码段特权级为3,如果直接jmp dword Code32Selector : 0则报错

选择子的RPL也应修改为3

相同特权级跳转可以不额外指定一个新的栈

1
2
3
4
5
6
7
; 5. jump to 32 bits code
; jmp dword Code32Selector : 0
push Stack32Selector ; 目标栈段选择子
push TopOfStack32 ; 栈顶指针位置
push Code32Selector ; 目标代码段选择子
push 0 ; 目标代码段偏移
retf

特权级转移

初识任务状态段( Task State Segment )

  • 处理器所提供的硬件数据结构,用于解决多任务解决方案
  • TSS中保存了关键寄存器的值以及不同特权级使用的栈(切换任务的时候就要保存这些任务上下文)

image-20220516162451813

TSS中保存不同特权级的栈信息

在TSS中只保存了3个栈的信息

  • 特权级0:ss0,esp0
  • 特权级1:ss1,esp1
  • 特权级2:ss2,esp2

image-20220516162901861

特权级转移时的栈变化

  • 低特权级–>高特权级( 调用门 )
    从 TSS 获取高特权级目标栈段(获取指定SShigh和ESPhigh的值)
    将低特权级栈信息压入高特权级栈中( SSlow 和 ESPlow压入高特权级栈中用于返回 )
  • 高特权级–>低特权级( retf )
    将低恃权级栈信息从高特权级栈中取出并恢复到 ss 和 esp

为什么TSS只保存了3个栈的信息?
理应而言是4个栈的信息啊,但是仔细想想TSS只有在调用门从低转移到高特权级的时候才会使用,去寻找高特权级的栈信息,而特权级3已经是最低的特权级了,自然无需存储

低特权级<->高特权级实验

任务目标:

  1. 定义 32 位核心代码段和数据段( Privilege = 0 )
  2. 定义 32 位任务代码段和数据段( Privilege = 3 )
  3. 由核心代码段跳转到任务代码段执行( 高 -> 低 )
  4. 在任务代码段中调用高特权级代码段打印字符串( Call Gate )

流程图:

从实模式转换到保护模式,刚转换到保护模式特权级仍然是0,然后进行一系列初始化工作,通过远返回指令retf转移到任务代码段,任务代码段需要通过调用门调用系统函数,然后系统函数在内核态执行,执行完后远返回指令retf返回到任务代码段

image-20220516164033228

注意事项:

  • 特权级转移时会发生栈的变换( 如何变换 ? )
  • 栈的变化需要在 TSS结构体中预先定义
  • TSS 结构体作为一个独立段定义( 描述符 , 选择子 )
  • 在核心代码段中加载具体的 TSS结构体( Itr TSSSelector )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
; TSS段
[section .tss]
[bits 32]
TSS_SEGMENT:
dd 0
dd TopOfStack32 ; 0特权级栈信息
dd Stack32Selector ;
dd 0 ; 1特权级栈信息
dd 0 ;
dd 0 ; 2特权级栈信息
dd 0 ;
times 4 * 18 dd 0 ;暂不涉及多任务切换,因此全部定义为0
dw 0
dw $ - TSS_SEGMENT + 2
db 0xFF

TSSLen equ $ - TSS_SEGMENT
;核心代码段
CODE32_SEGMENT:

mov ax, VideoSelector
mov gs, ax
mov ax, Data32Selector
mov ds, ax
mov ax, Stack32Selector
mov ss, ax
mov eax, TopOfStack32
mov esp, eax

mov ebp, DTOS_OFFSET
mov bx, 0x0c
mov dh, 12
mov dl, 33

call FunctionSelector : PrintString ;段选择子+段内偏移地址调用函数,调用都为特权级0的代码段,没问题

;模拟操作系统加载应用程序
mov ax, TSSSelector
ltr ax ;ltr通过选择子加载TSS字段

mov ax, TaskALdtSelector ;通过选择子加载ldt
lldt ax ;加载完ldt就可以使用局部段描述表的代码段

;以下都在ldt处定义
push TaskAStack32Selector ;低特权级任务A的压入到当前核心代码段高特权级栈中
push TaskATopOfStack32
push TaskACode32Selector ;压入入口地址
push 0 ;偏移为0
retf ;高特权级->低特权级

; 任务代码段
TASK_A_CODE32_SEGMENT:
mov ax, TaskAData32Selector
mov ds, ax

mov ebp, TASK_A_STRING_OFFSET
mov bx, 0x0c
mov dh, 14
mov dl, 29

call FuncPrintStringSelector : 0 ;调用门选择子会有特权级的变化

jmp $

; 系统函数
PrintString:
;.....

call FunctionSelector : PrintString段选择子+段内偏移地址调用函数,调用都为特权级0的代码段,没问题
跟踪调试可以看到cs低二位都是0,CPL(CurrentPrivilege Level)当前可执行代码段的特权级都是0,并没有发生特权级的改变

image-20220516175928683

image-20220516175835935

retf就会实现特权级的改变,特权级从高特权级0变为了低特权级3

image-20220516180151354

image-20220516180210887

任务代码通过调用门调用系统函数call FuncPrintStringSelector : 0调用门选择子会有特权级的变化,观察到cs低二位变为了00,提高到特权级0

image-20220516180838115

另外出现了一个问题,通过调用门调用PrintString时报错,mov [gs:edi], ax,ds源内存(ds属于用户内存,用户态特权级为3)拷贝到gs(gs属于系统内存,内核态特权级为0)中,这会造成内核数据的破坏

因此在全局GDT中将gs即显存段设置特权级也为3,这样在低特权级下printString向显存段写入也不至于报错

RPL

  • 位置意义:选择子或段寄存器的最低2 位
  • 请求意义:资源请求的特权级( 不同于当前特权级CPL )

image-20220516181204905

当需要请求获取某种资源时,处理器通过CPL,RPL 和 DPL共同确定请求是否合法 !

image-20220516181223560

特权级检查

数据段的访问规则(数据段是无可执行属性的内存段)

  • 访问者权限( CPL ) 高于或等于数据段特权级( DPL )
  • 请求特权级( RPL )高于或等于数据段特权级( DPL )
  • 即 : (CPL <= DPL) && (RPL <= DPL)

CPL=2,RPL=1,DPL=3 ?合法

合法,就不贴图了

CPL=0,RPL=3,DPL=2 ?不合法

bochs爆出了这样的错误,这意思是需要满足CPL <= DPL && RPL <= DPL(bochs报错信息稍微不友好)

image-20220517120013353

CPL=0,RPL=1,DPL=2 ?合法

image-20220517161037839

中途报了这样一个错,给ss段寄存器加载选择子时报错,这是因为栈选择子RPL不为0和栈描述符DPL不为0

对于栈段,当给SS寄存器赋值时,使用规则(CPL==RPL)&&(CPL==DPL)保证特权级的匹配,不仅如此,降特权级跳转(retf)的时候,目标代码段特权级与目标栈段特权级必须完全相同!即**(SS.RPL == CS.RPL)&&(SS.DPL == CS.RPL)**

将栈段修改好后,能成功运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
; 需要访问的数据段DPL
[section .dat]
[bits 32]
DATA32_SEGMENT:
DTOS db "fengyun'OS!", 0
DTOS_OFFSET equ DTOS - $$

Data32SegLen equ $ - DATA32_SEGMENT


; 任务代码段即CPL的值
[section .dat]
[bits 32]
CODE32_SEGMENT:
mov ax, VideoSelector
mov gs, ax

mov ax, Data32Selector
mov ds, ax

mov ax, Stack32Selector
mov ss, ax

mov eax, TopOfStack32
mov esp, eax

mov ebp, DTOS_OFFSET
mov bx, 0x0C
mov dh, 12
mov dl, 33

call PrintString

jmp $

选择子被段寄存器加载时 , 会进行保护模式的检查

  • 检查选择子的下标是否合法( 段描述符的合法性 )
  • 检查特权级是否合法( CPL& RPL <= DPL )
  • 检查特权级时CPL 和 RPL 之间不会进行比较

代码段的分类

非系统段(S=1)包括一致性代码段和非一致性代码段

系统段(S=0)LDT,TSS,各种门结构

image-20220516223031761

非系统段的分类

  • 一致性代码段 : X = 1,C = 1
  • 非一致性代码段 : X = 1,C = 0

image-20220516223103384

代码段之间的跳转规则( 不借助门描述符使用call或者jmp

  • 非一致性代码段
    代码段之间只能平级转移( CPL == DPL , RPL <= DPL )
  • 一致性代码段
    支持低特权级代码向高特权级代码的转移( CPL >= DLP ),否则就是不合法的
    虽然可以成功转移高特权级代码段 , 但是当前特权级不变,因此栈也不会变化

数据段只有一种,没有一致性和非一致性的区分;并且,数据段不允许被低特权级的代码段访问。

特权级降低转移时 , retf 指令会触发栈段的特权级检査

一致性代码段和非一致性代码段中的代码并没有本质区别,两种代码仅仅在于跳转时使用的合法性判断规则不同,因此一致性代码段到非一致性代码段的直接同级跳转是合法的

深入理解调用门

  • 调用门用于向高特权级的代码段转移( CPL > = DPLobject )
  • 调用门描述符的特权级低于当前特权级( CPL <= DPLgate )

门的作用类似于“蹦床”

image-20220516223316958

  • 调用门支持特权级同级转移,但是不支持降级跳转
  • 调用门同级转移被处理为普通函数调用或直接跳转
  • call 通过调用门能提升特权级,jmp 通过调用门只能同级转移且jmp“有去无回”不会在栈留下返回信息

通过调用门降特权级返回(retf)时

  • 对目标代码段以及栈段特权级检查即(SS.RPL == CS.RPL)&&(SS.DPL == CS.RPL)
  • 对相关段寄存器强制清零( 指向高特权级数据的段寄存器, )
1
2
3
4
5
mov ax, Data32Selector0
mov ds, ax
mov gs, ax

retf

函数返回前设置ds,gs为0特权级的数据,这样内核数据就不安全了

image-20220517165456935

image-20220517165534101

特权级与内核安全实例

用户程序想要访问获取操作系统内核中的私密教据 !

image-20220517104016806

漏洞分析

攻击者可以用非法手段获得了内核函数段选择子和偏移地址,然后非法手段构造调用门描述符和选择子,这样可以拷贝内核数据到用户指定的es处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
; Call Gate,非法手段获得调用门描述符和选择子
; 选择子, 偏移, 参数个数, 属性
FUNC_GETKERNELDATA_DESC : Gate FunctionSelector, GetKernelData, 0, DA_386CGate + DA_DPL3

[section .s32]
[bits 32]
; 想做破坏的用户代码段
CODE32_SEGMENT:
mov ax, VideoSelector
mov gs, ax

mov ax, UserData32Selector
mov es, ax ;es = UserData32Selector,拷贝的目的地

mov di, UDAT_OFFSET

call GetKernelDataSelector : 0 ;通过调用门妄图将内核数据拷贝到用户空间

mov ax, UserData32Selector
mov ds, ax

mov ebp, UDAT_OFFSET
mov bx, 0x0C
mov dh, 12
mov dl, 33

call PrintString ;打印内核数据

jmp $


[section .func]
[bits 32]
FUNCTION_SEGMENT:

; 模拟高特权级的内核数据拷贝函数,是给内核程序使用而不是给用户程序使用
; KernelData拷贝到es:di --> data buffer,
GetKernelDataFunc:

mov ax, KernelData32Selector
mov ds, ax

mov si, KDAT_OFFSET
mov cx, KDAT_LEN
call KMemCpy

retf

image-20220517172228665

可以看到拷贝成功了

初步解决方案:获取目的选择子中RPL的值

判断 RPL 的值是否为 SA_RPL0

  • true检查通过 , 可继续访问数据
  • false 特权级较低 , 触发异常

image-20220517172344324

小技巧- 通过下标为 0 的段描述符触发异常

1
2
3
mov ax, 0               ;使用0选择子
mov fs, ax ;段寄存器指向0位置处
mov byte [fs:0], 0 ;往0位置处写入数据,这样会触发异常

类似于

1
2
int* fs = 0;
*fs = 0;

最终实现检查RPL是否为0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
KMemCpy:
mov ax, es ;es取出目标选择子

call CheckRPL ;先判断选择子后二位是否为00再决定是否拷贝
; ....
; ....
; ....


; ax --> selector value
CheckRPL:
and ax, 0x0003
cmp ax, SA_RPL0
jz valid

mov ax, 0 ;使用0选择子
mov fs, ax ;段寄存器指向0位置处
mov byte [fs:0], 0 ;往0位置处写入数据,这样会触发异常

用户程序可以通过“伪造”选择子中的RPL值,从而绕开安全检查的机制( CheckRPL )。UserData32Selector equ (0x0004 << 3) + SA_TIG + SA_RPL0特权级修改为0,这样仍然存在漏洞,需要继续打补丁

解决思路:追踪真实的请求者

攻击者通过非法手段获得了内核函数段选择子和偏移地址,通过调用门来调用,意味着返回地址cs和eip会入栈,因此可以通过cs和eip获取攻击者的信息

  1. 在栈中获取函数远调用前CS寄存器的值( 攻击者 )
  2. 之前CS寄存器的值中获取真实的RPLcr ( 攻击者特权级 )
  3. 用RPLcr更新到数据缓冲区对应的段寄存器中
  4. 使用 CheckRPL对段寄存器进行安全检查

image-20220517180206559

进行反汇编得到通过选择子调用函数的地址,打上断点0x929d,然后看上面反编译0x929d的下一条指令地址是0x92a4相减就可以得到值是0x7,

然后运行到断点这里,查看寄存器的值,可以看到eip的值为0x0019,cs的值为0x000a因此推算出如果进入了函数之后栈应当存储eip值为0x0019+0x7=0x0020存储cs值应为0x000a

1
2
3
4
5
6
7
8
9
10
11
<bochs:1> break 0x929D
<bochs:2> c
<bochs:3> reg
CPU0:
rsp: 00000000_000003ff
rip: 00000000_00000019
<bochs:4> sreg
cs:0x000a, dh=0x0040d900, dl=0x92840021, valid=1
Code segment, base=0x00009284, limit=0x00000021, Execute-Only, Non-Conforming, Accessed, 32-bit
ss:0x0032, dh=0x0040d300, dl=0x96e403ff, valid=1
Data segment, base=0x000096e4, limit=0x000003ff, Read/Write, Accessed

然后单步执行观察栈顶状态

可以看到栈顶依次存储了CS(0x000a),EIP(0x0020),ESP(0x0032),SS(0x03ff),是符合预期的

1
2
3
4
5
6
7
8
9
10
11
12
13
<bochs:5> s
Next at t=15338987
(0) [0x0000000092a8] 0040:0000000000000000 (unk. ctxt): push ebp ; 55
<bochs:6> x /16bx ss:esp
[bochs]:
0x00000000000096d3 <bogus+ 0>: 0x20 0x00 0x00 0x00 0x0a 0x00 0x00 0x00
0x00000000000096db <bogus+ 8>: 0xff 0x03 0x00 0x00 0x32 0x00 0x00 0x00
<bochs:7> sreg
es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=0
cs:0x0040, dh=0x00409900, dl=0x92a80039, valid=1
Code segment, base=0x000092a8, limit=0x00000039, Execute-Only, Non-Conforming, Accessed, 32-bit
ss:0x0028, dh=0x00409300, dl=0x92e403ff, valid=1
Data segment, base=0x000092e4, limit=0x000003ff, Read/Write, Accessed

因此可以用mov cx, [esp + 4]取出先前cs寄存器的值,这样也可以取得调用者的真实的RPL的值

最终实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
; KernelData拷贝到es:di --> data buffer 
GetKernelDataFunc:
mov cx, [esp + 4] ;获取用户调用此函数前cs寄存器的值
and cx, 0x0003 ;获取RPLcr低二位真正的特权级

mov ax, es ;获取目前段寄存器的特权级
and ax, 0xFFFC ;清空调用者选择子RPL低二位
or ax, cx ;RPL低二位恢复为真正的特权级
mov es, ax ;恢复调用者选择子的真实特权级

mov ax, KernelData32Selector
mov ds, ax

mov si, KDAT_OFFSET

mov cx, KDAT_LEN

call KMemCpy

retf