# FILE STRUCTER ATTACK ### I. Giới thiệu - `FILE` là một cấu trúc trong linux dùng đê thao tác trên tệp, có 3 cấu trúc file ở libc luôn mở là `stdin`, `stdout` và `stderr`. Thực tế chúng là `_IO_2_1_stdin_`, `_IO_2_1_stdout_` và `_IO_2_1_stderr_`. - Cấu trúc của file được định nghĩa như sau: ```c 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 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; int _flags2; __off_t _old_offset; /* This used to be _offset but it's too small. */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE }; typedef struct _IO_FILE FILE; ``` - Nhìn ở trên ta có thể thấy rằng cầu trúc `FILE` được liên kết với nhau thành một `chain`(theo kiểu liên kết đơn) và đứng đầu là `_IO_list_all`. ![](https://hackmd.io/_uploads/ry53bApIn.png) - Nhưng cấu trúc FILE thực tế lại được bọc bởi một cấu trúc khác là `_IO_FILE_plus`. ```c 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); }; /* We always allocate an extra word following an _IO_FILE. This contains a pointer to the function jump table used. This is for compatibility with C++ streambuf; the word can be used to smash to a pointer to a virtual function table. */ struct _IO_FILE_plus { FILE file; const struct _IO_jump_t *vtable; }; extern struct _IO_FILE_plus _IO_2_1_stdin_; extern struct _IO_FILE_plus _IO_2_1_stdout_; extern struct _IO_FILE_plus _IO_2_1_stderr_; ``` ![](https://hackmd.io/_uploads/B1a6XRaI3.png) - Này là vừa in chuỗi `/bin/sh`. - Cấu trúc mới này giúp `FILE` thêm các hàm sử lý cho `FILE` thông qua `vtable`. Khi một hàm được gọi `vtable` sẽ xác định chức năng của hàm đó và gọi đến một hàm tương ứng, ví dụ như `puts` sẽ gọi đến `xsputn`. ### II. Kỹ thuật khai thác #### 1. Giả mạo Vtable - Hiện tại mình sử dụng source của `libc-2.35` do nó không có gì khác với các bản cũ. - Đầu tiên cái dễ nhìn nhất là giả mạo `vtable` do phân vùng trên `FILE` được lưu trên địa chỉ `rw-p` nên ta có thể ghi đè chúng. ```c #include <stdio.h> #include <stdlib.h> int main(void) { FILE *fp; long int *vtable_ptr_fake = malloc(0x10); fp=fopen("flag.txt","rw"); vtable_ptr_fake[7] = &system; // system fake __xsputn *(long long*)((long long)fp+0xd8) = vtable_ptr_fake; strcpy(fp,"/bin/sh"); fwrite("hi",2,1,fp); } ``` - fp ban đầu nó sẽ như thế này: ![](https://hackmd.io/_uploads/Ske7Zx0Uh.png) - Và sau khi overwrite lại `vtable`: ![](https://hackmd.io/_uploads/Hyd_WxA83.png) - `fwrite` sẽ gọi đến `__xsputn` hiện tại là `system` với argument tương ứng là `fp`, `buf`, `size`, do đó ta cần thay đổi giá trị trong `fp` thành `/bin/sh` để nó thực hiện `system("/bin/sh")`. ![](https://hackmd.io/_uploads/SyxBfeA82.png) - Ngoài các hàm thao tác trên file như trên (có f đứng đầu) thì vẫn có một số hàm dành riêng cho các chuẩn I/O đó là `puts`, `gets`, ... và lúc này ta sẽ ghi đè `vtable` của các I/O này. - Với kỹ thuật này thì mình đang thực hiện trên libc-2.23, và cũng đã thử trên phiên bản `2.27` những nó không hoạt động do ở phiên bản này nó đã thêm tính năng check `vtable`. - Kỹ thuật này yêu cầu có `libc` hoặc bt được các hàm như `system`, `execve` do đó không khuyến khích sử dụng. #### 2. FSOP - Kỹ thuật này sẽ thay đổi cấu trúc `_IO_FILE` thường là I/O để leak `libc` khi mà chương trình không cho ta điều kiện để leak `libc`. - Ở đây mình sẽ sử dụng hàm `puts` cho kỹ thuật này. - Đầu tiên ta cần hiểu hàm `puts` hoạt động như thế nào. ```c int _IO_puts (const char *str) { int result = EOF; size_t len = strlen (str); _IO_acquire_lock (stdout); if ((_IO_vtable_offset (stdout) != 0 || _IO_fwide (stdout, -1) == -1) && _IO_sputn (stdout, str, len) == len && _IO_putc_unlocked ('\n', stdout) != EOF) result = MIN (INT_MAX, len + 1); _IO_release_lock (stdout); return result; } weak_alias (_IO_puts, puts) libc_hidden_def (_IO_puts) ``` - `weak_alias` là một `defined` tức là khi gọi đến `puts` thì nó đang gọi đến `_IO_puts`. - Sau khi debug nhiều lần thì mình thấy rằng output được in ra do hàm `_IO_sputn` xử lý. Hàm này thực thế nó gọi đến `_IO_new_file_xsputn `. Trong hàm này nó xử lý rất nhiều cái và output được in ra sau khi `_IO_OVERFLOW` thực hiện, nó cũng chính là hàm mà ta cần phân tích. ```c int _IO_new_file_overflow (FILE *f, int ch) { if (f->_flags & _IO_NO_WRITES) /* SET ERROR */ { f->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } /* If currently reading or no buffer allocated. */ if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) { /* Allocate a buffer if needed. */ if (f->_IO_write_base == NULL) { _IO_doallocbuf (f); _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base); } /* Otherwise must be currently reading. If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end, logically slide the buffer forwards one block (by setting the read pointers to all point at the beginning of the block). This makes room for subsequent output. Otherwise, set the read pointers to _IO_read_end (leaving that alone, so it can continue to correspond to the external position). */ if (__glibc_unlikely (_IO_in_backup (f))) { size_t nbackup = f->_IO_read_end - f->_IO_read_ptr; _IO_free_backup_area (f); f->_IO_read_base -= MIN (nbackup, f->_IO_read_base - f->_IO_buf_base); f->_IO_read_ptr = f->_IO_read_base; } if (f->_IO_read_ptr == f->_IO_buf_end) f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base; f->_IO_write_ptr = f->_IO_read_ptr; f->_IO_write_base = f->_IO_write_ptr; f->_IO_write_end = f->_IO_buf_end; f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end; f->_flags |= _IO_CURRENTLY_PUTTING; if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED)) f->_IO_write_end = f->_IO_write_ptr; } if (ch == EOF) return _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base); if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */ if (_IO_do_flush (f) == EOF) return EOF; *f->_IO_write_ptr++ = ch; if ((f->_flags & _IO_UNBUFFERED) || ((f->_flags & _IO_LINE_BUF) && ch == '\n')) if (_IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base) == EOF) return EOF; return (unsigned char) ch; } libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow) ``` - hàm `_IO_OVERFLOW` được gọi như sau `_IO_OVERFLOW (f, EOF) == EOF`, do đó `ch == EOF` và `_IO_do_write`(đại loại là nó sẽ call write) sẽ được gọi. - `_IO_do_write` được truyền vào `buffer` tương ứng là `f->_IO_write_base` và đối với các phiên bản libc-2.31 trở xuống thì nó được cấp phát trên `libc`. ![](https://hackmd.io/_uploads/S1enXN1Pn.png) - Như ở hình trên, ta chỉ cần `modify` `f->_IO_write_base` 1 byte cuối của nó thành `08` là có thể in ra được `libc`. ![](https://hackmd.io/_uploads/HyTOEVkvh.png) - Nhưng để đến được đây mà dữ liệu không bị thay đổi thì ta chỉ cần pass qua cái cục nợ này `if (f->_flags & _IO_NO_WRITES)`và `if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)`. Dễ nhất là ta thây đổi `flags`. - Còn một cái nữa nó nằm trong `_IO_do_write` là `fp->_flags & _IO_IS_APPENDING`. ```c int _IO_new_do_write (FILE *fp, const char *data, size_t to_do) { return (to_do == 0 || (size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF; } libc_hidden_ver (_IO_new_do_write, _IO_do_write) static size_t new_do_write (FILE *fp, const char *data, size_t to_do) { size_t count; if (fp->_flags & _IO_IS_APPENDING) /* On a system without a proper O_APPEND implementation, you would need to sys_seek(0, SEEK_END) here, but is not needed nor desirable for Unix- or Posix-like systems. Instead, just indicate that offset (before and after) is unpredictable. */ fp->_offset = _IO_pos_BAD; else if (fp->_IO_read_end != fp->_IO_write_base) { off64_t new_pos = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1); if (new_pos == _IO_pos_BAD) return 0; fp->_offset = new_pos; } count = _IO_SYSWRITE (fp, data, to_do); if (fp->_cur_column && count) fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1; _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base); fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base; fp->_IO_write_end = (fp->_mode <= 0 && (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED)) ? fp->_IO_buf_base : fp->_IO_buf_end); return count; } ``` - 3 thứ ta cần để bypass là: ```c #define _IO_MAGIC 0xFBAD0000 #define _IO_CURRENTLY_PUTTING 0x0800 #define _IO_IS_APPENDING 0x1000 #define _IO_NO_WRITES 0x0008 ``` - `flags = _IO_MAGIC | _IO_CURRENTLY_PUTTING | _IO_IS_APPENDING` - Kỹ thuật này có thể sử dụng với nhiều phiên bản `libc` mới tuy nhiên sẽ khó khăn hơn. Do nó chuyển từ cấp phát `buffer` trên `libc` sang `heap` ở bản `libc-2.35` tuy nhiên ta có thể thay bằng các got điều đó dẫn đến phải thay đổi cả `f->_IO_write_ptr` vì nó sẽ tính ra size cho output. - Các hàm input như `gets` vẫn có gọi đến `_IO_OVERFLOW` do đó vẫn có thể sử dụng kỹ thuật này.