缓冲区溢出攻击 | 阻止

注:本文为 “缓冲区溢出攻击 | 阻止” 两篇文章合辑。


什么是缓冲区溢出攻击以及如何阻止它

APRIL 5, 2021 / Megan Kaczanowski

当写到内存的内容超过分配给它的大小时,就会发生缓冲区溢出。这种行为可能会导致数据损坏、程序崩溃,甚至是恶意代码的执行。

C、C++ 和 Objective-C 是容易出现缓冲区溢出漏洞的主要语言(因为它们比许多解释型语言更直接地处理内存),然而它们很大程度上奠定了互联网发展的基础。

即使代码是用"安全"语言(如Python)编写的,如果它调用任何用 C、C++ 或 Objective C 编写的库,它仍然有可能受到缓冲区溢出的影响。

内存分配

为了理解缓冲区溢出,我们有必要了解一下程序如何分配内存。在C语言中,你可以在编译时在堆栈中分配内存,也可以在运行时在堆中分配内存。

在堆栈上声明一个变量:int numberPoints = 10;

或者在堆上声明变量:int* ptr = malloc (10 * sizeof(int));

缓冲区溢出可以发生在栈(栈溢出)或堆上(堆溢出)。

一般来说,栈溢出比堆溢出更常被利用。这是因为栈包含一连串的嵌套函数,每个函数都返回调用函数的地址,在函数运行结束后,栈应该返回该地址。这个返回地址可以被替换为执行一段恶意代码的指令。

由于堆较少存储这些返回地址,因此更难发起攻击(尽管不是不可能)。堆上的内存通常包含程序数据,并且是在程序运行时动态分配的。这意味着堆溢出很可能要覆盖一个函数指针–比栈溢出更难也更不有效。

由于栈溢出是更常被利用的缓冲区溢出类型,我们将简要地挖掘它们的具体工作原理。

栈溢出

当一个可执行文件被运行时,它在一个进程中运行,每个进程都有自己的栈。当进程执行主函数时,它将发现并创建新的局部变量(这些变量将被推到堆栈的顶部)和对其他函数的调用(这些函数将创建一个新的栈帧)。

为了更清楚的展示,请参考下图:

Screen-Shot-2021-01-05-at-12.31.23-PM

https://en.wikipedia.org/wiki/Stack_(abstract_data_type)

什么是栈帧?

首先,调用栈基本上是一个特定程序的汇编代码。它是一个由变量和栈帧组成的栈,它告诉计算机以什么顺序执行指令。每个尚未完成执行的函数都会有一个栈帧,当前正在执行的函数在堆栈顶部。

为了持续的追踪栈帧,计算机在内存中保留了以下几个指针:

  • 栈指针: 指向进程调用栈的顶部(或压入栈的最后一个指针)。
  • 指令指针: 指向下一条要执行的CPU指令的地址。
  • 基准指针(BP):(也称为帧指针)指向当前栈帧的基点。只要程序在执行当前栈帧,它就保持不变(尽管栈指针会改变)。

例如,请看以下程序:

int main() {
    int j = firstFunction(5);
    return 0;
}
    
int firstFunction(int z) {
    int x = 1 + z;
    return x;
}

在调用 firstFunction 和执行语句int x = 1+z之后,调用栈会是这样的:

Screen-Shot-2021-04-03-at-12.04.52-PM

这里,main调用了firstFunction(目前正在执行),所以它在调用栈的顶部。返回地址是调用它的函数的内存地址(这是在创建栈帧时由指令指针持有)。仍在范围内的局部变量也在调用栈上。当它们被执行并超出范围时,它们会从栈顶部被“弹出”。

因此,计算机能够跟踪哪条指令需要被执行,判断以何种顺序执行。栈溢出是为了用攻击者自己设计的恶意地址覆盖这些保存的返回地址之一。

缓冲区溢出漏洞实例(C):

