FILE結(jié)構(gòu)體及漏洞利用方法

0x00 寫在前面

第一次接觸FILE結(jié)構(gòu)體遂庄,是在BCTF2018baby_arena中头岔,利用了FSOP塔拳,當(dāng)時感覺結(jié)構(gòu)體中字段太多了,沒有理清楚到底是什么關(guān)系峡竣。也是時隔非常非常非常久靠抑,咸魚的我終于開始系統(tǒng)的學(xué)習(xí)一下FILE結(jié)構(gòu)體。本篇基于libc-2.27源碼對該版本文件流的數(shù)據(jù)結(jié)構(gòu)适掰、相關(guān)操作以及一些常見的漏洞利用方法進行分析颂碧。

0x01 數(shù)據(jù)結(jié)構(gòu)

  • _IO_FILE_plus
    雖然每次都是說FILE結(jié)構(gòu)體,但其實FILE結(jié)構(gòu)體的外層還有一個結(jié)構(gòu)體类浪,叫做_IO_FILE_plus結(jié)構(gòu)體载城。該結(jié)構(gòu)體在glibc/libio/libioP.h中定義如下:
struct _IO_FILE_plus
{
    FILE file;
    const struct _IO_jump_t *vtable;
}

中間包含了一個我們常說的FILE結(jié)構(gòu)體,以及_IO_jump_t的一個虛表結(jié)構(gòu)體费就。

  • FILE/_IO_FILE
    glibc/libio/bits/types/FILE.h中可以看到FILE結(jié)構(gòu)體其實就是_IO_FILE結(jié)構(gòu)體诉瓦。
typedef struct _IO_FILE FILE;

_IO_FILE結(jié)構(gòu)體在glibc/libio/bits/libio.h中定義如下:

struct _IO_FILE
{
    int _flags;  /* High-order word is _IO_MAGIC; rest is flags. */

    /* The following pointers correspond to the C++ streambuf protocol. */
    char *_IO_read_ptr;        /* Current read pointer */
    char *_IO_read_end;      /* End of get area. */
    char *_IO_read_base;    /* Start of puback+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_FILE *_chain;
    int _fileno;
    ......
    int _flags2;
    ......
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
    struct _IO_FILE _file;
#endif
    ......
    size_t __pad5;
    int _mode;
    char _unused2[15 * sizeof(int) - 4 * sizeof(void*) - sizeof(size_t)];
};

其中_flags字段標(biāo)志了該FILE結(jié)構(gòu)體的讀寫等屬性,該字段的前2個字節(jié)固定為0xFBAD的魔術(shù)頭力细,其具體數(shù)值在glibc/libio/libio.h中進行宏定義如下:

/* Magic number and bits for the _flags field.  The magic number is
   mostly vestigial, but preserved for compatibility.  It occupies the
   high 16 bits of _flags; the low 16 bits are actual flag bits.  */
#define _IO_MAGIC         0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK    0xFFFF0000
#define _IO_USER_BUF          0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED        0x0002
#define _IO_NO_READS          0x0004 /* Reading not allowed.  */
#define _IO_NO_WRITES         0x0008 /* Writing not allowed.  */
#define _IO_EOF_SEEN          0x0010
#define _IO_ERR_SEEN          0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close.  */
#define _IO_LINKED            0x0080 /* In the list of all open files.  */
#define _IO_IN_BACKUP         0x0100
#define _IO_LINE_BUF          0x0200
#define _IO_TIED_PUT_GET      0x0400 /* Put and get pointer move in unison.  */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING      0x1000
#define _IO_IS_FILEBUF        0x2000
                           /* 0x4000  No longer used, reserved for compat.  */
#define _IO_USER_LOCK         0x8000

_IO_read_ptr字段為輸入緩沖區(qū)的當(dāng)前地址
_IO_read_end字段為輸入緩沖區(qū)的結(jié)束地址
_IO_read_base字段為輸入緩沖區(qū)的起始地址
_IO_write_base字段為輸出緩沖區(qū)的起始地址
_IO_write_ptr字段為輸出緩沖區(qū)的當(dāng)前地址
_IO_write_end字段為輸出緩沖區(qū)的結(jié)束地址
_IO_buf_base字段為輸入輸出緩沖區(qū)的起始地址
_IO_buf_end字段為輸入輸出緩沖區(qū)的結(jié)束地址
_chain字段為指向下一個_IO_FILE結(jié)構(gòu)體的指針睬澡,在gilbc/libio/libioP.h中有如下聲明:

extern struct _IO_FILE_plus *_IO_list_all;

該變量為一個單鏈表的頭結(jié)點,該單鏈表用于管理程序中所有的FILE結(jié)構(gòu)體眠蚂,并通過_chain字段索引下一個FILE結(jié)構(gòu)體煞聪,每個程序中該鏈表的最后3個節(jié)點從后往前固定為_IO_2_1_stdin_IO_2_1_stdout河狐、_IO_2_1_stderr米绕,之前是用戶新申請的FILE結(jié)構(gòu)體瑟捣,每次新申請的FILE結(jié)構(gòu)體會插在該鏈表的表頭。大概長成下面這樣:

_IO_list_all

值得注意的是栅干,在_IO_FILE結(jié)構(gòu)體定義的內(nèi)部有一個宏#ifdef _IO_USE_OLD_IO_FILE迈套,如果不存在_IO_USE_OLD_IO_FILE的宏定義,則會將后面的}以及下一個結(jié)構(gòu)體_IO_FILE_complete的定義頭給跳過碱鳞,即擴充了_IO_FILE結(jié)構(gòu)體桑李,使其擁有了更多的字段。_IO_2_1_stdin窿给、_IO_2_1_stdout贵白、_IO_2_1_stderrFILE結(jié)構(gòu)體均為擴展后的。比如某次調(diào)試中的_IO_2_1_stdout結(jié)構(gòu)如下(從_lock之后到vtable之前的字段均為擴展后的):
_IO_2_1_stdin_

  • _IO_jump_t
    該結(jié)構(gòu)體在glibc/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
};

如上圖所示崩泡,所有的FILE結(jié)構(gòu)體的虛表指針均指向虛表_IO_file_jumps禁荒,在進行IO操作時,都會調(diào)用到該結(jié)構(gòu)體中的函數(shù)角撞。

0x02 相關(guān)操作

fopen

fopenstdio庫中的函數(shù)呛伴,其在glibc/include/stdio.h中宏定義如下:

#define fopen(fname, mode) _IO_new_fopen(fname, mode)

stdio.h宏定義可知,平時我們常用的fopen函數(shù)其實為定義在glibc/libio/iofopen.c中的_IO_new_fopen函數(shù)谒所,該函數(shù)直接調(diào)用了__fopen_internal函數(shù)热康。

__fopen_internal

  • __fopen_internal函數(shù)中第58-65行聲明了一個locked_FILE結(jié)構(gòu)體變量指針new_f,該結(jié)構(gòu)體中主要包含了_IO_FILE_plus_IO_wide_data兩個結(jié)構(gòu)劣领,并為該聲明的變量分配了空間姐军。
  • __fopen_internal函數(shù)中第72行調(diào)用了_IO_no_init函數(shù)對新申請的locked_FILE結(jié)構(gòu)體進行了初始化,該函數(shù)在glibc/libio/genops.c中定義如下:
    _IO_no_init

    其中調(diào)用的_IO_old_init函數(shù)定義于其上方:
    _IO_old_init

    不難看出尖淘,_IO_old_init函數(shù)主要是對_IO_FILE_plus結(jié)構(gòu)體中的各個元素進行初始化奕锌,而_IO_no_init主要是對_IO_wide_data 結(jié)構(gòu)體中的各個元素進行初始化。通過兩個結(jié)構(gòu)體的初始化村生,初步猜測歇攻,_IO_FILE_plus結(jié)構(gòu)體中元素及虛表主要用于單字節(jié)的文件流處理流程中,_IO_wide_data結(jié)構(gòu)體中的元素及虛表主要用于寬字節(jié)的文件流處理流程中梆造。
  • __fopen_internal函數(shù)中第73行調(diào)用了_IO_JUMPS函數(shù)對結(jié)構(gòu)體的虛表進行了初始化,_IO_JUMPS函數(shù)在glibc/libio/libioP.h中宏定義如下:
#define _IO_JUMPS(THIS) (THIS)->vtable

即將_IO_FILE_plus結(jié)構(gòu)體中的虛表指針賦值為虛表_IO_file_jumps的地址葬毫。

  • __fopen_internal函數(shù)中第74行調(diào)用了_IO_new_file_init_internal函數(shù)將新初始化的結(jié)構(gòu)體鏈入_IO_list_all鏈表的頭部镇辉,該函數(shù)在glibc/libio/fileops.c中定義如下:
    _IO_new_file_init_internal

    該函數(shù)主要實現(xiàn)了將初始化好的_IO_FILE_plus結(jié)構(gòu)體鏈入_IO_list_all鏈表頭部的功能,其中鏈入鏈表的功能主要是由_IO_link_in函數(shù)進行實現(xiàn)贴捡,該函數(shù)定義在glibc/libio/genops.c中忽肛。除了實現(xiàn)鏈入功能外,還對_IO_FILE_plus結(jié)構(gòu)體加入了相應(yīng)的屬性烂斋,如CLOSED_FILEBUF_FLAGS(可關(guān)閉枫夺?)屬性以及_IO_link_in函數(shù)中的_IO_LINKED已鏈接屬性第115行,對_fileno函數(shù)賦值為-1列林,該字段代表該文件流在_IO_list_all鏈表中的序號联喘,此處賦值為-1相當(dāng)于對該字段進行一個非法數(shù)值的初始化,后面會有_IO_file_is_open函數(shù)專門對_fileno數(shù)值是否合法進行check切蟋。
  • __fopen_internal函數(shù)中第78行調(diào)用了_IO_file_open函數(shù),開始執(zhí)行真正意義上的fopen操作,該函數(shù)在glibc/libio/fileops.c中定義如下:
versioned_symbol(libc, _IO_new_file_fopen, _IO_file_fopen, GLIBC_2_1);

_IO_file_fopen函數(shù)等價于_IO_new_file_fopen函數(shù)蒿讥,該函數(shù)定義于同一文件的第211行(太長了就不一次性全部貼了)。

_IO_new_file_fopen(1)

_IO_new_file_fopen4個參數(shù)抛腕,分別是文件指針芋绸、文件名、屬性担敌、是否為32位摔敛,其中第一個參數(shù)為前面步驟初始化的_IO_FILE結(jié)構(gòu)體指針,第2全封、3兩個參數(shù)為用戶在調(diào)用stdio.hfopen函數(shù)傳入的參數(shù)马昙,第四個參數(shù)為glibc/libio、iofopen.c_IO_new_fopen函數(shù)調(diào)用__fopen_internal函數(shù)時傳入的常亮1售貌。該段代碼除了聲明變量外主要進行了2個操作:檢查該文件流是否打開给猾、根據(jù)調(diào)用參數(shù)的主屬性為該文件流添加flag
第一個操作通過調(diào)用_IO_file_is_open函數(shù)來實現(xiàn)颂跨,該函數(shù)在glibc/libio/libioP.h中宏定義如下:

#define _IO_file_is_open(__fp) ((__fp)->_fileno != -1)

即通過檢查FILE結(jié)構(gòu)體的_fileno是否為合法序號來判斷檢該文件流是否為已打開狀態(tài)敢伸。
第二個操作則是通過mode,即fopen函數(shù)第二個參數(shù)的第一個字符來確定該文件流的屬性恒削,并添加對應(yīng)的flag池颈。在寫入flag字段前,代碼中有3個比那里那個來分別存儲不同的屬性钓丰,這三個變量分別是omode躯砰、oflagsread_write携丁,其中omode標(biāo)志文件的讀寫屬性琢歇,oflags標(biāo)志文件的修改方式,read_write標(biāo)志文件內(nèi)容的讀寫方式梦鉴。有如下對應(yīng)關(guān)系:

mode omode oflags read_write
r O_RDONLY(只讀) NULL(無) _IO_NO_WRITES(不給寫)
w O_WRONLY(只寫) O_CREAT|O_TRUNC(新建/覆蓋) _IO_NO_READS(不給讀)
a O_WRONLY(只寫) O_CREAT|O_APPEND(新建/追加) _IO_NO_READS|_IO_IS_APPENDING(不給讀/給追加)

_IO_new_file_fopen繼續(xù)往后走李茫,代碼如下:

_IO_new_file_fopen(2)

該段代碼主要進行了2個操作:通過文件流副屬性獲取對應(yīng)的flag、調(diào)用_IO_file_open函數(shù)打開文件肥橙。
第一個操作與主屬性的表示相似魄宏,副屬性有如下的對應(yīng)關(guān)系:

mode omode oflags read_write _flags2
+ O_RDWR(可讀可寫) 不變 &_IO_IS_APPENDING 不變
x 不變 O_EXCL 不變 不變
b 不變 不變 不變 不變
m 不變 不變 不變 _IO_FLAGS32_MMAP
c 不變 不變 不變 _IO_FLAGS2_NOTCANCEL
e 不變 O_CLOEXEC 不變 _IO_FLAGS2_CLOEXEC

在將所有附屬性遍歷完后,會調(diào)用_IO_file_open函數(shù)用于打開文件并返回句柄存筏,該函數(shù)有6個參數(shù)宠互,該函數(shù)在glibc/libio/fileops.c中定義如下:

_IO_file_open

該函數(shù)中味榛,首先會判斷FILE結(jié)構(gòu)體的_flags2是否有_IO_FLAGS2_NOTCANCEL位,即是否含有c的副屬性予跌,若有則會調(diào)用__open_nocancel函數(shù)搏色,若無則會調(diào)用__open函數(shù),從這兩個函數(shù)傳入了相同的參數(shù)可以看出匕得,這兩個函數(shù)實現(xiàn)了相似的功能继榆,兩個函數(shù)在glibc/sysdeps/unix/sysv/linux/open64.c中有宏定義如下:

strong_alias(__libc_open64 ,  __open);
...
strong_alias(__open64_nocancel, __open_nocancel);

以及還有在某些情況__open64_nocancel函數(shù)可以等價為__libc_open64函數(shù)的定義。在同一文件中汁掠,兩個函數(shù)定義如下:

__libc_open64

__open64_nocancel

可以看到這兩個函數(shù)在return時均調(diào)用了INLINE_SYSCALL_CALL函數(shù)略吨,即到最后將帶有文件修改方式和讀寫屬性的flag作為參數(shù),調(diào)用SYSCALL進行打開文件操作考阱,并將句柄返回翠忠。(再往底層就是直接宏定義匯編代碼,就不繼續(xù)深究INLINE_SYSCALL_CALL函數(shù)內(nèi)部了)
返回后乞榨,回到_IO_file_open函數(shù)中秽之,接下來將打開文件后的文件流序號賦值給_fileno字段,之后調(diào)用了_IO_mask_flags將具有讀寫方式的屬性加入FILE結(jié)構(gòu)體的flags字段中吃既,若讀寫方式為a(追加)考榨,則會將文件末尾作為文件的偏移。最后會調(diào)用_IO_link_in函數(shù)確保該結(jié)構(gòu)體已鏈入_IO_list_all鏈表(因為在_IO_link_in函數(shù)中會有對_IO_LINKEDcheck鹦倚,所以并不是重復(fù)鏈入)河质,至此_IO_file_open函數(shù)執(zhí)行完畢。
_IO_file_open返回后回到_IO_new_file_fopen函數(shù)震叙,之后有的一個大段的if語句中掀鹅,大概是給之前初始化的_wide_data中的各元素進行賦值,在if函數(shù)的最后將FILE結(jié)構(gòu)體中的_mode字段賦值為1媒楼。該段代碼大概如下(語句太長就不貼了):

if(result != NULL)
{
    cs = strstr(last_recognized +1 , ",ccs=");
    if(cs != NULL)
    {
        ......                  //大概是給_wide_data中的各元素賦值
        result->_mode = 1;
    }
}

