WEB

WEB安全

漏洞复现

CTF

常用工具

实战

代码审计

Javaweb

后渗透

内网渗透

免杀

进程注入

权限提升

漏洞复现

靶机

vulnstack

vulnhub

Root-Me

编程语言

java

逆向

PE

逆向学习

HEVD

PWN

CTF

heap

Windows内核学习

其它

关于博客

面试

杂谈

段学习

环境:Visual Studio 2008

0x01 段探测

在前面CPU知识说过

DS是数据段,SS也是数据段,不过是主要管理堆栈的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "stdafx.h"
#include <Windows.h>

int value = 110;
int _tmain(int argc, _TCHAR* argv[])
{
__asm{
mov eax, 10;
mov dword ptr [value], eax
}
printf("%d",value);
system("pause");
return 0;
}

首先把value值当作全局变量

然后debug打开反汇编

![](../../../images/windowsdrviers/debug/segment study/1.png)

可以发现这个变量是存在ds来表示的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "stdafx.h"
#include <Windows.h>


int _tmain(int argc, _TCHAR* argv[])
{
int value = 110;
__asm{
mov eax, 10;
mov dword ptr [value], eax
}
printf("%d",value);
system("pause");
return 0;
}

将value存到函数里,就变成临时变量

这里直接看反汇编没有显示ss,用x64dbg可以发现用ss表示

![](../../../images/windowsdrviers/debug/segment study/2.png)

之前说过0x80000000以上是内核内存,尝试去读一下

汇编这样写是因为直接[0x80000000]会优化

达不到读指定地址内存的效果,所以这样绕了绕

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "stdafx.h"
#include <Windows.h>

int value1= 0x80000000;
int _tmain(int argc, _TCHAR* argv[])
{
int value = 110;
__asm{
mov eax, [value1]
mov eax, [eax]
mov value, eax
}
printf("%d",value);
system("pause");
return 0;
}

![](../../../images/windowsdrviers/debug/segment study/3.png)

可以看到是没有权限读的

在CPU基础里面还讲过cs是数据段,尝试能否修改数据段内的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "stdafx.h"
#include <Windows.h>

int value= 110;
int _tmain(int argc, _TCHAR* argv[])
{
int value = 110;
__asm{
mov ax,cs
mov ds,ax
mov ebx, 100
mov dword ptr ds:[value], ebx
}
printf("%d",value);
system("pause");
return 0;
}

![](../../../images/windowsdrviers/debug/segment study/4.png)

发现还是报错,所以得到结论数据段不可写,可读可执行

在上一篇还说到fs里面存放着TEB

先看看TEB的结构

![](../../../images/windowsdrviers/debug/segment study/5.png)

第一个成员变量是_NT_TIB,打开看看发现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//0x1c bytes (sizeof)
struct _NT_TIB
{
struct _EXCEPTION_REGISTRATION_RECORD* ExceptionList; //0x0
VOID* StackBase; //0x4
VOID* StackLimit; //0x8
VOID* SubSystemTib; //0xc
union
{
VOID* FiberData; //0x10
ULONG Version; //0x10
};
VOID* ArbitraryUserPointer; //0x14
struct _NT_TIB* Self; //0x18
};

在0x18的位置存储着自己的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "stdafx.h"
#include <Windows.h>

int value= 110;
int _tmain(int argc, _TCHAR* argv[])
{
int value = 110;
__asm{
mov eax, fs:[0x18]
mov [value],eax
}
printf("%x",value);
system("pause");
return 0;
}

![](../../../images/windowsdrviers/debug/segment study/6.png)

这里7FFDF000是TEB的地址,fs的只有0xfff的大小,另外几个常见的段都是0xffffffff大小,所以都是4GB

得到段有基址,权限,限长

从16位的段基址*16+偏移

到32位的段基址+偏移,这里的偏移有术语为逻辑地址,段地址+逻辑地址=线性地址

线性地址就是绝对地址,逻辑地址就是偏移

在调试的时候打开寄存器窗口,能看到段寄存器的值

1
CS = 001B DS = 0023 ES = 0023 SS = 0023 FS = 003B GS = 0000

下面就了解一下寄存器这么小的值,怎么实现权限,限长,基址

0x02 段选择子

先看FS = 003B

![](../../../images/windowsdrviers/debug/segment study/7.png)

根据这张图可以把003B展开成16位的二进制

0-1位RPL是用来判断权限的

第2位如果是0则查GDT表,1查LDT表,由于操作系统用的是当段模式,基本情况下用的是GDT表

第3-15位则是索引,指向GDT表的哪一项

将3B展开0000 0000 0011 1011

RPL=11 TI=0 所以要查GDT表 INDEX =0000 0000 0011 1吗,这里需要按4位重新排序

