RCX=RIP
RIP = MSR[]
R11=
= &(~MSR[]) (删除其中存在的位)
:
== 00000
格式:
TF/IF/DF/NT 位清零,主要注意关闭中断(IF)。
然后就会进入MSR[]的地址,开始执行R0代码(CPL也置零)
然后进入内核空间:
可以发现在一个名为 的段中进入了一个函数,该函数负责暂时接管这些内核调用。
通过切换内核gs环境,实际上是切换了MSR[]的值。 内核gs基地址指向KPCR(CPU控制块)结构体。
:
然后将用户层的rsp存储到KPCR(CPU控制块)中。
将KPCR中的Base作为当前页表(cr3寄存器)。 实现页表切换。
途中判断KPCR的状态是否为1,如果为1则跳过页表切换。 猜测是判断是否开启KPTI。
然后内核堆栈指针进行切换,使用0x38堆栈空间来存储图中的信息。 并判断KPCR中的信息设置AC标志位,限制内核访问用户空间。
然后将一些关于分支预测的配置保存到信息中。 并切换内核用户分支预测模式。 并关闭缓解措施(为了性能)
随后
如果跳转后判断上述两个AND Flag:
在指令排序之前和之后读取指令序列。
如果上面两个没有跳转的话,就会进入一个函数,就是一路向上调用的代码。 到了最上面,又会回到上图的位置(猜测是乱序执行的缓解措施)
然后进入真正的室内
判断是否是调试设置,是否使用DR7等,保存应用层的调试寄存器。还有一些Ums(用户态调度)内容
还有其他的机制本文没有讨论,所以假设不是Ums线程并且已经处理了调试信息,或者不是处于调试模式。然后跳转到
在:
之前KVAS保存rax/rcx/rdx的代码:
可以发现KVAS段中保存的rax/rcx/rdx已经恢复。 这里我们切换到C语言模式来命名这些变量,以表明它们不是以前从未见过的值。
而rax是从用户态传入的呼叫号码,并没有改变。 rcx是从用户层传递过来的第一个参数。 rdx 是第二个参数。 r8和r9在之前的环境中没有被修改过,所以r8 r9不需要恢复。
将第一个参数(调用号)存储在进程环境块中。
此时查到的值其实是直接来自于rsp,而rsp在整个函数中并没有进行调整,也就是说rsp的值来自于KVAS代码。
验证一下这个猜测:
dt显示该结构体的大小,发现其大小为0x190。
返回KVAS代码可以看到有一个栈空间分配操作0x158+0x38恰好是0x190
由于栈是颠倒存储的,所以如果看结构体的末尾(下图中的注释已经把数据按顺序排列好了),各个对应的变量就被传入了。
由此我们知道,KVAS中的堆栈分配实际上创建了一个
从这里开始,它是关于函数如何实际开始调度的逻辑:
1. 首先,>> 7 & 0x20 是真正的调用号,表明该函数是一个 GDI 函数。 可以看出GDI函数的调用号是开头的。 这次追踪到该函数的调用号,调用号为0x55。
2.然后通过&0xfff获取索引号
3.然后当当前线程不是Gui线程时,使用SSDT(able)表,当当前线程是Gui线程时,当当前线程是受限Gui线程时。
那么SSDT存储了什么? 能够 和 和 有什么不一样?
首先,这些表中存储的数据格式是完全相同的。 它们存储了每个系统调用函数的地址、函数个数、参数表等。
该表包含 GUI 函数,但 SSDT 不包含。 它们用在受限的GUI线程中,有些功能无法使用(好像还没用过,所以是一模一样的)。
能够的结构:
struct { PVOID ServiceTableBase; //函数表的基地址 PVOID ServiceCounterTable; //每个函数被调用的次数(不过似乎没有被使用) unsigned int NumberOfServices; //由函数表中函数个数。 PVOID ParamTableBase; //包含每个函数参数字节数表 (不过似乎没有被使用)};
然后进入功能表
结构:
int 函数偏移量[0x1d0]
这里存储的是每个函数相对于该表的偏移量以及通过堆栈传递的参数数量。 每个值都是 int 类型。 请注意,它不是 int。
格式:
|31 - 4 | 3 - 0 | 3 - 0
| 函数偏移| 通过堆栈传递的参数数量 |
实际参数个数=通过栈传递的参数个数+4; (不知道为什么+4,请参考调用约定的参数传递规则)
函数偏移量是根据这个表的偏移量来的,比如这个图中第一个函数:
4表示当前函数参数个数为4+4=8
其余的是偏移量(有符号)
那么函数0x55的地址就是
参数数量为 7 + 4 = 11
注意:调用Gui函数时,是第二个able结构,而不是第一个。 第一个结构仍然具有普通功能。
下面的代码和我们做同样的事情:
可以看到该能力已添加。 如果等于0x20,则相当于跳过第一个有能力的结构。
然后,如果当前调用的函数是GUI函数,并且有之前没有完成的图形工作,则会使用(猜测)进行绘制
这里取出参数个数。 如果有, * 8 后续代码不被IDA分析。
查看编译:
检查调用的原始数据段是否为用户层。 如果是这样并且如果传入的用户堆栈高于此值,则设置为阻止用户层使用内核地址作为堆栈地址。
然后以nd为基地址减去函数参数个数*8,最后跳转执行。 其实就是为了调用函数时的参数栈。
我们来看看nd上有什么:
它实际上就是参数的复制,将参数从用户栈复制到内核栈,然后用这个栈来调用函数。
nd函数的结尾正是C代码的下一句
这是对系统服务函数的调用。 根据不同的情况前后调用不同的函数。 在不考虑其他因素的情况下,可以发现else中直接调用了该函数。 然后就可以直接输入这个函数了。
至此,执行进程已成功进入系统服务函数()。