return result;
  • return后返回到__fopen_internal函數(shù)中乐尊,此時執(zhí)行到了__fopen_internal函數(shù)的最后一步,如果上面調(diào)用_IO_file_fopen函數(shù)打開文件失敗划址,則執(zhí)行函數(shù)第81-83行:調(diào)用_IO_un_link函數(shù)將鏈入_IO_list_all的結(jié)構(gòu)體摘除扔嵌,并free掉為其申請的空間,之后return NULL夺颤;若打開文件成功对人,則會執(zhí)行第79行:調(diào)用__fopen_maybe_mmap函數(shù)并返回。該函數(shù)在glibc/libio/iofopen.c中定義如下拂共;
    __fopen_maybe_mmap

    該函數(shù)會判斷_flags2字段中是否含有_IO_FLAGS2_MMAP位,即在打開文件時是否有m屬性姻几,還會檢查_flags字段中是否含有_IO_NO_WRITES位宜狐,即在打開文件時是否有r屬性势告。即在打開文件時有rm兩個屬性,則會執(zhí)行函數(shù)的主體部分抚恒,調(diào)用_IO_JUMPS_FILE_plus函數(shù)將重置FILE結(jié)構(gòu)體的vtable虛表咱台,該函數(shù)在glibc/libio/libioP.h中定義如下:
    _IO_JUMPS_FILE_plus

以上,完成了對fopen函數(shù)源碼的分析俭驮,該函數(shù)主要進行了3個操作:為文件流申請空間回溺;初始化FILE結(jié)構(gòu)體及虛表,包括將文件流鏈入_IO_list_all鏈表中混萝;打開文件流遗遵,包括讀取文件屬性以及利用系統(tǒng)調(diào)用打開文件。
通過閱讀源碼逸嘀,對文件屬性有了船新的認(rèn)識:

  • 除了日常用到的r/w/a/b/+之外還有x/m/c/e4個屬性车要,而且作為主屬性的r/w/a必須在fopen第二個參數(shù)的開頭,即只能wb而不能bw崭倘。
  • 在正常編寫代碼時宏觀能夠感到打開文件方式不一樣的屬性有r/w/a/+/x翼岁,而m/c/e這三個屬性的采用,僅僅會在系統(tǒng)進行打開文件操作過程中進行一些不太影響大局的判斷操作司光,在宏觀上感覺不到琅坡。這可能也是FILE結(jié)構(gòu)體中_flags_flags2兩個字段的區(qū)別。
  • 在上一點中所沒提到的b屬性残家,雖然一直知道是以二進制方式打開文件榆俺,但是在_IO_new_file_fopen函數(shù)中的關(guān)于副屬性的switch...case語句中,b屬性并沒有什么卵用跪削。沒有加入任何flag標(biāo)志位谴仙,只是將last_recognized賦值為b,即最后一個識別的屬性是b碾盐。就算不考慮后續(xù)代碼晃跺,只看switch...case語句的結(jié)果,當(dāng)b屬性后面跟有其他屬性毫玖,那么b屬性的case中沒有留下任何東西(其他屬性多多少少修改了_flags/_flags2字段)掀虎。

fread

fread函數(shù)的一般用法為

fread( void *buffer, size_t size, size_t count, FILE *stream );

該函數(shù)共有4個參數(shù),buffer代表接收從文件讀取數(shù)據(jù)的變量首地址付枫,size代表每個對象的大小烹玉,count代表對象的個數(shù),stream是代表文件流阐滩。即該函數(shù)實現(xiàn)了從stream中讀size * count字節(jié)數(shù)據(jù)并賦給buffer所指向的地址二打。該函數(shù)在glibc/libio/iofread.c中有如下宏定義:

weak_alias(_IO_fread , fread)

fread函數(shù)原形為_IO_fread函數(shù),該函數(shù)在同一文件中定義如下:

_IO_fread

該函數(shù)的最外層代碼比較短掂榔,邏輯也很清晰继效,首先在第34行調(diào)用了CHECK_FILE函數(shù)對將要輸入的文件流進行檢查症杏,該函數(shù)在glibc/libio/libioP.h中有宏定義如下:
CHECK_FILE

即當(dāng)IO_DEBUG被定義時,會對FILE結(jié)構(gòu)體的_flags字段進行_IO_MAGIC_MASK位的驗證瑞信,若不存在厉颤,則說明傳進來的不是FILE結(jié)構(gòu)體,就return 0凡简。
接下來在函數(shù)的第37逼友、39行分別調(diào)用了_IO_acquire_lock_IO_release_lock函數(shù),用來加鎖以及去鎖秤涩。
在中間的第38行帜乞,調(diào)用了_IO_sgetn函數(shù)進行讀入數(shù)據(jù)操作。經(jīng)過輾轉(zhuǎn)后發(fā)現(xiàn)該函數(shù)為虛表中的__xsgetn溉仑,即_IO_file_xsgetn函數(shù)挖函,該函數(shù)定義于glibc/libio/fileop.c中,是fread函數(shù)的關(guān)鍵浊竟。
_IO_file_xsgetn(1)

該函數(shù)中定義了4個變量怨喘,分別是want表示還要讀入的數(shù)據(jù)字節(jié)數(shù)、have表示輸入緩沖區(qū)中剩余的空間大小振定、count表示要讀出數(shù)據(jù)的個數(shù)必怜、s表示接收讀出數(shù)據(jù)的變量地址。由于我們剛從fopen初始化過來后频,因此FILE結(jié)構(gòu)體中的各字段仍是空值梳庆,因此會進入在第1302-1311行的if-else語句,該語句首先判斷該文件流中的_IO_save_base字段是否已經(jīng)賦值卑惜,即文件流是否有備份的緩沖區(qū)膏执,若有則會將該緩沖區(qū)free掉,并去掉_IO_IN_BACKUP位露久,最后調(diào)用_IO_doallocbuf函數(shù)更米。該函數(shù)在glibc/libio/genops.c中定義如下:
_IO_doallocbuf

該函數(shù)經(jīng)過一些檢查后會調(diào)用_IO_DOALLOCATE函數(shù),該函數(shù)在glibc/libio/libioP.h中有宏定義為#define _IO_DOALLOCATE(FP) JUMP0(__doallocate , FP)毫痕,即為虛表中的__doallocate征峦,對應(yīng)_IO_file_doallocate函數(shù),該函數(shù)在glibc/libio/filedoalloc.c中定義如下:
_IO_file_doallocate

可以看到在第94行給文件流字段加上了_IO_LINE_BUF字段消请,函數(shù)主要是在最后調(diào)用了malloc函數(shù)分配了size大小的空間給指針p栏笆,size在第83行被賦值為_IO_BUFSIZ,該字段有宏定義為8192,但在第84行調(diào)用了_IO_SYSSTAT函數(shù)臊泰,該函數(shù)為虛表中的__stat蛉加,對應(yīng)著_IO_file_stat函數(shù),該函數(shù)最終將調(diào)用syscall來獲取該文件狀態(tài),并初始化結(jié)構(gòu)體st针饥,初始化后的st如下圖:
struct stat st

因為在第97行存在判斷祟偷,因此size最后賦值為st.blksize0x1000字節(jié),即4K大小打厘。緊接著調(diào)用了_IO_setb函數(shù),該函數(shù)在glibc/libio/genops.c中定義如下:
_IO_setb

