# FILE ```c 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 }; struct _IO_FILE_complete { struct _IO_FILE _file; #endif #if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001 _IO_off64_t _offset; # if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T /* Wide character stream stuff. */ struct _IO_codecvt *_codecvt; struct _IO_wide_data *_wide_data; struct _IO_FILE *_freeres_list; void *_freeres_buf; # else void *__pad1; void *__pad2; void *__pad3; void *__pad4; 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)]; #endif }; ``` - Các cấu trúc FILE trong tiến trình sẽ được kết nối với nhau thông qua trường **`_chain`** để tạo thành một danh sách liên kết. - Trong thư viện **`I/O tiêu chuẩn`**, ba luồng tệp sẽ tự động được mở khi mỗi chương trình khởi động: **`stdin, stdout và stderr.`** (lưu ý rằng ba luồng tệp này nằm trong phần dữ liệu của libc.so.) - Nhưng trên thực tế, cấu trúc _IO_FILE được bao bọc bởi một cấu trúc khác _IO_FILE_plus, cấu trúc này chứa một con trỏ quan trọng vtable trỏ đến một loạt các con trỏ hàm. - Trong phiên bản **`libc2.23`**, độ lệch vtable 32 bit là 0x94 và độ lệch 64 bit là 0xd8 ``` struct _IO_FILE_plus { _IO_FILE file; IO_jump_t *vtable; } ``` - fread - fwrite - fopen - fclose - printf / puts # vtable - Nhắc lại kiến thức cũ: ``` struct _IO_FILE_plus { _IO_FILE file; IO_jump_t *vtable; } ``` - Đặc biệt, có một vtable trong cấu trúc _IO_FILE_plus và một số hàm sẽ lấy ra các con trỏ trong vtable và gọi chúng. - Ý tưởng của FILE EXPLOIT : Chiểm quyền điều khiển vtable -> thay đổi luồng thực thi của chương trình ``` 1. viết lại trực tiếp con trỏ hàm trong vtable, điều này có thể đạt được bằng cách ghi vào bất kỳ địa chỉ nào. 2. ghi đè con trỏ của vtable để trỏ đến bộ nhớ mà chúng ta điều khiển, sau đó đặt con trỏ hàm vào đó. ``` # FSOP ## Đi sâu vào glibc - Trong quá trình hoạt động của chương trình, luôn có 3 IO_FILE luôn mở : - `_IO_2_1_stderr_` - `_IO_2_1_stdout_` - `_IO_2_1_stdin_` -> tạo thành 1 DSLK ![image](https://hackmd.io/_uploads/Hkvzcl3a0.png) - Ngoài `IO_FILE` thì còn có `IO_FILE_plus` ```c struct _IO_FILE_plus { FILE file; const struct _IO_jump_t *vtable; }; ``` -> có thể hiểu : **_IO_FILE_plus = _IO_FILE + vtable** Xem thêm : https://hackmd.io/@kyr04i/SkF_A-fnn#2-DIVING-INTO-GLIBC-CODE ## Leak libc qua FSOP - demo bằng hàm **`puts`** ```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) ``` **`puts`** -------------------------- weak alias ----------------------------> **`_IO_puts`** - Hàm **`_IO_puts`** lại call **`_IO_sputn`** ![image](https://hackmd.io/_uploads/By5ZI766C.png) - điều đó có nghĩa là nó sẽ nhảy đến con trỏ được lưu trữ cho **key** ``__xsputn`` trong **stdout** ``FILE`` - **stdout** ánh xạ đến **key** ``__xsputn`` trong phương thức ``_IO_new_file_xsputn`` (xem GDB bên dưới) ```pwndbg pwndbg> p _IO_2_1_stdout_ $1 = { file = { _flags = 0xfbad2887, _IO_read_ptr = 0x7ffff7fae803 <_IO_2_1_stdout_+131> "\n", _IO_read_end = 0x7ffff7fae803 <_IO_2_1_stdout_+131> "\n", _IO_read_base = 0x7ffff7fae803 <_IO_2_1_stdout_+131> "\n", _IO_write_base = 0x7ffff7fae803 <_IO_2_1_stdout_+131> "\n", _IO_write_ptr = 0x7ffff7fae803 <_IO_2_1_stdout_+131> "\n", _IO_write_end = 0x7ffff7fae803 <_IO_2_1_stdout_+131> "\n", ... _wide_data = 0x7ffff7fad9a0 <_IO_wide_data_1>, }, vtable = 0x7ffff7faa600 <__GI__IO_file_jumps> } pwndbg> p __GI__IO_file_jumps $2 = { ... __overflow = 0x7ffff7e20e40 <_IO_new_file_overflow>, __underflow = 0x7ffff7e20b30 <_IO_new_file_underflow>, __uflow = 0x7ffff7e21de0 <__GI__IO_default_uflow>, __pbackfail = 0x7ffff7e23300 <__GI__IO_default_pbackfail>, __xsputn = 0x7ffff7e1f680 <_IO_new_file_xsputn>, ... __write = 0x7ffff7e1ef40 <_IO_new_file_write>, } ``` - Sau khi debug bằng chương trình chỉ có hàm **`puts`** đơn giản, ta thấy : ![image](https://hackmd.io/_uploads/rJZ4wQpa0.png) -> Kiểm tra [source](https://elixir.bootlin.com/glibc/glibc-2.35/source/libio/fileops.c#L1195) của <**`_IO_file_xsputn`**> ( khá dài nên ta sẽ chỉ tập trung những thứ có thể khai thác được ) : ```c size_t _IO_new_file_xsputn (FILE *f, const void *data, size_t n) { const char *s = (const char *) data; size_t to_do = n; int must_flush = 0; size_t count = 0; ... if (to_do + must_flush > 0) { size_t block_size, do_write; /* Next flush the (full) buffer. */ if (_IO_OVERFLOW (f, EOF) == EOF) /* If nothing else has to be written we must not signal the caller that everything has been written. */ return to_do == 0 ? EOF : n - to_do; /* Try to maintain alignment: write a whole number of blocks. */ block_size = f->_IO_buf_end - f->_IO_buf_base; do_write = to_do - (block_size >= 128 ? to_do % block_size : 0); if (do_write) { count = new_do_write (f, s, do_write); to_do -= count; if (count < do_write) return n - to_do; } ... } libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn) ``` - Đi thẳng vào vấn đề, sau khi debug thì hàm **`new_do_write`** là hàm sẽ in chuỗi ra màn hình ([source](https://elixir.bootlin.com/glibc/glibc-2.35/source/libio/fileops.c#L421)) ```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); ... } ... #define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN) ... ssize_t _IO_new_file_write (FILE *f, const void *data, ssize_t n) { ssize_t to_do = n; while (to_do > 0) { ssize_t count = (__builtin_expect (f->_flags2 & _IO_FLAGS2_NOTCANCEL, 0) ? __write_nocancel (f->_fileno, data, to_do) : __write (f->_fileno, data, to_do)); if (count < 0) { f->_flags |= _IO_ERR_SEEN; break; } to_do -= count; data = (void *) ((char *) data + count); } n -= to_do; if (f->_offset >= 0) f->_offset += n; return n; } ``` -> Ta có thể thấy nó sử dụng **`_IO_SYSWRITE`** để in chuỗi ( tương đương với sử dụng syscall ) ![image](https://hackmd.io/_uploads/SyCe9QTT0.png) - Điều này tương đương với : ``write(1, _IO_write_base, _IO_write_ptr - _IO_write_base)`` ### glibc check - Sau khi hiểu được 1 chút cách thức gọi hàm của hàm **`puts()`**, câu hỏi đặt ra là làm thế nào để chương trình in ra màn hình đúng chuổi ta thêm vào ????????? -> Cùng quay trở lại hàm **`_IO_file_xsputn`** ([source](https://elixir.bootlin.com/glibc/glibc-2.35/source/libio/fileops.c#L1195)) ```c size_t _IO_new_file_xsputn (FILE *f, const void *data, size_t n) { const char *s = (const char *) data; size_t to_do = n; int must_flush = 0; size_t count = 0; ... if (to_do + must_flush > 0) { size_t block_size, do_write; /* Next flush the (full) buffer. */ if (_IO_OVERFLOW (f, EOF) == EOF) /* If nothing else has to be written we must not signal the caller that everything has been written. */ return to_do == 0 ? EOF : n - to_do; ...... ...... ...... ``` -> Câu trả lời sẽ nằm ở hàm **`_IO_OVERFLOW`** ( và luôn nhớ rằng `_IO_OVERFLOW` có đối số thứ 2 là `EOF` ) [source](https://elixir.bootlin.com/glibc/glibc-2.35/source/libio/fileops.c#L730) của `_IO_OVERFLOW` : ```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) { ... } if (ch == EOF) return _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base); ... } libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow) ``` - Với `arg = EOF` : `_IO_new_file_overflow` -> call `_IO_do_write` nhưng trước đó sẽ check các kiểu -> đây chính là các điều kiện cần bypass - Sau khi FSOP thành công, chúng ta thắc mắc rằng ngay cả khi `put("hello world")` nhưng in ra string không chỉ là `"hello world"` vậy điều gì diễn ra trong puts nhỉ? -> thì câu trả lời nằm ở hàm `_IO_new_file_overflow` [_IO_new_file_overflow.c ](https://elixir.bootlin.com/glibc/glibc-2.35/source/libio/fileops.c#L730) ```c _IO_new_file_overflow (FILE *f, int ch) { if (f->_flags & _IO_NO_WRITES) /* SET ERROR */ { ... } /* 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) ... } ``` ### work flow ```c _IO_puts(str) = puts() |_ _IO_sputn = _IO_XSPUTN(__fp,__s,__n) |_ _IO_new_file_xsputn (stdout, str, len) |_ _IO_OVERFLOW = _IO_new_file_overflow (stdout, EOF) -> Check -> set up string to print (chính là đoạn mình bypass) |_ new_do_write(stdout, stdout->_IO_write_base, stdout->_IO_write_ptr - stdout->_IO_write_base) |_ _IO_new_file_write(stdout, stdout->_IO_write_base, stdout->_IO_write_ptr - stdout->_IO_write_base) |_ write(stdout->fileno, stdout->_IO_write_base, stdout->_IO_write_ptr - stdout->_IO_write_base) ``` ### define variables - Các biến đã được glibc [define](https://elixir.bootlin.com/glibc/glibc-2.35/source/libio/libio.h#L66) : ```c #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 ``` -> Sau khi đã có những thứ cần thiết, bypass thui ------> to be continue ### Bypass - Để có thể write những thứ mình muốn, cần thỏa mãn điều kiện : 1. Điều kiện 1 : ```c if (f->_flags & _IO_NO_WRITES) /* SET ERROR */ { f->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } ``` -> `_IO_NO_WRITES` = 0x0008 -> `f->_flags & _IO_NO_WRITES` return 0 2. Điều kiện 2 : ```c if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) { ... } ``` - `f->_IO_write_base` luôn trỏ đến libc_arena ---> `f->_IO_write_base == NULL` luôn False - `f->_flags & _IO_CURRENTLY_PUTTING` mà `_IO_CURRENTLY_PUTTING` = 0x0800 ---> ``stdout->_flags & 0x0800`` phải bằng 1 3. Đến hàm ``new_do_write``, ta cần skip điều kiện ![image](https://hackmd.io/_uploads/SyscwVTTA.png) - ``if (fp->_flags & _IO_IS_APPENDING)`` cần return **True** (để tránh rơi vào else if với 1 đống shit ở dưới ) - ``_IO_IS_APPENDING`` là 0x1000 -> ``stdout->_flags & 0x1000`` phải bằng 1 - Nhưng nếu không thể thì như thế nào nhỉ ??? - So sánh giá trị `fp->_IO_read_end` và `fp->_IO_write_base` nếu khác nhau, sẽ gọi hàm `_IO_SYSSEEK()`. Bởi vì giá trị của `fp->_IO_read_end` và `fp->_IO_write_base` đang bằng nhau nên có thể dễ dàng bypass được kiện check. ĐI sâu vào bên trong, `_IO_SYSSEEK()` sẽ gọi syscall `lseek()` với tham số là `offset=fp->_IO_write_base - fp->_IO_read_end`. Vì vậy, nếu `fp->_IO_write_base` < `fp->_IO_read_end` thì `offset` sẽ có giá trị âm và làm cho chương trình báo lỗi. Như vậy, để nó không xảy ra thì ta chỉ cần overwrite LSB của `fp->_IO_write_base` thành null byte, nhưng muốn chắc thì ta cũng overwrite LSB của`fp->_IO_write_base` thành null. #### Nhiệm vụ bypass ```c stdout->_flags & _IO_NO_WRITES == 0 stdout->_flags & _IO_CURRENTLY_PUTTING == 1 stdout->_flags & _IO_IS_APPENDING == 1 | | v _flags & 0x8 = 0 _flags & 0x800 = 1 _flags & 0x1000 = 1 -------------------------------------------------- ==> _flags = 1800 ``` ### target leak - Sau khi bypass thành công, vậy thì leak cái gì, leak như thế nào ??? - Lại nhắc về hàm `write(1, _IO_write_base, _IO_write_ptr - _IO_write_base)` vậy thì ta có những thứ cần thay đổi : - `_IO_write_base` : `_IO_do_write` (nói ở trên là sẽ call 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. ![image](https://hackmd.io/_uploads/r1LFgBaTA.png) -> chỉ cần `overwrite byte cuối` thành `08` -> leak libc ### warning - kỹ thuật nãy có thể áp dụng lên nhiều phiên bản libc mới (tất nhiên cách áp dụng sẽ khó khăn hơn) - vì từ glibc 2.35, chuyển từ cấp phát **buffer** trên libc sang heap - ta vẫn có thể thay đổi điều đó bằng GOT, cũng như thay đổi cả ``f->_IO_write_ptr`` (vì nó tính size cho output) - ngoài **puts**, hàm nhận input như **gets** vẫn gọi đến ``_IO_OVERFLOW`` ---> still available to use this technique