-
-
[原创] 【Windows 内核基础篇】- 内核入门 - 段基础
-
发表于: 1天前 593
-
保护模式下通过段划分内存的权限以及访问的权限
x86 和 x64都有六个段寄存器(Segment Register):
根据intel白皮书3a的介绍,CPU额外提供了三个数据段寄存器,可以作为程序额外的段,正常情况下可能只使用到DS、CS、SS这三个段寄存器
数据段中一般为全局变量,而局部变量一般会放入堆栈段中:
此时将value定义在main函数外,视为全局变量,可以看到,编译器会将该变量放在DS段中:
当将value定义为局部变量时:
此时的段会识别成SS堆栈段:
通过段选择子(Segment Selector)来确定段的属性,默认UserMode
可见部分:
对于段的属性描述,通过3.4.2描述:
段选择子总共是16位,分成三部分进行解析:
Table Indicator表示从LDT或者是GDT表中查找段描述符,Index则表明表的下标
可以发现DS和SS的段选择子都是0x0023
,而CS的段选择子是0x001B
,通过解析可以得到都是通过GDT表进行查表,通过WinDBG中的r gdtr
查询,r默认输出的是通用寄存器和段寄存器:
可以通过r后跟特定寄存器查询,这里gdtr不代表真的有gdtr这个寄存器,只是通过sdtr来存储gdt表的地址
通过dq gdtr
或者 dq 0x80b98800
查询:
可见部分的段描述符为64位QWORD
0x0023的Index为4,所以其对应的段描述符为:00cff300 0000ffff
0x001B的Index为3,所以其对应的段描述符为:00cffb00 0000ffff
intel白皮书3.4.5 Segment Descriptor描述了段描述符的组成部分:!
关于段类型(Segment Type) 在3.4.5.1 Code- and Data-Segment Descriptor Types中的表Table 3-1. Code- and Data-Segment Types:
最高位区分数据或代码段:0-Data,1-Code,低三位分别表示EWA和CRA,对比前面获取到的CS和DS的段描述符,也就是在Type的位置存在区别,DS的Type为0b0011
,CS的Type为0b1011
所以这里查表后的结果是:
CS段的属性为Code
,Description为: Execute/Read, accessed
DS段的属性为Data
,Description为:Read/Write, accessed
这里Accessed的含义是是否已经使用过该段,例如push esp
,此时会用到堆栈区域,该区域通过SS段选择子指向的段描述符进行管理,那么此时,即使将Accessed置0,内核也会将其设为1,表示该段在最后一次清零前被使用过
验证P段的有效性
首先找到GDT表中全0的部分:Index=9的部分,此时段选择子为0x004b
eq复制段选择子0x0023的部分到该位置下:
此时0x004b指向的段为有效段,内联asm将ds修改为0x004b
P位指明段是否有效:
通过eq将P位设为0:
此时执行直接就报错了,再将P设置为1:
此时不重新编译程序,直接运行:
G Flag决定limit的基础单位,若G = 0,limit的单位为byte,若G=1,limit的单位为一个页,那么此时对于:00cf7300 0000ffff
,limit的值为0xfffff,一个页的大小为4096字节=4KB=0x1000 Bytes,那么此时该段的最大空间为(0xFFFFF+1) * 0x1000
,这里的+1是因为0xfffff是最大索引,而不是最大长度,索引是从0开始的,所以其空间总共为0xFFFFF + 1
也就是0x100000
该标志位,取决于当前段的类型,会有不同的效果:
代码段下:
D/B = 0,则默认操作数是16bits
D/B = 1, 则默认操作数是32bits
决定堆栈的寻址空间是16bit还是32bit
代码段的段选择子为CS:在32位下,默认都是0x001B:
尝试修改cs:
发现是不存在mov cs, ax
这样的指令的,那么眼前只能通过jmp Far来实现CS的修改:jmp far 0x004B:0x00401F1C
:
此时由于0x004B的段描述符为空所以当想跨段跳转时会找不到段的信息,所以会进异常:
复制0x001B的段描述符到0x004B:
重新EA跨段跳转,成功将CS从0x001B
改为0x004B
: 注意这里修改CS之所以不会产生内核崩溃,是因为这里的CS只是对于当前进程而言,当进入内核时,会将这些段选择子的值都修复,从ring3进到ring0时也会有个上下文的转换,所以不会产生内核崩溃
目前默认的D/B=1,所以此时的默认寻址空间为32bit,所以操作数默认也是32bit,那么此时默认压栈的字节数为4bytes:
当D/B设置为0,由于此时CS是0x004B,所以指向的是修改后的段描述符,通过eq已将D/B位设置为0,
此时重新执行push eax,也只会压入2字节的栈,栈的寻址空间从32bit变为16bit
此时的esp从0x12FF8C
到0x12FF8A
:
在代码段是D/B位看的是D位,当该段为数据段时,那么D/B位看的是B位
将0x004b指向的段描述符修改为:0x008ff3000000ffff
此时已将ss修改为0x4b
此时的描述符已变成16bit寻址的数据段
向上拓展/向下拓展
正常情况,当D/B=1,limit=0xFFFFF,G=1,此时limit的描述空间为(0xFFFFF+1)* 0x1000,此时可描述空间为4GB,这4GB都可以访问,这种情况称为向上拓展,可以理解为include 4GB
而且当D/B=0时,limit=0xFFFFF,G=1,此时limit描述的4GB空间都无法访问,那么此时将limit设为0,那么limit描述的空间为0,反而4GB的空间都可以访问,这种情况称为向下拓展,可以理解为 exclude 4GB
此时将EWA设置为110,Type设置为Data,然后将limit设置为0,由于E=1,所以是向下拓展的,那么此时limit=0,可以描述空间除limit描述的部分,在32位下就是4GB:
总结:D位模式下,描述的是代码段下的寻址操作数,D=1=>32-bit, D=0=>16-bit
B位模式下,描述的是代码段或者数据段下的寄存器和寻址操作数,限制limit的描述空间
保护模式下,段模式为两种,一种是常用的段页模式,段保护+页保护机制,一种是纯段模式的保护机制
在纯段模式下,分为一致代码段和非一致代码段,在一致代码段下R3可以直接调用R0的代码
在段页模式下,R3是没办法直接调用R0的,因为存在页保护机制,无法直接从R3调用R0
[[裸函数]]:裸函数的概念和用法
在段选择子(Segment Selector),有RPL
在段描述符(Segment Descriptor),有DPL
另外还存在一个CPL,表示当前的权限,通过SS或者CS,一般情况下SS和CS的低2bit是一样,应该说必须是一致的,因为一个是跟堆栈段相关,一个是跟代码段相关,这两个段需要保持一致的权限,才能保证程序的正常运行
5.4 Type Checking中的Figure 5.3- Protection Rings:
该图写明了在保护模式下的权限分布,对于正常使用者,其实只需要了解有Ring 3和Ring 0即可,Ring 1和Ring 2在当前情况下不使用到
对于RPL(Request Privilege Level),位于段选择子Segment Selector中的低2位,所以其最大值为3
在数据段DS下,假设RPL=0,CPL=3,DPL=3,那此时能否访问得到,答案是可以的
前提是在数据段下,此时DS=0x0020
而此时通过GDT查表可以发现DPL=3,对于当前的代码段CS=0x0023,那么此时的CPL=3
在数据段下CPL <= DPL
时,即可访问到该数据段
在堆栈段下,必须DPL=CPL=RPL,
在代码段下,CPL=DPL即可
提权时,必须CS跟SS都改成0环权限才能进入0环,在一般情况下,CS的权限=SS的权限,只改一方都会进入失败
所以正常情况下只需要看CS即可知道SS的段权限
RPL = 0 去访问DPL=3的段,可以看到CS会自动设置成0x004B,自动修正成RPL=3,所以在CS段下RPL也不影响使用,只需要保证CPL == DPL
即可
跨段不提权:
代码实现跨段跳转:
此时就实现了jmp far,但是这里ret会报错,因为没有压入栈,所以我们可以通过标记的形式将ret压栈,再跳转即可:
优化代码:CS从0x001b-0x004b-0x001b:
修改shellcode[4] = 0x1b,然后再走一遍刚才的流程,通过jmp far跨段将CS修改回0x001b,这里设置成0x0018也行,反正程序会自动修正RPL=3
call有两种写法实现跨段,call far
和 call fword ptr
跨段前的寄存器:
跨段后的寄存器:
EIP的变化不管,CS是目的,这里伴随的还有个ESP的压栈,压入了8字节的数据,当前32位的程序,并且D/B=1,所以单次压栈压入的是4字节:
那么0x001b就是CS的值,先压入CS,再压入返回地址:
但是实际上也是只执行了ret,所以CS的值没有变化,仍旧是修改后的0x004B:
而由于只做了一次ret,所以esp的值指向了压入的CS的值,没有回到原来的地方
那么此时直接运行程序,直接报ESP错误了,此时就是堆栈不平衡导致的后续程序运行出错:
既然程序默认会将0x1b压栈,就说明后续会做返回,那么此时提供了一个retf
用来做段返回:
文中涉及的段解析代码已上传至附件
参考文献:
DS: Data Segment 数据段 可读可写不可执行
CS: Code Segment 代码段 可读可执行不可写
SS: Stack Segment 堆栈段 可读可写不可执行
ES、FS、GS
DS: Data Segment 数据段 可读可写不可执行
CS: Code Segment 代码段 可读可执行不可写
SS: Stack Segment 堆栈段 可读可写不可执行
ES、FS、GS
// test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <Windows.h>
int
value = 10;
int
_tmain(
int
argc, _TCHAR* argv[])
{
OpenProcess(0, 0, 0);
__asm {
mov eax, value;
}
return
0;
}
// test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <Windows.h>
int
value = 10;
int
_tmain(
int
argc, _TCHAR* argv[])
{
OpenProcess(0, 0, 0);
__asm {
mov eax, value;
}
return
0;
}
// test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <Windows.h>
int
_tmain(
int
argc, _TCHAR* argv[])
{
char
a[] =
"1234"
;
int
value = 10;
__asm {
mov eax, value;
}
return
0;
}
// test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <Windows.h>
int
_tmain(
int
argc, _TCHAR* argv[])
{
char
a[] =
"1234"
;
int
value = 10;
__asm {
mov eax, value;
}
return
0;
}
#include <Windows.h>
#include <iostream>
#define TI 0x04
#define RPL 0x03
#define INDEX 0xfff8
#define LDT 1
#define GDT 0
using
namespace
std;
void
analysisSegmentSelector(
WORD
selector) {
// RPL
cout <<
"RPL: "
;
cout << (selector & 0x03) << endl;
// TI
cout <<
" Specifies the descriptor table to use: "
;
(selector & TI) == 1 ? cout <<
"LDT"
<< endl : cout <<
"GDT"
<< endl;
// Index
cout <<
"Index: "
;
cout << ((selector & INDEX) >> 3) << endl;
}
int
main() {
// 0000 0000 0010 0011
WORD
selector = 0x0023;
analysisSegmentSelector(selector);
return
0;
}
#include <Windows.h>
#include <iostream>
#define TI 0x04
#define RPL 0x03
#define INDEX 0xfff8
#define LDT 1
#define GDT 0
using
namespace
std;
void
analysisSegmentSelector(
WORD
selector) {
// RPL
cout <<
"RPL: "
;
cout << (selector & 0x03) << endl;
// TI
cout <<
" Specifies the descriptor table to use: "
;
(selector & TI) == 1 ? cout <<
"LDT"
<< endl : cout <<
"GDT"
<< endl;
// Index
cout <<
"Index: "
;
cout << ((selector & INDEX) >> 3) << endl;
}
int
main() {
// 0000 0000 0010 0011
WORD
selector = 0x0023;
analysisSegmentSelector(selector);
return
0;
}
#include <Windows.h>
#include <iostream>
using
namespace
std;
#define SEG_BASE_24_31 0xff000000
#define SEG_BASE_16_23 0xff0000
#define SEG_BASE_0_15 0xffff
#define SEG_LIMIT_16_19 0x000f0000
#define SEG_LIMIT_0_15 0xffff
#define SEG_G 0x00800000
#define SEG_D_B 0x00400000
#define SEG_L 0x00200000
#define SEG_AVL 0x00100000
#define SEG_P 0x00008000
#define SEG_DPL 0x00006000
#define SEG_S 0x00001000
#define SEG_TYPE 0x00000f00
// Descriptor Type
#define SYSTEM_TYPE 0
#define CODE_DATA_TYPE 1
// Default operation size
#define BIT_16 0
#define BIT_32 1
VOID
analysisSegmentDescriptorOthers(
DWORD
descriptor) {
DWORD
seg_G = (descriptor & SEG_G) >> 0x17;
DWORD
seg_D_B = (descriptor & SEG_D_B) >> 0x16;
DWORD
seg_L = (descriptor & SEG_L) >> 0x15;
DWORD
seg_AVL = (descriptor & SEG_AVL) >> 0x14;
DWORD
seg_P = (descriptor & SEG_P) >> 0xf;
DWORD
seg_DPL = (descriptor & SEG_DPL) >> 0x0d;
DWORD
seg_S = (descriptor & SEG_S) >> 0x0c;
DWORD
seg_TYPE = (descriptor & SEG_TYPE) >> 0x08;
cout <<
"Granularity: "
<< hex << seg_G << endl;
cout <<
"Default operation size: "
;
seg_D_B == BIT_32 ? cout <<
"32-bit segment"
: cout <<
"16-bit segment"
;
cout << endl;
cout <<
"64-bit code segment: "
<< hex << seg_L << endl;
cout <<
"Available for use by system software: "
<< hex << seg_AVL << endl;
cout <<
"Segment present: "
<< hex << seg_P << endl;
cout <<
"Descriptor privilege level: "
<< hex << seg_DPL << endl;
cout <<
"Descriptor type: "
;
seg_S == SYSTEM_TYPE ? cout <<
"system"
: cout <<
"code or data"
;
cout << endl;
cout <<
"Segment type: "
<< hex << seg_TYPE << endl;
}
VOID
analysisSegmentDescriptor(
DWORD64
descriptor) {
DWORD
base = ((descriptor >> 0x10) & SEG_BASE_0_15)
^ ((descriptor >> 0x10) & SEG_BASE_16_23 )
^ ((descriptor >> 0x20) & SEG_BASE_24_31 );
DWORD
limit = (descriptor & SEG_LIMIT_0_15)
^ ((descriptor >> 0x20) & SEG_LIMIT_16_19);
cout <<
"Segment Limit: "
<< hex << limit << endl;
cout <<
"Segment base address: "
<< hex << base << endl;
analysisSegmentDescriptorOthers(descriptor >> 0x20);
}
int
main() {
// 0x0023
// 0000 0000 0010 0011
WORD
selector = 0x0023;
analysisSegmentSelector(selector);
DWORD64
descriptor = 0x00cff3000000ffff;
analysisSegmentDescriptor(descriptor);
cout <<
"-------------------------------------------------"
<< endl;
// 0x001B
// 0000 0000 0001 1011
selector = 0x001B;
analysisSegmentSelector(selector);
descriptor = 0x00cffb000000ffff;
analysisSegmentDescriptor(descriptor);
return
0;
}
#include <Windows.h>
#include <iostream>
using
namespace
std;
#define SEG_BASE_24_31 0xff000000
#define SEG_BASE_16_23 0xff0000
#define SEG_BASE_0_15 0xffff
#define SEG_LIMIT_16_19 0x000f0000
#define SEG_LIMIT_0_15 0xffff
#define SEG_G 0x00800000
#define SEG_D_B 0x00400000
#define SEG_L 0x00200000
#define SEG_AVL 0x00100000
#define SEG_P 0x00008000
#define SEG_DPL 0x00006000
#define SEG_S 0x00001000
#define SEG_TYPE 0x00000f00
// Descriptor Type
#define SYSTEM_TYPE 0
#define CODE_DATA_TYPE 1
// Default operation size
#define BIT_16 0
#define BIT_32 1
VOID
analysisSegmentDescriptorOthers(
DWORD
descriptor) {
DWORD
seg_G = (descriptor & SEG_G) >> 0x17;
DWORD
seg_D_B = (descriptor & SEG_D_B) >> 0x16;
DWORD
seg_L = (descriptor & SEG_L) >> 0x15;
DWORD
seg_AVL = (descriptor & SEG_AVL) >> 0x14;
DWORD
seg_P = (descriptor & SEG_P) >> 0xf;
DWORD
seg_DPL = (descriptor & SEG_DPL) >> 0x0d;
DWORD
seg_S = (descriptor & SEG_S) >> 0x0c;
DWORD
seg_TYPE = (descriptor & SEG_TYPE) >> 0x08;
cout <<
"Granularity: "
<< hex << seg_G << endl;
cout <<
"Default operation size: "
;
seg_D_B == BIT_32 ? cout <<
"32-bit segment"
: cout <<
"16-bit segment"
;
cout << endl;
cout <<
"64-bit code segment: "
<< hex << seg_L << endl;
cout <<
"Available for use by system software: "
<< hex << seg_AVL << endl;
cout <<
"Segment present: "
<< hex << seg_P << endl;
cout <<
"Descriptor privilege level: "
<< hex << seg_DPL << endl;
cout <<
"Descriptor type: "
;
seg_S == SYSTEM_TYPE ? cout <<
"system"
: cout <<
"code or data"
;
cout << endl;
cout <<
"Segment type: "
<< hex << seg_TYPE << endl;
}
VOID
analysisSegmentDescriptor(
DWORD64
descriptor) {
DWORD
base = ((descriptor >> 0x10) & SEG_BASE_0_15)
^ ((descriptor >> 0x10) & SEG_BASE_16_23 )
^ ((descriptor >> 0x20) & SEG_BASE_24_31 );
DWORD
limit = (descriptor & SEG_LIMIT_0_15)
^ ((descriptor >> 0x20) & SEG_LIMIT_16_19);
cout <<
"Segment Limit: "
<< hex << limit << endl;
cout <<
"Segment base address: "
<< hex << base << endl;
analysisSegmentDescriptorOthers(descriptor >> 0x20);
}
int
main() {
// 0x0023
// 0000 0000 0010 0011
WORD
selector = 0x0023;
analysisSegmentSelector(selector);
DWORD64
descriptor = 0x00cff3000000ffff;
analysisSegmentDescriptor(descriptor);
cout <<
"-------------------------------------------------"
<< endl;
赞赏
- [原创] 【Windows 内核基础篇】- 内核入门 - 段基础 594
- [原创]驱动通信基础 3840
- [原创]PE解析思路 18018
- [原创] SWPUCTF 2019 easyRE 9114
- [原创] ACTF2020 Splendid_MineCraft 7133