文章目录
上一课我们已经实现了利用中断提权的方式让自己写的函数拥有了零环的权限,但是为了更方便的写零环代码。我们还需要搞清楚中断现场的上下文环境,什么资源可以被使用,什么资源不能被使用。如果不清楚现场的环境,就很容易写出有问题的代码,从而导致系统蓝屏。
如何获取中断现场环境
比较方便的一个方式是在函数内部将当前的寄存器包括通用寄存器 段寄存器甚至是控制寄存器等等全部打印出来。
中段现场环境
观察中断现场堆栈环境
我们加上下面几行代码,来观察一下中断之前和之后的堆栈环境,看一下int 20这条指令对堆栈的影响
DWORD g_esp[2];
void __declspec(naked) IdtEntry()
{
__asm
{
mov [g_esp+4], esp;
iretd;
}
}
void go()
{
__asm mov[g_esp], esp;
__asm int 0x20;
}
运行程序,可以看到在中断提权之前,地址是一个三环的esp,而中断之后esp的地址则变成了零环的地址。
接下来我们在代码中加入一条int3指令,在调试器中观察一下堆栈内的数据情况
kd> dps esp
a6e63c9c 0040114f
a6e63ca0 0000001b
a6e63ca4 00000246
a6e63ca8 0012ff44
a6e63cac 00000023
......
可以看到栈顶的数据是0x0040114f,接着用ub命令观察这个地方的反汇编
kd> ub 0040114f
00401131 668cc0 mov ax,es
00401134 66a380334000 mov word ptr ds:[00403380h],ax
0040113a 668ce0 mov ax,fs
0040113d 66a37c334000 mov word ptr ds:[0040337Ch],ax
00401143 668ce8 mov ax,gs
00401146 66a378334000 mov word ptr ds:[00403378h],ax
0040114c 58 pop eax
0040114d cd20 int 20h
可以发现,这个地方就是我之前写的代码,也就是三环的返回地址。堆栈中的其他数据含义如下:
- 0000001b是三环的cs
- 00000246是三环的eflags
- 0012ff44是三环的esp
- 00000023是三环的ss
总结:通过中断门进入零环之前需要先保存三环的寄存器环境来达到再次返回三环的目的
观察中断现场的寄存器环境
接下来我们修改一下代码,把所有的寄存器尽可能全部打印出来。接着运行我们写的程序
可以看到中断现场前后的寄存器环境中,ESP从三环栈切换到了零环栈,CS从0x1B变成了0x8,代码提权也是从这里体现的,另外ss也发生了变化。
那么问题也就来了,这些寄存器改变的背后是一种什么样的机制,另外这些变化的寄存器都是从哪来的。要想知道这个问题的答案,需要了解一个东西叫做段选择子
段选择子
段寄存器结构
段寄存器结构可以抽象成以下的结构
struct SegMent {
WORD selector; //段选择子
WORD attribute; //属性
DWORD base; //基址
DWORD limit; //段限长
}
- selector: 首先可见部分16位对应上面SegMent的selector成员。在 OD 中,可以看到段寄存器后面就跟着一个数字,比如 ds 后面的 0023。而 0023 后面的部分就是,剩余部分不可见部分,不过 OD 也给我们展示出来了
- attribute: attribute 属性记录了该段是否有效,是否可读写等权限。如果往一个不可写的段执行写数据,会报异常。
- base 表示基地址
- limit 表示段界限,如果在超出了段界限进行读写,会报错。
在段寄存器中,16位的段选择子是可见的,其余80位的不可见的。我们主要关注段选择子
例如上图中CS的段选择子位0x23,SS的段选择子为0x2B。段选择子就是一个数字,一共有16位,结构如下:
| 1 | 0 | 字节
|7654321076543 2 10| 比特
|-------------|-|--| 占位
| INDEX |T|R | 含义
| |I|P |
| | |L |
- INDEX:高13位表示的是在GDT数组或LDT数组的索引号
- TI:Table Indicator,这个值为0表示查找GDT,1则查找LDT
- RPL:请求特权级。以什么样的权限去访问段。
变化的段寄存器的具体含义
明白了段选择子的结构,接下来就来拆解其中的具体含义,以CS从0x1B变成了0x8为例:
0x1B=00011011
- 索引号:00011=3,代表查找GTB表下标为3的描述符
- TI:0代表查GDT表
- RPL:11=3 代表特权等级为3 也就是三环权限
0x8=00001000
- 索引号:00001,代表查找GTB表下标为1的描述符
- TI:0代表查GDT表
- RPL:0 代表特权等级为0,也就是零环权限
总结:CS段寄存器从0x1B变成0x8,主要是代表特权等级从三环提升到了零环。
权限切换了,esp堆栈和相应的段寄存器ss自然也会跟着切换。因为栈数据是线程的核心资源,特权切换必须伴随栈切换
遗留问题:SS段寄存器和栈顶指针来自于哪?
我们已经知道代码提权的时候是需要切换栈的。那么,还剩下一个问题:就是SS段寄存器和栈顶指针是来自于哪?
答案是来自于TSS 任务状态段
什么是TSS
TSS 全称task state segment,是指在操作系统进程管理的过程中,任务(进程)切换时的任务现场信息。注意不要把TSS和任务切换关联起来,它只是一块用于保存任务现场的一些数据
Inter白皮书给出的TSS在内存中的图是这样的:
看的一脸懵逼对吧?我也看的一脸懵逼。抽象成结构体就是这样的
typedef struct TSS {
DWORD link; // 保存前一个 TSS 段选择子,使用 call 指令切换寄存器的时候由CPU填写。
// 这 6 个值是固定不变的,用于提权,CPU 切换栈的时候用
DWORD esp0; // 保存 0 环栈指针
DWORD ss0; // 保存 0 环栈段选择子
DWORD esp1; // 保存 1 环栈指针
DWORD ss1; // 保存 1 环栈段选择子
DWORD esp2; // 保存 2 环栈指针
DWORD ss2; // 保存 2 环栈段选择子
// 下面这些都是用来做切换寄存器值用的,切换寄存器的时候由CPU自动填写。
DWORD cr3;
DWORD eip;
DWORD eflags;
DWORD eax;
DWORD ecx;
DWORD edx;
DWORD ebx;
DWORD esp;
DWORD ebp;
DWORD esi;
DWORD edi;
DWORD es;
DWORD cs;
DWORD ss;
DWORD ds;
DWORD fs;
DWORD gs;
DWORD ldt;
// 这个暂时忽略
DWORD io_map;
} TSS;
TSS的工作细节
TSS在任务切换过程中起着重要作用。在任务切换的过程中,首先,处理器中的各寄存器的当前值被自动保存到TR(任务寄存器)所指定的TSS中;然后下一任务的TSS选择子被装入TR;最后,从TR所指定的TSS中取出各寄存器的值送到处理器的各寄存器中,从而实现任务切换。
中断提权的任务切换过程
三环程序通过中断门进入到零环时,首先会保存当前的寄存器环境到TR所指定的TSS中,这一点我们已经在中断现场的堆栈环境中见过了。
然后将零环所需要的选择子装入TR。
最后从TSS中取出各寄存器送到当前环境下。这其中就包括了ss段寄存器和栈顶指针。我们也可以从TSS的结构体中看到0环的ss段寄存器和esp栈顶指针。
总结:ss段寄存器和esp栈顶指针来自于TSS
实验代码
最后 附上本次实验所用到的代码
#include "pch.h"
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
DWORD g_eax[2], g_ecx[2], g_edx[2], g_ebx[2];
DWORD g_esp[2], g_ebp[2], g_esi[2], g_edi[2];
WORD g_cs[2], g_ds[2], g_ss[2], g_es[2], g_fs[2], g_gs[2];
WORD g_tr;
void __declspec(naked) IdtEntry()
{
__asm
{
mov [g_eax + 4], eax;
mov [g_ecx + 4], ecx;
mov [g_edx + 4], edx;
mov [g_ebx + 4], ebx;
mov [g_esp + 4], esp;
mov [g_ebp + 4], ebp;
mov [g_esi + 4], esi;
mov [g_edi + 4], edi;
push eax;
mov ax, cs;
mov [g_cs + 2], ax;
mov ax, ds;
mov[g_ds + 2], ax;
mov ax, ss;
mov[g_ss + 2], ax;
mov ax, es;
mov[g_es + 2], ax;
mov ax, fs;
mov[g_fs + 2], ax;
mov ax, gs;
mov[g_gs + 2], ax;
str ax;
mov g_tr, ax;
pop eax;
int 3;
iretd;
}
}
void go()
{
__asm
{
mov[g_eax], eax;
mov[g_ecx], ecx;
mov[g_edx], edx;
mov[g_ebx], ebx;
mov[g_esp], esp;
mov[g_ebp], ebp;
mov[g_esi], esi;
mov[g_edi], edi;
push eax;
mov ax, cs;
mov[g_cs], ax;
mov ax, ds;
mov[g_ds], ax;
mov ax, ss;
mov[g_ss], ax;
mov ax, es;
mov[g_es], ax;
mov ax, fs;
mov[g_fs], ax;
mov ax, gs;
mov[g_gs], ax;
pop eax;
}
__asm int 0x20;
}
//eq 80b95500 0040ee00`00081040
int main()
{
if ((DWORD)IdtEntry != 0x401040)
{
printf("wrong addr:%p", IdtEntry);
exit(-1);
}
go();
printf("eax:%p,\tecx:%p\tedx:%p\tebx:%p\n", g_eax[0], g_ecx[0], g_edx[0], g_ebx[0]);
printf("esp:%p,\tebp:%p\tesi:%p\tedi:%p\n", g_esp[0], g_ebp[0], g_esi[0], g_edi[0]);
printf("cs:%p,\tds:%p\tss:%p\tes:%p\tfs:%p\tgs:%p\n", g_cs[0], g_ds[0], g_ss[0], g_es[0], g_fs[0], g_gs[0]);
printf("eax:%p,\tecx:%p\tedx:%p\tebx:%p\n", g_eax[1], g_ecx[1], g_edx[1], g_ebx[1]);
printf("esp:%p,\tebp:%p\tesi:%p\tedi:%p\n", g_esp[1], g_ebp[1], g_esi[1], g_edi[1]);
printf("cs:%p,\tds:%p\tss:%p\tes:%p\tfs:%p\tgs:%p\n", g_cs[1], g_ds[1], g_ss[1], g_es[1], g_fs[1], g_gs[1]);
printf("tr:%p\n", g_tr);
system("pause");
}