关于内核HOOK的安全问题
现象: 驱动实现HOOK多个内核函数,虚拟机运行无问题出现 本地机运行多数情况正常,但偶尔在卸载驱动时会出现BSOD,代码:IRQL_NOT_LESS_OR_EQUAL 使用GOOLE与BAIDU了一番,这个错误应该属于内存访问失败吧!
思考几次BSOD时的本机情况,一般为开启了多个软件时导致,重新检查驱动代码,醒悟为在UNHOOK时,没有做任何检测,直接将所有HOOK的函数还原然后卸载驱动,并且UNHOOK代码的IRQL级别高,不会被系统打断 因此,假设系统中某进程正在调用被HOOK的函数并正好运行至我们的代理函数时被其它进程抢先,并一直未得到继续运行,而此时我们运行UNHOOK,将代理函数自内存中销毁,然后等待的进程得到处理器后,继续执行指令时,原内存地址中的指令已不存在,因而就会发生BSOD了!
既然原因大概知道了,那就想办法解决吧!
思考1:
首先想到的解决方案是: 创建全局变量做计数器使用 在代理函数入口使计数器+1,返回时-1. 在UNHOOK时判断计数器是否为0 不为0则等待计数器归0后再进行UNHOOK
方案有了,那就继续思考可行性:
(1)使用计数器进行函数调用的统计,如何保证计数器进行加减操作时不被打断?
嗯,记得之前看书的时候,提到过这种情况下可以使用自旋锁,来达到锁定操作不被打断的目的. 但如果是一个使用的比较频繁的函数,使用自旋锁会不会使性能降低? 另外如果被一个低优先级的进程抢先了资源后,紧接被高于它优先级的进程抢先,?这种情况是不是就叫所谓的死锁?因为CPU始终运行在等待状态上,而资源得不到释放? 那如何防止这种情况的发生呢?
不行,对自旋锁的概念太模糊,看书找资料!
下面引用WINDOWS驱动开发里的介绍
哦,SHIT! 再看书上关于自旋锁的介绍后才发现自己真够屎的!!!明明人家说的清清楚楚明明白白的,自旋锁,就是用来防止操作不被打断(注意上面标红的地方,其实自己是知道的,但是根本没理解这是啥意思,,,

),那又如何存在低优先级线程获取资源后被高优先级抢先的情况呢?~ 不过问题又产生了,,既然不会被打断,又如何会出现死锁呢?同时,证明了我的猜想,使用自旋锁肯定会影响性能. 为了巩固学到的东西,再次找资料,看看什么情况下会发生死锁!
呃,看见这个想起来,计算机组成原理上有这个介绍的嘛(忘的一干二净了,

) 不过死锁很容易理解,两个CPU,三个资源,两个内核任务 其中每个内核任务需要三个资源才可以达到执行条件,CPU1抢了资源1,CPU2抢了资源2,3 两个互相等吧….这就是死锁了,那自锁呢,还是不太明白~~~只能理解为 CPU获取资源后,释放资源前,又一次申请该资源!
呃,不知道对不对大牛们指点一下!
到这就大概就明白了,使用自旋锁与计数器应该是可以实现我的需求,自死锁是代码编写时没有注意所产生的,这个可以防止发生了,影响性能嘛~~~反正只做个计数器加减操作,不会影响太大,决定就用这种方法了
(2)但是!突然又冒出个问题:我在UNHOOK时,判断计数器是否为0,是要在提升当前IRQL后判断呢?还是要在提升前判断呢?
如果是在提升级别前进行判断,那假设我判断计数器为0后,CPU被其它进程抢先,并且该进程调用了被HOOK的函数,然后使计数器+1 此时CPU又被UNHOOK抢过来进行下面的处理,还是得BSOD不是~~~
如果在提升级别后判断,那其它进程得不到运行,计数器就不会减1了啊, 嗯?多核的还好,可以有另一个CPU来将进程运行完(咋感觉这概念很有问题啊?),可单核的呢?另外多核的还存在一个多核同步问题.
(3)换个想法,不用计数器,把自旋锁test-and -set放在代理函数开头,然后在代理函数返回前释放锁资源,在UNHOOK的时候同样进行test-and-set,UNHOOK结束后释放锁资源.不行,先不管其它的问题,这样写的话,性能影响实在太大了.等等,,,UNHOOK判断计数器的时候,不提升当前IRQL级别,而是使用自旋锁,UNHOOK结束后释放锁资源 嗯,这样可以解决多核同步问题,单核的嘛?只能是在代理函数中判断当前CPU数量,如果是单核的,那自旋锁的释放就要等到代理函数执行结束后再进行了,这样其实就等于让CPU在调用被HOOK的函数时,始终不被打断,也就是同一时刻只有一个调用到该函数的线程在运行,性能的影响是相当的大啊..
只能思考到这个地步了,还好现在多数都是多核机,面临的窘状不会太多…只是感觉这方法很笨啊,而且不知道思考的路子对与不对,大牛们要是能坚持看到这,还是烦劳指点小菜一两手吧!
思考2:
本来想解决方案的时候一下冒出来的两个方案,上面的是第一个,第二个是不使用自旋锁而使用原子操作进行计数器的加减,不过在上面方案的聚体思考过程中,这个方案已经被否决了,因为不使用自旋会出现的问题上面已经考虑过了!(补充:其实这是一个逻辑性错误,我此时所想的原子操作,是指原子的加减,而思考的自旋,是指自旋后可以运行一段相当多的内容, 级别不同而已! 也说不清楚当时是怎么想的,想到的就想上了,反正就是乱七八糟就对了!)
思考3:
这个是在方案1结束时想到的,,因为想到了线程调度的问题,不管是多核还是单核,线程调度时,CPU需要保存当前线程的上下文,所以应该不会存在一个线程被多个CPU分别调度的情况,那方案一的结果就是不言而喻了,所以就只能是把计数器的判断提前,不管是用自旋还是提升IRQL,都必须要在这个操作之前判断当前的计数器情况,那就又回到了老问题,计数器判断通过的瞬间,线程被调度,其它线程又访问了被HOOK的函数并且运行到了自己的代理函数中,继续BSOD(

), 到这就把自己的方案一给否决了,然后换思路思考! 想到最近研究的DNF驱动保护,有人分析的结果是:”驱动一运行,就使用ExAllocateXXXX分配一块内存,然后实现HOOK,然后结束自己的线程” 嗯!就是这个了, 我也可以在驱动入口处先申请一块内存,然后把自己的代理函数写入这块内存中 代理函数中的自旋锁与计数器仍然是必须的, 然后在UNHOOK的时候,先进行UNHOOK操作 再进行计数器的判断(不使用任何自旋与提升IRQL),当计数器清0后, 使用ExFreeXXXX来释放分配的内存. 好像这是我能想到的最好的办法了?感觉很简单啊,竟然用了一个上午的时间想出来的这个方案, 不过不知道是否可行?
从头看到尾的牛人,你是我的偶像,也是我的救星,烦请您抬抬手,动动口,就告诉我一下,这个办法可不可以用?还有没有什么漏掉的地方?又有没有更好的办法?小弟千恩万谢啊~