双机调试环境配置

虚拟机

bcdedit添加调试启动

拷贝当前启动项

1
bcdedit /copy {current} /d debug

如果拷贝成功可以获得新启动项ID

img

将启动项添加到当前启动顺序末尾

1
bcdedit /displayorder {dd64745d-0a47-11eb-bc4d-cd0e0677722b} /addlast

设置全局调试参数

1.使用串口COM 1进行通信,波特率为115200。

1
bcdedit /dbgsettings SERIAL DEBUGPORT:1 BAUDRATE:115200

2.物理双机1394串口调试,通道为23

1
bcdedit /dbgsettings 1394 CHANNEL:23

3.使用USB端口进行双机调试,目标名称为DEBUGGING

1
bcdedit /dbgsettings USB TARGETNAME:DEBUGGING

这里我们使用第一项即可。

启用引导的调试

1
bcdedit /bootdebug {dd64745d-0a47-11eb-bc4d-cd0e0677722b} ON

启用操作系统的调试

1
bcdedit /debug {dd64745d-0a47-11eb-bc4d-cd0e0677722b} ON

设置等待时间为30秒

1
bcdedit /timeout 30

实体机

WinDbg的配置

在快捷方式 右键->属性 加入参数

1
"G:\Windows Kits\10\Debuggers\x86\windbg.exe" -y srv*E:\WINDDK\symbols*http://msdl.microsoft.com/download/symbols;srv*E:\symbols*http://msdl.microsoft.com/download/symbols;E:\symbols -b -k com:port=//./pipe/com_1,baud=115200,pipe

WinDbg Preview的配置

DbgX.Shell.exe发送到桌面快捷方式,同样在属性处添加参数

1
"C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2007.6001.0_neutral__8wekyb3d8bbwe\DbgX.Shell.exe" -k com:port=//./pipe/com_1,baud=115200,pipe

符号路径需要在软件内部的Settings进行配置,与上面WinDbg的相同。

1
srv*E:\WINDDK\symbols*http://msdl.microsoft.com/download/symbols;srv*E:\symbols*http://msdl.microsoft.com/download/symbols;E:\symbols

保护模式

什么是保护模式?

保护模式,是一种80286系列和之后的x86兼容CPU操作模式。

x86CPU的3个模式:实模式、保护模式、虚拟8086模式。
保护模式又分为:段保护模式、段页保护模式。

什么是段?

在了解什么是段之前,需要先了解一下CPU的发展历史

型号 位宽 总线位宽 地址位 寻址空间 备注
4004 4 4 单元格 640B 1971-11-15
8008 8 8 单元格 16K s1972-4-1
8080 8 8 16 64K 1972-4-1
8086 16 16 20 1M 1978-6-8 X86起源
8088 外部总线最高是8位,其他参数与8086一致
80186 16 16 20 1M 简单认为比8086多了几条指令而已
80286 16 16 24 16M 多任务,多用户系统核心(半保护模式)
80386 32 32 32 4G 包含了分页的虚拟内存机制(保护模式)
80486 32 32 32 4G

在Windows NT之前(80286之前),程序都是在实模式下运行,段是用来划分内存程序区域的,当一个程序想要调用另外一个程序,或是访问另一个程序的内存,只需要修改段的位置即可。

img

为了安全性,在Windows NT之后,就出现了虚拟内存的技术,每个程序都会得到一份独立的2GB虚拟线性地址和共享的2GB高地址,程序与程序直接不能直接访问内存,如果想要访问其他程序的内存,则需要通过内核的API进行操作。

img

那么这就会出现一个问题,既然高2G的内存是共享的,为什么进程在没有进内核态的时候无法访问高2G的内存地址呢?

这个就使用了页保护技术,将内存空间隔离。

那么问题又来了,既然有了页保护,那么还要段来做什么呢?有页不就够了吗?

段在保护模式下是一种权限,主要是用来限制R3的资源,如指令、寄存器。

在16位的时代(实模式),段的作用就是用来区分内存然后寻址,实模式下有四个段分别是CS SS DS ES。

到了保护模式,已经不需要用段来进行寻址了,inter为了向下兼容汇编,就把段用作权限划分。

