最近学习中安网培提供的缓冲区溢出基础课程,记录了些在进行缓冲区溢出定位学习的体会,与大家分享一下,由于是初学难免有错误,希望各位多多指点。:)
缓冲区溢出原理:
就像老师讲的倒啤酒的例子,向一个杯子里灌啤酒,杯子装不下了,啤酒就溢出了。在计算机内存中,当某个数据,超过了处理程序限制的范围时,该数据就会造成程序的执行溢出(overflow)。
但计算机是怎样存储数据的,为什么可能造成程序限制的范围呢?进程是如何组织内存中的数据的?
所以我觉得在掌握缓冲区溢出基础之前,应对内存中数据存储方式有所了解。
从内存存储分配的角度看,缓冲区就是一段连续分配的内存空间。在程序的函数调用中,缓冲区一般通过堆栈来实现。堆栈是一个后进先出的队列,它的生长方向与内存的生长方向正好相反,正因为这一特点使得溢出攻击可能实现。我通过一个例子谈谈我对堆栈存贮的理解:
假设程序中有一个函数overflow(buf),当程序调用这个函数时,计算机会将调用此函数的一些地址和参数压入堆栈中,其顺序为:buf,overflow函数返回地址(以下简称ret),EBP寄存器指向的地址(以下简称EBP),如果函数中有局部变量,接下来会在堆栈中再分配这些局部变量空间。当函数调用结束时,局部变量空间将被丢弃(但不会清空),然后弹出EBP恢复调用函数前堆栈帧的指向,最后弹出返回地址(ret)到EIP继续执行下一步主程序。
总结一下:
调用函数时堆栈中数据存储为(栈顶->栈底):局部变量->EBP->ret->buf。
可以看到,当程序如果使用局部变量时,其长度大于预分配给局部变量空间的时候,多余长度的字符将按照内存存贮方向填充,而前面提到堆栈的生长方向与内存生长方向相反,于是EBP,ret就可能被多余的字符所覆盖,在函数返回时ret已不是原来的值,而是一个错误的值,这个值被弹入EIP后,cpu可不管它是不是原来的地址,就照EIP指向的地址去执行程序,可想而知这个错误地址指向的内存地方不是正确的主程序下一步语句,错误也就出现了,就是所说的溢出了,呵呵。
如果我们精心设计一个这种多余的字符,使得它所覆盖的ret地址能够指向我们自己编写的shellcode并加以执行,也就达到了我们利用缓冲区溢出漏洞的目的。
了解了溢出产生的原因,当我们遇到存在这种漏洞的程序并打算加以利用时,就要准确确定这个溢出点,也就是需要填充多少字符刚好能够覆盖我们需要的ret。
由于水平有限,还不足以分析存在缓冲区漏洞的较大程序(惭愧,惭愧),因此参考教学视频写了一个模拟的小例子,练习一下中安网培课程中介绍的利用报错对话框来确定溢出点的方法。
第一步:
test1.c
#include
#include
int overflow(char *buf)
{
char output[8];
strcpy(output,buf);
printf("buf的地址是:%p\n",buf);
printf("output内容为:%s\n",output);
return 0;
}
int main()
{
char buf[26];
for(int j=0;j<26;j++)
{
buf[j]='A';
}
overflow(buf);
return 0;
}
运行后发现溢出:

第二步:
修改buf的内容为:
#include
#include
int overflow(char *buf)
{
char output[8];
strcpy(output,buf);
printf("buf的地址是:%p\n",buf);
printf("output内容为:%s\n",output);
return 0;
}
int main()
{
char buf[26];
for(int j=0;j<26;j++)
{
buf[j]=97+j%10;
}
overflow(buf);
return 0;
}
编译执行缓冲溢出:
第三步:
将buf的内容改为整除:
#include
#include
int overflow(char *buf)
{
char output[8];
strcpy(output,buf);
printf("buf的地址是:%p\n",buf);
printf("output内容为:%s\n",output);
return 0;
}
int main()
{
char buf[26];
for(int j=0;j<26;j++)
{
buf[j]=97+j/10;
}
overflow(buf);
return 0;
}
编译执行溢出:

结果分析:
第一步中我们在buf中填充了26个A,导致溢出产生;
第二步中,buf中仍然填充了26个字符,但此时字符是有规律的按照a~j(十进制ASCII码为97~107)循环连接成的一个长度为26的字符串。为什么是这样?因为我们用10取余,余数一定不会大于10,加上97,其范围就为97~107啦。呵呵
第三步中,我们将j用10整除,那就是将这个长度为26的字符串按10位一段,分成了三段,各段中填充的字符相同(见图中的output内容为)
后两步中溢出出错的地址分别为0x66656463和0x62626262。
现在根据这两个地址来确定溢出点:第二步中由于是10个字符一循环的字符串,而其最末尾的溢出地址为0x63,而我们起始循环字符为0x61(十进制97),因此推算溢出点位置的尾数为0x63-0x61=2;第三步中,我们是逢10变换字符的,所得的出错地址落在0x62(十进制98)区间内,即在0x62-0x61=1段内。因此我们推算溢出点位置在第10×1+2=12字符处。
为了验证我们的结论,可以做个检验,我们将buf的第12个字符开始4个字节(EIP的长度为4个字节)的字符设为“LOVE",其余的均填为'A'。按照预计,那么在溢出出错时返回的地址应该为0x45564f4c(因为'L'是0x4c,'O'是0x4f,'V'是0x56,'E'是0x45,而buf入内存时是以字为单位,前面的字符入高位,显示出的地址为'VELO'顺序,呵呵)。
第四步,测试溢出:
#include
#include
int overflow(char *buf)
{
char output[8];
strcpy(output,buf);
printf("buf的地址是:%p\n",buf);
printf("output内容为:%s\n",output);
return 0;
}
int main()
{
char buf[26];
for(int j=0;j<26;j++)
{
buf[j]='A';
}
buf[12]='L';
buf[13]='O';
buf[14]='V';
buf[15]='E';
overflow(buf);
return 0;
编译运行结果为

与我们设想的一样,找到溢出点了。
找到溢出点后就可以进行shellcode的编写了。
以上是根据错误框提示来确定溢出点的方法。此法我是在windows平台下尝试,至于linux下是否可行,需尝试过才知道,希望有兴趣的朋友可以试试linux下是否可行,有结果可要通知我哦,我的QQ是164938(小心)。
这种方法比较巧妙,当然还有其它的方法,注意,在利用这种方法时尽量不要开启softice或trw之类的调试软件,可能会有一些误导的(本人就曾吃了这个亏:)
因为是菜鸟,才接触缓冲区溢出,上文错误之处请指出,也希望各位多多指点。