栈溢出浅析

To be the apostrophe which changed“ Impossible” into“ I’ m possible” ——Failwest

一.前言

每周末最起码得写点东西吧,算是给自己的笔记吧,要不然怎么对得起这两天free time呢! 所以这篇就诞生了:)

二.系统栈的工作原理

先扔图,网上看到了张图,觉得很形象很生动很详细了

依据图中,我们先来看看内存的功能吧。根据不同的操作系统,一个进程可能被分配到不同的内存区域去执行。但是不管什么样的操作系统、什么样的计算机架构,进程使用的内存都可以按照功能大致分成以下 4 个部分。

  • 代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指并执行。
  • 数据区:用于存储全局变量等。
  • 堆区:进程可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。动态分配和回收是堆区的特点。
  • 栈区:用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行。

这种简单的内存划分方式是为了让您能够更容易地理解程序的运行机制。

为了更好的理解进程的内存使用,我们来贴出0day书上的这个图吧

在了解了内存相关知识后,接下来,我们再来看看栈,栈是一个数据结构,可以添加或者删除数据,但是得遵循“后进先出的原则,它是通过push操作来把数据压入栈中,通过pop操作删除数据。栈可以实现为一个数组,总是从数组的一端插入和删除元素。

我们来看0day中的一个demo,甩出代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
intfunc_B(int arg_B1, int arg_B2)
{
int var_B1, var_B2;
var_B1=arg_B1+arg_B2;
var_B2=arg_B1-arg_B2;
return var_B1*var_B2;
}
intfunc_A(int arg_A1, int arg_A2)
{
int var_A;
var_A = func_B(arg_A1,arg_A2) + arg_A1 ;
return var_A;
}
int main(int argc, char **argv, char **envp)
{
int var_main;
var_main=func_A(4,3);
return var_main;
}

我们来看看该函数在代码区中的分布示意图,甩图

当 CPU 在执行调用 func_A 函数的时候,会从代码区中 main 函数对应的机器指令的区域跳转到 func_A 函数对应的机器指令区域,在那里取指并执行;当 func_A 函数执行完闭,需要返回的时候,又会跳回到 main 函数对应的指令区域,紧接着调用 func_A 后面的指令继续执行main 函数的代码。来看看取指轨迹示意图:

这些代码区中精确的跳转都是在于系统栈巧妙地配合过程中完成的,当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并把它压入栈中。这个栈帧中的内存空间被它所属的函数独占,正常情况下是不会和别的函数共享的。当函数返回时,系统栈会弹出该函数所对应的栈帧。那在调用函数的过程中,伴随的系统栈中的操作如下:

程序的运行中每一个函数独占自己的栈帧空间,并且正在运行的函数的栈帧总是在栈顶,win32系统提供两个特殊的寄存器用于标识位于系统栈顶端的栈帧。

  • ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
  • EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

寄存器对栈帧的标识作用如图:

函数栈帧: ESPEBP 之间的内存空间为当前栈帧, EBP 标识了当前栈帧的底部, ESP标识了当前栈帧的顶部。

而在函数栈帧中,一般包含以下几类重要信息:

  • 局部变量:为函数局部变量开辟的内存空间。
  • 保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过堆栈平衡计算得到),用于在本帧被弹出后恢复出上一个栈帧。
  • 函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。

除了与栈相关的寄存器外,还有另一个至关重要的寄存器。

EIP:指令寄存器(Extended Instruction Pointer),其内存放着一个指针,该指针永远指向下一条等待执行的指令地址。

那从安全角度,EIP就很有意思,可以说如果控制了 EIP 寄存器的内容,就控制了进程。

最后,直接甩图函数调用的实现:

三.一个简单实例

说干就干,甩代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#define PASSWORD "1234567"
int verify_password (char *password)
{
int authenticated;
char buffer[8]; // add local buffto be overflowed
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}
main()
{
int valid_flag=0;
char password[1024];
while(1)
{
printf("please input password: ");
scanf("%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
break;
}
}
}

这是一个密码验证程序,来看一下该程序的栈帧布局:

正常来讲当我输入设定好的 "1234567"时,会验证通过;但是该程序段strcpy(buffer,password)存在栈溢出漏洞, 当输入的password`大于7个字符时会让buffer[8]数组越界,buffer[8],buffer[9]buffer[10]buffer[11]等......将写入相邻的变量authenticated中,进而越界字符的ASCII码会修改authenticated的值,从程序中就可以看到authenticated变量的值来源于strcmp函数的返回值,之后会返回给main函数作为密码验证成功与否的标志变量:当authenticated` 为 0 时,表示验证成功;反之,验证不成功。那如果这段溢出数据恰好把 authenticated 改为0,则程序流程将被改变。

我们首先来编译改程序,记得关闭linux的栈保护机制

1
2
3
4
gcc -o stack1 stack.c						// 默认情况下,不开启Canary保护
gcc -fno-stack-protector -o stack1 stack.c //禁用栈保护
gcc -fstack-protector -o stack1 stack.c //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o stack1 stack.c //启用堆栈保护,为所有函数插入保护代码

我们根据这个程序的漏洞来输入"qqqqqqqq"时,来看运行结果:

同样,通过验证。此时,我们可以分析到该栈帧数据:

局部变量名 内存地址 偏移 3 处的值 偏移 2 处的值 偏移 1 处的值 偏移 0 处的值
buffer[8] 0x0012FB18 0x71 (‘q’) 0x71 (‘q’) 0x71 (‘q’) 0x71 (‘q’)
0x0012FB1C 0x71 (‘q’) 0x71 (‘q’) 0x71 (‘q’) 0x71 (‘q’)
authenticated 被覆盖前 0x0012FB20 0x00 0x00 0x00 0x01
authenticated 被覆盖后 0x0012FB20 0x00 0x00 0x00 0x00

故而我们即使不知道正确的密码“ 1234567”,只要输入一个为8 个字符的字符串,那么字符串中隐藏的第 9 个截断符 NULL 就应该能够将 authenticated 低字节中的 1 覆盖成 0,从而绕过验证程序!

严格说来,并不是任何 8 个字符的字符串都能冲破上述验证程序。由代码中的 authenticated=strcmp(password,PASSWORD),我们知道 authenticated 的值来源于字符串比较函数 strcmp 的返回值。按照字符串的序关系,当输入的字符串大于"1234567"时,返回 1,这时 authenticated在内存中的值为 0x00000001,可以用字串的截断符 NULL淹没 authenticated 的低位字节而突破验证;当输入字符串小于"1234567"时(例如,"0123"等字符串),函数返回-1,这时 authenticated 在内存中的值按照双字-1 的补码存放,为 0xFFFFFFFF,如果这时也输入 8 个字符的字符串,截断符淹没 authenticated低字节后,其值变为 0xFFFFFF00,所以这时是不能冲破验证程序的。

四.参考

【1】0day 第二版

【2】https://kevien.github.io/2017/08/16/linux%E6%A0%88%E6%BA%A2%E5%87%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/

Finally!

###