在80386之后,段就由4个变成了6个,分别是CS SS DS ES FS GS。

  • 每个段分别表示的内容:

    CS:代码段

    DS:全部地址(数据)

    SS:栈数据

    ES:额外段(使用串指令时,如movsb、movsw等就会使用ES描述)

    FS:额外段(在windows下,应用层保存的是TEB的地址,内核中保存的是KPCR

    GS:无论是Linux还是Windows,在x86下都没有使用

什么是TEB、PEB?什么是KPCR?

每一个线程,由两个结构来描述,内核描述的结构叫ETHREAD(执行体),而ETHREAD又包含了KTHREAD(微内核),在微内核里主要负责线程的调度以及各种内核环境相关的东西,执行体则负责保存R3下的一些数据,比如他属于哪一个进程,还有3环下应用程序块的一些对接。(这部分日后细说)。

应用层描述的结构叫TEB(Thread Environment Block线程环境块),主要作用是在R3下描述线程的属性,如当前上下文的环境是什么,寄存器里面保存的是什么,是否到R0了等等。

当从3环进入0环之后FS保存的就是KPCR(CPU的控制块),FSKPCR占用后怎么获取当前线程呢?在0环中KPCR有一个当前线程的一个成员,直接调用即可获取当前线程。至于KPCR具体的作用,这个也是日后再说。

PEB则是进程环境块,日后再说日后再说。

验证每个段的作用

ds

当我们用汇编读写某一个地址时:
mov dword ptr ds:[0x123456],eax
其实内部是这样的ds.base+0x123456

在VS中编写一段内联汇编,这是标准的写法

1
2
3
4
5
6
7
8
9
10
11
12
int value = 110;
int main()
{
__asm
{
mov eax, 10;
mov dword ptr ds:[value] , eax;
}

printf("%d\r\n", value);

}

这段汇编的作用是把eax的值10赋值到value这个全局变量的地址中。

那么我把这里的ds删掉会怎么样呢?

img

也是可以正常运行的。

img

那么是不是不需要写段就可以正常运行呢?

其实并不是,我们只需要查看反汇编就可以看到,是编译器默认给我们加上了

img

用这种方法,就可以测试出每个段会在什么地方使用。

ss

接下来我们把value移到main内部,使其变成局部变量。

img

可以看到,编译器自动添加了SS段

img

cs

测试修改CS段中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int value = 110;
int main()
{
__asm
{
mov ax, cs;
mov ds, ax;
mov ebx, 0x100;
mov dword ptr ds : [value] , ebx;
mov ax, es;
mov ds, ax;
}
printf("%d\r\n", value);

}

运行后直接报错

img

代码段只能读和执行。

fs

TEB的地址

1
2
3
4
5
6
7
8
9
10
11
int value = 0x110;
int main()
{
__asm
{
mov eax, dword ptr fs : [0x18] ;
mov dword ptr ds : [value] , eax;
}
printf("%x\r\n", value);

}

输出结果

img

怎么验证呢?

我们打开OD可以看到FS的值是一样的

img

那么这个0x18又是什么东西呢?

其实就是TEB结构体的Self指针的偏移

img

深入了解Windows内核和揭示超过60000种未公开的结构体☛[Vergilius]

1
现在可以得出一个结论,段有权限,有基址,有限长。

段选择子

什么是段选择子?

段选择符是一个16位的段标识符。它并不直接指向该段,而是指向定义该段的段描述符。

段选择子可以在VS调试窗口中的寄存器窗口查看

img

也可以在一些调试工具的寄存器窗口查看,如od:

img

或是在Windbg中使用r指令查看寄存器

img

一个长度仅16位的段选择子是如何包含这么多信息的?

Intel手册里给出了这样一张图,这张图描述的就是段选择子的含义:

img

  • 0~1位为RPL(Requested Privilege Level)段请求权限

    RPL保存在选择子的最低两位。RPL说明的是进程对段访问的请求权限,意思是当前进程想要的请求权限。RPL的值由程序员自己来自由的设置。权限等级只有两位,意味着代表四种权限。

    00:0环

    01:1环

    10:2环

    11:3环

  • 第2位为TI(Table Indicator)标记

    当TI标记为0时,将去查询GDT

    当TI标记为1时,将去查询LDT

    由于windows使用的是单段模式,所以默认使用的是GDT表,所以在正常情况下TL都为0。

  • 3~15位是一个索引

    当确定需要查询的是GDT表或是LDT表后,就使用此索引来确定所需要的数据是在表中的哪一项。

拆分段选择子

这里使用img作为例子

首先将30转换为16位二进制得到img

将后面的三位排除,这三位分别代表RPLTI

img

然后将剩下的Index按照4位补齐就变成了

img

将0110转换为10进制得到6

所以需要在GDT表中查查找表的从0开始第6项

GDT(Global Descriptor Table)全局描述符表

GDT 的线性基地址在 GDTR (GDT寄存器)中,又因为每个描述符占 8 字节,因此,描述符在表内的偏移地址是索引号乘以 8。当处理器在执行任何改变段选择器的指令时(比如 pop、 mov、jmp far、 call far、 iret、 retf),就将指令中提供的索引号乘以 8 作为偏移地址,同 GDTR 中提供的线性基地址相加,以访问 GDT。如果没有发现什么问题(比如超出了 GDT 的界限),就自动将找到的描述符加载到不可见的描述符高速缓存部分。 加载的部分包括段的线性基地址、段界限和段的访问属性。此后,每当有访问内存的指令时,就不再访问 GDT 中的描述符,直接用当前段寄存器描述符高速缓存器提供线性基地址。

查看GDT寄存器

GDT寄存器里保存着GDT表的基址,需要注意的是GDT寄存器并不是真实存在的寄存器,只是Intel微软描述存在这个寄存器,并在Windbg设计的时候加入了gdtr这样一个查看gdt寄存器的命令,没有任何gdtr的指令。

Windbg中使用gdtr查看GDT表的基址

img

查看GDT表的长度

Windbg中使用gdtl查看GDT表的长度

img

可以看到,GDT表的长度为3ff,注意这里的3ff并不是说GDT表中存在3ff个项,而是说这张表有0x400个字节那么大。

这个0x400是可以根据Index的索引最大长度算出来的。

Index的索引最大长度为13字节,2^13=8192

GDT表内每一项为8字节所以要除个8

8192/8=1024

102416进制就是0x400

查看GDT表的内容

Windbg中使用d指令查看内存,默认查看方式为byte

GDT表中的每一项为8字节所以要使用qword方式查看,在windbg中写为dq

img

这里继续用fs段的Index作为例子,在上文中fs段段选择子的Index是6

所以应该是这项

img

还可以直接dq 80b99000+6*8这样显示的第一项就为对应Index的项了

img

如果我想知道Index的位置,每次都要拆开段选择子的内容?那岂不是很麻烦?

其实并不需要拆,只需要去掉二进制的后3位即可。

依旧使用img作为例子

0x30&FF8得到结果0x30

然后就在表中找到地址为30的项即可

img

现在GDT表中对应的Index项已经找到了,那么问题又来了,通过Index找到的这八个字节是啥?

GDT表里的每一个项称为段描述符

在R3下获取GDT表

在R3下可以使用sgdt(SAVE GDT)指令来读取GDT表的首地址到虚拟内存。

段描述符

在上文中我们得知了GDT表里的每一个项称为段描述符

段描述符是GDT或LDT中的一个数据结构,它为处理器提供诸如段基址,段大小,访问权限及状态等信息。

intel手册里段描述符这一章节有这样一个图,这张图描述的就是段描述符的含义:

img

AVL-可供系统软件使用
BASE-段基地址
D / B-默认操作大小(0 = 16-位段; 1 = 32位段)
DPL-描述符特权级别
G-粒度
LIMIT-段限制
P-段存在
S-描述符类型(0 =系统; 1 =代码或数据)
TYPE-段类型

拆解数据、代码段描述符

这里使用834093f2 fc003748作为例子

img

从后往前拆

Linit:0000 3748

Base:83f2fc00

Type:3

P,DPL,S这三个的值需要把9转换成二进制得到1001,然后填入标志内。

P:1

DPL:0

S:1

同理需要将4转换为二进制得到100,从左到右填入标志即可。

AVL:0

Def:0(21位默认是0)

D/B:0

G:1

数据、代码段描述符中各个标志的含义

P (段存在)标志

P标志指出该段当前是否在内存中(1表示在内存中,0表示不在)。当指向该段描述符的段选择符装载人段寄存器时,如果这个标志为0,处理器会产生一个段不存在异常(NP)。

这里用一个小实验证明P标志的作用

首先使用WinDbg查看gdt表中未被使用的位置

img

可以看到48这个位置为0,说明未被使用,这里就使用这个位置进行测试

编写内联汇编,这段汇编的意思是把ds段的值指向我们要用的48位置的表项,需要注意的是value需要用全局变量,不然编译器会自动替换成ss段。

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

ULONG value = 1000;
int _tmain(int argc, _TCHAR* argv[])
{
__asm
{
mov ax,0x4B;
mov ds,ax;
mov eax,dword ptr ds:[value];
mov ax,es;
mov ds,ax;
}
system("pause");
return 0;
}

运行发现崩溃,因为48处内存为0

img

接下来在WinDbg上把一个数据段的表项拷贝去48位置

img

再次查看GDT表的内存,可以看到,值已经成功写入48的位置

img

再次运行程序,发现可以正常运行

img

这时候尝试把P位改成0,0111是7,所以这里把f改成7

img

dq检查修改成功

img

再次运行程序,发现同样报错

img

通过实验可以证明,P位如果为0,则整个段都无效。

S(描述符类型)标志

系统在解析段的时候,首先会解析P标志,确定段可用之后,再检测S标志。

当S为0时,段描述符将会解析为为代码,数据描述符。

img

当S为1时,段描述符将会被解析为系统描述符,关于系统描述符的部分请查看拆解系统描述符

limit(段限长)标志

此标志保存了段最大的长度,与G标志配合使用。

Base基地址域

确定该段的第0字节在4GB线性地址空间中的位置。

1
逻辑地址(变量)+ Base =线性地址

这里用一个实验证明Base段是如何发挥作用的,这段内联汇编功能就是把value的值赋给value1。

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
#include <Windows.h>

ULONG value = 0x1000;
ULONG value1 = 0;

int _tmain(int argc, _TCHAR* argv[])
{
printf("value_adderss: %x\r\n",&value);
printf("value:%x\r\n",value);
printf("value1:%x\r\n",value1);
__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("--------------\r\n");
printf("value1:%x\r\n",value1);

system("pause");
return 0;
}

首先正常运行一遍

img

接下来把Base改成00000001

img

再次运行代码

img

可以看到value1的值变成了1000010,这是为啥呢?让我们来去value的内存12a7000看一看。

这是修改Base前输出的内容

img

修改了Base后,取值的地址+1就变成了

img

G(粒度)标志

G标志确定段限长扩展的增量。当G标志为0,段限长以字节为单位; G标志为1,段限长以4KB为单位。

G=1 limit是按照页为单位的,一个页的大小为0x1000,4096字节。(一般用来描述代码段)

因为是从0开始所以要先加1

如 (000fffff+1)*0x1000 - 1字节

或000fffff * 0x1000 +0xFFF

G=0 limit是按照字节为单位的(一般用来描述数据段)

如FFF=0x1000=4096字节 刚好等于一个页的大小,超出就不可以访问了。

TYPE域

当段描述符中的S标志(描述符类型)为1时,该描述符为代码段描述符或者数据段描述符。

img

表中已经很人性化的把数据段和代码段分隔开来了,只要值Type的值小于8,那么就是数据段,反之大于8就是代码段。

数据段:

E (Expand):limit的方向,规定段是向上扩展还是向下扩展

W (Write):是否可写

A (Accessed):当这个段被使用过一次之后,A就会被置为1。

如果E 位是用来确定栈是向上拓展的话,Base到Limit的范围是可以访问的。

如果E 位是用来确定栈是向下拓展的话,Base到Limit的范围是不可以访问的。除了Base到Limit的范围其他的才是可以访问的范围。

代码段:

C (Conforming):一致位,用于区分一致代码段非一致代码段非一致代码段就是我们现在所用的代码,R3只能访问R3,R0只能访问R0,比如在应用层不可以直接调用内核函数,而一致代码段在R3下可以直接调用R0的函数,但是R0不可以降权调用R3的函数。

R (Read): 是否可读

A (Accessed):当这个段被使用过一次之后,A就会被置为1。

以上面的Tpye:3作为例子

将3转换为二进制得到0011

类型域的最高位(段描述符的第二个双字的第11位)将决定该描述符为数据段描述符(为0)或者代码段描述符(为1)。

也就是0011中的第一个0表示了这个段描述符描述的是一个数据段。

img

D/B标志

D

D:描述代码段的作用,如果为1则按照默认操作数是32位,如果为0则是16位。

注意cs段是无法直接修改的,如mov cs,ax

要修改cs的话要间接修改,也就是使用跨段跳转 格式为JMP 去哪个段:地址jmp 0x4b:0x12345678

测试cs段的DB位的作用:

首先 同样把GDT表中48位置的内容修改,这里DB位为1

img

到OD中测试,修改断点处汇编为jmp 0x4b:0x11d1f2e,如果正常执行的话将会跳转到0x11d1f2e的位置执行push 0x14

img

执行后可以看到CS已经被修改成了0x4B

img

现在继续执行这行push指令,发现一切正常,14被压入栈内9C减4的位置

img

接下来测试把D/B位改为0

img

再次测试,请注意 执行push前栈地址为0x056F88C,正常情况下把0x14压栈是放在8C-4也就是88的位置

img

执行push,发现0x14以16字节的方式压入了8A的位置,而且栈全乱了

img

B

堆栈段(由SS寄存器所指向的数据段)这 个标志被称为B (big) 标志,它为隐含的栈操作(如push, pop和call) 确定栈指针值的位位数。如果该>标志为1,则使用的是32位的栈指针,该指针放在32位的ESP寄存器中;若该标志为0,则使用的是放在16位SP寄存器中的16位的栈指针。如果该堆栈段为一个向下扩展的数据段,B标志还确定了该堆栈段的地址上界。—— IA-32 Intel 架构软件开发人员手册

从intel官方文档的描述中可以得知,B位有两个作用,第一个是对SS寄存器所指向的数据段(一般为栈)的操作的指针位数有影响,另一个是确定栈为向上还是向下扩展。

在这里我测试了两种情况

1
2
CS DB = 1 SS DB=0
CS DB = 0 SS DB=0

这两种情况都会触发异常,所以无法查看效果。

DPL(Descriptor Privilege Level)描述符特权级

要说DPL的话,就不得不先来说说RPLCPL

Intel设置DPLRPLCPL以实现段的分级和权限检查。

RPL: Requested Privilege Level, 请求特权级
CPL: Current Privilege Level,当前特权级
DPL: Descriptor Privilege Level,描述符特权级

RPL存在于段选择子(Segment Selector)中,CPL存在于段寄存器中,如(CS, SS, DS)
RPLCPL都占用2个bit, 取值范围0~3, 值越小,特权级越高。

作为段选择子的时候,csss比较特殊,它们的RPL代表着当前进程的特权级,因此,二者的RPL又叫CPL

是不是有点绕,没关系,可以通过实验来理解一下。

数据段

测试数据段权限:

在Windbg中把0x48的位置设置DPL为3

img

OD设置ds段选择子为0x4B,也就是RPL=3,CPL=3并使用ds进行一下内存操作,给栈里赋个100。

三行指令执行完毕,一切正常。

img

接下来在Windbg中把0x48的位置设置DPL为0

img

再次测试,发现直接触发异常,并且DS的值被还原成了23(至于为什么会还原成23,日后再说)

img

接下来测试RPL小于DPL的情况

先把DPL改成3,再把0x4B改成0x48进行测试

img

总结

普通数据段的权限检查规则

CPL <= DPL && RPL <= DPL 注意这里的小于是指权限,不是数值。

数据段CPL和RPL的权限大于或者等于DPL,可以切换,反之不能。

代码段

使用Windbg将48处修改为代码段

img

内联汇编实现修改CS段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void test()
{
printf("test");
}

int _tmain(int argc, _TCHAR* argv[])
{
char buf[6]={0,0,0,0,0x4B,0};
*(int *)&buf[0]=(int)test;
__asm
{
call fword ptr ss:[buf];
}
return 0;
}

测试可以正常运行

img

但是这样会导致堆栈不平衡

我们都知道,在调用函数的时候会将返回地址压入栈中,因为在执行CALL FWORD的时候,不单单压入了返回地址,还压入了被修改之前的段选择子

img

所以ret的时候要使用retf,让它返回6个字节堆栈才能平衡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

void __declspec(naked) test()
{
__asm
{
retf;
}
}

int _tmain(int argc, _TCHAR* argv[])
{
char buf[6]={0,0,0,0,0x4B,0};
*(int *)&buf[0]=(int)test;
__asm
{
call fword ptr ss:[buf];
}
return 0;
}

还可以使用jmp,但是要自己进行堆栈平衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void __declspec(naked) test()
{
__asm
{
retf;
}
}

int _tmain(int argc, _TCHAR* argv[])
{
char buf[6]={0,0,0,0,0x4B,0};
*(int *)&buf[0]=(int)test;
__asm
{
push 0x1b;
lea eax,[haha];
push eax;
jmp fword ptr ss:[buf];
haha:
}
return 0;
}

现在测试把0x4B改成0x48运行,可以正常运行,查看寄存器发现,系统又帮我们改回了0x4B,因为判断权限的时候会用RPL去与DPL。

img

接下来将 f 改成 9

img

测试运行 触发异常

img

测试将0x48改为0x4B,依旧触发异常

img

然后,手动将栈内保存的1B改为18,测试retf的权限,发现依旧异常

img

总结

代码段的权限检查规则

必须要CPL=DPL

RPL在提权的时候没有用,在降权的时候才有用,因为判断权限的时候会用RPL去与DPL。

CALL JMP 权限往高或者同等级下 可切换cs。

retf iretd 权限同等或者低于当前权限 可切cs。

拆解系统描述符

段描述符这一章节的前半部分,说的都是数据、代码段描述符,也就是S位等于0的情况,当S位为1时描述符就是系统描述符

系统描述符又可以分为两类:系统段描述符门描述符系统段描述符指向系统段(LDTTSS段)。门描述符它们自身就是“门“,它们或者持有指向在代码段的过程的入口点的指针,或者持有TSS (任务门)的段选择符。表3-2显示了对系统段描述符和门]描述的类型域的译码。

