古时的军队大体分为两部分,一是前线作战的士兵,二是后方运粮的人员,两者共同组成一个系统,双方配合才能完成作战任务。两者之间保持互相通信,前者就可以派人向后者催粮,后者就可以派人向前者运粮,而这个运粮(通信)的关键就在于粮道,如果被敌军切断,那么前线士兵就无法向后勤人员征求粮食(请求服务),后勤人员也就无法为前线士兵运送粮食(提供服务),此可谓之拒绝服务攻击。
以图示之,前线5万大军因吃不上饭而开始出现军心不稳的情况了。。。
那么程序有类似的“粮道”吗,有的话在哪里呢?答案就要从3环进0环说起了。
Windows系统下有几个重要的DLL:Kernel32.dll、User32.dll、Gdi32.dll、Ntdll.dll。运行在3环的应用程序不可避免的会使用这些Dll提供的API,而这些API大部分会通过Ntdll.dll进入0环,参考《加密与解密》的第7章的图7.6。
举一个例子来说明这个过程吧!用Windbg和win7 x86双机调试。
Kernel32.dll中的ReadProcessMemory,其代码如下:
其中只有一条jmp指令,跳到一个导入函数里去执行,我们看看该函数是什么:
原来是kernelbase.dll中的同名函数,继续反汇编,在+12附近的位置又调用了一个导入函数:
继续看该导入函数是什么,原来是ntdll.dll中的NtReadVirtualMemory函数:
此时就到了关键的地方了。ntdll.dll中的NtReadVirtualMemory函数非常简单,就4句,非常简单,它的作用是什么呢?
在ntdll.dll中,大部分API也是这样的格式:
mov eax, API索引号 mov edx, 一个地址 ;该地址里的值是一个函数地址 call [edx] ;调用这个函数,实现进入0环 retn 14h
API索引号就是在3环调用了一个API,这个API在内核也就是0环,会有对应的内核函数,这个内核函数的地址在SSDT表中位于某个位置,这个位置就是索引号。进0环后,得找到对应的内核函数进行调用吧,就是根据这个索引号找到的。
一个地址,里面存的值是一个函数地址,反汇编看一下就知道了:
原来这个函数名为KiFastSystemCall,如果CPU支持快速调用的话,那么call [edx]就是call KiFastSystemCall,如果是通过中断门进入0环的话,那么call [edx]就是call KiIntSystemCall。
KiFastSystemCall里的sysenter指令实现进入0环。操作系统在执行这条指令前会把进0环后需要的东西(CS/SS/ESP/EIP)提前准备好,存入一个叫做MSR的寄存器中,等到sysenter指令执行时,就会从MSR寄存器中取出这些值赋给相应的寄存器,其中eip寄存器就得到一个值,这个值就是nt!KiFastCallEntry函数的地址。
那其实就明白了,通过快速调用进0环后的第一个要执行的函数就是KiFastCallEntry。这个函数的作用就是拿着前面传下来的API索引号,获取相应的内核函数,调用之,从而实现应用层想要的功能。整个过程整理如下:
把其他API的调用过程也画上去,看看整体的流程:
其中的“粮道”我把它选为了Ntdll.dll中的KiFastSystemCall,即上图蓝色的线,因为这个函数实现进入0环,3环的API就得通过这个必经之路进入0环。如果把这个必经之路给断了,那岂不是3环API进不了0环,从而0环无法为3环提供服务,这就达到了内核级拒绝服务攻击。还有一个地方也可以选为“粮道”,那就是MSR寄存器,进0环后要执行的函数地址是来源于MSR寄存器中的eip值,所以修改MSR寄存器也可以达到截断“粮道”的目的。
根据第2节可知,3环进0环的关键函数是Ntdll.dll中的KiFastSystemCall,而且是以下面的形式来对它进行调用的:
mov edx, 7ffe0300 call [edx] ; call KiFastSystemCall
7ffe0300地址里保存了KiFastSystemCall的地址。
其实这里涉及一个名为_KUSER_SHARED_DATA的数据结构,在3环和0环分别定义了一个该结构,其中在3环,这个结构地址固定为7ffe0000;在0环,这个结构地址固定为ffdf0000。这两个结构的内容完全相同,它们偏移+300处的成员名为SystemCall,保存的正好就是KiFastSystemCall函数的地址,所以上面的7ffe0300就是这么来的。看图。
好,既然3环API进0环,要访问7ffe0000+300处的成员,来实现进0环,那么我们把这个成员给改了(改之前拍个快照):
ed 7ffe0000+300 12345678
把7ffe0000+300处的成员,随便改成12345678,那么3环进0环时,就会去12345678地址里去执行,而这个地址肯定是无意义的,看看会发生什么情况,根据我的实验得到:
Windbg首先有动静,问我中断还是忽略
选b的话,Windbg会再次询问我相同的问题,再次选b,就会中断了
此时的栈回溯信息如下:
如果选i的话,步骤和结果如下:
系统跑起来了,问题是,鼠标移到记事本里,会改变形状为插入型,移动到桌面会显示正常的光标,移动到开始菜单处会显示漏洞状,但是均无法点击。向虚拟机发送Ctrl+Alt+Delete键,也是没有反应的。
整个系统就保持这种状态,既不会蓝屏,Windbg也不会继续捕获到异常。
(注:图片是gif格式的,不知道能不能播放,而且录制的时候没有录制出光标的变化效果,大家有兴趣的自己做实验就可以看出变化的)
修改ffdf0000+300处的成员也会使系统出现这种现象。以下是修改ffdf0000+300中断后的栈回溯信息。
根据以上的的实验可知,修改进0环的关键位置,使得整个系统发生了无响应而不蓝屏的现象,大概可以解释成:是3环进0环的地方出了问题,而这个进0环的函数又是属于3环的,所以问题发生在3环而不是0环,故系统内核没有崩溃。
7ffe0000+300、ffdf0000+300这两个地方用来耍流氓是挺合适的,发现你对我的程序有分析行为,那我就阻断你进0环的路,你的程序得不到内核的服务,于是什么分析行为都无法进行下去了,这个就是断其粮道的含义。其实对一些函数设置钩子,使其他调用者得不到正确的结果,也可以叫做拒绝服务攻击。
那么怎么对付这个流氓行为呢?从前面的两个栈回溯信息就可以看出端倪。
如果反过来呢?我们在分析的时候,触发了检测代码,从而发生了这种系统无响应而不蓝屏的现象,于是就没有蓝屏文件为我们提供信息,而如果又不知道3环进0环的原理,那么即便是看Windbg的栈回溯信息,也没法对付,所以很可能就想不到是程序的“粮道”出了问题。
代码就不写了,有兴趣的同学可以自己写个驱动,随便检测一个进程,然后修改这两个7ffe0000+300、ffdf0000+300试试,看看有什么效果。
一点思考:系统无响应而不蓝屏,说明用户层废了,而内核还活着,此时我们提前写个驱动打算一直循环干活,那么在系统无响应期间,驱动是否也在工作呢?如果在工作的话,那其他规范化的程序是不是就不能收集我们的种种行为呢?(应用层软件显然无法收集我们的行为了,内核层的驱动一般要和应用层通信,此时无法通信了,功能肯定大打折扣。)
《加密与解密》
《软件调试》
[看雪官方培训]《安卓高级研修班(网课)》9月班开始招生!顶尖技术、挑战极限、工资翻倍!
最后于 21小时前 被minorory编辑 ,原因: 增加一点思考