Hacking Team Flash 0day漏洞学习笔记

      周日的夜晚,与囧桑下载下来Hacking Team之前爆出的flash 0day漏洞,怀着紧张激动的心情,在自己的机子上做了实验,经测试,在我的虚拟机(一个sp3的XP,貌似没装Flash)上根本跑不出Flash漏洞,在我的Win10宿主机上跑,貌似刚升级的Flash,洞被补上了,也不能用。最后在囧桑的Win7机子上实验,点击了网页上的按钮,出现了测试用的程序--计算器。那一瞬间老激动了。

     言归正传,下面开始Hacking Team的Flash 0day漏洞学习。感谢Hacking Team 公司的挖洞专家们这么敬业,给了一个有注释有README的项目,让我这初学者表示可以慢慢啃了。
 
漏洞原理分析
     漏洞成因在于Flash对ByteArray内部的buffer使用不当,而造成了Use After Free漏洞,被释放后的内存区域可以立即被新创建的Vector对象重用,从而出现UaF漏洞。
 
漏洞利用代码分析
     MyClass类 核心类,用于触发UaF漏洞。
     MyClass1类 是ByteArray的子类,其中包含4个对象。
     MyClass2类 是MyClass1类的子类,也是ByteArray的子类,有四个对象,且用重复且大量的属性来扩充MyClass2类的大小。
     本人新手,将对整个代码进行分析,以免自己以后忘记。
     首先是绘制整个flash区域的代码。红框框里将点击事件与函数btnClickHandler进行绑定。
 
     
  随后看看这个btnClickHandler函数。
 
     
  关键就是这个TryExpl函数,它实际上就是触发整个漏洞的关键,相当于整个Flash利用程序的入口点。这个函数的主要目的是为了破坏Vector.<uint>的长度值,再接着查找破坏后的内存区域,寻找受破坏的Vector对象,然后根据操作系统的位数,以及操作系统的特征决定到底执行哪个PayLoad代码。
 
    
     
  首先定义一个长为90的数组a,数组以3个元素为一组,元素1为一个MyClass2的实例,元素2为一个ByteArray即字符数组,当new一个ByteArray时,AVM将会为数组分配0x1000大小的内存(相当于操作系统的内存申请机制,每次申请大小为0x1000,为一个内存区块),随后这个数组的长度设置为0xfa0。而a这个数组里将会有30个ByteArray数组。
 
     然后数组a的末尾开始修改倒数第二个ByteArray,行为是将一个MyClass的对象赋给那个ByteArray的第四个元素,这个元素类型为Byte而MyClass是个对象,所以AVM会试图将其转化为Byte类型,从而调用MyClass类中的valueOf方法。
 
  valueOf函数中new一个5个元素的数组_va,随后修改静态变量ByteArray数组_ba的长度为0x1100,这是关键,由于这个操作会触发ByteArray的缓冲区buffer的重新分配(原来这个buffer长度为0xfa0,占地0x1000,现在长度要求为0x1100了,所以要重新分配内存),旧的buffer区域被释放。紧接着分配5个Vector.<uint>对象,每个对象长度为0x3f0,占地0x1000字节,且Vector对象的长度值存储于Vector缓冲区的前四位区域。这时分配的区域将会试图使用之前已经释放的ByteArray _ba的内存区域。而_va[x]指向的就是之前_ba的地址,且该地址仍然在这个函数的调用方的栈中存储,即esi这个寄存器中存储着_ba[3]的地址,而这个地址现在实质上已经指向了_va[?]这个Vector的第四位,返回的0x40就被写到了这个内存中了。随后的_va[x]前四位如下所示。(具体哪个被分配到了_ba释放的内存不得而知,但是只有出现UaF的_va[x]才会出现异常的长度,从而被检测。)
_va[x]:     f0     03     00     00     =>     va[0]:     f0     03     00     40
那么以后再去读取这个Vector的长度就会错误的返回一个超巨大的长度0x400003f0。 
 
 
     继续对TryExpl进行分析,因为_ba[3]已经指向在MyClass.valueOf里重新分配的区域,所以不出意外这里返回_ba[3]的值是0不会变化。接着对_va中的每个Vector.<uint>对象进行检测,找到那个被我们刚才改了长度值的Vector的首地址。再接着令k等于1094,相当于一个足够远离首地址的位置,检查v[k]中的内容是否为0x11223344,即该指针是否还处于MyClass2的范围内,并逐渐接近Vector的首地址。当找到非0x11223344的地址时,该区域就是MyClass2的a0这个变量的位置,这个位置为MyClass2的id。之后令这个位置为MyCLass2实例mc,改变mc的长度为0x123.
 
 
     随后就是根据k走过的距离是不是大于30来确定是不是在64位系统上运行的程序,再确定系统类型,是win还是Mac来,32位还是64位。
 
 
 
 