img

门描述符

为了能够访问不同特权级的代码段,处理器提供了一 个特殊的描述符集合,叫做门描述符

调用门(Call Gate)

调用门为不同特权级间的进程控制转换提供了便利。它们一般只用在操作系统中或者使用特权级保护机制的程序里。

img

Offset in Segment:段中的偏移

Segment Selector:段选择子

  • 准备一个进入0环的函数
  • 获取这个函数的地址
  • 最好关闭地址随机化(ASLR),不然运行一次要改一次调用门的偏移
  • 在test函数内必须使用int 3,因为IDE的断点是R3的断点,进入R0就会失效。
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

#include <Windows.h>

__declspec(naked) void test()
{
__asm
{
int 3;
retf;
}
}

int _tmain(int argc, _TCHAR* argv[])
{
char buf[6]={0,0,0,0,0x4b,0};
printf("test func addr:%x\r\n",test);
system("pause");
__asm
{
xor eax,eax;
call fword ptr buf;
}
system("pause");
return 0;
}

首先运行程序得到test函数的地址

img

构造调用门

1
40EC00 00081000

Offset in Segment 15:00:40EC00 00081000

Segment Selector:40EC00 00081000

Param.Count:40EC00 00081000

567位默认为0:40EC00 00081000

Type(1110):40EC00 00081000

