■ 数据访问时的权限check
一、 访问data segment时(ds、es、fs 及gs)
1、 程序指令要访问数据时,data segment selector 被加载进 data segment register(ds、es、fs 和 gs)前,处理器会进行一系列的权限检查,通过了才能被加载进入segment register。处理器分为两步进行检查:
★ CPL(当前程序运行的权限级别)与RPL(位于selector中的 RPL)作比较,并设置有效权限级别为低权限的一个。
★ 得出的有效权限级别与DPL(segment descriptor 中的DPL)作比较,有效权限级别高于DPL,那么就通过。低于就不允许访问。
2、举个例子:
如果:CPL = 3、RPL = 2、DPL = 2。那么
EPL = CPL > RPL ? CPL : RPL; if (EPL <= DPL) { /* 允许访问 */ } else { /* 失败,#GP 异常生产,执行异常 */ } 或者: |
也就是要访问目标data segment,那么必须要有足够的权限,这个足够的权限就是:当前运行的权限级别及选择子的请求权限级别要高于等于目标data segment的权限级别。
二、 访问stack segment时
1、 该问stack 时的权限检查更严格,CPL、RPL及DPL三者必须相等才能通过该问请求。
2、 举个例子:
if (CPL == RPL && RPL == DPL && CPL == DPL) { /* 允许访问 */ } else { /* 失败,#GP 异常生产,执行异常 */ } |
也就是说每个权限级别有相对应的statck segment。不能越权访问,即使高权限访问低权限也是被拒绝的
■ 控制权的转移及权限检查。
权限检查的4个要素:
★ CPL:处理器当前运行的级别,也就是:当前 CS 的级别,在 CS 的 BIT0 ~ Bit1
★ DPL:访问目标代码段所需的级别。定义在 segment descriptor 的 DPL 域中
★ RPL: 通过 selector 进行访问时,selector 内定义的级别。
★ conforming/nonconforming:目标代码属于 nonconforming 还是 conforming 定义在segment descritptor 的 C 标志位中
x86 的各方面检查依赖于目标代码段是 nonconforming 还是 conforming 类型
一、 直接转移(far call 及 far jmp)
1、 直接转移定义为不带gate selector或 taskselector的远调用。当执行一条 call cs:eip 或 jmp cs:eip 指令时,cs 是目标代码段的selector,处理器在加载指令操作数中的cs进cs register前,要进行一系列的权限检查,控制权的转移权限分两部分,根据目标代码段descriptor定义的两种情况:
1)、nonconforming target code segment
★ 直接转移后的权限级别是不能必改变的。因此,CPL 必须要等于目标代码段的 DPL。
★ 要有足够的请求权限进行访问。因此,目标代码段选择子的RPL <= CPL
2)、conforming target code segment
★ conforming code segment 允许访问高权限级别的代码。这里只需检查 CPL >= DPL 即可,RPL 忽略不检查。
★ 转移后CPL不会改变。
2、 以上两步通过后,处理器加载目标代码段的CS 进入CS register,但权限级别不改变,继而RPL被忽略。
★ 处理器根据CS selector在相应的descriptor table 找到 code segment descriptor。CS 的Bit2(TI域) 指示在哪个descriptor table 表查找,CS.TI = 0 时在GDT查找,CS.TI = 1时在LDT 查找。
★ CS的Bit15~Bit3 是selector index 值,处理器基于GDT或LDT来查找segment descriptor。具体是:GDTR.base 或 LDTR.base + CS.SI × 8 得出code segment descritpro。
★ 处理器自动加载code segment descriptor 的 base address、segment limit及attribute 域进入 CS register的相应的隐藏域。
★ 转到CS.base + eip 处执行指令
总结:用代码形式来说明直接转移 call cs:eip 这条指令
例: call 1a:804304c (即cs = 1a, eip = 804304c)
target_cs = 1a; target_eip = 0x0804304c; CPL = CS.RPL; /* 当前执行的代码段的权限级别就是CPL */ RPL = target_cs.RPL; /* 目标段 selector 的低3位是RPL */ target_si = target_cs.SI; /* 目标段 selector 的索引是Bit15~Bit3 */ target_ti = target_cs.TI; /* 目标段selector的描述符表索引是Bit2 */ CODESEG_DESCRIPTOR target_descriptor; if (target_ti == 0) { /* target_cs.TI为0 就是参考到 GDT(全局描述符表) */ } else { /* 否则就是参考 LDT (局部描述符表)*/ target_descriptor = LDTR.base + target_si * 8; DPL = target_descriptor.DPL; /* 获取DPL */ if (target_descriptor.type & 0x06) { /* conforming */ } else { /* nonconforming */ /****** go ahead … …******/ goto target_descriptor.base + target_eip; /* 跳转到目标地址执行 */ DO_GP_EXCEPTION: /* 执行 #GP异常点 */ |
二、 使用call gate 进行控制权的转移
使用call gate进行转移控制,目的是建立一个利用gate进行向高权限代码转移的一种保护机制。gate符相当一个进入高权限代码的一个通道。
对于指令 call cs:eip 来说:
★ 目标代码的selector是一个门符的选择子。用来获取门描述符。
★ 门描述符含目标代码段的selector及目标代码的偏移量。
★ 目标代码的ip值被忽略。因为门符已经提供了目标代码的偏移量。
1、 权限的检查
★ 首先,必须要有足够的权限来访问gate符,所以:CPL <= DPLg(门符的DPL)且:RPL <= DPLg
★ 进前代码向高权限代码转移,所以,对于conforming类型的代码段来说,必须CPL >= DPLs(目标代码段的DPL)
★ 对于nonconforming类型代码段来说,必须CPL = DPLs
★ call 指令改变当前权限,而jmp指令不改变当前权限。
总结:
if ((CPL <= DPLg) && (RPL <= DPLg)) { /* 足够的权限访问门符 */ if (target.C == CONFORMING) { /* 目标代码属于 conforming类型 */ /* 向高权限级别代码转移控制 */ if (CPL >= DPLs) { /* 通过访问 */ } else { /* 失败,#Gp异常发生 */ } } else { /* 目标代码属于 nonconforming 类型 */ } else { /* 没有足够权限访问门符 */ |
2、 控制权的转移
指令:call 1a:804304c (其中1a是call gate selector)
gate_selector = 0x1a; /* call gate selector */ RPL = gate_selector.RPL; /* 门符选择子RPL */ CPL = CS.RPL; /* 当前代码段低3位是CPL*/ CALLGATE_DESCRIPTOR call_gate_descriptor; /* 门符的描述符 */ call_gate_si = gate_selector.SI; /* 门符 selector 的索引 */ /* 获取 target code segment descriptor */ target_cs = call_gate_descriptor.selector; /* 获取门符的目标代码段选择子 */ if (target_cs_ti == 0) DPLg = call_gate_descriptor.DPL; /* 获取门符的DPL */ if (CPL <= DPLg && RPL <= DPLg) { /* 有足够权限访问门符 */ /* 允许访问 */ /* 跳转到目标代码执行 */ return; DO_GP_EXCEPTION: /* 执行异常 */ |
三、 使用中断门或陷井门进行转移
中断门符及陷井门必须存放在IDT中,IDT表也可以存放call gate。
1、 中断调用时的权限检查
用中断门符进行转移时,所作的权限检查同call gate相同,区别在于intterrupt gate 转移不需要检查RPL,因为,没有RPL需要检查。
★ 必须有足够的权限访问门符,CPL <= DPLg
★ 向同级权限代码转移时,CPL == DPLs,向高权限代码转移时,CPL > DPLs
总结
if (CPL <= DPLg) { /* 有足够权限访问门符 */ if (CPL >= DPLs) { /* 允许访问目标代码头 */ } else { /* 失败,#GP异常发生 */ } } else { |
2、 控制权的转移
发生异常或中断调用时
★ 用中断向量在中断描述符表查找描述符:中断向量×8,然后加上IDT表基址得出描述符表。
★ 从查找到的描述符中得到目标代码段选择子,并在相应的GDT或LDT中获取目标代码段描述符。
★ 目标代码段描述符的基址加上门符中的offset,确定最终执行入口点。
中断或陷井门符转移的总结:
例: int 0x80 指令发生的情况
vector = 0x80; INTGATE_DESCRIPTOR gate_descriptor = IDTR.base + vector * 8; CODESEG_DESCRIPTOR target_descriptor; TSS tss = TR.base; /* 得到TSS 内存块 */ DPLg = gate_descriptor.DPL; target_cs = gate_descriptor.selector; if (CPL <= DPLg) { /* 允许访问门符 */ if (target_cs.TI == 0) { /* index on GDT */ DPLs = target_descriptor.DPL; if (CPL > DPLs) { /* 向高权限代码转移 */ /* 根据目标代码段的DPL值来选取相应权限的stack结构 */ /* 以下必须保护旧的stack结构,以便返回 */ } else if (CPL == DPLs) { *--esp = EFLAGS; /* eflags 寄存器入栈 */ /* 分别将 NT、NT、RF及VM标志位清0 */ if (gate_descriptor.type == I_GATE32) { /* 假如是中断门符 */ CS = target_selector; /* 加载目标代码段 */ /* 执行中断例程 */ } else { |
■ 堆栈的切换
控制权发生转移后,处理器自动进行相应的堆栈切换。
1、 当转向到同权限级别的代码时,不会进行堆栈级别的调整,也就是不进行堆栈切换。
2、 当转向高权限级别时,将发生相应级别的堆栈切换。从TSS块获取相应级别的stack结构。
例:假如当前运行级别CPL为2时,发生了向0级代码转移时:
TSS tss = TR.base; /* 从TR寄存器中获取TSS 块 */ CPL = 2; /* 当前运行级别为2 级*/ DPL = 0; /* 目标代码需要级别为 0 级 */ /* 根据目标代码需要的级别进行选取相应的权限级别的stack结构 */ *--esp = SS; /* 保存旧的stack结构 */ /* 然后再作相当的保存工作,如保存参数等 */ *--esp = CS; /* 最后保存返回地址 */ |
■ 控制权的返回
当目标代码执行完毕,需要返回控制权给原代码时,将产生返回控制权行为。返回控制权行为,比转移控制权行为简单得多。因为,一切条件已经在交出控制权之前准备完毕,返回时仅需出栈就行了。
1、 near call 的返回
近调用情况下,段不改变,即CS不改变,权限级别不改变。仅需从栈中pop返回地址就可以了。
2、 直接控制权转移的返回(far call或far jmp)
直接控制权的转移是一种不改变当前运行级别的行为。只是发生跨段的转移。这时,CS被从栈中pop出来的CS值加载进去,处理器会检查CPL与这个pop出来的选择子中的RPL进行检查,相符则返回。不相符则发生 #GP异常。
总结:假如当前运行的目标代码执行完毕后,将要返回。这时CPL为2
CPL = 2; /* 当前代码运行级别为 2 */ … … EIP = *esp++; /* pop出原EIP 值 */ CS = *esp++; /* pop出原CS值 */ if (CPL == CS.RPL) { |
3、 利用各种门符进行向高权限代码转移后的返回
从高权限代码返回低权限代码,须从stack中pop出原来的stack结构。这个stack结构属于低权限代码的stack结构。然后直接pop 出原返回地址就可以了。
总结:
CPL = 0; /* 当前运行级别为 0 级 */ … … EIP = *esp++; /* 恢复原地址 */ CS = *esp++; /* 恢复原地址及运行级别 */ ESP = *esp ++; /* 恢复原stack结构 */ return ; /* 返回 */ |