PayLoad代码分析
  •      ShellWin64类 是MyClass的子类,是Win64上的PayLoad代码,为的就是打开计算器程序。
  •      ShellMac64类 是MyClass的子类,是Mac上的PayLoad代码,为的也是打开计算器程序。
  •      ShellWin32类 是MyClass的子类,是Win32上的PayLoad代码,为的也是打开计算器程序。
ShellWin32
     首先观察ShellWin32的初始化操作,v是中间被我们修改的Vector向量,而v[k]则是MyClass2中的变量a0,那么v[k-4]则是MyClass2中的o1,即该MyClass2对象的实际地址,与0xfffff000相与可以获得其前5位即该MyClass2对象所在的0x1000块的首地址,这时减去0x1000就获得了中间Vector的首地址,再加上8则是跳过了Vector头部的8位到达了Vector中存储值的首地址,即v[0]的实际地址,接着将该实际地址存入vAddr中。
     这个vAddr作为实际地址相当有用。
     通过v[(address-vAddr)>>>2]就可以访问任意地址内存了,这个在ShellWin32中被封装成了Get。
 
 
  通过mc.o1=obj ;返回v[k-4]-1就可以返回任意对象的绝对地址了。即先将obj放入到MyClass2的o1中,再去读取o1这个位置中的值,该值减一就是该对象的绝对地址。这个也在ShellWin32中被封装成了GetAddr。
 
 
  首先初始化,将一些参数存入ShellWin32的内部变量中,随后通过验证v[vAddr-8]位置存储的值和v中的长度是否一致,确保传入的v和vAddr是否匹配。
 
 
  随后进入执行函数。
  第一部分是获取shellcode这个Vector.<uint>对象的首地址,随后获取shellcode中的数据区地址。其中的_isDbg在MyClass中定义。
 
 
  其中Vector实际存储的缓冲区指针,即真正的shellcode位置在Release和Debug版分别放置于0x18和0x1c的位置。为了顾全所有版本,还注意到Flash11.4版本之前是不存在这个便宜区别的,11.4之后的Debug版才在缓冲区指针前加入了一个0x00000001字段。下图中是Vector的内存分布,左侧是11.4之前的Release和Debug,右侧是11.4以后的Release和Debug。不得不说HackTeam写代码为了保证向后兼容以及针对Release和Debug版本,也是做了很多工作的。
 
 
开启ShellCode执行权限
      随后进入获得kernel32.VirtualProtect()地址的流程。
 
 
  进入FindVP函数。
 
 
      b是dll或者exe文件中的虚函数表指针。将b指针对齐0x10000,再减去0x400000,随后减去_vAddr得到PE文件头部,看看DOS_HEADER,(真的吗,我也不清楚这里为何如此,有人知道的话请给我留言,么么哒~),随后一路推导到达NT_HEADER,获得了IMPORT_DIRECTORY导入目录,在导入目录中寻到了kernel32.dll。
 
 
  随后再在kernel32.dll中找到了VirtualProtect API的地址。
 
 
     接着调用VirtualProtect开启shellcode这段区域的执行权限。
 
 
 
  首先生成一个FunctionObject类的对象Payload,然后获得该对象的地址。随后的一段比较难以理解。刚开始以为要把AS3里的许多结构体都撸明白才能算出偏移,其实没这么蛋疼,看看Flash中的FunctionObject::AS3_call就全明白了。
 
 1 int AS3_call(void *this, int thisArg, int *argv, int argc)
 2 push    ebx
 3 push    esi
 4 mov     esi, ecx
 5 mov     ecx, [esp+8+thisArg]
 6 mov     eax, [esi]
 7 mov     edx, [eax+8Ch]
 8 push    edi
 9 push    ecx
