段学习
环境: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
| struct _NT_TIB { struct _EXCEPTION_REGISTRATION_RECORD* ExceptionList; VOID* StackBase; VOID* StackLimit; VOID* SubSystemTib; union { VOID* FiberData; ULONG Version; }; VOID* ArbitraryUserPointer; struct _NT_TIB* Self; };
|
在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来修改
在修改段描述符后会发现这样子程序也可以正常执行下去
接下来把D/B位置0
1
| eq 80b98848 008ffb000000ffff
|
然后修改一下汇编
![](../../../images/windowsdrviers/debug/segment study/14.png)
跳到0x811088然后执行2次push
按照正常来说,2次push之后栈会向上+8