P,DPL,0(1110):40EC00 00081000

Offset in Segment 31:16:40EC00 00081000

将构造门写入gdt表

img

执行代码,test函数的int 3成功断下

img

并且CS变成了8

img

接下来调用R0输出函数DbgPrint验证是否提权成功

在WinDbg查看DbgPrint的地址

img

得到首地址83e4f41f,构造一个函数指针。

  • 注意!必须要保存环境并把fs设置为0x30(KPCR),否则蓝屏
  • 由于__cdecl是外平栈,所以我们要自己手动add esp
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
// Call_Gate.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <Windows.h>

typedef int (__cdecl *DbgPrintProc)(const char * _Format, ...);
DbgPrintProc DbgPrint = NULL;
char * TestStr = "R0 HELLO";

__declspec(naked) void test()
{
__asm
{
int 3;
pushfd;
pushad;
push fs;

mov ax,0x30;
mov fs,ax;

mov eax,[TestStr];
push eax;
call DbgPrint;
add esp,0x4;

pop fs;
popad;
popfd;


retf;
}
}

int _tmain(int argc, _TCHAR* argv[])
{
DbgPrint = (DbgPrintProc)0x83e4f41f;
char buf[6]={0,0,0,0,0x4b,0};
printf("test func addr:%x\r\n",test);
system("pause");
__asm
{
xor eax,eax;
call fword ptr buf;
}
system("pause");
return 0;
}

