前言

在现在的网络攻击中,缓冲区溢出方式的攻击占据了很大一部分,缓冲区溢出是一种非常普遍的漏洞,但同时,它也是非常危险的一种漏洞,轻则导致系统宕机,重则可导致攻击者获取系统权限,进而盗取数据,为所欲为。

其实缓冲区攻击说来也简单,请看下面一段代码:

void main(int argc, char *argv[]) {     char buffer[8];     if(argc > 1) strcpy(buffer, argv[1]); }

当我们在对argv[1]进行拷贝操作时,并没对其长度进行检查,这时候攻击者便可以通过拷贝一个长度大于8的字符串来覆盖程序的返回地址,让程序转而去执行攻击代码,进而使得系统被攻击。

本篇主要讲述缓冲区溢出攻击的基本原理,我会从程序是如何利用栈这种数据结构来进行运行的开始,试着编写一个shellcode,然后用该shellcode来溢出我们的程序来进行说明。我们所要使用的系统环境为x86_64 Linux,我们还要用到gcc(v7.4.0)、gdb(v8.1.0)等工具,另外,我们还需要一点汇编语言的基础,并且我们使用AT&T格式的汇编。

就我个人而言,作为一个新手,我还是比较怂来写这篇文章而言的,如果你发现又任何的错误或者不恰当的地方,欢迎指出,希望这篇文章对您有帮助。

进程

在现代的操作系统中,进程是一个程序的运行实体,当在操作系统中运行一个程序是,操作系统会为我们的程序创建一个进程,并给我们的程序在内存中分配运行所需的空间,这些空间被称为进程空间。进程空间主要有三部分组成:代码段,数据段和栈段。如下图所示:

栈是一种后入先出的数据结构,在现代的大多数编程语言中,都使用栈这种数据结构来管理过程之间的调用。那什么又是过程之间的调用呢,说白了,一个函数或者一个方法便是一个过程,而在函数或方法内部调用另外的过程和方法便是过程间的调用。我们知道,程序的代码是被加载到内存中,然后一条条(这里指汇编)来执行的,而且时不时的需要调用其他的函数。当一个调用过程调用一个被调用过程时,所要执行的代码所在的内存地址是不同的,当被调用过程执行完后,又要回到调用过程继续执行。调用过程调用被调用过程时,需要使用call指令,并在call指令后指明要调用的地址,例如call 地址,当被调用过程返回时,使用ret指令来进行返回,但是并不需要指明返回的地址。那么程序是怎么知道我们要返回到什么地方呢?则主要是栈的功劳:执行call指令时,程序会自动的将call的下一条指令的地址加入到栈中,我们叫做返回地址。当程序返回时,程序从栈中取出返回地址,然后使程序跳转到返回地址处继续执行。

另外,程序在调用另一个过程时需要传递的参数,以及一个过程的局部变量(包括过程中开辟的缓冲区)都要分配在栈上。可见,栈是程序运行必不可少的一种机制。

但是,聪明的你可能一想:不对,既然程序的返回地址保存在栈上,过程的参数以及局部变量也保存在栈上,我们可以在程序中操纵参数和局部变量,那么我们是否也能操作返回地址,然后直接跳转到我们想要运行的代码处呢?答案当然是肯定的。

改变程序的返回地址

我们看这也一个程序。

example.c void func() {         long *res;         res = &res + 2;         *res += 7; }  void main() {         int x = 1;         func();         x = 0;         printf("%d\n", x); }

我们在shell中使用如下命令编译运行一下,对于gcc编译时所用的参数,我们先卖个关子。

$ `gcc -fno-stack-protector example.c -o example` $ ./example

你或许会说:“哎呀呀,不用看了,这么简单,运行结果是0嘛”。但结果真的是这样嘛。其实,这个程序的运行结果是1。“什么,这怎么可能是1嘛,不得了不得了”

还记的我们提到的我们可以在程序中改变过程的返回地址吗?在func中,看是是对res进行了一些无意义的操作,但是这却改变了func的返回地址,跳过了x = 0这条赋值命令。让我们从汇编的层面上看一下这个程序是如何执行的。