該函數(shù)主要實現(xiàn)了對_IO_buf_base_IO_buf_end兩個字段進行賦值贺辰。到這里可以知道_IO_doallocbuf函數(shù)實現(xiàn)了給文件流分配4K空間用作緩存緩沖區(qū)的操作户盯。緊接著回到_IO_file_xsgetn函數(shù)是一個100多行的while循環(huán):
_IO_file_xsgetn(2)

  • n <= 4K
    因為是剛剛完成初始化的文件流,第一次進行fread時所以會進入第1341行的判斷語句饲化,則會調(diào)用__underflow函數(shù)莽鸭,該函數(shù)在glibc/libio/genops.c中定義如下:

    __underflow

    該函數(shù)經(jīng)過一系列檢查后調(diào)用了_IO_UNDERFLOW函數(shù),該函數(shù)即虛表中的__underflow字段吃靠,對應(yīng)_IO_new_file_underflow函數(shù)硫眨,該函數(shù)定義在glibc/libio/fileop.c中,進入函數(shù)后按照當(dāng)前狀態(tài)會直接跳過前面的檢查巢块,直接從第520行開始執(zhí)行礁阁,如下圖:
    _IO_new_file_underflow(partial)

    首先將readwrite6個字段都初始化為_IO_buf_base,之后調(diào)用_IO_SYSREAD函數(shù)嘗試從fp中讀_IO_buf_end - _IO_buf_base4K大小的數(shù)據(jù)到從_IO_buf_base開始的空間中族奢,該函數(shù)的原形為虛表中的__read姥闭,對應(yīng)_IO_file_read函數(shù)。該函數(shù)在glibc/libio/fileops.c中定義如下:
    _IO_file_read

    該函數(shù)再往下就是調(diào)用__read()__read_nocancel()直接進行系統(tǒng)調(diào)用進行讀入越走,返回值為實際讀取的大小棚品,賦值給count。若正常讀取成功廊敌,則會將_IO_read_endoffset字段增加count的大小铜跑,返回_IO_read_ptr的值。調(diào)用完該函數(shù)后骡澈,返回_IO_file_xsgetn函數(shù)锅纺,此時該文件中最多4K大小的數(shù)據(jù)已經(jīng)被讀入到了緩存緩沖區(qū)中,且_IO_read_base_IO_read_end兩個指針分別對應(yīng)這想要讀入的數(shù)據(jù)的起始位置和終止位置秧廉,相當(dāng)于輸入緩沖區(qū)伞广,此時執(zhí)行continue,重新開始循環(huán)疼电,進入第1316行的判斷語句嚼锄,直接調(diào)用memcpy函數(shù),將數(shù)據(jù)從輸入緩沖區(qū)中拷貝如目標(biāo)變量中蔽豺,_IO_read_ptr字段加上相應(yīng)大小区丑,此時所有數(shù)據(jù)全部讀入,want賦值為0,一路執(zhí)行沧侥,最后跳出循環(huán)可霎。

  • n > 4K
    若讀取數(shù)據(jù)大小大于4K,則不會進入第1341行的判斷句宴杀,而是繼續(xù)往下執(zhí)行:

    _IO_file_xsgetn(3)

    緊接著調(diào)用了_IO_setg_IO_setp兩個函數(shù)癣朗,這兩個函數(shù)在glibc/libio/libioP.h中有宏定義如下:
    _IO_setp/_IO_setg

    即實現(xiàn)了將FILE結(jié)構(gòu)體中的readwrite相關(guān)共6個指針均初始化為_IO_buf_base的功能。隨后在1357行的判斷中旺罢,是將之前分配的緩沖區(qū)大小作為一個block_size旷余,因為這條分支接下來會直接調(diào)用_IO_SYSREAD函數(shù),將數(shù)據(jù)直接從文件流中讀入變量中而不經(jīng)過緩沖區(qū)扁达,所以為了(優(yōu)化性能正卧?),每次只讀取block_size大小的數(shù)據(jù)跪解,即4K炉旷。調(diào)用完_IO_SYSREADwant會減去4K再次進行循環(huán),直到最后小于4K的一部分叉讥,會和上面n <= 4K經(jīng)歷相同的過程窘行,并退出循環(huán)。

至此节吮,分析完了fread函數(shù)主要流程抽高,尤其是_IO_file_xsgetn函數(shù)的執(zhí)行流程。fread函數(shù)主要進行了1個操作透绩,調(diào)用_IO_file_xsgetn函數(shù)翘骂,當(dāng)然加鎖也是比較重要的。_IO_file_xsgetn函數(shù)帚豪,主要進行了3個操作:調(diào)用_IO_doallocbufFILE結(jié)構(gòu)體分配緩沖區(qū)碳竟;當(dāng)n <= block_size時,調(diào)用_IO_file_underflow將文件流中的數(shù)據(jù)讀入緩沖區(qū)再調(diào)用memcpy從緩沖區(qū)拷貝至目標(biāo)變量中狸臣;當(dāng)n > block_size時莹桅,大部分先對齊到block_size,調(diào)用_IO_SYSREAD函數(shù)直接從文件流讀入到目標(biāo)變量烛亦,若還有剩余的數(shù)據(jù)再用老方從走緩沖區(qū)拷貝到目標(biāo)變量中诈泼。
通過閱讀源碼,對FILE結(jié)構(gòu)體以及其中各字段所代表的含義有了船新的認(rèn)識:

  • 首先是從_IO_read_ptr_IO_buf_end8個字段煤禽,原本認(rèn)為是共申請了3個緩沖區(qū)铐达,通過閱讀源碼后知道了只申請了1個緩沖區(qū),其中_IO_buf_base_IO_buf_end指向這個緩沖區(qū)的兩端檬果,其余6readwrite字段沒事的時候都與_IO_buf_base的值相同瓮孙,在進行讀操作時唐断,相當(dāng)于3read指針起作用控制緩沖區(qū)中的內(nèi)容,可以推出在進行寫操作時杭抠,3read指針與_IO_buf_base的值相同脸甘,而3write指針獨起作用控制緩沖區(qū)中的內(nèi)容。
  • 其次_IO_save_base_IO_save_end3個字段偏灿,因為在該段代碼中只存在幾處判斷丹诀,并沒有實際用處,所以判斷大概是為了保存某個時刻緩沖區(qū)而設(shè)置的指針翁垂。
  • 最后是在調(diào)試時發(fā)現(xiàn)的FILE結(jié)構(gòu)體有多種形態(tài)忿墅,比如我在調(diào)試時看到的FILE結(jié)構(gòu)體實際上是glibc/libio/bits/libio.h中定義的_IO_FILE_complete結(jié)構(gòu)體。最終究其原因沮峡,是因為有個宏判斷把定義結(jié)構(gòu)體的大括號給吃掉了。如下圖:
    騷騷的宏判斷

fwrite

fwrite函數(shù)的一般用法為

fwrite(const void* buffer, size_t size, size_t count, FILE* stream);

fread函數(shù)相似亿柑,該函數(shù)共有4個參數(shù)邢疙,buffer代表存儲要寫入文件數(shù)據(jù)的首地址,size代表每個對象的大小望薄,count代表對象的個數(shù)疟游,stream代表文件流。即該函數(shù)實現(xiàn)了將buffersize * count字節(jié)數(shù)據(jù)寫入stream文件流的操作痕支。該函數(shù)在glibc/libio/iofwrite.c中有如下宏定義:

weak_alias(_IO_fwrite , fwrite)

fwrite函數(shù)原形為_IO_fwrite函數(shù)颁虐,該函數(shù)在同一文件中定義如下:

_IO_fwrite

也是一樣的調(diào)用了CHECK_FILE函數(shù)進行檢查,以及在調(diào)用關(guān)鍵函數(shù)前后加鎖與去鎖卧须。在第39行調(diào)用了函數(shù)_IO_sputn另绩,該函數(shù)在經(jīng)過一系列定義和宏定義后為虛表中的__xsputn,即_IO_new_file_xsputn函數(shù)花嘶,該函數(shù)定義在glibc/libio/fileops.c中笋籽,是fwrite函數(shù)的關(guān)鍵。
_IO_new_file_xsputn(1)

該函數(shù)中有3個比較重要的變量椭员,分別是s表示放有待寫入數(shù)據(jù)變量的首地址车海,to_do表示還需要寫入的字節(jié)數(shù)尼酿,must_flush表示是否需要刷新緩沖區(qū)库继。接下來也是按照剛從fopen初始化完的狀態(tài)開始分析。此時各字段均為空比勉,也沒有_IO_LINE_BUF埋同、_IO_CURRENTLY_PUTTING屬性州叠,所以不會進入第1233、1250行的判斷句莺禁,所以count變量沒有被賦值仍然為0留量,也不會進入第1254行的判斷語句。而to_do代表的需要寫入的字節(jié)數(shù)沒有變,因此會直接進入第1262行的判斷語句楼熄,如下圖:
_IO_new_file_xsputn(2)

進入語句后忆绰,聲明了兩個變量block_sizedo_write,之后直接調(diào)用了_IO_OVERFLOW函數(shù)可岂,該函數(shù)為虛表中的__overflow错敢,即_IO_new_file_overflow函數(shù),該函數(shù)在glibc/libio/fileops.c中定義如下:
_IO_new_file_overflow(1)

