返回首页

微信公众号

3.4 崩溃回溯分析
设置弹幕颜色
设置弹幕类型
0:00 / 0:00
速度
显示弹幕
海量弹幕
弹幕透明度
0.5
0.75
正常
1.25
1.5
2
[x]
Player version
Player FPS
Video type
Video url
Video resolution
Video duration
课件

崩溃函数的定位

       我们这次课程研究的对象是上次dump出来的完整版的转储文件以及从XP系统中提取出来的mshtml.dll程序。对于这两个文件的分析,均可在真实系统中执行。

       我们首先打开WinDbg,选择File菜单中的Open Crash Dump选项:



       找到待分析的转储文件并打开:



       打开转储文件以后,WinDbg会显示转储文件的概要信息、注释信息和异常信息。可以发现,出问题的地方是mshtml.dll中地址为0x7e278c83位置的mov语句,不清楚为什么这里的ecx所指向的内容不是合法地址,所以我们研究的目标就是找出ecx这个值是在哪里被改变的。可以在IDA中直接来到这个位置分析一下:



       可以发现,该语句是CElement::GetDocPtr(sub_7E278C83)函数的第一条语句,而这个函数也非常简单,一共只有四行代码。如果说想要弄清楚ecx值的由来,那么从当前函数所提供给我们的信息是无法得知的了,这个ecx的值只可能是调用当前函数的语句之前被改变的,所以我们可以使用IDA给我们的交叉引用功能来看一下:



       不幸的是,在当前模块中,一共有696个地方调用了这个函数,这就给我们的分析带来了困扰,于是我们只能回到WinDbg,通过栈空间的特性来定位了。

       我们知道,大家所使用的计算机系统都是基于栈架构的,栈是进行函数调用的基础。栈中记录了软件运行的丰富信息,观察和分析栈的内容是转储文件分析的一项重要手段。函数调用时会通过CALL指令将返回地址记录在栈上,而通过从栈顶向下遍历每个栈帧来追溯函数的调用过程,就被称为栈回溯。在WinDbg中,我们可以通过k命令进行栈的回溯:



       这里所展示的是在当前崩溃位置执行栈回溯的结果。其中的每一行所表示的是当前线程用户态栈上面的一个栈帧,其中的第一行所表示的是我们当前出现崩溃的函数。每个函数下面的一行是调用这个函数的上一级函数,也被称为父函数。因此函数的整体调用顺序是由下向上的。最下面一行是栈中的第一个栈帧,对应的是当前线程的启动函数GlobalWndProc,它位于mshtml这个模块。倒数第二行所执行的是位于mshtml中的CServerEnumCacheAry::`scalar deleting destructor'函数,同理,之后还调用了jscript模块中的NameTbl::Invoke函数。

       如果我们从横向分析,其中的第一列是栈帧的基地址,因为x86架构中通常使用EBP寄存器来记录栈帧的基地址,所以这一行的列名称叫做ChildEBP,表示子栈帧(子函数)的基址寄存器(EBP)的值。第二列的名称是RetAddr,即“返回地址”,这个地址是当前函数的父函数中的指令地址,也就是调用当前函数的那个Call指令的下一条指令的地址。

       可以发现,当前崩溃函数是CElement::GetDocPtr,而这个函数在执行完毕以后,会去执行0x7e44c4c8地址的内容,也就是回到其父函数的位置。那么我们就可以在IDA中来到这个地址看一下:



       我们通过之前的分析知道,崩溃是由于ecx寄存器的值出现问题而导致的。那么在这个call语句的上方可以看到,ecx的值是由ebx的值赋予的,而ebx的值又是由esi决定的,这里将esi作为地址,取该地址中的内容赋给ebx,所以我们下面还需要弄清楚这个esi的值是哪来的。


崩溃回溯分析

       由IDA给我们生成的图像可以明显看到,esi的来源有三个,分别是[eax+8]、[eax+4] 以及[eax],也就是说,这里同样会把eax寄存器中的值作为地址,并取该地址中的内容。而不论是哪种情况,eax的值都是由[ebp+var_8]所决定的,而由这个函数头部的信息可以得知,var_8也就是-8:



       于是我们就知道了,整个流程中,对崩溃起了作用的就是[ebp-8]。它其实是一个dword即4字节大小的局部变量空间。这个空间在被当前函数申请出来以后,只有在一个地方被怀疑修改了内容,从而影响了后续的使用,即位于0x7E44C475处的CALL语句:



       这里首先将这个变量空间的内容作为唯一的参数入栈,之后便调用了sub_7E27E248函数,我们需要重点关注这唯一的参数,即arg_0的异常变动情况,并且还能发现,这个函数只有一个参数,而且是个struct EVENTPARAM * *类型的指针:



       可以发现,这里主要出现了两个流程,即左边的loc_7E27E265以及右边的loc_7E341EF4。进一步分析发现,右边的流程并不会对参数arg_0进行异常的改变,因为这个流程会利用and操作将[ebp+arg_0]的内容清零,然后再赋以0x80020003这个值。但是左边的流程就不一样了,因为出现了一个不可控的因素,即eax的内容,它可能会对[ebp+arg_0]的内容产生异常的影响。而这个eax的值是由[ecx+18h]所决定的,于是还要对ecx寄存器中的内容溯源。

       或者我们也可以使用F5功能,让代码更加直观一些:



       可以看到,当前函数是CEventObj类型,程序使用this指针获取当前类型偏移为6的内容,通过类型转换可以发现,这个偏移的内容属于EVENTPARAM **结构。

       返回到上一级函数,我们看一下在当前Call语句上方的ecx寄存器内容的变化情况:



       可以看到,距离刚才我们分析的Call语句最近的两条影响ecx内容的是[ebp+var_C],直接对ecx执行了赋值的操作,而[ebp+var_C]的内容在最开始又是被ecx所决定的、在这里,ecx将自身的内容赋值给了函数的第三个局部变量,这个局部变量保存在[ebp-C]的位置。在这里我们看不到ecx的来源,因此还要继续如同之前所讲的那样,依靠WinDbg的栈回溯和IDA的交叉引用功能进行确定,于是就可以知道ecx的值来源是位于0x7E44C60E位置的CEventObj::get_srcElement函数:



       在这里,该函数的第一个参数被解引用后,赋值给了ecx寄存器,而这个第一个参数的内容可以在WinDbg中通过kb指令进行栈回溯获取到:



       可以看到,get_srcElement函数的第一个参数的内容是0x01d3eec0,而骇客就可以通过控制这个参数的内容进行攻击。

       这里我们再总结一下整个执行流程。程序在一开始会调用CEventObj::get_srcElement函数,将第一个参数解引用赋值给ecx之后,就来到了CEventObj::GenericGetElement函数,之后程序在0x7E44C475位置调用了CEventObj::GetParam函数,在这里会将[ecx+18h]的内容赋给[[ebp+arg_0]],而这里面的arg_0其实就是ebp+var_8,即ebp-8。我们刚才分析过了,程序只可能执行左边的流程,于是接下来程序对eax自身进行异或运算,也就是将eax寄存器清零,这样在当前函数的返回的时候,test语句会检测eax的内容,发现是零,跳转到0x7E44C485的流程,该流程最后会再一次对刚才的test结果进行验证,于是会跳转到0x7E44C4B2的流程:



       在这里,程序会将ebp-8进行解引用的操作,赋值给eax,然后对eax也进行解引用的操作,并赋值给esi,也就相当于esi的值为[[ebp-8]]:



       接下来继续执行,对esi进行解引用,也就是相当于把[[[ebp-8]]]的值赋给ebx,再赋值给ecx,由于[ecx],即[[[[ebp-8]]]]中的内容不是合法地址,于是出现了报错的情况。也就是说,程序可以对ebp-8进行三次正常的解引用操作,而第四次则会报错。在WinDbg中观察,则如下图所示:



       由此可见,崩溃是由于在进行了多次的解引用之后,出现了不可识别的指针所导致的。而源头就是CEventObj::get_srcElement函数。


小结

       我们本次课程主要为大家介绍了溯源的一些基本方法,希望大家掌握,因为我们的分析都是一环扣一环的,只有掌握好了本节课的内容,才能更好地理解我们之后的课程知识。

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册