10 mov     ecx, esi
11 call    edx
12 mov     ebx, eax
13 mov     eax, [esi+8]
14 mov     ecx, [eax+14h]
15 mov     edx, [ecx+4]
16 mov     eax, [esi]
17 mov     edi, [edx+0B0h]
18 mov     edx, [eax+90h]
19 lea     ecx, [esp+0Ch+thisArg]
20 push    ecx
21 mov     ecx, esi
22 call    edx
23 mov     ecx, [esp+0Ch+argv]
24 mov     eax, [eax]
25 mov     edx, [edi]
26 mov     edx, [edx+1Ch]
27 push    ecx
28 mov     ecx, [esp+10h+argc]
29 push    ecx
30 push    ebx
31 push    eax
32 mov     ecx, edi
33 call    edx
34 pop     edi
35 pop     esi
36 pop     ebx
37 retn    0Ch

 



  Payload是个FunctionObject类型,最终对其调用call的时候最终到达FunctionObject::AS3_call,其内部调用core()->exec->call跳转到Payload对象JIT(Just In Time,即时编译)出来的代码。由于是FunctionObject和ExecMgr的继承关系,可以通过自身结构定位到ExeMgr的虚函数表。
     在刚进入AS3_call时,ecx也就是随后的esi指向了Payload对象,也就是esi=p(GetAddress(Payload))。参考上述汇编代码中红色字体部分,可知ExecMgr对象的虚函数表位于[[[[esi+8]+0x14]+4]+0xB0],偏移0处就是PayLoad的虚函数表,就是CallVP中的ptbl所指。后三行的三个Get指令表明,虚函数表的首地址ptbl的值存入p1中,需要执行VirtualProtect的首地址存放于payload实际地址+0x1c位置,取出放于p2中,地址区域长度为xLen,存放于payload实际地址+0x20位置,取出存放于p3中。core->exec->call会牵涉一些其他虚函数的调用,所以以上CallVP对虚函数表上下共0x400字节全部备份。然后在被备份的那段内存区域改写0x1C处为VirtualProtect的地址,0x20处填写需要改写执行权限的内存区域地址长度。而args设置为一个存放了0x41个元素的数组(为什么是0x41,不该是0x40么,多个1不就是NO_access么)。
 
 
  现在调用Payload.call就会跳转到VirtualProtect执行了。观察上述的汇编代码,core->exec->call(env,thisArg,argc,argv)和VirtualProtect(lpAddress,dwSize,flNewProtect,lpflOldProtect)一样都是四个参数。其中argc和argv好理解,就是CallVP中调用Payload.call传入的参数个数和参数所在数组,只要让参数个数为0x40,flNewProtect就变成RWX,而argv本来只想存储参数的可写堆内存,就不用管了(这里还是不清楚为什么上面的args的长度为0x41,这样argc不就等于0x41了么,就不是执行权限了啊),前两个参数实际上是位于Payload对象的0x1c和0x20位置的,如下图所示,其中左侧为Payload对象的内存,右侧是core->exec->call()的栈。
  
 
 如此一来,直接改写Payload的内存就可以控制VirtualProtect的参数了。代码中的apply(null,args)就是call(null*0x41)。之所以是0x41个null而非0x40个null,是因为第一个null是调用者而不是参数。call(object,param1,param2...)等价于call.apply(object,new Array(param1,param2,...))。args按说完全是参数数组了,长度还要设置为0×41是因为apply的调用者为call,而object指针不会从apply向call传递。在call看来传过来参数包含了object指针和参数,所以数组的第一个参数就作为了object指针,也就是这种传递过程会吃掉一个参数。
     调用VirtualProtect后再恢复修改过的虚函数表和Payload内存就可以执行shellcode了。
 
执行Shellcode
     上述执行VirtualProtect的方法用于执行shellcode也是可行的。但是人家偏不复用,非要换个方法,就是直接替换掉Payload的MethodEnv->Methodinfo里存储的JIT后的代码地址,这个与Core Security提出的绕过CFG方法相当类似(见参考链接2)。
     Payload对象中0x1C偏移的位置存储了MethodEnv指针,而MethodEnv中的0x8偏移的位置指向了MethodInfo,而MethodInfo的0x4偏移的位置就是_implGPR,存储了JIT后的代码地址,所以调用Shellcode的代码就变成了如下图所示。
 
 
   随后替换了Payload函数对象中JIT后的代码地址为shellcode的地址。
 
 
  随后对利用Payload.call进行shellcode的触发,但是上次是替换了ExecMgr的虚函数call,这次是直接替换了JIT后的代码地址。JIT代码的调用不在CFG监测的范围之内,而ExecMgr的虚函数表由于频繁使用考虑性能也没有CFG守护。(CFG:control flow guard控制流保护,见参考链接3)。
 
     其实使用JIT的方法调用shellcode好处还在于,shellcode可以返回需要的结果,并让ActionScript获取指导下一步操作,比如EAX存储了CreateProcessA的执行结果,若返回前构造一个这样的代码,就可以让EAX转化为atom。
 
1 04DDE32D  SHL  EAX,3
2 04DDE330  ADD  EAX,6
3 04DDE333  LEAVE
4 04DDE334  RETN

 

  
  当执行完call之后,AS3可以根据进程创建情况判断沙箱的限制,来决定是否进一步部署内核漏洞利用代码进行提权。
 
结语
    HackTeam的Flash Exploit在超长Vector获取前的堆分布,获得后的代码执行阶段加入了不少独辟蹊径的技巧,对不同版本和操作系统的考量也让利用代码异常稳定。只是如今随着Adobe在18.0.0.209时引入了Vector内存隔离和类似对抗JIT-Spray的长度异或校验(见参考链接[4]),这套利用模板已经不能走得更远了。一整年的喧嚣后,Flash大概也想清静一下了。 
 
参考:
posted @ 2015-08-10 15:01  伪筒子叔  阅读(659)  评论(0编辑  收藏  举报