探索黑客技术攻防,实战研究与安全创新

导航菜单

再论 CVE-2011-0073 Firefox漏洞

漏洞触发原因分析过程:

拿到POC之后,用   FireFox 3.6.16打开正常弹出计算器。打开  JS文件,破环shellcode,取得异常,通过栈回溯找到异常点:

QQ截图20160908095853.png

很明显,这是一个覆盖虚表指针,然后由于虚表函数被调用,导致了shellcode被执行。

又是一个use after free的经典例子。

再上溯栈中函数返回值,发现调用:我们暂定call1046657e调用的函数为BugTrigger。

QQ截图20160908095912.png

恢复JS中的shellcode,重新用调试器加载。在BugTrigger函数的入口处设置断点,调试器断了下来。此时通过栈回溯我们会发现:

JS中的代码在汇编中体现了出来。首先创建了一个对象,然后进入了我们的   BugTrigger函数。为了方便以后的分析,我们在 call operator new处加入一个软件断点。在进入  BugTrigger之前,我们查看eax、也就是新创建对象指针对应的堆内容:

1.png

jS中的代码在汇编中体现了出来。首先创建了一个对象,然后进入了我们的   BugTrigger函数。为了方便以后的分析,我们在 call operator new处加入一个软件断点。在进入  BugTrigger之前,我们查看eax、也就是新创建对象指针对应的堆内容:

2.png

由于每次运行创建对象返回的指针不同,出于便于描述的考虑,我们把 eax这个对象指针命名为 ObjPointer。

在 BugTrigger函数开始处,有个执行流程的跳转判断:

3.png

我们断点处可以看出,此时eax=[ObjPointer]+8。也就是说,此时会对[ObjPointer]+8处的DWORD判断是否为0。

4.png

[[ObjPointer]+8]不为0,虚表函数得到调用。进入calldword ptr [ecx+70]时,我们在BugTrigger函数的入口处设置断点。程序在此处断下。也就是说,BugTrigger函数在调用虚函数时,又嵌套调用它自己。

这里我们发现一个小情况:再次查看比较判断处eax地址的内存情况,惊奇的会发现,[[ObjPointer]+8]已经被修改为0了。

1.png

此时栈回溯的信息告诉我们,调用BugTrigger的地址居然不是   104A947F,说明BugTrigger不只被一个函数调用。我们IDA查找,发现了另外一处调用BugTrigger的地方:

2.png

前面已经说明,[[ObjPointer]+8]为 0会导致虚函数得不到调用,因此我们在[ObjPointer]+8地址处设置内存写入断点。查看[[ObjPointer]+8]何时被修改为  0的。

重新运行 poc调试,调试器在内存写入  eax地址时断下

3.png

在程序执行时果然这个值被修改。这个时候我们查看内存中是否已经有喷射值,发现堆喷射没有进行。在另外溢出调用 BugTrigger的函数开始处设置断点,调试器断在下图所示:

(这时需要我们注意:断点这个执行实在第一次调用 BugTrigger函数时   call虚函数时断下来的。)

5.png

程序又一次调用 call BugTrigger,在进入函数内部时压入的参数和之前的 eax是一样的。

由于之前判断处[[ObjPointer]+8]值已经被修改为 0了,程序跳过了call [ecx+0x70]。

1.png

执行完 BugTrigger之后,程序对对象指针进行了删除,并清零(见图  7)。其实就是  JS中 ZLUurXitDV.clearSelection();函数的汇编实现过程。

在创建完这个对象并对此对象进行删除后,开始了堆喷射。其实就是为了覆盖刚刚删除了的 ObjPointer对象的内容,以便有可能覆盖虚表指针。进而在此对象的虚函数再次调用时控制 EIP。

对 ObjPointer地址处下内存写入断点,不一会,喷射就到了这里:

2.png

搜索整个内存,我们可以发现喷射是有规律的:

3.png

通过连续的申请堆空间,总有一次会覆盖ObjPointer。

以上实则是第一次进入BugTrigger函数调用虚函数时,虚函数进行的所有操作。对应的JS代码就是

ZLUurXitDV.select(0);


它会执行invalidateRange: function(s,e)里面的操作,销毁对象指针ObjPointer,堆喷射填充ObjPointer所指向的内存区域,其中当然包括把ObjPointer+8处设置为一个非零值。然后返回。

程序接着进行一个判断,既是[ObjPointer+8]是否为零,直接导致跳转到  0x1046657f处继续执行。如下图所示:

1.png

由于 ObjPointer已经被  0000000f 0000000f 0000000f 0000000f ad de所覆盖。0x0f000000处又被喷射所覆盖,整个对象的虚函数表都能被控制。作者采用了相当精巧的 ROP方法来过 DEP,稍后我们会发现,喷射的每个值都是极为精确的。

总结上面的漏洞触发过程,我们发现一切操作都是围绕 BugTrigger函数来进行的:第一次属于创建对象后正常调用 BugTrigger然后返回。对应  JS中的:

2.png

第二次调用   BugTrigger时,call虚函数的过程中,删除销毁了这个创建的对象指针ObjPointer,并通过堆喷射覆盖了 ObjPointer所在内存的数据(ObjPointer此时是无效的)。

等到我们从调用的虚函数中返回时,这个对象指针所在的区域已经和初始创建时大不相同了。直接导致虚函数再次被调用,也就是 user after free。对应 JS中的:

3.png

漏洞利用的分析:

继续分析。Eax已经变成0x0f000000处的值,由于之后它要自加 8后[eax]与0比较。故而把 0x0f000000处值设为0x0efffffc,加上 8后为0x0f000004。正好位于我们可控制的数据堆 0x0f000000的第二个DWORD,由于[0x0f000004]要与 0比较,为了让虚函数得到调用从而控制 EIP,必须使 0x0f000004处的值不为0。这完全是我们能控制的!!!

ROP作者将[0x0f000004]设为0x0f000014,为了描述方便,我们设置此值为 shellAddr。

继续执行,eax=shellAddr。我们可以发现,不仅可以 call到任意地址,我们甚至可以先设置一些参数然后直接调用函数来执行。为了描述方便,我们设可控堆内存0x0f000000为ControAddr,调用函数 ControlFunc。那么shellAddr = [ControlAddr +0x4]通过调用虚函数,可用的函数调用就是ControlFunc(shellAddr,[ControAddr+0xc],[ControAddr+0x10]);其中 shellAddr=[ControlAddr+0x4]我们查看一下设置堆状态的函数:(把堆内存变成可执行,绕过 DEP)

1.png

这个函数有 4个参数,我们要想达到目的,其他参数都好办,需要注意的有两个:

1,参数 flNewProtect需要为0x40(PAGE_EXECUTE_READWRITE)使 shellcode所在内存段可执行;

2,参数 lpflOldProtect可以为硬编码地址,但是该地址必须是可写的我们可以在程序运行的地址空间中找一个来设置。

我们看一下在我们压入[ControAddr+0x10]前的堆栈状态:

2.png

很不幸,我们需要的最后一个可写的参数在固有的堆栈上是 0。这个时候我们需要转换思路,既然一次不行,我们通过两次调用来保证堆栈上的压入参数满足要求。这个时候,我们的思路是:既然可控的参数有 3个,我们首先找到一个少于三个参数的函数进行执行,这个函数要无关痛痒,对程序没有影响。函数调用返回后,堆栈上必然遗留有我们多压的一个或者两个参数。当再次调用时,我们可控的参数已经不是 3个了,而是4个、甚至是5个!

ROP作者选择的函数是LoadLibrary。这个函数只有一个参数,极为符合我们的要求!!!

现在让我们来构造吧:

第一次 call,调用 LoadLibrary时,shellAddr,也就是[ControlAddr +0x4]为函数需要的唯一一个参数。根据压栈顺序,我们需要[ControAddr+0xc]为我们第二次 call需要的参数。

既然需要的是可写的,干脆就把[ControAddr+0xc]设置为 0x0f000000吧。[ControAddr+0x10]是我们不需要的东东,就设置为 0吧。Call返回后,堆栈状态为:

1.png

第二次 call,调用 VirtualProtect时,只需要将其余参数设置为我们需要的就行了。

这里会有几个疑问:

第一,怎么保证虚函数得到循环调用?其实就是刚才那个判断 [ObjPointer+8]不为零即可。在第二次 call时,esi变成了[ControlAddr+8],最终[[[ControlAddr+8]]+8]会变成 VirtualProtect的第一个参数,我们需要设置它为功能shellcode开始处。这里我们设置[ControlAddr+8]=0x0f000018,而[0x0f000018]=0x0f000014,[0x0f000014+8]=功能 shellcode开始处(或者前面)。第二次调用的参数此时 ControlAddr'=0x0f000018。

第二,你怎么获取这两个函数的地址呢?在有ASLR的情况下,这个确实比较难。但是作者发现了规律:没有 ASLR的情况下:模块mozcrt19.dll中:

[0x781a9188+0x70]=VirtualProtect函数的地址

[0x781a9094+0x70]=LoadLibrary函数的地址


最终,ControlAddr的堆区域分布为

QQ截图20160908101048.png

第三次调用 call时,ControlAddr''= 0x0f000030,通过运行,最终会 call到shellcode上去,完成对 DEP的完全绕过,并执行shellcode。Shellcode的代码为

hellcode:

0F00004C

0F00004D

0F00004F

0F000050

0F000051

0F000052

0F000057

0F000058

0F00005A

0F00005B

0F00005F

0F000062

.......

60

pushad

xor

31C0

eax, eax

50

push

push

push

call

pop

eax

50

eax

50

eax

E8 00000000

5A

0F000057

edx

89D6

mov

esi, edx

edx

52

push

add

830424 3B

83C2 25

83C6 2E

dword ptr [esp], 3B

edx, 25

add

add

esi, 2E

这里就不分析了。有兴趣可以自己分析,其实就是弹出一计算器。

总结:

第一次分析漏洞触发的原因,而且是堆环境复杂的浏览器漏洞,一开始真是不知道如何下手。感觉这种use after free的漏洞就是通过栈回溯找到对象的创建、销毁、再使用过程。尤其是搞清楚为何会再次被使用,像这个漏洞,它能再次被使用是由于喷射覆盖了对象内存区域的原因。笔者一开始去掉了堆喷射,却怎么也触发不了异常,怎么都想不明白。后来思路一换,把shellcode破坏掉,然后栈回溯慢慢分析。最终找到了原因。

此次的 ROP构造真是一个精细活,尤其是要在 BugTrigger的跳转中通过控制[esi+8]的值不为0来反复调用虚函数(call[ecx+0x70])。并且过DEP的函数地址是通过稳定的偏移获取的。这些在上面都有详细说明,就不多说了。总之,这是一个经典的useafterfree漏洞实例。

本文为网络安全技术研究记录,文中技术研究环境为本地搭建或经过目标主体授权测试研究,内容已去除关键敏感信息和代码,以防止被恶意利用。文章内提及的漏洞均已修复,在挖掘、提交相关漏洞的过程中,应严格遵守相关法律法规。