龙盟编程博客 | 无障碍搜索 | 云盘搜索神器
快速搜索
主页 > 软件开发 > C/C++开发 >

通过覆盖__atexit进行缓冲区溢出攻击

时间:2009-12-22 15:42来源:未知 作者:admin 点击:
分享到:
通过覆盖__atexit进行缓冲区溢出攻击 --静态编译版本的heap溢出 原作者: Pascal BoUChareine 原文: specific proof of concept with statically linked binaries and heap overflows>> 译者注:这片文章可能很早就出

  通过覆盖__atexit进行缓冲区溢出攻击

  

--静态编译版本的heap溢出

  

  

原作者: Pascal BoUChareine

  

原文: <<__atexit in memory bugs -

  

specific proof of concept with statically linked binaries and heap overflows>>

  

  

  

  

译者注:这片文章可能很早就出来了,我看国内也没有人介绍,干脆就

  

翻译出来一块儿共享吧,如有什么错误的地方,欢迎指正。

  

mailto:alert7@21cn.com

  

  

介绍:

  

本文讨论了类似通过覆盖.dtors进行缓冲区溢出攻击的技术。归根结底

  

是想方设法改变程序的执行流程,使之最终执行我们想要执行的代码。本文

  

假设读者熟悉普通的缓冲区溢出技术。

  

  

  

鸣谢:

  

感谢Andrew R. Reiter看了这片文档,纠正一些错误。

  

  

  

内容:

  

  

I. atexit()的基本知识

  

II. atexit()的执行

  

III. EXPloitation的概念

  

IV. Eggshell的定位

  

V. 一个exploit的例子

  

  

  

  

I. atexit()基本知识

  

  

先让我们看看手册:

  

  

NAME

  

atexit - 注册一个在exit时候被调用的函数

  

  

SYNOPSIS

  

#include

  

  

int

  

atexit(void (*function)(void))

  

  

DESCRIPTION

  

atexit()函数注册一个给定的函数,该函数在程序exit时候被调用

  

(不管是通过exit(3)或者还是通过从程序的main函数中返回)。

  

注册的函数是反序被调用的;没有参数。至少32个函数总是可以被注册

  

的,只要有充分的分配的内存,更多的函数也是答应的。

  

  

看看下面程序的基本指令:

  

  

char *glob;

  

  

void test(void)

  

{

  

printf("%s", glob);

  

}

  

  

void main(void)

  

{

  

atexit(test);

  

glob = "Exiting.

";

  

}

  

当执行时,应该在标准输出上显示"Exiting" .

  

  

II. atexit()的执行

  

  

atexit是做为libc函数导出的。

  

执行过程使用了一个静态的atexit结构,该结构包含了那些在退出时候被

  

调用的函数的一个数组,在调用atexit函数的时候会插入一个结构(我们

  

将称它为"fns"),在fns中有一个变量保存着下一个空的索引(我们称

  

它为"ind"),当fns满的时候,一个指针(我们称为next)指向了下一个

  

被使用的atexit结构.

  

  

struct atexit {

  

struct atexit *next; /* next in list */

  

int ind; /* next index in this table */

  

void (*fns[ATEX99v_SIZE])(); /* the table itself */

  

};

  

  

当atexit()被调用时,它填充fns[ind],增加ind,这时ind就是下一个在fns中空的索引。

  

当fns满的时候,一个新的atexit的结构被分配,并且它的next变量指向了最后

  

被使用的那个。

  

  

注重:一般atexit的使用的是不需要next的,它在初始化的时候被设置为

  

NULL。

  

  

当exit()被调用,它分析最后定义的atexit结构,并且执行在fns[ind]中的

  

函数,减少ind,依次执行。

  

  

当exit()被调用的时候,需要查看一些退出函数,然而,atexit()需要写它,

  

atexit结构被分配是做为一个全局符号的,(在*bds上是__atexit, 在Linux上

  

是__exit_funcs), 并且导出给其他函数的。

  

  

译者注:假如你第一次读这片文章,你可能会忽视了atexit()和__atexit

  

(在*bds上是__atexit, 在linux上是__exit_funcs)的关系。

  

  

__atexit就是被atexit函数使用的一个内部变量,下面有个图指

  

示了atexit()如何利用__atexit的。

  

  

III. Exploitation的概念

  

  

这部分不是很准确。需要依靠执行时候的内存映象,依靠你的OS,还受许多

  

其他的因数的影响。

  

  

我们首先要知道__atexit在内存中的分配地址,判定那里是可以重写的地址。所以我

  

写了个简单的例子。

  

  

extern void * __atexit;

  

  

int main(void)

  

