保护模式篇——段和门小结
写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。
看此教程之前,问几个问题,基础知识储备好了吗?上一节教程学会了吗?上一节课的练习做了吗?没有的话就不要继续了。
🔒 华丽的分割线 🔒
写在前面
如果您看本系列进行学习保护模式的话,抱歉给您的长期等待带来的不便。最近主要是忙于学业,作业比较多。其次就是回顾之前的总结有没有疏漏,也花费了不少时间。总之,该写的博客,还是得写的。
练习及参考
本次答案均为参考,可以与我的答案不一致,但必须成功通过。第一题的答案可以比我的更详细。
1️⃣ 自己构造任务段通过CALL
实现任务切换,要求使用0环的段。
🔒 点击查看答案 🔒
本人在8003f090
作为任务段存储地址,值为0000E912`FD740068
,注意一定构造正确的TSS并填写正确的TSS地址,否则会容易蓝屏。
首先填写的地址就是任务切换所需要的EIP
,注意填对了,如下图所示:
然后显示出TSS
的地址,这个至关重要,填好在GDT表
中,如下图所示:
其次填CR3
,通过!process 0 0
获得并写入,如下图所示:
然后运行跑起来,任务切换成功,如下图所示:
🔒 点击查看代码 🔒
#include "stdafx.h"
#include <Windows.h>
#include <stdlib.h>
DWORD dwOK;
DWORD dwESP;
DWORD dwCS;
void __declspec(naked) test()
{
dwOK=1;
__asm
{
//int 3;
//为什么不要上面的指令呢?因为断到WinDbg,走几步就会蓝屏。
//因为EFLAGS的NT位被改了,所以不要它。注意用Call的时候一定不要调试。
mov eax,esp;
mov dwESP,eax;
mov word ptr [dwCS],ax;
iretd;
}
}
int main(int argc,char * argv[])
{
char stack[100]={0}; //自己构造一个堆栈使用
DWORD cr3=0;
DWORD addr=0;
printf("请输入目标地址:\n");
scanf("%x",&addr);
DWORD tss[0x68]={
0x0, //link
0x0, //esp0
0x0, //ss0
0x0, //esp1
0x0, //ss1
0x0, //esp2
0x0, //ss2
0, //cr3 *
(DWORD)addr, //eip *
0, //eflags
0, //eax
0, //ecx
0, //edx
0, //ebx
((DWORD)stack) + 100, //esp *
0, //ebp
0, //esi
0, //edi
0x23, //es *
0x08, //cs *
0x10, //ss *
0x23, //ds *
0x30, //fs *
0, //gs
0, //idt
0x20ac0000 //IO权限位图,VISTA之后不再用了,从其他结构体拷贝出来。
};
printf("tss地址:%x\n",tss);
printf("请输入CR3:\n");
scanf("%x",&cr3); //通过WinDbg指令进行获取:!process 0 0
tss[7]=cr3;
char buffer[6]={0}; //构造任务段
*(WORD*)(&buffer[4])=0x93;
__asm
{
call fword ptr [buffer];
}
printf("切换成功,获取的值:dwESP=%x\tdwCS=%x\n",dwESP,dwCS);
system("pause");
return 0;
}
结语
到现在我们学习了什么是段,什么是门,什么是段的机制,到现在您应该有一些比较清晰的了解和掌握。然而读取内存不仅得过的了段这一关,还得过的了页这一关。什么是页,什么是页的机制。将会到下一篇进行讲解。
为什么要写这一篇小节呢?一是总结自己之前学过的东西,回头一看其实也不太难,不过说实在的刚开始学起来挺头秃的。二是提醒大家要学习完一个阶段,每天回头看看我写的教程。因为这教程是我学习过程中写的,可能有不太完善的地方,一旦有漏洞就会及时修改。保护模式仅仅靠我写的东西是学不完的,还有很多细节的东西我还未涉及,但我学到的部分会尽快的补充。三是提醒大家要及时完成练习,因为后面还是会用到的。
学完段的内容,难道没有个小节作业呢?这个是额外的练习,可以不做,但还是建议做一做,下面的题目将是对之前未讲到的细节的补充。
练习
1️⃣ 用代码获得GDT
表的基地址和大小。
🔒 点击查看答案 🔒
#include "stdafx.h"
#include <stdlib.h>
int main(int argc, char* argv[])
{
char gdt[6]={0};
_asm
{
sgdt gdt;
}
printf("base : %X\nlimit : %X\n",*(unsigned int*)&gdt[2],*(unsigned short*)gdt);
system("pause");
return 0;
}
2️⃣ 构造一个3环的代码段描述符,我想把它用请求权限为3的选择子加载到CS
当中。比如0x1B
指向的段描述符,我用0x18
请求CS的值会变成0x1B
吗?
🔒 点击查看答案 🔒
这个我就不详细描述了,自己实验一下就知道了,还是`0x18`。
3️⃣ 使用调用门我们都知道需要用retf
将堆栈中的压入的值重新弹到寄存器,从而返回,从此有一个大胆的想法,我们自己构造压入的值,使用0环的,用retf
能提权吗?
🔒 点击查看答案 🔒
可以说想法很美好,现实很残酷。先看看实现结果如下图:
为什么不能,我们看一下白皮书,可以看到如下句子:
A far return that requires a privilege-level change is only allowed when returning to a less privileged level (that is,the DPL of the return code segment is numerically greater than the CPL).
远返回系列指令只能降级返回所以通过这个方式提权别想了。
4️⃣ 一天通过中断门提权,使用下面的代码,发现运行后蓝屏了,是为什么呢?
//构造段描述符:00809A00`0000FFFF ,填写在 0x8003f090 ,如果用0环的权限选择子为 0x90
//构造中断门:0040EE00`00901020 ,填写在 0x8003f4f0 ,即索引为 0x1E
#include "stdafx.h"
void __declspec(naked) test()
{
_asm
{
int 3;
iretd;
}
}
int main(int argc, char* argv[])
{
_asm
{
int 0x1E;
}
return 0;
}
🔒 点击查看答案 🔒
这道题是专门给给搞不清楚iret
和iretd
的同志下的坑。
如下是《Intel白皮书》的原话,它在 PDF 中的第1059页:
IRET and IRETD are mnemonics for the same opcode. The IRETD mnemonic (interrupt return double) is intended for use when returning from an interrupt when using the 32-bit operand size; however, most assemblers use the IRET mnemonic interchangeably for both operand sizes.
简单是说IRET
和IRETD
是一样的硬编码IRETD
是用来32位返回用的。但是大多数汇编程序使用两种操作数大小的 IRET 助记符可以互换,OD 就很好的落实到了这点:
但是不幸的是,它们是不一样的指令,Intel
只说是Opcode
是一样的,没说前缀不一样呀。在继续之前我们先看白皮书的一句话:
The instruction prefix 66H can be used to select an operand size other than the default.
简单点说,0x66
这一个前缀指令,不明白的可以看一下 羽夏笔记——硬编码(32位) ,里面比较详细的介绍了硬编码是基础,也略微提及到了这一个前缀。我们来看看正确的解释是啥:
看到没有,iretd
指令比iret
指令多一个0x66
这个指令,这个指令正式反转默认寻址方式的前缀。在32位系统默认寻址32位,如果有这个指令,就用16位寻址。这会明白为什么我会下这个坑吧?所以答案就是把iret
改为iretd
。
5️⃣ 验证当3环提权到0环,不管怎么破坏能够不蓝屏回去后,重新再次进入后也不会蓝屏。
🔒 点击查看代码 🔒
#include "stdafx.h"
#include <windows.h>
DWORD _ESP=0;
void __declspec(naked) test()
{
_asm
{
mov edx,[esp]; //保存返回地址
sub esp,0x10; //破坏堆栈平衡
/*自己重新模拟一个调用门调用的0环堆栈*/
push 0x23; //ss,通过OD获得
mov eax,_ESP;
push eax;
push 0x1B; //cs,通过OD获得
push edx;
retf; //不平栈,直接返回
}
}
char buffer[6]={0,0,0,0,0x9B,0};
int main(int argc, char* argv[])
{
_asm
{
mov _ESP,esp;
call fword ptr [buffer]; //第一次调用
mov _ESP,esp;
call fword ptr [buffer]; //第二次调用
}
return 0;
}
🔒 点击查看答案 🔒
代码我已详细写了注释了,我用的调用门是0040EC00`0090D480
,被填写到0x8003f098
,而调用门使用的段描述符的值为:00CF9A00`0000FFFF
,被填写到0x8003f090
。
至于为什么不蓝屏,看看每次进0环的时候的esp
,你就为什么不蓝屏,也感受到TSS
的魅力了。
6️⃣ 将某一代码片运行到1环。
🔒 点击查看提示 🔒
Win系统没有自主提供 ESP1 和 SS1 怎么办?我想你应该知道咋办了。
🔒 点击查看代码 🔒
//eq 8003f0d8 0040E912FD740068 ;TSS描述符 D9,注意正确
//eq 8003f0b0 00CFBB000000FFFF ;cs:B1
//eq 8003f0b8 00CFB3000000FFFF ;ss:B9
//eq 8003f0c0 FFC0B3DFF0000001 ;fs:C1,这个东西是关键,运气不好容易因线程切换蓝屏
#include "stdafx.h"
#include <Windows.h>
DWORD dwOK;
DWORD dwESP;
DWORD dwCS;
void __declspec(naked) test()
{
dwOK=1;
__asm
{
mov eax,esp;
mov dwESP,eax;
mov word ptr [dwCS],cs;
iretd;
}
}
int main(int argc,char * argv[])
{
char stack[100]={0}; //自己构造一个堆栈使用
DWORD cr3=0;
DWORD addr=0;
printf("请输入目标地址:\n");
scanf("%x",&addr);
//下一步构造TSS,标有*说明必填有效值
DWORD tss[0x68]={
0x0, //link
0x0, //esp0
0x0, //ss0
((DWORD)stack) + 100, //esp1 *
0xB9, //ss1 *
0x0, //esp2
0x0, //ss2
0, //cr3 *
(DWORD)addr, //eip *
0, //eflags
0, //eax
0, //ecx
0, //edx
0, //ebx
((DWORD)stack) + 100, //esp
0, //ebp
0, //esi
0, //edi
0x23, //es *
0xB1, //cs *
0xB9, //ss *
0x23, //ds *
0xC1, //fs *
0, //gs
0, //idt
0x20ac0000 //IO权限位图,VISTA之后不再用了,从其他结构体拷贝出来。
};
printf("tss地址:%x\n",tss);
printf("请输入CR3:\n");
scanf("%x",&cr3); //通过WinDbg指令进行获取:!process 0 0
tss[7]=cr3;
char buffer[6]={0}; //构造任务段
*(WORD*)(&buffer[4])=0xDB;
__asm
{
push fs;
call fword ptr [buffer];
pop fs;
}
printf("切换成功,获取的值:dwESP=%x\tdwCS=%x\n",dwESP,dwCS);
system("pause");
return 0;
}
7️⃣ 逆向int3
/int 8
中断(注:只把第一层看明白逻辑即可,就算看不懂也没有太大的关系,剩下的部分都会在后面的教程逐步讲解深入)。
🔒 点击查看答案 🔒
在看详细答案之前,我们先搞懂3号中断和8号中断是什么,看下面的图:
3号中断正好是我们已知的断点,而8号中断是双重错误。说到双重错误,那么什么是双重错误呢?在程序执行过程中,可能会出现一个异常。当异常产生时,即错误产生时,CPU会走指定中断进行异常处理。但在异常处理过程中,也可能会发生错误。如果再次发生错误,CPU将会走8号中断,即所谓的双重错误。对于Windows系统来说,基本就是以蓝屏的形式出现。
既然我要分析代码,必然要找到函数地址。根据中断门的结构不难找到它的函数地址。我们先用Windbg
来找到函数。先以int 8
为例。
根据IDT表和索引找到中断门804e8e00`00080d02
后,得到地址804e0d02
。然后输入uf 804e0d02
即可显示。
虽然 Windbg 能够很快的找到函数,但它不太方便。都知道大名鼎鼎的 IDA ,如何通过它找到我需要分析的函数呢?
首先我们先找到自己的内核文件,找到C:\WINDOWS\system32
这个路径,找到的内核文件如下图所示:
为什么是两个内核程序呢?这是由于分页的原因。WinXP有两种分页模式:10-10-12分页和2-9-9-12分页。10-10-12分页使用的内核程序是ntoskrnl.exe
,2-9-9-12分页是ntkrnlpa.exe
。我们把它们拖到实体机进行分析。
打开 IDA ,然后把ntoskrnl.exe
拖进去,如下图所示:
点击确定,然后IDA询问检测到调试信息,是否从微软服务器下载符号,点击否。因为WinXP的符号微软早不搞了。如下图所示:
然后我们自己加载符号,根据下图所示操作,打开加载符号文件:
按照图示操作进行加载符号,注意找到自己的符号安装位置再参考下图:
不清楚自己当前的内核文件是啥咋么办?可以用 PCHunter 进行检查:
根据WinDbg给出的函数,根据其特征找到指定函数,具体分析过程就不再赘述了。
下一篇
本文来自博客园,作者:寂静的羽夏 ,一个热爱计算机技术的菜鸟
转载请注明原文链接:https://www.cnblogs.com/wingsummer/p/15353642.html