int main() {
    bufferOverflow();
 }
 
 bufferOverflow() {
    char textLine[10];
    printf("Enter your line of text: ");
    gets(textLine);
    printf("You entered: ", textLine);
    return 0;
 }

这个简单的例子读入了任意数量的数据(gets会读入到文件的末尾或换行符)。想一想我们上面看过的调栈,你就会明白为什么这很危险。如果用户输入的数据多于变量被分配的数量,用户输入的字符串将覆盖调用栈的下一个内存位置。如果它足够长,它甚至可能覆盖调用函数的返回地址。

计算机对此的反应取决于栈的实现方式和特定系统中内存的分配方式。对缓冲区溢出的反应是不可预测的,从程序故障,到崩溃,再到执行恶意代码。

为什么会发生缓冲区溢出?

缓冲区溢出之所以成为如此重大的问题,是因为 C 和 C++ 中的许多内存操作函数不执行任何边界检查。缓冲区溢出现在相当有名,它们也非常普遍地被利用(例如,WannaCry 利用了缓冲区溢出)。

缓冲区溢出最常见的情况是,代码依赖于外部输入数据,这种方式过于复杂,程序员不容易理解其行为,或者它有代码直接范围之外的依赖关系。

网络服务器、应用服务器和网络应用环境都容易受到缓冲区溢出的影响。

用解释型语言编写的环境是个例外,尽管解释者本身也容易受到溢出的影响。

如何减轻缓冲区溢出的影响

  • 使用解释性的语言 这并不容易受缓冲区溢出的影响。
  • 避免使用不进行缓冲区检查的函数(例如,在C语言中,用 fgets() 代替 gets()。)
  • 使用能够帮助识别不安全函数或错误的编译器。
  • 使用金丝雀 这是一种 “防护值”,可以帮助防止缓冲区溢出。它们被插入到堆栈中的返回地址之前,并在返回地址被访问之前被检查。如果程序检测到金丝雀值的变化,它将中止进程,防止攻击者得逞。警戒值要么是随机的(所以,攻击者很难猜到),要么是一串字符,由于技术原因,不可能被覆盖。
  • 重新安排局部变量 所以普通变量(单个固定大小的数据对象)高于数组变量(包含多个值)。这意味着,如果数组变量真的溢出,它们不会影响普通变量。这种技术,当与警戒值相结合时,它可以帮助抵御缓冲区溢出攻击。
  • 将一个栈变为不可执行 通过设置NX(No-eXecute)位,防止攻击者将shellcode直接插入栈中并在那里执行它。这并不是一个完美的解决方案,因为即使是不可执行的栈也会成为缓冲区溢出攻击的受害者,如返回到libc攻击。当栈框架的返回地址被替换成已经在进程地址空间的库的地址时,这种攻击就会发生。此外,并非所有的CPU都允许设置NX位。
  • ASLR(地址空间布局随机化),它可以作为一种一般的防御措施(以及对返回到libc攻击的特殊防御)。这意味着,无论何时一个库文件或其他函数被运行中的进程调用,其地址都会被一个随机数移位。这使得几乎不可能将一个固定的进程内存地址与函数联系起来,这意味着攻击者很难,甚至不可能知道从哪里调用特定的函数。在许多版本的Linux、OS X和Android中,ASLR是默认开启的(可以在命令行中切换关闭)。

关于 Stack Underflow 的说明:

当同一个程序的两个部分以不同的方式处理同一个内存块时,也有可能出现缓冲区溢出的漏洞。例如,如果你分配了一个大小为 X 的数组,但用大小为 x<X 的数组填充它,后来你试图检索所有 X 字节,你很可能得到 X-x 字节的垃圾数据。

从本质上讲,你可能已经拉出了一些数据,这些数据是以前使用该内存时留下的。最好的情况是,它是没有任何意义的垃圾,而最坏的情况是,它是攻击者可能会滥用的敏感数据。

