原文來自安全客,作者:raycp
原文鏈接:https://www.anquanke.com/post/id/177910

前言

最近打算詳細整理下IO FILE相關的筆記,不少地方都是知道個大概,因此這次打算從源碼出發,把IO FILE相關的東西都過一遍。

思路大致是fopenfwrite以及fread之類的IO函數的源碼分析,再到libc2.24對vtable檢查之前的利用方式,再到vtable檢查的分析以及相應的對抗方式。

第一篇fopen詳解,主要是基于源碼的分析,源碼的動態調試建議大家使用帶調試符號的glibc,再次給大家推薦pwn_debug,可以很方便的安裝帶調試符號的glibc,使用debug模式即可。

源碼分析

首先編寫一個簡單的調用fopen函數的c程序。

#include<stdio.h>

int main(){

    FILE*fp=fopen("test","wb");
    char *ptr=malloc(0x20);
    return 0;
}

編譯出來之后使用pwn_debug的debug模式開啟程序,或者指定帶調試符號的glibc,我這里使用的glibc版本是2.23。接下來開始分析。

gdb跟進去fopen函數,可以看到fopen實際上是 _IO_new_fopen函數,該函數在/libio/iofopen.c文件中,可以看到它調用的是__fopen_internal

_IO_FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
  return __fopen_internal (filename, mode, 1);
}

跟進去__fopen_internal中,關鍵源碼如下:

_IO_FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
  struct locked_FILE
  {
    struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
    _IO_lock_t lock;
#endif
    struct _IO_wide_data wd;
  } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));  ## step 1 分配內存

...

  _IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps); ## step 2 null初始化結構體數據 
...

  _IO_JUMPS (&new_f->fp) = &_IO_file_jumps; ## 設置vtable為_IO_file_jumps
  _IO_file_init (&new_f->fp); ## step 3 將file結構體鏈接進去_IO_list_all

...
  # step 4 打開文件
  if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
    return __fopen_maybe_mmap (&new_f->fp.file);

}

整個__fopen_internal函數包含四個部分: 1. malloc分配內存空間。 2. _IO_no_init 對file結構體進行null初始化。 3. _IO_file_init將結構體鏈接進_IO_list_all鏈表。 4. _IO_file_fopen執行系統調用打開文件。

下面詳細分析跟進去每個子函數進行分析。

malloc分配內存空間

可以看到首先調用malloc函數分配了一個struct locked_FILE大小的結構體,這個結構體比函數剛開始的地方定義,在64位系統中為0x230,該結構體包含三個_IO_FILE_plus_IO_lock_t_IO_wide_data,其中_IO_FILE_plus為使用的IO FILE的結構體。執行完malloc后內存狀態如下:

_IO_no_init 對file結構體進行null初始化

在分配完空間后,接著就調用_IO_no_init函數去null初始化結構體,跟進去該函數,函數在/libio/genops.c中:

void
_IO_old_init (_IO_FILE *fp, int flags)
{
  fp->_flags = _IO_MAGIC|flags;
  fp->_flags2 = 0;
  fp->_IO_buf_base = NULL;
  fp->_IO_buf_end = NULL;
  fp->_IO_read_base = NULL;
  fp->_IO_read_ptr = NULL;
  fp->_IO_read_end = NULL;
  fp->_IO_write_base = NULL;
  fp->_IO_write_ptr = NULL;
  fp->_IO_write_end = NULL;
  fp->_chain = NULL; /* Not necessary. */

  fp->_IO_save_base = NULL;
  fp->_IO_backup_base = NULL;
  fp->_IO_save_end = NULL;
  fp->_markers = NULL;
  fp->_cur_column = 0;
...
  fp->_vtable_offset = 0;
...
}
void
_IO_no_init (_IO_FILE *fp, int flags, int orientation,
         struct _IO_wide_data *wd, const struct _IO_jump_t *jmp)
{
  _IO_old_init (fp, flags);
  fp->_mode = orientation;
  ...
      ## 初始化fp的_wide_data字段
      fp->_wide_data = wd;
      fp->_wide_data->_IO_buf_base = NULL;
      fp->_wide_data->_IO_buf_end = NULL;
      fp->_wide_data->_IO_read_base = NULL;
      fp->_wide_data->_IO_read_ptr = NULL;
      fp->_wide_data->_IO_read_end = NULL;
      fp->_wide_data->_IO_write_base = NULL;
      fp->_wide_data->_IO_write_ptr = NULL;
      fp->_wide_data->_IO_write_end = NULL;
      fp->_wide_data->_IO_save_base = NULL;
      fp->_wide_data->_IO_backup_base = NULL;
      fp->_wide_data->_IO_save_end = NULL;

      fp->_wide_data->_wide_vtable = jmp;
    ...
  fp->_freeres_list = NULL;
}