所以变成0000 0000 0000 0111=7,这样就知道查GDT的第7项,因为数组都是0开始的所以实际是查第8项

打开windbg输入r指定查看寄存器

r gdtr能查看gdt地址,但是gdtr不是一个真的寄存器

r gdtl是gdt表的长度,x86看到是0x3ff字节

使用dp查看gdt表

![](../../../images/windowsdrviers/debug/segment study/8.png)

在这里可以看到8位的数据,这些被称为段描述符

0x03 段描述符

![](../../../images/windowsdrviers/debug/segment study/9.png)

上面是段描述符的结构图片

用上面得到的段描述符对一下

1
2
3
4
5
6
7
8
9
10
11
12
7f40f3fd`f0004000

limit:04000 #这里限长不太对不知道为啥
base: 7ffdf000 #这里运气不错直接看到了fs的基址,因为有些情况是看不到的,如果复现的时候没看到也没事只要会断就行了
type: 3
s:1
DPL:3
P:1
AVL:0
0:0
D/B:1
G:0

观察下面的权限表可以发现type=3代表可读可写可访问的数据段

![](../../../images/windowsdrviers/debug/segment study/10.png)

再来看一个

在内核中cs=8

展开就是0000 0000 0000 1000,那么就是第1项

1
2
3
4
5
6
7
8
9
10
11
12
13
00cf9b00`0000ffff

limit: fffff
base:00000000
type:b 代码段,可执行
S:1
DPL:0
P:1
AVL:0
0:0
D/B:1
G:1
G=1时limit是按照页为单位, 由于是0开始,所以0xfffff*0x1000+0xfff(0的页)=0xfffffff G=0代表limit按照字节为单位

0x04 作业

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
807d5800  00000000`00000000 00cf9b00`0000ffff
807d5810 00cf9300`0000ffff 00cffb00`0000ffff
807d5820 00cff300`0000ffff 80008b7d`5c0020ab
807d5830 8040937c`c0004fff 7f40f3fd`f0004000
807d5840 0000f200`0400ffff 00000000`00000000
807d5850 8000897d`7d200067 8000897d`7cb00067
807d5860 00000000`00000000 00000000`00000000
807d5870 800092b9`880003ff 00000000`00000000
807d5880 00000000`00000000 00000000`00000000
807d5890 00000000`00000000 00000000`00000000
807d58a0 8000897d`7d900067 00000000`00000000
807d58b0 00000000`00000000 00000000`00000000
807d58c0 00000000`00000000 00000000`00000000
807d58d0 00000000`00000000 00000000`00000000
807d58e0 00000000`00000000 00000000`00000000
807d58f0 00000000`00000000 00000000`00000000

下面开始练习

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
00cf9300`0000ffff
limit: fffff
base:00000000
type:3 数据段
S:1
DPL:0 内核
P:1
AVL:0
0:0
D/B:1
G:1

00cffb00`0000ffff
limit: fffff
base:00000000
type:b 代码段,可执行
S:1
DPL:3 用户
P:1
AVL:0
0:0
D/B:1
G:1

80008b7d`5c0020ab
limit: 020ab
base: 807d5c00
type:b 代码段,可执行
S:1
DPL:0 内核
P:0
AVL:0
0:0
D/B:0
G:0

8040937c`c0004fff
limit: 04fff
base:807cc0004
type:3
S:1
DPL:0 内核
P:1
AVL:0
D/B:1
0:0
G:0

0000f200`0400ffff
limit: 0ffff
base: 00000400
type: 2
S:1
DPL:3 内核
P:1
AVL:0
D/B:0
0:0
G:0

8000897d`7d200067
limit: 00067
base:807d7d20
type:9
S:1
DPL:0
P:1
AVL:0
D/B:0
0:0
G:0

8000897d`7cb00067
limit: 00067
base:807d7cb0
type:9
S:1
DPL:0
P:1
AVL:0
D/B:0
0:0
G:0

800092b9`880003ff
limit: 03ff
base: 80b98800
type: 2
S:1
DPL:0 内核
P:1
AVL:0
D/B:0
0:0
G:0