资料来源/进一步的阅读:


缓冲区溢出攻击

作者:Florian @ 2013-08-10 18:54

缓冲区溢出(Buffer Overflow)是计算机安全领域内既经典而又古老的话题。随着计算机系统安全性的加强,传统的缓冲区溢出攻击方式可能变得不再奏效,相应的介绍缓冲区溢出原理的资料也变得 “大众化” 起来。其中看雪的《0day 安全:软件漏洞分析技术》一书将缓冲区溢出攻击的原理阐述得简洁明了。本文参考该书对缓冲区溢出原理的讲解,并结合实际的代码实例进行验证。不过即便如此,完成一个简单的溢出代码也需要解决很多书中无法涉及的问题,尤其是面对较新的具有安全特性的编译器 —— 比如 MS 的 Visual Studio2010。接下来,我们结合具体代码,按照对缓冲区溢出原理的循序渐进地理解方式去挖掘缓冲区溢出背后的底层机制。

一、代码 <=> 数据

顾名思义,缓冲区溢出的含义是为缓冲区提供了多于其存储容量的数据,就像往杯子里倒入了过量的水一样。通常情况下,缓冲区溢出的数据只会破坏程序数据,造成意外终止。但是如果有人精心构造溢出数据的内容,那么就有可能获得系统的控制权!如果说用户(也可能是黑客)提供了水——缓冲区溢出攻击的数据,那么系统提供了溢出的容器——缓冲区。

缓冲区在系统中的表现形式是多样的,高级语言定义的变量、数组、结构体等在运行时可以说都是保存在缓冲区内的,因此所谓缓冲区可以更抽象地理解为一段可读写的内存区域,缓冲区攻击的最终目的就是希望系统能执行这块可读写内存中已经被蓄意设定好的恶意代码。按照冯·诺依曼存储程序原理,程序代码是作为二进制数据存储在内存的,同样程序的数据也在内存中,因此直接从内存的二进制形式上是无法区分哪些是数据哪些是代码的,这也为缓冲区溢出攻击提供了可能。

img

图1 进程地址空间分布

图1是进程地址空间分布的简单表示。代码存储了用户程序的所有可执行代码,在程序正常执行的情况下,程序计数器(PC指针)只会在代码段和操作系统地址空间(内核态)内寻址。数据段内存储了用户程序的全局变量,文字池等。栈空间存储了用户程序的函数栈帧(包括参数、局部数据等),实现函数调用机制,它的数据增长方向是低地址方向。堆空间存储了程序运行时动态申请的内存数据等,数据增长方向是高地址方向。除了代码段和受操作系统保护的数据区域,其他的内存区域都可能作为缓冲区,因此缓冲区溢出的位置可能在数据段,也可能在堆、栈段。如果程序的代码有软件漏洞,恶意程序会“教唆”程序计数器从上述缓冲区内取指,执行恶意程序提供的数据代码!本文分析并实现栈溢出攻击方式。

二、函数栈帧

栈的主要功能是实现函数的调用。因此在介绍栈溢出原理之前,需要弄清函数调用时栈空间发生了怎样的变化。每次函数调用时,系统会把函数的返回地址(函数调用指令后紧跟指令的地址),一些关键的寄存器值保存在栈内,函数的实际参数和局部变量(包括数据、结构体、对象等)也会保存在栈内。这些数据统称为函数调用的栈帧,而且是每次函数调用都会有个独立的栈帧,这也为递归函数的实现提供了可能。

img

图2 函数栈帧

如图所示,我们定义了一个简单的函数 function,它接受一个整形参数,做一次乘法操作并返回。当调用 function (0) 时,arg 参数记录了值 0 入栈,并将 call function 指令下一条指令的地址 0x00bd16f0 保存到栈内,然后跳转到 function 函数内部执行。每个函数定义都会有函数头和函数尾代码,如图绿框表示。因为函数内需要用 ebp 保存函数栈帧基址,因此先保存 ebp 原来的值到栈内,然后将栈指针 esp 内容保存到 ebp。函数返回前需要做相反的操作 —— 将 esp 指针恢复,并弹出 ebp。这样,函数内正常情况下无论怎样使用栈,都不会使栈失去平衡。