运行程序后WinDbg成功输出字符串

img

测试读取gdt表内容

  • 取esp+2的位置是因为 sgdt去到的不单止是gdt表的地址还有他的限长03ff
  • 写value = 100;的原因是因为如果只有一个定义,编译器会认为你没有使用他,只会给他一个线性地址,不会挂上物理页,就会导致缺页蓝屏。
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
#include <Windows.h>

typedef int (__cdecl *DbgPrintProc)(const char * _Format, ...);
DbgPrintProc DbgPrint = NULL;
char * TestStr = "R0 HELLO";
int value = 0;

__declspec(naked) void test()
{
__asm
{
//int 3;
pushfd;
pushad;
push fs;

mov ax,0x30;
mov fs,ax;

int 3;
sub esp,8;
mov eax,esp;
sgdt [eax];
mov eax,[esp+2];
mov eax,[eax+8];
mov value,eax;

add esp,8;

pop fs;
popad;
popfd;


retf;
}
}

int _tmain(int argc, _TCHAR* argv[])
{
DbgPrint = (DbgPrintProc)0x83e4f41f;
char buf[6]={0,0,0,0,0x4b,0};
printf("test func addr:%x\r\n",test);
system("pause");
value = 100;
__asm
{
xor eax,eax;
call fword ptr buf;
}
printf("%x\r\n",value);
system("pause");
return 0;
}