該函數(shù)首先判斷文件是否含有_IO_NO_WRITES屬性缕粹,即在fopen操作時是否為r只讀選項稚茅,若是,則不會執(zhí)行該函數(shù)直接返回平斩。接著判斷是否不含_IO_CURRENTLY_PUTTING屬性或者_IO_write_base字段為空亚享,其中_IO_CURRENTLY_PUTTING屬性在該函數(shù)的第785行會進行賦值,因此該語句為判斷沒有正常執(zhí)行過_IO_new_file_overflow函數(shù)或執(zhí)行過但沒有分配緩沖區(qū)的情況绘面,會調(diào)用_IO_doallocbuf函數(shù)分配緩沖區(qū)欺税,之后調(diào)用_IO_setg將與read相關(guān)的3個字段都賦值為_IO_buf_base,之后也會進行_IO_in_backup的檢測揭璃,這幾步操作在上一節(jié)_IO_new_file_underflow函數(shù)中有過詳細(xì)描述晚凿,因此不再贅述。
_IO_new_file_overflow(2)

緊接著將readwrite6個字段都賦值為_IO_buf_base瘦馍,并給文件流加上_IO_CURRENTLY_PUTTING屬性歼秽。由于在_IO_new_file_xsputn調(diào)用該函數(shù)時的第二個參數(shù),即函數(shù)中的ch變量為EOF情组,因此燥筷,會在第790行調(diào)用_IO_do_write函數(shù)并返回,傳入的3個參數(shù)分別為FILE結(jié)構(gòu)體指針院崇、_IO_write_base以及_IO_write_ptr - _IO_write_base = 0荆责。該函數(shù)在glibc/libio/fileops.c中有宏定義為_IO_new_do_write函數(shù),其定義如下:
_IO_new_do_write

可以看到_IO_new_do_write函數(shù)中亚脆,因為本次傳入的第3個參數(shù)to_do0做院,因此不會進行任何操作直接返回0,而不會去執(zhí)行new_do_write函數(shù)濒持。返回后回到_IO_new_file_xsputn的第1266行键耕,不等于EOF,于是會繼續(xù)執(zhí)行柑营。給block_size賦值為申請的空間大小屈雄,即4Kdo_write代表通過調(diào)用new_do_write函數(shù)進行寫入的數(shù)據(jù)大小官套,該數(shù)值是與block_size進行對齊的酒奶。接下來也是根據(jù)to_doblock_size的大小蚁孔,函數(shù)將分成不同的流程。

  • to_do < 4K
    若要寫入的數(shù)據(jù)小于4K惋嚎,則do_write = 0杠氢,不會進入第1275行的判斷語句,而是在1287行直接調(diào)用_IO_default_xsputn函數(shù)進行處理另伍,該函數(shù)在glibc/libio/genops.c中定義如下:

    _IO_default_xsputn

    該函數(shù)主要實現(xiàn)了將數(shù)據(jù)從變量中拷貝到結(jié)構(gòu)體緩沖區(qū)中的操作鼻百,主要有兩種情況,一種是當(dāng)要拷貝的數(shù)據(jù)大于20字節(jié)時摆尝,會直接調(diào)用__mempcpy進行拷貝温艇,如果小于等于20,則用for循環(huán)逐字節(jié)進行拷貝堕汞,若緩沖區(qū)大小不夠勺爱,則會調(diào)用_IO_OVERFLOW刷新緩沖區(qū)。
    值得注意的是讯检,_IO_default_xsputn函數(shù)僅僅實現(xiàn)了將數(shù)據(jù)從變量中拷貝到了從_IO_write_base_IO_write_ptr為止的緩沖區(qū)中邻寿,并沒有寫入文件。

  • to_do >= 4K
    若要寫入的數(shù)據(jù)大于4K视哑,則do_write被賦值為4K的倍數(shù),將在第1277行調(diào)用new_do_write函數(shù)進行寫入誊涯,該函數(shù)在glibc/libio/fileops.c中定義如下:

    new_do_write

    首先會判斷是否具有_IO_IS_APPENDING屬性挡毅,即判斷該文件是由a屬性打開還是由w屬性打開:若為w屬性,則將_offset字段賦值為-1暴构;若為a屬性跪呈,則_offset字段不變,這正是寫文件時覆蓋和追加方式的體現(xiàn)取逾。之后判斷_IO_read_end_IO_write_base相等耗绿,若不等則調(diào)用_IO_SYSSEEK函數(shù)。對于本次調(diào)用的流程砾隅,這兩個判斷語句不會有太大的影響误阻。主要是在函數(shù)的第457行執(zhí)行了_IO_SYSWRITE函數(shù),該函數(shù)為虛表中的__write晴埂,即為_IO_new_file_write函數(shù)究反,該函數(shù)在glibc/libio/fileops.c中定義如下:
    _IO_new_file_write

    該函數(shù)也是通過調(diào)用__write函數(shù)或__write_nocancel函數(shù)進行系統(tǒng)調(diào)用,將to_do長度的數(shù)據(jù)從data中寫入文件中儒洛。調(diào)用完_IO_SYSWRITE函數(shù)后精耐,若讀入成功,即count不為0琅锻,在之后會調(diào)用_IO_adjust_column函數(shù)去更新FILE結(jié)構(gòu)體中的_cur_column字段卦停,該字段代表文件的行數(shù)+1)向胡,該函數(shù)在glibc/libio/genops.c中定義如下:
    _IO_adjust_column

    該函數(shù)通過判斷上面通過_IO_SYSWRITE函數(shù)寫入的數(shù)據(jù)中含有多少\n字符來確定文件中增加了多少行。調(diào)用完_IO_adjust_column后返回到new_do_write函數(shù)惊完,之后會調(diào)用_IO_setg函數(shù)以及各種賦值操作將文件流中read僵芹、write相關(guān)6個字段都賦值為_IO_buf_base,類似于初始化的操作专执。綜上淮捆,new_do_write函數(shù)完成了將對齊block_size大小的數(shù)據(jù)不通過緩沖區(qū),直接從變量寫入文件流的操作本股。從new_do_write函數(shù)返回后攀痊,之前未對齊的剩余的數(shù)據(jù),則會在下方調(diào)用_IO_default_xsputn函數(shù)拷貝到緩沖區(qū)中拄显。

至此苟径,分析完了fwrite函數(shù)主要流程,尤其是_IO_new_file_xsputn函數(shù)的執(zhí)行流程躬审。fwrite函數(shù)與fread相互對仗棘街,fread是將文件中數(shù)據(jù)直接讀入變量,或先從文件讀入FILE結(jié)構(gòu)體緩沖區(qū)承边,再利用read相關(guān)指針進行間接讀入變量遭殉;fwrite是將數(shù)據(jù)直接從文件中寫入變量或?qū)懭?strong>FILE結(jié)構(gòu)體中的緩沖區(qū)。_IO_new_file_xsputn作為實現(xiàn)fwrite的重要函數(shù)博助,主要進行了****個操作:調(diào)用_IO_new_file_overflow函數(shù)為FILE結(jié)構(gòu)體申請緩沖區(qū)险污;若n < block_size時,直接調(diào)用__mempcpy函數(shù)拷貝到緩沖區(qū)中富岳;若n >= block_size時蛔糯,會將對齊到block_size的部分用系統(tǒng)調(diào)用直接讀入文件,剩余部分按與n < block_size相同的方法拷貝到緩沖區(qū)中窖式。
通過閱讀源碼蚁飒,對fwrite函數(shù)有了船新認(rèn)識:

  • 本以為fwrite只是把fread的讀操作變成寫操作,其他都是相同的萝喘。然而fread執(zhí)行完后不論多少數(shù)據(jù)都讀入了變量中淮逻,而fwrite執(zhí)行完后未對齊block_size大小的數(shù)據(jù)仍在緩沖區(qū)中,推測在執(zhí)行fclose函數(shù)或在程序退出時才會真正的寫入文件中阁簸。
  • _IO_new_file_overflow函數(shù)并不僅僅有上文中描述的調(diào)用_IO_doallocbuf申請緩沖區(qū)的作用弦蹂,其主要擔(dān)負(fù)著刷新緩沖區(qū)的作用:第一種調(diào)用情況為上文中所提到的,當(dāng)_IO_buf_base字段為空强窖,即還未初始化緩沖區(qū)時凸椿,則會調(diào)用_IO_doallocbuf函數(shù)進行申請緩沖區(qū);若緩沖區(qū)已經(jīng)初始化翅溺,且_IO_write_end == _IO_write_ptr脑漫,即緩沖區(qū)已滿時髓抑,則會把這些待寫入內(nèi)容寫入文件,之后會將_IO_write_ptr賦值為_IO_buf_base优幸,相當(dāng)于清空緩沖區(qū)的操作吨拍。

