栈溢出

目标

学会使用淹没相邻变量或返回地址的方法利用缓冲区溢出漏洞。

测试步骤与结果

本次实验的源码如下所示

stackvar.c
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
31
32
33
#include <stdio.h>
#include <string>
#define PASSWORD "1234567"

int verify_password (char *password)
{
int authenticated;
char buffer[8];// add local buff
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//overflowed 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;
}
}
}

验证缓冲区溢出的发生

  1. 正常情况下,输入正确密码,程序提示 “输入正确”;输入错误密码(少于 8 位),程序提示 “输入错误”。
  2. 进入 ollydbg,在函数调用处打断点。开始运行程序,输入一个较短的密码,就以 "444" 为例。
  3. strcpy 的调用点在 0x401055 处。
  4. 输入的密码存储在 0x12fb7c 处,要拷贝到 0x12fb18 处。
  5. 0x12fb18 处的数据如下,显示为 00 34 34 34,恰好是 "4" 的 ascii 码十六进制表示

淹没相邻变量改变程序流程

拷贝前后,内存的变化如下

已经观察到 0x12fb20 位置的变量被覆盖了

拷贝前后,内存变化如下

0x12fb20 的值被覆盖成了 0x00000000。导致函数返回值为零,认证通过。

虽然也淹没了 0x12fb20 处的 authenticated 变量,但是我们输入的密码小于 1234567strcmp 会返回 -1-1 是用补码表示的,末尾的 \0 只可以淹没 -1 补码的后两位,程序不会向我们预想的方向走去。

淹没返回地址改变程序流程

为了方便调试,我们使用文件来输入 “密码”。

1
2
3
4
5
6
if(!fp=fopen("password.txt","rw+"))
{
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
  1. 找到输入点。这个地址跳转到 “验证成功” 的输出上。
  2. 先填满 8 字节的 buffer 数组,4 字节的 authenticated 变量,4 字节的 ebp,接下来的 4 个字节我们就可以放上我们的返回地址。
  3. 运行程序,显示 “验证成功”,但由于直接跳转地址破坏了栈平衡,程序崩溃。 此时的栈结构如此图所示

测试结论

覆盖的过程中发生了什么?我认为可以用以下几张图来表示

两种溢出方法都是比较“危险”的,一旦被利用,会造成意想不到的后果。轻则引起程序崩溃,重则引发安全漏洞。

思考题

程序的源码如下

stackOverrun.c
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
31
32
33
34
35
36
37
38
39
/*
StackOverrun.c
This program shows an example of how a stack-based
buffer overrun can be used to execute arbitrary code. Its
objective is to find an input string that executes the function bar().
*/
#include <stdio.h>
#include <string.h>

void foo(const char* input)
{
char buf[10];

//What? No extra arguments supplied to printf?
//It's a cheap trick to view the stack 8-)
//We'll see this trick again when we look at format strings.
printf("My stack looks like:\n%p\n%p\n%p\n%p\n%p\n% p\n%p\n%p\n%p\n%p\n\n");

//Pass the user input straight to secure code public enemy #1.
strcpy(buf, input);
printf("%s\n", buf);

printf("Now the stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n");
}

void bar(void)
{
printf("Augh! I've been hacked!\n");
}

int main(int argc, char* argv[])
{
//Blatant cheating to make life easier on myself
printf("Address of foo = %p\n", foo);
printf("Address of bar = %p\n", bar);

foo(argv[1]);
return 0;
}

本次的目标是通过栈溢出,想办法使 bar 函数得到执行。

  1. 程序通过命令行参数执行了 foo 函数,在 foo 函数中,有一个 10 个字节长的 buf 字符数组,在第 14 行中发生了未经检查的无界向有界拷贝的行为,很容易引发溢出。分析到这里,思路就很简单了:参数中先用任意字符填满 buf
    ,然后再填入 4 字节的 bar 函数的地址。

  2. 先调试,填上一个较短的参数看看发生了什么

  3. 发现我们给的这个参数在 DS 段中存储着,而且运行时程序会打印出 foobar 函数的地址值。所以 bar 函数地址我们已经有了。

  4. 我们的命令行参数从 DS 段拷贝到了 0x12ff60 的位置。

  5. 构造 payload 时要注意考虑到内存的对齐因素,缓冲区是 10 个字节长没错,但是填充时要淹没掉 0x40109b 的返回地址值,又要使最后 4 个字节为 bar 函数的地址值,所以要填上 12 个任意字符。综合考虑,构造 payload 如下。

  6. 把这个值复制到调试命令行中,重启程序

  7. 此时已经可以看到 0x12ff6c 中存储的返回地址值变成了 0x401060。前面的 0x12ff60~0x12ff68 也已悉数占满。

  8. F8 单步跟下来,可以发现程序紧接着跳到了 0x401060bar 函数的地址,目标达到了。紧接着程序就因为栈不平衡而崩溃了。

  9. 覆盖返回地址法利用全过程图示

接下来,我们使用 jmp esp 的方法来完成程序的破解。

  1. 使用 od 自带的插件 overflow return address 寻找可用的 jmp esp 地址,查找结果如下

  2. 本次实验选用了一个位于 ntdll.text 段的 jmp esp,地址为 77f8948bh

    根据 jmp esp 构造相关知识,构造 payload 为 12 个填充字符 "a"+jmp esp 地址 0x77f8948b(大端书写)+shellcode(call bar() 的机器码)。下面简单看一下运行该参数之后,栈的情况。

  3. foobar 的地址输出等同于方法一

  4. 将命令行的内容复制到缓冲区时,观察输出,可以发现 0x12ff6c 被覆盖成了 jmp esp 的地址

  5. 在函数 return 之前,注意到它即将返回到一个内核地址 77f8948b

  6. 陷入内核的一瞬间,可以看到 eip 指针所指向的位置上,命令为 jmp esp。观察右侧知 esp 现在的值为 12ff70h,接下来,程序到 12ff70h 执行内容。

  7. 跳转到 12ff70 地址后,其中所写入的数据都被当作指令来执行,我们的 shellcode 被成功解析为了 call 指令。

  8. 果然它紧接着调用了 00401060 处的 bar 函数。

  9. 调用完成后,它返回了 12ff70 处,然后继续向下执行指令,直到崩溃。

    如果没有任何中断的话,接下来地址的所有的 hex 数据都会按照命令来执行,相当危险……

  1. 刚才执行的过程中成功产生预期输出。

  2. 刚才的 payload 可以正常在命令行中传参执行,成功触发 bar 函数。

  3. jmp esp 法利用全过程图示