可以看到函數最主要的功能是初始化locked_FILE里面的_IO_FILE_plus結構體,基本上將所有的值都初始化為null以及默認值,同時將_wide_data字段賦值并初始化。初始化結束后,FILE結構體如下:

_IO_file_init將結構體鏈接進_IO_list_all。

在執行完_IO_no_init函數后,回到__fopen_internal函數,函數將_IO_FILE_plus結構體的vtable設置成了_IO_file_jumps,然后調用_IO_file_init_IO_FILE_plus結構體鏈接進入_IO_list_all鏈表,跟進去函數,函數在/libio/fileops.c中:

void
_IO_new_file_init (struct _IO_FILE_plus *fp)
{

  fp->file._offset = _IO_pos_BAD;
  fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS;
  ## 調用_IO_link_in和設置_fileno
  _IO_link_in (fp);
  fp->file._fileno = -1;
}
libc_hidden_ver (_IO_new_file_init, _IO_file_init)

看到這個函數的主體就是調用了_IO_link_in函數,跟進去,函數在/libio/genops.c中:

void
_IO_link_in (struct _IO_FILE_plus *fp)
{
  ## 檢查flag的標志位是否是_IO_LINKED
  if ((fp->file._flags & _IO_LINKED) == 0)
    {
      ## 設置_IO_LINKED標志位
      fp->file._flags |= _IO_LINKED;
      ...
      fp->file._chain = (_IO_FILE *) _IO_list_all;
      _IO_list_all = fp;
      ++_IO_list_all_stamp;
      ...
    }
}
libc_hidden_def (_IO_link_in)

之前一直都知道FILE結構體是通過_IO_list_all的單鏈表進行管理的,這里_IO_link_in函數的功能是檢查FILE結構體是否包含_IO_LINKED標志,如果不包含則表示這個結構體沒有鏈接進入_IO_list_all,則再后面把它鏈接進入_IO_list_all鏈表,同時設置FILE結構體的_chain字段為之前的鏈表的值,否則直接返回。

所以_IO_file_init主要功能是將FILE結構體鏈接進入_IO_list_all鏈表,在沒執行_IO_file_init函數前_IO_list_all指向的是stderr結構體: 執行完后可以看到_IO_list_all指向的是申請出來的結構體: 同時此時FILE結構體的_chain字段指向了之前的stderr結構體:

_IO_file_fopen打開文件句柄

將FILE結構體鏈接到_IO_list_all鏈表后,程序返回到__fopen_internal中,接下來就調用_IO_new_file_fopen函數,跟進去該函數,函數在libio/fileops.c文件中:

_IO_FILE *
_IO_new_file_fopen (_IO_FILE *fp, const char *filename, const char *mode,
            int is32not64)
{

  ...
  ## 檢查文件是否以打開,打開則返回
  if (_IO_file_is_open (fp))
    return 0;
  ## 設置文件打開模式
  switch (*mode)
    {
    case 'r':
      omode = O_RDONLY;
      read_write = _IO_NO_WRITES;
      break;
      ...    
     }
  ...
  ## 調用_IO_file_open函數
  result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write,
              is32not64);
  ...
}
libc_hidden_ver (_IO_new_file_fopen, _IO_file_fopen)

函數先檢查文件描述符是否打開,然后設置文件打開的模式,最后調用_IO_file_open函數,跟進去_IO_file_open函數,該函數在/libio/fileops.c里面:

_IO_FILE *
_IO_file_open (_IO_FILE *fp, const char *filename, int posix_mode, int prot,
           int read_write, int is32not64)
{
  int fdesc;

  ...
  # 調用系統函數open打開文件  
  fdesc = open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
  ...
  # 將文件描述符設置到FILE結構體的相應字段_fileno里
  fp->_fileno = fdesc;
  ...
  #再次調用_IO_link_in
  _IO_link_in ((struct _IO_FILE_plus *) fp);
  return fp;
}
libc_hidden_def (_IO_file_open)

函數的主要功能就是執行系統調用open打開文件,并將文件描述符賦值給FILE結構體的_fileno字段,最后再次調用_IO_link_in函數,確保該結構體被鏈接進入_IO_list_all鏈表。

執行完_IO_new_file_fopen函數后,FILE結構體為:

該函數執行完后,程序返回FILE結構體指針,分析結束

小結

看完代碼后,可以將fopen整體的流程可以歸納為:

  1. malloc分配內存空間。
  2. _IO_no_init 對file結構體進行null初始化。
  3. _IO_file_init將結構體鏈接進_IO_list_all鏈表。
  4. _IO_file_fopen執行系統調用打開文件。

整個流程還是比較簡單的,fopen返回之后_IO_list_all鏈表指向返回的FILE結構體,且FILE結構體的_chain字段指向之前的結構體(沒有其他額外打開文件的話,將是指向stderr),同時其他的字段大多都是默認的null值,vtable存儲的是__GI__IO_file_jumps函數表,截圖如下。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/920/