sub esp,44h 指令为局部变量开辟了栈空间,比如 ret 变量的位置。理论上,function 只需要再开辟 4 字节空间保存 ret 即可,但是编译器开辟了更多的空间(这个问题很诡异,你觉得呢?)。函数调用结束返回后,函数栈帧恢复到保存参数 0 时的状态,为了保持栈帧平衡,需要恢复 esp 的内容,使用 add esp,4 将压入的参数弹出。

之所以会有缓冲区溢出的可能,主要是因为栈空间内保存了函数的返回地址。该地址保存了函数调用结束后后续执行的指令的位置,对于计算机安全来说,该信息是很敏感的。如果有人恶意修改了这个返回地址,并使该返回地址指向了一个新的代码位置,程序便能从其它位置继续执行。

三、栈溢出基本原理

上边给出的代码是无法进行溢出操作的,因为用户没有“插足”的机会。但是实际上很多程序都会接受用户的外界输入,尤其是当函数内的一个数组缓冲区接受用户输入的时候,一旦程序代码未对输入的长度进行合法性检查的话,缓冲区溢出便有可能触发!比如下边的一个简单的函数。

void fun(unsigned char *data)
{
  unsigned char buffer[BUF_LEN];
  strcpy((char*)buffer,(char*)data);//溢出点
}

这个函数没有做什么有 “意义” 的事情(这里主要是为了简化问题),但是它是一个典型的栈溢出代码。在使用不安全的 strcpy 库函数时,系统会盲目地将 data 的全部数据拷贝到 buffer 指向的内存区域。buffer 的长度是有限的,一旦 data 的数据长度超过 BUF_LEN,便会产生缓冲区溢出。

img

图 3 缓冲区溢出

由于栈是低地址方向增长的,因此局部数组 buffer 的指针在缓冲区的下方。当把 data 的数据拷贝到 buffer 内时,超过缓冲区区域的高地址部分数据会 “淹没” 原本的其他栈帧数据,根据淹没数据的内容不同,可能会有产生以下情况:

1、淹没了其他的局部变量。如果被淹没的局部变量是条件变量,那么可能会改变函数原本的执行流程。这种方式可以用于破解简单的软件验证。

2、淹没了 ebp 的值。修改了函数执行结束后要恢复的栈指针,将会导致栈帧失去平衡。

3、淹没了返回地址。这是栈溢出原理的核心所在,通过淹没的方式修改函数的返回地址,使程序代码执行 “意外” 的流程!

4、淹没参数变量。修改函数的参数变量也可能改变当前函数的执行结果和流程。

5、淹没上级函数的栈帧,情况与上述 4 点类似,只不过影响的是上级函数的执行。当然这里的前提是保证函数能正常返回,即函数地址不能被随意修改(这可能很麻烦!)。

如果在 data 本身的数据内就保存了一系列的指令的二进制代码,一旦栈溢出修改了函数的返回地址,并将该地址指向这段二进制代码的其实位置,那么就完成了基本的溢出攻击行为。

img

图 4 基本栈溢出攻击

通过计算返回地址内存区域相对于 buffer 的偏移,并在对应位置构造新的地址指向 buffer 内部二进制代码的其实位置,便能执行用户的自定义代码!这段既是代码又是数据的二进制数据被称为 shellcode,因为攻击者希望通过这段代码打开系统的 shell,以执行任意的操作系统命令 —— 比如下载病毒,安装木马,开放端口,格式化磁盘等恶意操作。

四、栈溢出攻击