成功读取

img

如果需要带参的话在段选择子里的Param.Count加上参数的数量40EC01 00081000,然后call之前把参数压栈。

因为加上参数之后是入栈了5个参数所以要retf 0x4,不然会栈不平衡,导致蓝屏。而且retf不单止会平衡R0的栈,还会把R3的栈平衡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <Windows.h>

__declspec(naked) void test()
{
__asm
{
int 3;
retf 0x4;
}
}

int _tmain(int argc, _TCHAR* argv[])
{
char buf[6]={0,0,0,0,0x4b,0};
system("pause");
__asm
{
push 0x12345678;
call fword ptr buf;
}
system("pause");
return 0;
}

运行在断点断下的时候查看R0的esp发现这个值是被放到了中间

img

这时我们再查看R3下的栈12fe4c,发现这里也保存了一份

img

其实我们push是push到R3的esp,然后cpu根据Param.Count的值无脑复制过去的。

  • 要使用调用门提权的话 必须要CPL==DPL
中断门(Interrupt Gate)
什么是中断?

中断是一种机制,用来处理硬件需要向CPU输入信息的情况。 比如鼠标,键盘等。

中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。

硬件 ->中断

软件->模拟中断->异常

IDT表(Interrupt Descriptor Table)中断描述符表
  • r ldtr 查看IDT表基址

img

  • r ldtl 查看IDT表长度

img

  • 查看IDT表

img

IDT表的每一项成员只要有效,都是一个函数。

拆解中断门

img

中断门与调用门的结构基本一致,区别在于没有了参数部分和8-12位有一个位’D’来确定门的大小,当D为1 = 32位,当D为 0 = 16位。

进入中断门

大家最熟知的int3中断指令就可以进入中断门,int3是Intel特意设计过的指令,他的硬编码为CC只占用了一个字节。

打开调试器输入int3可以看见硬编码为CC

img

如果输入int 3在中间多加个空格硬编码就会变成CD 03

img

这两种写法都代表int3中断。

但是除了int3其他中断就不可以这样写了,如int 2,调试器会直接提示无法识别。

img

想要使用int 2必须要在中间加入空格

img

int后面跟的123是IDT表的索引号,如int 0IDT表的第一个元素。

在WinDbg中使用!idt命令可以查看idt表中每个元素对应的中断函数,如!idt 3查看int3的函数地址

img

为了证明这是int3的函数,使用bp对该函数下一个int3断点

img

再在R3下设置一个int3,执行后CPU立马挂了,因为形成了递归中断。

img

使用中断门提权,并分析中断门

我们先自己构造出一个中断门,首先定义一个函数,并得到他的地址。

img

利用这个地址,构造一个中断门0040ee00 00081000,找到IDT表的空位写入,可以看到IDT表的0x100的位置是空的,我把值写到这。

img

写入

img

编辑代码,因为int后面跟的是表项所以写0x20

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

void __declspec(naked) test()
{
__asm
{
int 3;
iretd;
}

}

int _tmain(int argc, _TCHAR* argv[])
{
printf("Text func addr: %x\r\n",test);
system("pause");
__asm
{
int 0x20;
}
return 0;
}

执行后成功断下,证明成功进入中断门

img

接下来在int 0x20处下一个断点,观察进入R0前的寄存器。

img

这里我复制一份

1
2
3
4
5
6
CPP
EAX = 00000000 EBX = 7FFD8000 ECX = 1379E52E EDX = 003E33C8 ESI = 0012FE64 EDI = 0012FF30 EIP = 00401061 ESP = 0012FE64 EBP = 0012FF30 EFL = 00000246

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

OV = 0 UP = 0 EI = 1 PL = 0 ZR = 1 AC = 0 PE = 1 CY = 0

接着单步进入test函数触发int3断点,在WinDbg上查看R0的寄存器和栈,首先查看栈:

img

栈顶被压入了五个值,分别是:

返回地址:0x0401063

R3的CS段:0x1b

R3的EFLAGS寄存器:0x346

R3ESP:0x012fe64

R3的SS段:0x23

查看寄存器

img

可以看到,CS变成了8,SS变成了10,EFLAGS变成了46,fs变成了30(自动帮我们切换成了KPCR),由于他只帮我们切换到KPCR,没帮我们改回去,所以返回的时候可能会有异常。最好在进R0之前先保存一下fs。

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
#include <Windows.h>

void __declspec(naked) test()
{
__asm
{
int 3;
iretd;
}

}

int _tmain(int argc, _TCHAR* argv[])
{
printf("Text func addr: %x\r\n",test);
system("pause");
__asm
{
push fs;
int 0x20;
pop fs;
}

return 0;
}