8000897d`7d900067
limit: 00067
base:807d7d90
type:9
S:1
DPL:0
P:1
AVL:0
D/B:0
0:0
G:0

0x05 段描述符P位

先写下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "stdafx.h"

#include <Windows.h>
ULONG value = 0;
int _tmain(int argc, _TCHAR* argv[])
{
__asm{
mov ax, 0x4b
mov ds, ax
mov eax, dword ptr ds:[value]
}
system("pause");
return 0;
}

0x4b当段肯定是报错的,根据段选择子可以知道在第9项,根据图片发现是空的

接下来使用windbg修改段描述符,把第4项的数据段复制到第9项的位置

1
eq 807d5848 00cff3000000ffff

dp查看后发现修改成功,再次运行程序不报错

修改P位

1
eq 807d5848 00cf73000000ffff

再次运行程序又报错了,说明第一步就是校验P来知道段是否可用

0x06 段描述符G位

在上面写过G=1时limit是页为单位

根据00cff3000000ffff得知当前G=1

接下来打印value的地址查看在哪里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "stdafx.h"

#include <Windows.h>
ULONG value = 0;
int _tmain(int argc, _TCHAR* argv[])
{
printf("%x\n", &value);
__asm{
mov ax, 0x4b
mov ds, ax
mov eax, dword ptr ds:[value]
}
system("pause");
return 0;
}

![](../../../images/windowsdrviers/debug/segment study/11.png)

发现地址远超fffff,这时候把G修改为0

1
eq 807d5848 004ff3000000ffff

在mov eax, dword ptr ds:[value]位置报错,说明线长是存在的

0x07 段描述符base位

前面提到base+逻辑地址=线性地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "stdafx.h"

#include <Windows.h>
ULONG value = 0x1000;
ULONG value1 = 0;
int _tmain(int argc, _TCHAR* argv[])
{
printf("%x\n", &value);
__asm{
mov ax, 0x4b
mov ds, ax
mov ebx, dword ptr ds:[value]
mov ax, es
mov ds, ax
mov dword ptr ds:[value1], ebx
}
printf("%x\n",value1);
system("pause");
return 0;
}

这段汇编的操作其实是修改了value1的指针,让value1也指向value,正常运行之后输出value1=0x1000

下面修改base

1
eq 807d5848 004ff3000001ffff

在base后面加上1,那么线性地址=base+逻辑地址+1

本来在内存中0x1000是00 10 00 00

加1之后变成10 00 00 ??,这里的问号是不确定的值,因为不知道内存的下一个位置有什么内容

![](../../../images/windowsdrviers/debug/segment study/12.png)

在这里是FE,代码证明了base是有用的

0x08 段描述符TYPE位

在上面有TYPE的表

有EWA几个属性,E是决定limit向上还是向下的,W是可写,A是代表该段是否使用过,是CPU来统计的属性

来做个实验测试一下A,根据上面的表格,3是被使用过的,所以将3修改为2

1
eq 807d5848 00cff2000000ffff

然后写这样一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "stdafx.h"

#include <Windows.h>
ULONG value = 0x1000;
ULONG value1 = 0;
int _tmain(int argc, _TCHAR* argv[])
{
printf("%x\n", &value);
__asm{
mov ax, 0x4b
mov ds, ax
mov ax,es
mov ds, ax
}

printf("%x\n",value1);
system("pause");
return 0;
}

在这段汇编里面没有对该段进行读写,只是访问一下

![](../../../images/windowsdrviers/debug/segment study/13.png)

发现只是访问段TYPE位还是被修改了,接下来测试读写属性

1
eq 807d5848 00cff0000000ffff

将TYPE置零变为只读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "stdafx.h"

#include <Windows.h>
ULONG value = 0x1000;
ULONG value1 = 0;
int _tmain(int argc, _TCHAR* argv[])
{
printf("%x\n", &value);
__asm{
mov ax, 0x4b
mov ds, ax
mov eax, dword ptr ds:[value] //不影响读取
mov dword ptr ds:[value], 1 //不能向段内写
mov ax,es
mov ds, ax
}

printf("%x\n",value1);
system("pause");
return 0;
}

这里说明不止页可以控制权限,段也能控制权限

引出有2种保护模式,段保护模式,段页保护模式

因为段保护没有页所以只有段来限制,段的保护功能太弱,可能存在于单片机

接下来说到代码段保护CS,目前水平修改不了,代码段有2种,非一致代码段和一致代码段

非一致代码段R3只能访问R3,R0只能访问R0,应用层不能直接调内核函数

一致代码段R3能直接调用R0函数,R0调不了R3

0x09 段描述符D/B位

D 描述代码段的作用,如果D是1就按照操作系统默认的走,如果为0则按照16位走

同样的修改0x4b的位置,这次将代码段的段描述符拿过来

1
eq 80b98848 00cffb000000ffff

这边gdt的位置变了因为重启过了不要在意

接下来随便拿一个exe放到x64dbg里面

cs不能像ds一样通过mov来修改,需要用jmp来修改

1
jmp far 0x4b:0x011E1088

在修改段描述符后会发现这样子程序也可以正常执行下去

接下来把D/B位置0

1
eq 80b98848 008ffb000000ffff

然后修改一下汇编

![](../../../images/windowsdrviers/debug/segment study/14.png)

跳到0x811088然后执行2次push

按照正常来说,2次push之后栈会向上+8