上述过程虽然理论上能完成栈溢出攻击行为,但是实际上很难实现。操作系统每次加载可执行文件到进程空间的位置都是无法预测的,因此栈的位置实际是不固定的,通过硬编码覆盖新返回地址的方式并不可靠。为了能准确定位 shellcode 的地址,需要借助一些额外的操作,其中最经典的是借助跳板的栈溢出方式。

根据前边所述,函数执行后,栈指针 esp 会恢复到压入参数时的状态,在图 4 中即 data 参数的地址。如果我们在函数的返回地址填入一个地址,该地址指向的内存保存了一条特殊的指令 jmp esp—— 跳板。那么函数返回后,会执行该指令并跳转到 esp 所在的位置 —— 即 data 的位置。我们可以将缓冲区再多溢出一部分,淹没 data 这样的函数参数,并在这里放上我们想要执行的代码!这样,不管程序被加载到哪个位置,最终都会回来执行栈内的代码。

img

图 5 借助跳板的栈溢出攻击

借助于跳板的确可以很好的解决栈帧移位(栈加载地址不固定)的问题,但是跳板指令从哪找呢?“幸运” 的是,在 Windows 操作系统加载的大量 dll 中,包含了许多这样的指令,比如 kernel32.dll,ntdll.dll,这两个动态链接库是 Windows 程序默认加载的。如果是图形化界面的 Windows 程序还会加载 user32.dll,它也包含了大量的跳板指令!而且更 “神奇” 的是 Windows 操作系统加载 dll 时候一般都是固定地址,因此这些 dll 内的跳板指令的地址一般都是固定的。我们可以离线搜索出跳板执行在 dll 内的偏移,并加上 dll 的加载地址,便得到一个适用的跳板指令地址!

//查询dll内第一个jmp esp指令的位置
int findJmp(char*dll_name)
{
  char* handle=(char*)LoadLibraryA(dll_name);//获取dll加载地址
  for(int pos=0;;pos++)//遍历dll代码空间
  {
    if(handle[pos]==(char)0xff&&handle[pos+1]==(char)0xe4)//寻找0xffe4 = jmp esp
    {
      return (int)(handle+pos);
    }
  }
}

这里简化了搜索算法,输出第一个跳板指令的地址,读者可以选取其他更合适位置。LoadLibraryA 库函数返回值就是 dll 的加载地址,然后加上搜索到的跳板指令偏移 pos 便是最终地址。jmp esp 指令的二进制表示为 0xffe4,因此搜索算法就是搜索 dll 内这样的字节数据即可。

虽然如此,上述的攻击方式还不够好。因为在 esp 后继续追加 shellcode 代码会将上级函数的栈帧淹没,这样做并没有什么好处,甚至可能会带来运行时问题。既然被溢出的函数栈帧内提供了缓冲区,我们还是把核心的 shellcode 放在缓冲区内,而在 esp 之后放上跳转指令转移到原本的缓冲区位置。由于这样做使代码的位置在 esp 指针之前,如果 shellcode 中使用了 push 指令便会让 esp 指令与 shellcode 代码越来越近,甚至淹没自身的代码。这显然不是我们想要的结果,因此我们可以强制抬高 esp 指针,使它在 shellcode 之前(低地址位置),这样就能在 shellcode 内正常使用 push 指令了。

img

图 6 调整 shellcode 与栈指针

调整代码的内容很简单:

add esp,-X
jmp esp

第一条指令抬高了栈指针到 shellcode 之前。X 代表 shellcode 起始地址与 esp 的偏移。如果 shellcode 从缓冲区起始位置开始,那么就是 buffer 的地址偏移。这里不使用 sub esp,X 指令主要是避免 X 的高位字节为 0 的问题,很多情况下缓冲区溢出是针对字符串缓冲区的,如果出现字节 0 会导致缓冲区截断,从而导致溢出失败。

第二条指令就是跳转到 shellcode 的起始位置继续执行。(又是 jmp esp!)