那么问题来了,调用门和中断门很像,而且都可以用来提权,那么两者的区别是什么呢?——屏蔽中断

想要了解什么是屏蔽中断,需要先看一下EFLAGS的解析图:

img

可以看到EFLAGS第九位为IF位(Interrupt Enable Flag)中断启用标志位,设置IF=1,则允许中断;设置IF=0,则禁止中断。

在刚刚R0寄存器的图片中可以看到,EFLAGS被修改成了46,转换为二进制00 0100 0110,第九位为0,所以屏蔽了中断。

除了进入中断门屏蔽中断外,还可以使用cli命令屏蔽中断。

  • CLI (Clear Interrupt) 禁止中断发生
  • STI (Set Interrupt) 允许中断发生

这两个指令只能在内核模式下执行,不可以在用户模式下执行;

1
2
3
4
5
6
7
__asm
{
cli;
xxxxxxxxxx;
xxxxxxxxxx;
sti;
}

除了IF位,进入中断门还会将VM(虚拟8086模式)、TF(单步位)、NT(任务嵌套位)位清空,以防止中断嵌套。

陷阱门(Trap Gate)

img

陷阱门与中断门基本一致,唯一的区别是陷阱门不会将IF位(中断启用标志位)清空,只会将VM(虚拟8086模式)、TF(单步位)、NT(任务嵌套位)位清空。

构造一个陷阱门0040ef00 00081000

进入陷阱门后查看eflags寄存器

img

246转换为二进制10 0100 0110IF位为1

任务
任务段(TSS)

任务段存在于GDT表中,使用r tr命令可查看任务段的段选择子。

img

再到GDT表中找到0x28的位置,这个就是任务段。

img

  • 任务段结构

使用dt命令查看任务段结构

img

Intel的白皮书上也有对任务段结构的描述图片

img

当任务段切换之后,上一任务段的寄存器也被替换成当前任务段的寄存器,当通过Previous Task Link回到上一个任务段时,寄存器又会替换回去。因为这种操作效率太低,所以Windows、Linux等操作系统都没用使用到任务段。

  • 任务段描述符

img

任务段的描述符与数据段、代码段的差不多,不同的地方在:

  1. 以为是系统段所以12为0
  2. 取消了D/B位
  3. Type处有个B,这个位表示是否正忙(当前有没有被使用)

使用dg命令可以自动解析段

img

使用dt 结构 base可以查看结构中的内容

img

可以看到esp0为0x83f28cb0 ss为0x10,当调用或中断门从R3切换到R0就会替换成这两个值。

  • 实现一个任务段

注意!tss段的大小必须大于104字节!

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
108
#include <Windows.h>

char esp3[0x2000] = {0};
char esp0[0x2000] = {0};

__declspec(naked) void test()
{
__asm
{
int 3;
iretd;
}

}

struct _KiIoAccessMap
{
UCHAR DirectionMap[32]; //0x0
UCHAR IoMap[8196]; //0x20
};

typedef struct _KTSS
{
USHORT Backlink; //0x0
USHORT Reserved0; //0x2
ULONG Esp0; //0x4
USHORT Ss0; //0x8
USHORT Reserved1; //0xa
ULONG NotUsed1[4]; //0xc
ULONG CR3; //0x1c
ULONG Eip; //0x20
ULONG EFlags; //0x24
ULONG Eax; //0x28
ULONG Ecx; //0x2c
ULONG Edx; //0x30
ULONG Ebx; //0x34
ULONG Esp; //0x38
ULONG Ebp; //0x3c
ULONG Esi; //0x40
ULONG Edi; //0x44
USHORT Es; //0x48
USHORT Reserved2; //0x4a
USHORT Cs; //0x4c
USHORT Reserved3; //0x4e
USHORT Ss; //0x50
USHORT Reserved4; //0x52
USHORT Ds; //0x54
USHORT Reserved5; //0x56
USHORT Fs; //0x58
USHORT Reserved6; //0x5a
USHORT Gs; //0x5c
USHORT Reserved7; //0x5e
USHORT LDT; //0x60
USHORT Reserved8; //0x62
USHORT Flags; //0x64
USHORT IoMapBase; //0x66
struct _KiIoAccessMap IoMaps[1]; //0x68
UCHAR IntDirectionMap[32]; //0x208c
}KTSS,*PKTSS;

KTSS tss = {0};


int _tmain(int argc, _TCHAR* argv[])
{
memset(esp3,0xcc,sizeof(esp3));
memset(esp0,0xcc,sizeof(esp0));

printf("tss addr = %x\r\n",&tss);

tss.Eax =0;
tss.Ecx =0;
tss.Edx =0;
tss.Ebx =0;

tss.Ebp =0;
tss.Esi =0;
tss.Edi =0;
tss.Cs = 0x8;
tss.Ss = 0x10;
tss.Ds = 0x23;
tss.Es = 0x23;
tss.Fs = 0x30;

tss.Esp =(ULONG)(esp3+0x2000-8);
tss.Esp0 =(ULONG)(esp0+0x2000-8);
tss.Ss = 0x10;
tss.Eip =(ULONG)test;

printf("KTSS size:%x\r\n",sizeof(KTSS));
DWORD dwCr3 = 0;
printf("请输入CR3:");
scanf("%x\n",&dwCr3);
tss.CR3 = dwCr3;

printf("func addr = %x\r\n esp0 = %x\r\n esp3 = %x\r\n",test,tss.Esp0,tss.Esp);

system("pause");

char bufcode[]={0,0,0,0,0x48,0};
__asm
{
call fword ptr bufcode;
}

system("pause");
return 0;
}