fclose

fopen函數(shù)相同,在glibc/include/stdio.h中有如下宏定義:

#define fclose(fp) _IO_new_fclose(fp)

fclose函數(shù)的原形為_IO_new_fclose函數(shù)网杆,該函數(shù)在glibc/libio/iofclose.c中定義如下:

_IO_new_fclose

該函數(shù)主要是fopen函數(shù)的逆過程羹饰,首先在判斷文件流是否含有_IO_file_flags_IO_SI_FILEBUF后,函數(shù)會執(zhí)行_IO_un_link函數(shù)碳却,該函數(shù)在glibc/libio/genops.c定義如下:
_IO_un_link

該函數(shù)是_IO_link_in函數(shù)的逆過程队秩,主要實現(xiàn)了將文件流從_IO_list_all鏈表中卸下,以及一些對結(jié)構(gòu)體中字段的善后操作昼浦。接著調(diào)用了_IO_file_close_it函數(shù)馍资,該函數(shù)在glibc/libio/fileops.c定義如下:
_IO_file_close_it

該函數(shù)在第134行判斷是否為寫屬性的文件流,以及是否進行過寫操作关噪,若有鸟蟹,則會調(diào)用_IO_do_flush函數(shù),該函數(shù)在glibc/libio/libioP.h中有宏定義如下:
_IO_do_flush

可以看到該函數(shù)直接是針對文件流的整個緩沖區(qū)去調(diào)用了_IO_do_write函數(shù)使兔,即實現(xiàn)了將仍存在于緩沖區(qū)中的數(shù)據(jù)寫入文件的操作建钥。之后調(diào)用_IO_SYSCLOSE函數(shù),該函數(shù)對應(yīng)虛表中的__close虐沥,即_IO_file_close函數(shù)熊经,該函數(shù)在glibc/libio/fileops.c中定義如下:
_IO_file_close

該函數(shù)直接調(diào)用了__close_nocancel函數(shù)去執(zhí)行系統(tǒng)調(diào)用對文件進行關(guān)閉。返回到_IO_new_file_close_it之后緊接著調(diào)用_IO_setb置蜀、_IO_setg、_IO_setp等函數(shù)將文件流中所有readwrite字段置0悉盆,并在第159-161行將_flags盯荤、_fileno_offset修改為一個關(guān)閉狀態(tài)的屬性。返回到_IO_new_fclose函數(shù)后焕盟,主要去執(zhí)行了_IO_FINISH函數(shù)秋秤,該函數(shù)為虛表中的__finish,即對應(yīng)_IO_new_file_finish函數(shù)脚翘,該函數(shù)在glibc/libio/fileops.c中定義如下:
_IO_new_file_finish

該函數(shù)一次調(diào)用了_IO_do_flush灼卢、_IO_SYSCLOSE以及_IO_default_finish函數(shù),其中_IO_default_finish函數(shù)在glibc/libio/genops.c中定義如下:
_IO_default_finish

可以看到_IO_new_file_finish中調(diào)用的幾個函數(shù)来农,均在前面正常關(guān)閉的流程中有過調(diào)用鞋真,所以基本上都不會去執(zhí)行。

至此沃于,完成了對fclose函數(shù)流程的分析涩咖,總的來說代碼比較短暫海诲,也都是對前面已經(jīng)執(zhí)行代碼的一個逆過程,因此并沒有太多需要注意的地方檩互,主要還是印證了前面在fwrite結(jié)尾的一個預(yù)測特幔,會在關(guān)閉文件流時去調(diào)用了_IO_do_flush函數(shù)將緩沖區(qū)內(nèi)的數(shù)據(jù)寫入文件。

0x03 利用方法

  • 利用偽造stdout進行任意地址讀
    stdout闸昨,即標(biāo)準(zhǔn)輸出蚯斯,默認(rèn)為當(dāng)前終端,其本質(zhì)也為一個FILE結(jié)構(gòu)體饵较,利用這種方法最終達到將任意地址數(shù)據(jù)輸出到終端供我們進行讀的效果拍嵌。因此首先定位到fwrite函數(shù)中輸出的位置,即調(diào)用_IO_SYSWRITE的地方告抄。由前小節(jié)的分析可知撰茎,只有在new_do_write函數(shù)中有對_IO_SYSWRITE的調(diào)用,而調(diào)用new_do_write理論上有兩個地方打洼,一個是在_IO_new_file_xsputn函數(shù)中的第1266行調(diào)用_IO_OVERFLOW函數(shù)中會有調(diào)用龄糊,以及在第1277行直接進行調(diào)用。
    調(diào)用_new_do_write

    第一種調(diào)用情況募疮,在利用_IO_OVERFLOW函數(shù)通過調(diào)用_IO_do_write函數(shù)炫惩,實現(xiàn)間接調(diào)用new_do_write函數(shù)將_IO_write_base_IO_write_ptr之間的數(shù)據(jù)進行寫入。
    首先我們需要獲得執(zhí)行到_IO_OVERFLOW函數(shù)的條件阿浓,主要還是第1262行的to_do + must_flush > 0他嚷,因為正常函數(shù)調(diào)用時傳進來的to_do均大于0,因此必然會進入該語句芭毙,所以前置條件幾乎算是沒有筋蓖;
    接下來進入_IO_OVERFLOW函數(shù)內(nèi)部進行分析調(diào)用new_do_write函數(shù)的條件。
    條件判斷(1)

    可以看到在我們最終調(diào)用_IO_do_write函數(shù)之前會有2個比較關(guān)鍵的判斷:第747行判斷是否有寫權(quán)限退敦,若沒有則會報錯并返回粘咖,因此搜集到的第一個必要條件為:f->_flags & _IO_NO_WRITES == 0;接下來一個重要判斷是在第754行的判斷侈百,經(jīng)過上一節(jié)的分析我們知道這個判斷是用來判斷緩沖區(qū)是否初始化的瓮下,若未初始化則會進入該判斷語句,由于在該判斷分支中最后會將FILE結(jié)構(gòu)體中的write相關(guān)指針進行重新賦值钝域,從而破壞了我們事先構(gòu)造好的write指針讽坏,所以這個判斷也不能進入,因此搜集到的第二個必要條件為f->_flags & _IO_CURRENTLY_PUTTING != 0例证。
    條件判斷(2)

    之后進入_IO_do_write函數(shù)路呜,這個函數(shù)比較簡單,只要滿足f->_IO_write_ptr - f->_IO_write_base != 0便可調(diào)用new_do_write函數(shù);最后在new_do_write函數(shù)中拣宰,只要滿足fp->_IO_read_end == fp->_IO_write_base即可調(diào)用_IO_SYSWRITE_IO_write_base_IO_write_ptr之間的數(shù)據(jù)輸出党涕。
    第二種調(diào)用情況,是在第1277行直接調(diào)用new_do_write函數(shù)巡社,因為此處第二個參數(shù)膛堤,即輸出的數(shù)據(jù)段起始地址為外部傳入?yún)?shù),而不能通過偽造FILE結(jié)構(gòu)體來進行控制晌该,因此不能實現(xiàn)任意地址讀的功能肥荔,直接PASS掉。

綜上朝群,我們只要偽造滿足這4個條件的stdout結(jié)構(gòu)體就能夠?qū)崿F(xiàn)任意地址讀燕耿,其中第1、2個條件為文件流不能具有_IO_NO_WRITES(0x8)屬性姜胖,且具有_IO_CURRENTLY_PUTTING(0x800)屬性誉帅,而且_flags位自帶一個_IO_MAGIC(0xfbad0000),因此構(gòu)造的_flags0xfbad0800右莱。第3蚜锨、4個條件就和read、write指針息息相關(guān)了慢蜓,根據(jù)條件只要構(gòu)造_IO_read_end = _IO_write_base = (想要leak的起始地址)亚再,_IO_write_ptr = (想要leak的結(jié)束地址),其他3個沒有提到的指針置0就可以了晨抡。
因此偽造的fake_FILE結(jié)構(gòu)體大概長這樣(一般用got表進行libcleak):