通过上述方式便能获得一个较为稳定的栈溢出攻击。

五、shellcode 构造

shellcode 实质是指溢出后执行的能开启系统 shell 的代码。但是在缓冲区溢出攻击时,也可以将整个触发缓冲区溢出攻击过程的代码统称为 shellcode,按照这种定义可以把 shellcode 分为四部分:

1、核心 shellcode 代码,包含了攻击者要执行的所有代码。

2、溢出地址,是触发 shellcode 的关键所在。

3、填充物,填充未使用的缓冲区,用于控制溢出地址的位置,一般使用 nop 指令填充 ——0x90 表示。

4、结束符号 0,对于符号串 shellcode 需要用 0 结尾,避免溢出时字符串异常。

前边一直在围绕溢出地址讨论,并解决了 shellcode 组织的问题,而最核心的代码如何构造并未提及 —— 即攻击成功后做的事情。其实一旦缓冲区溢出攻击成功后,如果被攻击的程序有系统的 root 权限 —— 比如系统服务程序,那么攻击者基本上可以为所欲为了!但是我们需要清楚的是,核心 shellcode 必须是二进制代码形式。而且 shellcode 执行时是在远程的计算机上,因此 shellcode 是否能通用是一个很复杂的问题。我们可以用一段简单的代码实例来说明这个问题。

缓冲区溢出成功后,一般大家都会希望开启一个远程的 shell 控制被攻击的计算机。开启 shell 最直接的方式便是调用 C 语言的库函数 system,该函数可以执行操作系统的命令,就像我们在命令行下执行命令那样。假如我们执行 cmd 命令 —— 在远程计算机上启动一个命令提示终端(我们可能还不能和它交互,但是可以在这之前建立一个远程管道等),这里仅作为实例测试。

为了使 system 函数调用成功,我们需要将 “cmd” 字符串内容压入栈空间,并将其地址压入作为 system 函数的参数,然后使用 call 指令调用 system 函数的地址,完成函数的执行。但是这样做还不够,如果被溢出的程序没有加载 C 语言库的话,我们还需要调用 Windows 的 API Loadlibrary 加载 C 语言的库 msvcrt.dll,类似的我们也需要为字符串 “msvcrt.dll” 开辟栈空间。

xor ebx,ebx ;//ebx=0

push 0x3f3f6c6c ;//ll??
push 0x642e7472 ;//rt.d
push 0x6376736d ;//msvc
mov [esp+10],ebx ;//'?'->'0'
mov [esp+11],ebx ;//'?'->'0'
mov eax,esp ;//"msvcrt.dll"地址
push eax ;//"msvcrt.dll"
mov eax,0x77b62864 ;//kernel32.dll:LoadLibraryA
call eax ;//LoadLibraryA("msvcrt.dll")
add esp,16

push 0x3f646d63 ;//"cmd?"
mov [esp+3],ebx ;//'?'->'\0'
mov eax,esp;//"cmd"地址
push eax ;//"cmd"
mov eax,0x774ab16f ;//msvcrt.dll:system
call eax ;//system("cmd")
add esp,8

上述汇编代码实质上是如下两个函数调用语句:

Loadlibrary(“msvcrt.dll”);
system(“cmd”);

不过在构造这段汇编代码时需要注意不能出现字节 0,为了填充字符串的结束字符,我们使用已经初始化为 0 的 ebx 寄存器代替。另外,在对库函数调用的时候需要提前计算出函数的地址,如 Loadlibrary 函数的 0x77b62864。计算方式如下:

int findFunc(char*dll_name,char*func_name)
{
  HINSTANCE handle=LoadLibraryA(dll_name);//获取dll加载地址
  return (int)GetProcAddress(handle,func_name);
}

这个函数地址是在本地计算的,如果被攻击计算机的操作系统版本差别较大的话,这个地址可能是错误的。不过在《0day安全:软件漏洞分析技术》中,作者提供了一个更好的方式,感兴趣的读者可以参考该书提供的代码。因此构造一个通用的shellcode 并非十分容易,如果想让攻击变得有效的话。