{

  

static char scbuf[128];

  

char *mabuf;

  

  

mabuf = (char *) malloc(128);

  

  

printf("__atexit at %p

", __atexit);

  

printf("malloced at %p

", mabuf);

  

printf("static at %p

", scbuf);

  

return 0;

  

}

  

  

编译一下,有以下的结果:

  

  

pb@nod [405]$ gcc -o at at.c

  

pb@nod [406]$ ./at

  

__atexit at 0x280e46a0

  

malloced at 0x804b000

  

static at 0x8049660

  

  

pb@nod [407]$ gcc -o at -static at.c

  

pb@nod [408]$ ./at

  

__atexit at 0x8052ea0

  

malloced at 0x8055000

  

static at 0x8052e20

  

  

  

以上已经足够说明问题了.可许你已经知道,动态编译的版本是通过一个

  

mmap()调用来装载libc库函数的。 (0x280e46a0)现在看起来是我们不能修改

  

的, 但是静态版本是可以的。

  

  

在静态编译的二进制中,libc被保存在程序的heap区,因此,__atexit的位置

  

在我们的静态scbuf四周。在这个例子中,__atexit和scbuf相差0x80个字节。

  

它意味着他们是位置连续的。假如你了解heap溢出,构造它应该不是件很难的

  

事情。

  

  

在静态的字符缓冲区后面构造自己的atexit结构,覆盖__atexit变量,可以使

  

exit()执行在内存中的任何地方。比如执行我们的eggshell。为了构造它,我

  

们需要明白atexit()是如何利用__atexit变量的,看下面类似gdb的输出:

  

  

0 127 128 132 136 140

  

(an eggshell with nops) (next) (ind) (fns[0]) (fns[1])

  

0x90909090 ..... 0x00000000 0x00000001 0xbffff870 0x00000000

  

  

for (p = __atexit; p; p = p->next)

  

for (n = p->ind; --n >= 0;)

  

(*p->fns[n])();

  

  

第一种方法你可以使'ind'为正值,比如上面图使ind为1,fns[0]为

  

eggshell的地址,但是这样构造出来的atexit结构中包含了'\0'。我

  

们没有办法使用。

  

  

第二种方法是使p->next指向一块我们精心构造的atexit结构的内存。

  

我们仅仅需要使ind为负的,可以不管fns的数组。

  

  

但是,我们到底如何找到那块空间呢?

  

  

IV. Eggshell的定位

  

  

我要为这件事干一两杯啤酒。

  

  

读了execue的手册和内核execve的执行过程,使我想起了我写的第一个

  

c语言程序。我们知道,argc是参数的个数,argv是以null结尾的数组

  

(包含了以null结尾的字符串),envp是环境变量。一个正在执行的程序

  

要得到这些信息是轻易的。

  

因此,在stack的顶部,一个 "vector table" 包含了这些信息当然还包括一些

  

其他的(例如信号掩码)。让我们看看在stack上的argv的存放:

  

  

0xbfbffb60: 0x00000000 0x00000005 0xbfbffc5c 0xbfbffc84

  

0xbfbffb70: 0xbfbffc8a 0xbfbffc8f 0xbfbffc92 0x00000000

  

  

在该例子中,argc是5。有5个指针指向5个argv元素。最后一个是以NULL结尾的。

  

  

上面看到的,使你想起那个atexit结构了吗?:)

  

  

  

该图完美的描绘了atexit的结构!ind=5,argv[4]是被调用函数的地址。所有

  

的工作预备就绪,但是还差点。我们只要猜测在stack上的 vector table的

  

正确地址就可以了,在__atexit->next填上该地址,在__atexit->ind填上负的,

  

这样一切都OK了。

  

  

猜测argv[]的地址需要依靠你的OS。我看了一下/sys/kern/kern_exec.c,

  

读了一下这个函数:

  

  

  

/*

  

* Copy strings out to the new process address space, constructing

  

* new arg and env vector tables. Return a pointer to the base

  

* so that it can be used as the initial stack pointer.

  

*/

  

register_t *

  

exec_copyout_strings(imgp)

  

  

  

这个函数解释了如何计算argv的vector table地址,你的计算基于地址PS_STRING

  

(stack的基地址,less结构的ps_string大小),信号掩码的大小,"SPARE_USERSPACE"

  

这个变量在我的freebsd上被定义256(可能这个变量被setproctitle()函数使用),和一些

  

其他复杂的东西。

  

  

为了使用可移植的计算方法,我使用了下面自我调用的方法来执行argv[]。

  

首先,假如你要利用有问题的程序的话,你需要把条件都预备好。但是不能以

  

非凡的参数调用自己。在第二次调用时,argv应该正确的被定位,然后再调

  

用有问题的程序。

  

  

有了这两个技术,我想你应该有了一个高效率的缓冲区溢出的方法,而不再需要

  

计算offset了。

  

  

译者注:这种两次execve技术很不错,两次execve出来的进程的argv的地址是

  

一样的。所以就不需要猜测argv的地址了

  

  

注重:对于format bug来说,这个技术听起来很强大。_

  

精彩图集

赞助商链接