windows内核_2
@[TOC]
中断现场
1.esp在进入内核前后的变化
修改代码保存int 0x20前后两个esp的值,可以发现3环是一个小地址,0环是一个大地址
1 | #include<stdio.h> |
尝试读取其他寄存器
1 | #include<stdio.h> |
改变的有 cs,ss,esp
描述符基础
实模式和保护模式
先说实模式,实模式出现于早期8088CPU时期。当时由于CPU的性能有限,一共只有20位地址线,8个16位的通用寄存器,以及4个16位的段寄存器。
访问内存的格式为 (段基址:段偏移量)
就是物理地址 = 段基址<<4 + 段内偏移
保护模式:CPU的地址线的个数也从原来的20根变为现在的32根,所以可以访问的内存空间也从1MB变为现在4GB,寄存器的位数也变为32位。所以实模式下的内存地址计算方式就已经不再适合了。所以就引入了现在的保护模式,实现更大空间的,更灵活也更安全的内存访问。
我们的偏移值和实模式下是一样的,就是变成了32位而已,而段值仍旧是存放在原来16位的段寄存器中,但是这些段寄存器存放的却不再是段基址了,毕竟之前说过实模式下寻址方式不安全,我们在保护模式下需要加一些限制,而这些限制可不是一个寄存器能够容纳的,于是我们把这些关于内存段的限制信息放在一个叫做全局描述符表(GDT)的结构里。全局描述符表中含有一个个表项,每一个表项称为段描述符。而段寄存器在保护模式下存放的便是相当于一个数组索引的东西,通过这个索引,可以找到对应的表项。段描述符存放了段基址、段界限、内存段类型属性(比如是数据段还是代码段,注意一个段描述符只能用来定义一个内存段)
也就是说,我们寻址的方式还是段基址+偏移地址。不过段基址不直接放在段寄存器了,而是开一个表,叫 GDT,即全局描述表,把段的基址、还有这个段的其它一些信息,比如权限什么的。
全局描述符表位于内存中,需要用专门的寄存器指向它后, CPU 才知道它在哪里。这个专门的寄存器便是GDTR(一个48位的寄存器),专门用来存储 GDT 的内存地址及大小
而在保护模式下要生成最终的地址,显然就变成了先到 GDT 里拿段基址,再和偏移地址组合起来。而 GDT 由于存了很多段,所以就需要有个指针指向哪个段,这个指针就是段选择子,平时放在段寄存器里。
GDTR GDT LDTR LDT
结合上面的知识,再总结一下GDTR GDT LDTR LDT
GDTR是一个长度为48bit的寄存器,内容为一个32位的基地址和一个16位的段限。其中32位的基址是指GDT在内存中的地址。
GDT是全局描述附表,主要存放操作系统和各任务公用的描述符,如公用的数据和代码段描述符、各任务的TSS描述符和LDT描述符。
LDTR是局部描述符寄存器,由一个可见的16位寄存器(段选择子)和一个不可见的描述符寄存器组成(描述符寄存器实际上是一个不可见的高速缓冲区)。
LDT是局部描述符表,主要存放各个任务的私有描述符,如本任务的代码段描述符和数据段描述符等。
这里再说一下啊LDT 和LDTR,局部描述符表(LDT)可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。
除了GDT之外,IA-32还允许程序员构建与GDT类似的数据结构,它们被称作LDT(Local Descriptor Table,局部描述符表),但与GDT不同的是,LDT在系统中可以存在多个,并且从LDT的名字可以得知,LDT不是全局可见的,它们只对引用它们的任务可见,每个任务最多可以拥有一个LDT。另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。
LDT只是一个可选的数据结构,你完全可以不用它。使用它或许可以带来一些方便性,但同时也带来复杂性,如果你想让你的OS内核保持简洁性,以及可移植性,则最好不要使用它。
实验结果
所以cs的改变其实就是段选择子的改变,而ss,esp就是因为堆栈的切换而变化,特权切换必须伴随着堆栈的切换(TSS)
(windbg调试信息不一定准确)
再次开中断
之前做的开中断实验,不加入开中断指令,就会在0环产生一个死循环,而加上一个开中断指令,由于线程切换,会导致蓝屏。这是因为虽然我们进行了提权,但是还没有权限去使用0环的api函数,那如何保证在线程切换的条件下,不会使虚拟机蓝屏呢?
需要增加两行汇编代码环境
1 | push 0x30 |
1 | #include<stdio.h> |
fs在3环指向teb,可以保证结构化异常处理,而在0环指向kpcr 才能保证线程调度,所以我们应该把fs指向kpcr即可
实验结果:发现结束不了进程,卡死但是不会发生蓝屏,结束不了进程的原因就行只有0环返回3环时才可以杀死进程,而我们一直在0环里循环操作,无法返回3环,进而不能结束进程,不过我们可以用windbg命令直接nop掉循环代码。
内核API调用
1当我们从内核返回的时候,还是需要把fs改成0x3b,这样可以保证在3环运行不会出错
1 | push 0x30 |
利用内核的api函数尝试申请一个地址,也可以用dbgprint尝试输出
1 | #include<stdio.h> |
成功申请到一个地址
_InlineHook
3环程序在低2g的地址是不一样的(私有地址空间),在高2g的地址是一样的(共有地址空间),我们尝试在ntkrnlpa.exe的3环到0环的入口地址做一个hook,这个需要两个程序:
第一个是patch ntkrnlpa.exe的kifastcallentry函数,让他跳到我们后面写的代码里
1 | #include<stdio.h> |
第二个是在我们的地址里填写代码,再跳转回来
1 |
|
注意要先运行2再运行1,否则会发生蓝屏,用windbg反汇编,查看结果
现在hook函数已经写好了,现在准备填入自己的内容,可以用来测试函数被调用的次数
1 | pushad |