首先获得tss段的地址0x409030,构造一个任务段描述符0000e940 903020ac

img

将任务段描述符写入GDT表中0x48的位置

img

使用!process 0 0命令得到程序CR3(页表基址)

img

输入CR3后确定值正常后回车

img

成功断下

img

此时使用r tr命令查看断段选择子可以看见变成了0x48

img

使用dg tr也可以看到变化

img

寄存器也被替换成我设置好的值

img

使用dt命令查看结构可以看见Previous Task Link从0变成了0x28

img

再查看0x28的结构,可以看到里面保存了切换到我们自己的任务段之前各个寄存器的值。

img

这段代码当走到iretd返回之后会蓝屏,因为test函数里使用了int3,而int3会将NT位(任务嵌套位)清空,而使用iretd进行返回的时候,iretd首先会查询当前有没有任务嵌套,也就是查看NT位。如果NT位为1,就会去找TSSBacklink,拿到上一个任务的tr,然后替换寄存器返回。如果NT位为0,iretd会直接查栈的esp+0返回。在上面的代码里把esp都初始化为了0xcc,所以导致iretd返回到0xcc导致蓝屏。

修改test函数内代码防止蓝屏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CPP
__declspec(naked) void test()
{
__asm
{
int 3;
pushfd;
pop eax;
or eax,4000;
push eax;
popfd;
iretd;
}
}

^_^:
注意 这里有可能导致蓝屏或cpu卡死的其他两点
test函数里的int3
28没用置为空闲
28cr3没有保存
还没空测试,先记着

任务门

img

需要注意的是,任务段是构建在GDT表中的,而任务门则需要构造在IDT表中。

先把任务段描述符写入GDT中的0x48位置

img

接着构造任务门写入IDT表+0x100的位置中

img

修改代码,将call改为int

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
108
109
110
111
112
113
114
115
#include <Windows.h>

char esp3[0x2000] = {0};
char esp0[0x2000] = {0};

__declspec(naked) void test()
{
__asm
{
int 3;
pushfd;
pop eax;
or eax,4000;
push eax;
popfd;
iretd;
}

}

struct _KiIoAccessMap
{
UCHAR DirectionMap[32]; //0x0
UCHAR IoMap[8196]; //0x20
};

typedef struct _KTSS
{
USHORT Backlink; //0x0
USHORT Reserved0; //0x2
ULONG Esp0; //0x4
USHORT Ss0; //0x8
USHORT Reserved1; //0xa
ULONG NotUsed1[4]; //0xc
ULONG CR3; //0x1c
ULONG Eip; //0x20
ULONG EFlags; //0x24
ULONG Eax; //0x28
ULONG Ecx; //0x2c
ULONG Edx; //0x30
ULONG Ebx; //0x34
ULONG Esp; //0x38
ULONG Ebp; //0x3c
ULONG Esi; //0x40
ULONG Edi; //0x44
USHORT Es; //0x48
USHORT Reserved2; //0x4a
USHORT Cs; //0x4c
USHORT Reserved3; //0x4e
USHORT Ss; //0x50
USHORT Reserved4; //0x52
USHORT Ds; //0x54
USHORT Reserved5; //0x56
USHORT Fs; //0x58
USHORT Reserved6; //0x5a
USHORT Gs; //0x5c
USHORT Reserved7; //0x5e
USHORT LDT; //0x60
USHORT Reserved8; //0x62
USHORT Flags; //0x64
USHORT IoMapBase; //0x66
struct _KiIoAccessMap IoMaps[1]; //0x68
UCHAR IntDirectionMap[32]; //0x208c
}KTSS,*PKTSS;

KTSS tss = {0};


int _tmain(int argc, _TCHAR* argv[])
{
memset(esp3,0xcc,sizeof(esp3));
memset(esp0,0xcc,sizeof(esp0));

printf("tss addr = %x\r\n",&tss);

tss.Eax =0;
tss.Ecx =0;
tss.Edx =0;
tss.Ebx =0;

tss.Ebp =0;
tss.Esi =0;
tss.Edi =0;
tss.Cs = 0x8;
tss.Ss = 0x10;
tss.Ds = 0x23;
tss.Es = 0x23;
tss.Fs = 0x30;

tss.Esp =(ULONG)(esp3+0x2000-8);
tss.Esp0 =(ULONG)(esp0+0x2000-8);
tss.Ss = 0x10;
tss.Eip =(ULONG)test;



printf("KTSS size:%x\r\n",sizeof(KTSS));
DWORD dwCr3 = 0;
printf("请输入CR3:");
scanf("%x",&dwCr3);
tss.CR3 = dwCr3;

printf("func addr = %x\r\n esp0 = %x\r\n esp3 = %x\r\n",test,tss.Esp0,tss.Esp);

system("pause");

char bufcode[]={0,0,0,0,0x48,0};
__asm
{
int 0x20;
}

system("pause");
return 0;
}

执行代码,成功进入任务门

img

References:

《牛逼的火哥》

《IA-32卷3:系统编程指南》(中文版)

《火哥4合1的intel手册》

以及OneTrainee大佬的博客