fake_FILE

在我們最終偽造好這個FILE結(jié)構(gòu)體后再去調(diào)用針對stdout進行輸出的函數(shù)就可以實現(xiàn)任意地址讀了氛悬,常見的函數(shù)一般有printf、fwrite耘柱、puts等如捅。

  • 利用偽造stdout進行任意地址寫
    _IO_new_file_xsputn函數(shù)中,不僅調(diào)用了_IO_SYSWRITE進行了輸出调煎,而且也有調(diào)用__mempcpy函數(shù)將要輸出的數(shù)據(jù)寫入緩沖區(qū)的操作镜遣,該操作在函數(shù)中的第1258行,如下圖:
    寫入緩沖區(qū)

    根據(jù)調(diào)用的參數(shù)汛蝙,可通過偽造FILE結(jié)構(gòu)體達到將s中數(shù)據(jù)向我們可控指針f->_IO_write_ptr中寫的操作烈涮,即不可控制內(nèi)容的任意地址寫朴肺〗呀#可以看到執(zhí)行該語句的最主要判斷為count > 0,而count變量在上方有多次賦值操作戈稿,對于已經(jīng)控制了FILE結(jié)構(gòu)體的我們來說輕而易舉西土,但在執(zhí)行之前會與to_do變量進行比較,取較小的長度進行寫入鞍盗,其中to_do變量為調(diào)用時想要寫入的長度需了,一般為字符串的長度跳昼。如,puts("12345678")在調(diào)用該函數(shù)時肋乍,to_do的值就為8鹅颊,s變量就是指向12345678這個字符串的開頭。

綜上墓造,我們只需要構(gòu)造如下的fake_FILE就能夠?qū)崿F(xiàn)將字符串寫到指定內(nèi)存的操作堪伍。

fake_FILE

雖然看起來不能實現(xiàn)任意地址寫任意數(shù)據(jù),但若是輸出我們輸入的值時觅闽,則可實現(xiàn)任意地址寫任意數(shù)據(jù)帝雇;就算輸出的都是固定字符串,能夠?qū)⒁恍┡袛鄻?biāo)志改為其他值蛉拙,而改變程序正常執(zhí)行流程有時還是很有用的尸闸。

  • 利用偽造stdin進行任意地址寫
    stdin,即標(biāo)準(zhǔn)輸出孕锄,默認(rèn)為鍵盤吮廉,其本質(zhì)也為一個FILE結(jié)構(gòu)體,利用這種方法最終達到由我們從鍵盤輸入的數(shù)據(jù)寫到任意地址的效果硫惕。同樣茧痕,首先我們需要定位到fread函數(shù)中獲取輸入的位置,即調(diào)用_IO_SYSREAD函數(shù)的地方恼除。由前小節(jié)分析可知僅有在_IO_UNDERFLOW函數(shù)中會調(diào)用_IO_SYSREAD函數(shù)進行寫入踪旷, 程序在_IO_file_xsgetn函數(shù)中是通過第1344行調(diào)用__underflow函數(shù)來間接調(diào)用該函數(shù),如下圖:
    條件判斷(1)

    與利用stdout進行任意地址讀有所不同的是豁辉,并不是每次執(zhí)行都會順理成章地去調(diào)用_IO_UNDERFLOW函數(shù)令野,而會有很多外部的限制條件。首先我們可以看到在調(diào)用__underflow函數(shù)之間就有了許多的限制徽级。從調(diào)用處開始往上看气破,首先最息息相關(guān)的兩個條件判斷支,第一個為第1341行的判斷要求fp->_IO_buf_base != NULL && want < fp->_IO_buf_end - fp->_IO_buf_base餐抢,其中want變量的值為傳入的參數(shù)n现使,即想要讀入的字節(jié)數(shù);第二個條件為不滿足第1316行的判斷語句旷痕,即want > fp->_IO_read_end - fp->_IO_read_ptr碳锈。其實除了這2個判斷語句外,在第1302行的判斷也不能為真欺抗,因為進入該分支后會調(diào)用_IO_doallocbuf函數(shù)重新分配緩沖區(qū)售碳,會破壞我們事先構(gòu)造好的FILE結(jié)構(gòu)體,但由于該判斷句的條件包含于1341行的判斷句,所以不單獨算一個條件贸人。滿足這兩個條件后间景,程序正常執(zhí)行到了__underflow函數(shù),如下圖:
    條件判斷(2)

    可以看到我們最終要調(diào)用的_IO_UNDERFLOW函數(shù)在該函數(shù)的最終結(jié)尾處艺智,因此要小心的過掉上面的各個分支倘要。通過觀察,大部分分支在正常情況下都不會進入十拣,需要注意的是一個在第296行的if語句不能滿足碗誉,因此第3個條件為fp->_IO_read_ptr >= fp->_IO_read_end,接下來進入_IO_UNDERFLOW函數(shù)內(nèi)部父晶,即_IO_new_file_underflow函數(shù)哮缺,如下圖:
    條件判斷(3)

    _IO_new_file_underflow函數(shù)的第531行調(diào)用了_IO_SYSREAD函數(shù)的調(diào)用,其上方仍有許多if分支甲喝,看似特別多尝苇,其實只有第478行的影響比較大,即第4個條件為fp->_flags & _IO_NO_READS == 0埠胖。之后第484糠溜、487行的分支雖然都不能進入,但這些判斷條件在調(diào)用前已經(jīng)囊括了直撤,因此不做重復(fù)搜集非竿;第500行的條件分支也沒有進去的必要,之后就能夠順利地調(diào)用_IO_SYSREAD函數(shù)谋竖,實現(xiàn)向fp->_IO_buf_base地址寫入從鍵盤輸入_IO_buf_base - _IO_buf_end長度數(shù)據(jù)的功能红柱。

綜上,我們只要偽造滿足這4個條件的stdin結(jié)構(gòu)體就能夠?qū)崿F(xiàn)任意地址寫蓖乘,其中第1個條件與調(diào)用時本來要寫入的數(shù)據(jù)長度有關(guān)锤悄,我們要偽造的寫入大小應(yīng)該比其本要寫入的大小更大。比如嘉抒,在調(diào)用read(0 , buf , 0x10)函數(shù)時零聚,我們構(gòu)造的_IO_buf_end - _IO_buf_base就應(yīng)該大于0x10;第2、3個條件綜合起來可構(gòu)造_IO_read_ptr == fp->_IO_read_end來同時滿足,也不知道給個什么值许起,就默認(rèn)為0吧;第4個就是_flags屬性的問題了蚂会,不包含_IO_NO_READS
因此偽造的fake_FILE結(jié)構(gòu)體大概長這樣:

fake_FILE

在我們最終偽造好這個FILE結(jié)構(gòu)體后再去調(diào)用針對stdin進行輸入的函數(shù)就可以實現(xiàn)任意地址寫了狈定,常見的函數(shù)一般有scanf颂龙、fread、gets纽什、fgets等措嵌。

  • 修改stdin->_IO_buf_end而導(dǎo)致的堆溢出
    再次回到執(zhí)行fread函數(shù)過程中在_IO_new_file_underflow中調(diào)用_IO_SYSREAD將文件中的數(shù)據(jù)讀入緩沖區(qū)的地方,即_IO_new_file_underflow的第531行芦缰,此時調(diào)用如下:
    _IO_SYSREAD(fp, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base)
    通過上文的分析可知企巢,_IO_buf_base指向所申請的堆的頭部,_IO_buf_end指向所申請的堆的末尾让蕾,在執(zhí)行fread過程中當(dāng)文件大小 <= 緩沖區(qū)大小時(這個大小通常是4K)浪规,會執(zhí)行到這一步,將文件中數(shù)據(jù)全部讀入緩沖區(qū)中探孝。但笋婿,正如這個調(diào)用語句所示,此處的緩沖區(qū)大小是由_IO_buf_end - _IO_buf_base所決定顿颅。
    有這樣一種場景缸濒,在申請過緩沖區(qū)后,_IO_buf_base指向緩沖區(qū)的頭部粱腻,而通過某些手段庇配,我們能夠修改_IO_buf_end為別的更大的值,在執(zhí)行相應(yīng)函數(shù)時則會造成堆溢出绍些。而這種手段捞慌,我們可以在有unsortbin attack的條件下,很輕松地將_IO_buf_end修改為main_arena上的很大的值柬批,那么我們就可以構(gòu)造這樣一個文件或輸入啸澡,一直覆蓋到main_arena之前的free_hook等敏感指針。

  • 劫持vable
    FILE結(jié)構(gòu)體的虛表的利用氮帐,早在libc-2.23版本時就有了FSOP之類的偽造vtable劫持控制流的利用方法等锻霎,但隨著版本的更新,從2.24版本開始揪漩,對調(diào)用vtable的合法性檢查也開始進行了check旋恼,這就導(dǎo)致了前版本中直接更改vtable的利用方法變得不可行。同樣奄容,在本文的2.27版本中冰更,在調(diào)用vtable之前也是有這一定的check機制。下面我們就分析vtablecheck機制昂勒,以及利用方法蜀细。
    首先我們來定位vtable調(diào)用的地方,從前小節(jié)的分析中戈盈,可以找到許多調(diào)用的地方奠衔,比如_IO_new_file_xsputn函數(shù)第1266行調(diào)用的_IO_OVERFLOW函數(shù)就是vtable的入口谆刨。追隨_IO_OVERFLOW的腳步,可以在glibc/libio/libioP.h中找到如下宏定義:

#define _IO_OVERFLOW(FP, CH) JUMP1(__overflow, FP, CH)
......
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
......
#define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))

通過多個宏定義归斤,我們可以發(fā)現(xiàn)痊夭,在調(diào)用vtable所代表的函數(shù)之前,首先調(diào)用了IO_validate_vtable函數(shù)脏里,該函數(shù)在glibc/libio/libioP.h中定義如下:

IO_validate_vtable

該函數(shù)首先判斷了調(diào)用的vtable函數(shù)地址是否在__start___libc_IO_vtable__stop___libc_IO_vtable之間她我,若在此之間,則說明是libc中的合法vtable地址迫横。若不在這個區(qū)間番舆,則會調(diào)用第876行的_IO_vtable_check函數(shù)進行進一步的檢查。該函數(shù)在glibc/libio/vtable.c中定義如下:
_IO_vtable_check

由于存在預(yù)編譯頭矾踱,且SHARE恨狈、PTR_DEMANGLE是有定義的,因此會執(zhí)行上面兩個部分的檢查呛讲。第一部分為判斷引用的虛表指針是否為默認(rèn)命名空間外重構(gòu)的虛表指針拴事,其中atomic_load_relaxed函數(shù)為獲得加載指針的當(dāng)前值,PTR_DEMANGLE函數(shù)則是類似canary之類的一個保護虛表不被修改的函數(shù)圣蝎;第二部分則是檢查引用的虛表指針是否為動態(tài)鏈接庫中加載的函數(shù)刃宵。但我們在2.23版本中通常將堆或棧上的一塊區(qū)域用來偽造為FILE結(jié)構(gòu)體,同樣vtable也就接在這個fake_FILE結(jié)構(gòu)體的后面徘公,所以上面的3個條件都不會滿足牲证。因此,在新版本中用曾經(jīng)的利用方法最終會執(zhí)行到_IO_vtable_check函數(shù)的第72行关面,報錯并結(jié)束進程坦袍。
上面介紹完了新版本中加入的3個對vtablecheck機制,下面講講大神們是如何繞過check并再次實現(xiàn)利用的等太。
由于_IO_vtable_check函數(shù)中的第一個檢查捂齐,有PTR_DEMANGKE函數(shù)的存在,幾乎時不能夠偽造對應(yīng)的條件缩抡;而第二個檢查奠宜,若能夠偽造,則可以選擇其他更方便的利用方法瞻想,而不用繼續(xù)在vtable的利用上死磕了压真,因此這兩個檢查在正常情況下難以繞過。于是在調(diào)用_IO_vtable_check函數(shù)前的檢查則成了關(guān)鍵蘑险,即調(diào)用的虛表指針必須在__start___libc_IO_vtable__stop___libc_IO_vtable之間滴肿。然后大佬們就找到了這樣一組內(nèi)部虛表_IO_str_jumps/_IO_wstr_jumps_IO_file_jumps/_IO_wfile_jumps相對應(yīng),但其中的函數(shù)都換成了另外一組佃迄,如下圖:
_IO_str_jumps

并且發(fā)現(xiàn)其中的__finish對應(yīng)的函數(shù)大有可為泼差,_IO_str_finish函數(shù)在glibc/libio/strops.c中定義如下:
_IO_str_finish

可以看到贵少,當(dāng)滿足條件時,會將結(jié)構(gòu)體中的_s._free_buffer作為函數(shù)指針堆缘,將_IO_buf_base作為參數(shù)進行執(zhí)行滔灶,因此我們可以試著利用原來FSOP的利用方法,來構(gòu)造一個滿足條件的fake_FILE套啤。

FSOP方法在之前的文章中有過介紹,在此只做簡單描述随常。該方法的精髓為當(dāng)程序從main函數(shù)返回或調(diào)用exit函數(shù)或libc進行abort操作時潜沦,會調(diào)用_IO_flush_all_lockp函數(shù)去遍歷_IO_list_all鏈表中的每一個FILE結(jié)構(gòu)體,而當(dāng)FILE結(jié)構(gòu)體滿足(_mode <= 0) && (_IO_write_ptr > _IO_write_base)時绪氛,則會調(diào)用_IO_OVERFLOW函數(shù)唆鸡,即vtable指針+ 0x18位置的函數(shù)。

因此枣察,對應(yīng)到此處争占,想要成功調(diào)用_IO_str_finish函數(shù),則需要在FSOP的基礎(chǔ)上將的vtable改為_IO_str_jumps - 8序目,此為調(diào)用函數(shù)前的利用條件臂痕。在進入函數(shù)后,首先要滿足判斷條件_IO_buf_base != NULL以及_flags & _IO_USER_BUF == 0猿涨,最后是利用條件_s._free_buffer == system_addr || _IO_buf_base == "/bin/sh\x00"_s._free_buffer == one_gadget握童。其中_IO_strfile結(jié)構(gòu)體在glibc/libio/strfile.h中定義如下:

_IO_strfile結(jié)構(gòu)體

綜上,我們想要需要構(gòu)造如下的fake_FILE結(jié)構(gòu)體來利用FSOP方法來繞過新版本中對vtablecheck叛赚,從而達到利用的目的澡绩。

fake_FILE

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市俺附,隨后出現(xiàn)的幾起案子肥卡,更是在濱河造成了極大的恐慌,老刑警劉巖事镣,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件步鉴,死亡現(xiàn)場離奇詭異,居然都是意外死亡璃哟,警方通過查閱死者的電腦和手機唠叛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沮稚,“玉大人艺沼,你說我怎么就攤上這事≡烫停” “怎么了障般?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵调鲸,是天一觀的道長。 經(jīng)常有香客問我挽荡,道長藐石,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任定拟,我火速辦了婚禮于微,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘青自。我一直安慰自己株依,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布延窜。 她就那樣靜靜地躺著恋腕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪逆瑞。 梳的紋絲不亂的頭發(fā)上荠藤,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音获高,去河邊找鬼哈肖。 笑死,一個胖子當(dāng)著我的面吹牛念秧,可吹牛的內(nèi)容都是我干的牡彻。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼出爹,長吁一口氣:“原來是場噩夢啊……” “哼庄吼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起严就,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤总寻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后梢为,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體渐行,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年铸董,在試婚紗的時候發(fā)現(xiàn)自己被綠了祟印。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡粟害,死狀恐怖蕴忆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情悲幅,我是刑警寧澤套鹅,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布站蝠,位于F島的核電站,受9級特大地震影響卓鹿,放射性物質(zhì)發(fā)生泄漏菱魔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一吟孙、第九天 我趴在偏房一處隱蔽的房頂上張望澜倦。 院中可真熱鬧,春花似錦杰妓、人聲如沸藻治。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽栋艳。三九已至恰聘,卻和暖如春句各,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背晴叨。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工凿宾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人兼蕊。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓初厚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親孙技。 傳聞我的和親對象是個殘疾皇子产禾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容