tgjarwl的博客

道可道,非常道;名可名,非常名

记录生活,记录更好的自己。


inline hook崩溃引发的思考

                                    一念天堂、一念地狱、止贪欢喜、转念花开

总结

有时候出于一些监控的目的,不得不对一些api去做一些hook,这里主要讲inline hook或者api hook等等引发的一些问题。有时候会出现一些莫名其妙的崩溃,当然这里的崩溃不是代码内部自己的逻辑没有处理好的那种崩溃。而是那些崩溃到其它地方的崩溃,这些崩溃看似跟我们的代码没有关系,却或多或少跟我们有些关系。根据最近处理的这几起崩溃,也引发了我的一些思考。这些崩溃都是在我们hook的函数内部出现了异常,异常被上抛后,要么是意外的中断了,导致没有找到异常处理函数,要么是处理了,但是在恢复上下文的时候,由于寄存器被修改然后死掉了。那么出现这些问题后,我们又该如何去规避?

首先请记住 系统api是可以进行随意hook的,但是程序内部的函数则要小心,那可能是噩梦的开始

为了方便叙述总结,下面分几个场景去展开。。。

热身工作

更稳定的做法是调用一些第三方的库,如mhook或者detours等等,这些库进行hook都是需要目标函数的地址,和我们自己替代目标函数的地址,如: avatar

在hook前需要去看目标函数的情况,是系统api,还是一个内部的函数,这些函数的调用方式,我们是要进行完全托管还是只是有一个执行的机会就可以。请注意红色的这一句话,他们决定了我们走的路是天堂,还是地狱

案例一、手写shellcode

在x86时代,那时候的天还是很蓝,风也会偶尔吹着白衬衫,栈帧是靠ebp建立和回溯的,SEH的信息也都在栈帧的内部。为了完成对某一个局部地区的hook(请明白这里的局部地区,是任何的可执行地址,可能是函数内部任何的地址),只需要插入一个jmp 指令,在跳转到的目标只需要写入这么两行关键的指令,pushad,pushfd,然后就可以随意的干啥都行,然后调用完成后,在popfd,popad,最后jmp到原先的地址继续执行,例如下图: avatar

上图的第一个call [xxxxxxxx] 是真正要调用的函数,那两个call eax是为了在进入退出去做点事情。在正常情况下,这些shellcode成功的达到了我们的目的,我们常规的写法都是这么写的。

问题出在何处?

时代发展到x64年代,空气开始有轻微的污染,为了治理大气污染,于是微软想出来了一个手段。栈帧不再靠着原先的ebp,SEH信息也不再放在栈帧中,而是放到了异常数据目录中。因为这个程序出现了64位的版本,我们需要跟进,我们做了同样的事情,我们把32位的汇编升级到了64位汇编,自测,提测,发版,一切都是如此的正常。如下图: avatar

有一天,这个内部函数中,抛出了一个异常,结果程序死掉了。为了对比测试,去掉了这个点,结果正常了。我想了很久没想明白,于是动手反复的对比调试,最终发现一个问题,为什么发生问题后,没有被外面的函数捕获处理?

带着这个问题,我研究了SEH,异常等等有关联的东西。手工调试了n多的东西,最终发现了问题的根源,在写64位代码的时候,正常编译的api,系统会为我们创建一个栈帧,这个栈帧是存放在异常数据目录里面的。由于我们的shellcode没有这个栈帧,导致系统往下遍历栈帧的时候,缺少关键信息,就中止了这个过程,最后抛出了二次异常,杀掉了进程。

如何破解这个局面?

64位系统尽量不用shellcode。如果实在想用呢?遵从一个原则,在shellcode内部要jmp到真正的目标,不要call过去。因为不鼓励,所以就不说具体的写法了,希望你能悟到吧

回到32位的地方,那个shellcode中也没有栈帧的建立,push ebp, mov ebp, esp。这句标志性的话。为何他的栈帧是正常的?

因为它用了它外部调用函数的栈帧。这也恰好解释了为何32位正常,64位会死掉的原因。

案例二、我不手写shellcode,那我直接用detours hook这个api可以吧?

detours就是一辆汽车,我们在高速上开的太顺畅了,以致于我们想当然的认为,在泥泞的道路上,一样会畅通无阻。或者说,因为无知,所以无惧。

被hook后的api,大概是这个样子的。在函数的开头会插入一条 jmp 指令,跳转到我们自己实现的stub函数中,在我们的函数中在调用原先的函数,例如: avatar

如果这是一个系统api,那么一切顺利,如果这是一个内部的api呢?尤其是一个内部会抛异常,来通过异常机制达到返回目的的api呢?这时我们可能会想,我们的代码肯定已经建立栈帧了,绝壁,没有问题,那么崩溃又是如何产生的呢?

整个链条的栈帧是正确的,这个是肯定的。关键问题就出在,在生成汇编的时候,不可避免的汇编代码要用到一些寄存器。而寄存器的使用是有一个约定的,易失寄存器和非易失寄存器。区别就是,易失寄存器的在函数内部可以随意修改,非易失寄存器修改前需要保存起来,使用后必须恢复。

avatar

可以看到我们的汇编代码确实是保存了这些寄存器。到目前为止都是正常的。突然被我们调用的原函数内部抛出了一个异常,由于我们是不会处理的,异常就会抛给上层的函数去处理。刚好上层的函数就是等着处理这个异常,于是,异常就走到了处理逻辑。结果就死了,它在内部访问了一个不存在的寄存器地址,到这里可能就会明白了,因为我们在函数开头修改了这个寄存器,当异常发生时,我们没能及时的恢复这个寄存器的值。这种崩溃一般不会崩溃到一个固定的地方,而是千人千面,崩的乱七八糟,最大的误解是没崩溃到我们的模块。

如何破解?

不要去hook程序内部的api,尤其是会在某一个时刻抛出异常的api。如果可能尽量再执行完自己的代码后,手工恢复原本的返回地址,然后jmp到原本的函数。 通过jmp过来后,pushfd,pushad,push 参数 ,call 我们的api,popfd,popad,然后 push 原本的返回地址 ret 做到0修改原本的内容。

三、最后

这两个案例只是inline崩溃中的比较典型的例子,本来是想写的详细一些的,奈何,时间太长了,有好多的资料都找不到了,希望大家遇到比较典型的东西后也能归类,总结。

最后一个问题,系统api真的可以随意hook吗?

希望遇到类似的问题谨慎对待!!!