首页
社区
课程
招聘
[原创] 【Windows 内核基础篇】- 内核入门 - 段基础
发表于: 1天前 593

[原创] 【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从0x12FF8C0x12FF8A:
图片描述
图片描述

在代码段是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 farcall 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;
 

[注意]APP应用上架合规检测服务,协助应用顺利上架!

上传的附件:
收藏
免费 2
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//