六、汇编语言自动转换

写出 shellcode 后(无论是简单的还是通用的),我们还需要将这段汇编代码转换为机器代码。如果读者对 x86 汇编十分熟悉的话,选择手工敲出二进制代码的话也未尝不可。不过我们都希望能让计算机帮助做完这些事,既然开发环境提供了编译器,用它们帮忙何乐而不为呢?既不用 OllyDbg 工具,也不适用其他的第三方工具,我们写一个简单的函数来完成这个工作。

//将内嵌汇编的二进制指令dump到文件,style指定输出数组格式还是二进制形式,返回代码长度
int dumpCode(unsigned char*buffer)
{
  goto END ;//略过汇编代码
BEGIN:
  __asm
  {
    //在这里定义任意的合法汇编代码
    
  }
END:
  //确定代码范围
  UINT begin,end;
  __asm
  {
    mov eax,BEGIN ;
    mov begin,eax ;
    mov eax,END ;
    mov end,eax ;
  }
  //输出
  int len=end-begin;
  memcpy(buffer,(void*)begin,len);
    //四字节对齐
  int fill=(len-len%4)%4;
  while(fill--)buffer[len+fill]=0x90;
  //返回长度
  return len+fill;
}

因为C++是支持嵌入式汇编代码的,因此在函数内的汇编代码都会被整成编译为二进制代码。实现二进制转换的基本思想是读取编译器最终生成的二进制代码段数据,将数据导出到指定的缓冲区内。为了锁定嵌入式汇编代码的位置和长度,我们定义了两个标签BEGIN和END。这两个标签在汇编语言级别会被解析为实际的线性地址,但是在高级语言级是无法直接使用这两个标签值的,只能使用goto语句跳转使用它们。但是我们可以顺水推舟,使用两个局部变量在汇编级记录这两个标签的值!

//确定代码范围
UINT begin,end;
__asm
{
  mov eax,BEGIN ;
  mov begin,eax ;
  mov eax,END ;
  mov end,eax ;
}

这样就可以得到嵌入式汇编的代码范围了,使用 memcpy 操作将代码数据拷贝到目标缓冲区即可(后边还用 nop 指令将代码按照四字节对齐)。不过我们还需要注意一个问题,嵌入式汇编在函数执行时也会执行,这显然不可以,我们只是把它当作数据而已(是数据?还是代码?),因此在函数开始的地方我们使用 goto 语句直接跳转到嵌入式会变语句的结尾 ——END 标签!

七、攻击测试

按照上述内容,相信不难构造出一个简单的 shellcode 并攻击之前提供的漏洞函数。但是如果使用 VS2010 测试的话可能会碰到很多问题。经过大量的调试和资料查询,我们需要设置三处 VS 的项目属性。

1、配置 -> 配置属性 ->C/C+±> 基本运行时检查 = 默认值,避免被检测栈帧失衡。

2、配置 -> 配置属性 ->C/C+±> 缓冲区安全检查 = 否,避免识别缓冲区溢出漏洞。

3、配置 -> 配置属性 -> 链接器 -> 高级 -> 数据执行保护 (DEP)= 否,避免堆栈段不可执行。

从这三处设置看来,目前的编译器已经针对缓冲区溢出攻击做了大量的保护工作(显然这会降低程序的执行性能,因此允许用户配置),使得传统的缓冲区溢出攻击变得没那么 “猖狂” 了,但是在计算机安全领域,“道高一尺,魔高一丈”,总有人会找到更隐蔽的攻击方式让编译器开发者措手不及。本文除了分析缓冲区溢出攻击的原理之外,更希望读者能从中感受到代码安全的重要性,并结合编译器提供的安全功能让自己的代码更加安全高效。


via:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值