# PWN cheatsheet ###### tags: `pwn 👻` [toc] ## info - [x64 syscall table](https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/) - [x86 syscall table](https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#x86-32_bit) - libc_version - download from docker ubuntu tarballs - https://partner-images.canonical.com/core/ - libc_db: 用 offset 查 libc 版本 - https://libc.rip - https://libc.blukat.me/ ## debug symbols libc 1. 查看 https://github.com/matrix1001/glibc-all-in-one 來下載不同版本的 libc 2. 將 debug symbol 的 libc 丟到 `/usr/lib/debug` (可能會很亂) ### gdb 手動設 - 設定 LD_PRELOAD:`set exec-wrapper env "LD_PRELOAD=./libc-2.29.so"` (for 2.29) - 有些時候直接用 `set environment LD_PRELOAD=./libc.so` 就可以,有時後不行 - 設定 debug path:`set debug-file-directory /usr/lib/debug/lib/x86_64-linux-gnu` ref: https://stackoverflow.com/questions/10000335/how-to-use-debug-version-of-libc ### glibc source code ```shell= sudo apt-get install glibc-source cd /usr/src/glibc sudo tar xvf glibc-2.31.tar.xz ``` libc package (dbg prefix - debug symbols) https://mirror.tuna.tsinghua.edu.cn/ubuntu/pool/main/g/glibc/ In gdb: `dir /usr/src/glibc/glibc-2.31/exit.c` make debug symbol glibc ``` mkdir OWO && cd OWO ../glibc-2.31/configure --prefix $PWD --enable-debug make -j4 # elf/ld.so == linker # ./libc.so == libc 在用 ld_changer 改 binary 的 ld,用 LD_PRELOAD=./libc.so ./binary 來執行 ``` ## helper - shell command ``` - 看 got offset: objdump -R - 看保護機制: checksec - 靜態分析 asm: objdump -d - 看 lib info: ldd ./binary - 看 symbol offset: readelf -s - strace syscall: strace -e trace=read,write ./<elf> - checksec: 查看保護機制 - 查看 binary 的 pid: pidof <elf> - 看使用到的 linker and libc: ldd <elf> - LD_SHOW_AUXV=1 ./<elf> - ncat -vc <elf> -kl <127.0.0.1> <port>: 開啟 binary server (要裝 nmap) - ltrace: ltrace shows parameters of invoked functions and system calls ``` - gdb ``` - reg info: info registers rax - info locals: 看 local var - print environ: 印出環境變數的 address - telescope 0x1234567(addr) 123(number): 印出 memory 的 content - set reg value: set $eip=0xValue - stack frame: stack 40 - gdb -q: quite 打開 gdb - b main: 在 main 設斷點 - r: run - vmmap: virtual memory map - x/40gx <address>: (g: 8 bytes, w: 4 bytes, b: 1 byte, s: string 到 `\x00`) - x/10i <address>: address 的 inst - set {long}0x123456789abc=1234: 修改某 memory address 的值 - i r: 查看 reg 的 value - xinfo <address>: 查看 address 的位址以及該區段的 info - s: 執行一行(若 function 則進入) - si: 執行一行 asm - fin: 執行 function 並跳回上層 - attach pid: debug running process - p var: print var 的 value - info function <function_name>: 查看 function - info locals: 看 local variable - info auxv: 看 auxiliary vector - lay asm - lay src - bt: back strace, 看 function stack - heapinfo 看 bin & chunk - heapbase heap base address - telescope - context: 重現當前 reg stack 的資料 ``` - gdb trace libc ``` #### In shell sudo apt-get install libc6-dbg ; 安裝帶有 debug symbols 的 libc sudo cp /etc/apt/sources.list /etc/apt/sources.list~ # 備份 sudo sed -Ei 's/^# deb-src /deb-src /' /etc/apt/sources.list # 將 source 的 repo 加到 apt 內 sudo apt-get update sudo apt install glibc-source ; 會在 /usr/src/glibc/ 拿到一包 glibc src code sudo tar xvf glibc-2.31.tar.xz ; 解壓縮 #### In gdb gdb > dir /path/to/src/code/malloc.c gdb > start ``` - pwntools ``` fmtstr_payload(arg_offset, {target: value}, write_size='byte', numbwritten=already_written_bytes) ``` - disable ptrace limitation ``` echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope ``` - extract text section ``` objcopy -j.text -O binary X X.bin ``` ## base - LD_LIBRARY_PATH, LD_PRELOAD - 跑不同版本 libc - loader 的版本也需要更改, 用 patch 的方式改 ld 的版本 - x64 calling convention - user: rdi rsi rdx rcx r8 r9 - syscall: rdi rsi rdx r10 r8 r9 - syscall id: rax - return value: rax - common instruction ``` mov lea syscall leave ; mov rsp, rbp; pop rbp lea rax, [rbx + 0x10] ; rax = rbx + 0x10 ``` - syscall (`strace ./binary`) ``` sys_read: 0, fd, *buf, count sys_write: 1, fd, *buf, count sys_open: 2, filename, flags, mode (flags, mode 通常放 0, 0) sys_execve: 0x3b (59), *"/bin/sh", 0, 0 ``` - protection (`checksec`) ``` NX: No Exection gcc -z execstack Canary: defeat bof, 在 return 前做 canary check - security_init() => fs:0x28 (stack guard) - canary end with \x00 gcc -fno-stack-protector: no canary gcc -fstack-protector-all: 所有 function 皆開啟 canary ASLR: - Address Space Layout Randomization - stack, heap, shared libraries echo 0 | sudo tee /proc/sys/kernel/randomize_va_space PIE: - Position Independent Executable - binary randomization gcc -fno-pie FULL RELRO: - load binary 後就接好 library, 並設為唯讀 ``` - link library ``` LD_LIBRARY_PATH=/path/to/your/libc export LD_LIBRARY_PATH ``` - binary layout - ![](https://i.imgur.com/K9VkQrp.png) ## Shellcode - defeat NX ``` mmap // assign memory protection mode mprotect // 更改現有 memory protection mode ``` - 好用的 printable shellcode in x86 ``` A 0: 41 inc ecx B 0: 42 inc edx C 0: 43 inc ebx D 0: 44 inc esp E 0: 45 inc ebp F 0: 46 inc esi G 0: 47 inc edi H 0: 48 dec eax I 0: 49 dec ecx J 0: 4a dec edx K 0: 4b dec ebx L 0: 4c dec esp M 0: 4d dec ebp N 0: 4e dec esi O 0: 4f dec edi P 0: 50 push eax Q 0: 51 push ecx R 0: 52 push edx S 0: 53 push ebx T 0: 54 push esp U 0: 55 push ebp V 0: 56 push esi W 0: 57 push edi X 0: 58 pop eax Y 0: 59 pop ecx Z 0: 5a pop edx f A 0: 66 41 inc cx f B 0: 66 42 inc dx f C 0: 66 43 inc bx f D 0: 66 44 inc sp f E 0: 66 45 inc bp f F 0: 66 46 inc si f G 0: 66 47 inc di f H 0: 66 48 dec ax f I 0: 66 49 dec cx f J 0: 66 4a dec dx f K 0: 66 4b dec bx f L 0: 66 4c dec sp f M 0: 66 4d dec bp f N 0: 66 4e dec si f O 0: 66 4f dec di f P 0: 66 50 push ax f Q 0: 66 51 push cx f R 0: 66 52 push dx f S 0: 66 53 push bx f T 0: 66 54 push sp f U 0: 66 55 push bp f V 0: 66 56 push si f W 0: 66 57 push di f X 0: 66 58 pop ax f Y 0: 66 59 pop cx f Z 0: 66 5a pop dx p a 0: 70 61 jo 0x63 q a 0: 70 61 jno 0x63 r a 0: 72 61 jb 0x63 s a 0: 73 61 jae 0x63 t a 0: 74 61 je 0x63 u a 0: 75 61 jne 0x63 4 a 0: 34 61 xor al, 0x61 j a 0: 6a 61 push 0x61 0A0 0: 30 41 30 xor BYTE PTR [ecx+0x30], al ``` - int 0x80 (`\xcd\x80`) 怎麼生 ``` ## \xcd for i in range(ord('a'), ord('z')+1): print(chr(0xff ^ i ^ 0xcd)) ord('S') = 0xff ^ ord('a') ^ 0xcd 0xcd = ord('S') ^ 0xff ^ ord('a') ## 0x80 for i in range(ord('0'), ord('9')+1): print(chr(0xff ^ i ^ 0x80)) ord('O') = 0xff ^ ord('0') ^ 0x80 0x80 = ord('O') ^ 0xff ^ ord('0')** ``` ## BOF - danger function ``` gets scanf vscanf strcpy // 補 null strcat // 補 null ``` - bypass canary - leak canary at runtime - non-linear write (不須改動 canary 就修改 return address) ## ROP (Return Oriented Programming) - stack pivoting - ROP size 不夠時使用 - 第一次主要透過 BOF 改變 rbp 到**已知可控**的位置 (稱作 bss) - 第二次寫入 ROP chain, 並透過 ret2leave, 讓 rsp 遷到 bss, 夠使用到 bss 的 stack 上 return address - defeat ASLR / PIE - leak stack / library / code_base ``` libc => <__libc_start_main+243> code_base => return_main_addr ``` - ROP gadget ``` ROPgadget --binary ./binary --multibr --only "pop|ret" # search ROP ROPgadget --binary ./binary --string "/bin/sh" # search string ``` ### ROP Advanced > gcc 4.8 以上, 64 bit 下有許多通用 gadget - 正常情況下 call `__libc_csu_init` **不會做事情**,因為他只是負責 call init entry 而已,而 init entry 也不會幹嘛 - 但是這個 function 有需多 push pop register 的 instruction 可以使用 - csu gadget - 使用情況 - `pop rdx ; ret` 找不到 - 沒有 libc (? - `__libc_csu_init` ``` mov rdx, r14 mov rsi, r13 mov edi, r12d call qword ptr [r15+rbx*8] ... pop rbx pop rbp pop r12 pop r13 pop r14 pop r15 ret ``` #### analysis - bypass full relro(.got 不可寫但又需要 leak): 讓 stack 落在 .got 與 .data 的交界 - x64 的 register 中,rX == rXx + 一個 prefix - pop rax == 58, pop r8 == 4158, 41 即為 prefix - r12~r15 為 callee saved, 所以 pop r12 ~ r15 很常見, 不過不用擔心, pop r14 == 415e/ pop rsi == 5e - 一些通用 gadget 1. 控制 rdi rsi (`_libc_csu_init`) ``` call qword ptr [r12+rbx*8] // 放 pop ; ret (把 rip pop 掉,繼續跑下面的 ROP) pop rsi ; pop r15 ; ret pop rdi ; ret ``` 2. 想控 rax ``` jmp rax 一定會有 (jop) gets/fgets 會讓 rax 變成 rdi strcpy/strncpy (strncpy, rdx 設 0,完全不 crash,但還是會有一樣效果) alarm call 第二次時回傳上剩餘等待的時間 (unsigned int) ``` 3. 控制 rcx ``` 不常用 通常會 function call (strcpy ecx = 輸入字串) syscall 完後 rcx == rip ``` 4. 寫 rax 到 memory (int `_start`) ``` push rax push rsp mov r8, XX mov rcx, XX mov rdi, XX call __libc_start_main@plt # 改掉 got ``` #### attack - 先用 `pop_rsp_r13_r14_r15_ret` 來把 stack 遷到 got 上,這樣就能把 got value 放到 register 內 - `mov cs:__bass_start, 1` 會有 add ebx, esi ; ret 的 gadget - csu_init 用 rbx 來做 push,這樣才不會被其他東西蓋到 - 能控 r12, rbx 那些,就可以用 csu_init 來做事情 - 通常在 call function 時, stack 會殘留 function 使用的一些痕跡,此時就會有 libc 之類的東西可以用 - csu 可以控 r12, r13, r14, r15 值,並且在 csu 有 `call [r12+8*rbx]` 可以用 - 所以透過 pop r12, r13, r14, r15 做攻擊 ``` mov rdx, r13 mov rsi, r14 mov edi, r15d call [r12+8*rbx] ``` - `_dl_runtime_resolve` 在 relro 全開時無法用 - rbx 要設 0,以及 rbp 要設 1 #### leak - call `_IO_file_write` 可以 leak libc (大概 == write) - 利用不斷在 bss 使用 `gets`,使留下的 `_IO_file_write` 可以被 call 到,並藉著控制 rdi, rsi, rdx `_IO_new_file_write(stdin, got, 8)` - stdin 此時可以任意控,只要 `*(ptr+0x70)` 為 1 就好 (stdout),這樣就能 leak https://github.com/lattera/glibc/blob/master/libio/fileops.c#L1180 `__write (f->_fileno, data, to_do)` offset of `_fileno` == 0x70 ### ROP to control got ## GOT - GOT (Global Offset Table) ``` - runtime load libc function address 1. call puts@plt 2. jmp puts@got 3. 2 case - case 1: 已經 binding 過 => 直接跳 libc - case 2: 跳到 puts@plt+6, 做一些事 - push 一些東西, 跳到 .plt - jmp 到 GOT 中 loader 在 runtime load 進的 libc function address - dynamic section offset - link_map address - dl_runtime_resolve <here> - resolve 後, GOT 會被寫入 libc function address - 又稱作 lazy binding ``` - GOT hijacking - 改 puts@got 成 system - `puts("/bin/sh")` == `system("/bin/sh")` GOT 的前三個 entry 分別是: 1. .dynamic section address 2. link_map 3. `_dl_runtime_resolve` 整個 runtime resolve 的 function prototype:`_dl_runtime_resolve(*link_map, rel_offset)` ## FSB - danger function ``` printf(buf) fprintf(buf) sprintf(buf) ``` - 位置 - 0 -> rdi - 1 -> rsi - 2 -> rdx - 3 -> rcx - 4 -> r8 - 5 -> r9 - 6 -> rsp - 7 -> rsp+8 ... - attack - read: (for leak) - stack_base - rbp - environ ptr (point to environ string) - P.S 求 stack address 時, 算 address 之間彼此的 offset 會比較準, 因為即使 stack_base 知道, 每次 function frame 的起始位置也會 random - canary - libc_base (return addr like `libc_start_main + 243, 242, 235 ...`) - write - ptr as arg - helper format ``` - %n$s: 參數在 stack 中為 ptr, 輸出 ptr 指向位址內所放的值 - %p: 參數其 address - %k$n : %n的功能, 但指定第 k 個參數 ($k)` - 預設為對應到的 argument,而如果沒有指定 k$,**可以在一次 fmt 之內按照順序修改多次** - write size - $n: 4 bytes - $hn: 2 bytes - $hhn: 1 bytes - printed char 會繼承 - %***c: 輸出特定數量的字元, 能餵給 %X$n. - %X$n: 接收先前總印出的字元數量, 在丟給 X$ ptr 所指向位址, 透過字元輸出數量 & int overflow 控制 target addr 最終的值 (e.g. 對 $n 來說, 印出超過 256 就會回到 0 開始, 所以印出 257, 在 target addr 最終的值就會是 1) e.g. %Ac%B$n: argB 當作 ptr 寫入 4 個 bytes, value 為 ``` - ![](https://i.imgur.com/hdg9Kjn.png) - %n, %hn 預設為將之前印出來 character 數量,寫到對應 argument ptr 指到的位置,而如果沒有指定 k$,**可以在一次 fmt 之內按照順序修改多次** - k$ 會在 fmt 前將 ptr 指到的 address 存來,因此不能動態改變 - `printf("%*");`:`*` 代表取對應函式參數的值 - 用來計算總共寫多少次的 function ``` python written = 0 def next_byte(n, bits): global written written_masked = written & ((1 << bits) - 1) if written_masked < n: written += n - written_masked return n - written_masked else: written += ((1 << bits) - written_masked) + n return ((1 << bits) - written_masked) + n ``` - pwntool fmtstr 的使用方式 ``` writes = { addr_retaddr + 0: addr_system, addr_retaddr + 8: addr_binsh, <target>: <value> } payload = fmtstr_payload(7, writes, numbwritten=0, write_size='byte') fmtstr_payload(<offset>, <dict>, <number of char has been printed>, <hhn, hn or n>) ``` ## FILE exploit ### FILE structure - system read write 時會先從 disk 讀一大段並且放在 kernel buffer, 並且放在 user space - 降低 disk I/O 次數 - `FILE struct` - high level - file stream descriptor - 由 `fopen()` 產生 - fread/fwrite <=> stream buffer <=> read/write (user mode) - <=> kernel buffer <=> disk (kernel mode) - 降低 system call 的次數 - 宣告 `FILE struct` 時為宣告 `_IO_FILE_plus` - `_IO_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; // file descriptor 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 }; ``` - flag: `_flags` "w+", "r" ... - stream buffer: `_IO_read_ptr` ~ `_IO_buf_end` - fd: `_fileno` 由 `system()` open 產生 - `_IO_FILE_plus` ```c // 平常在使用的 FILE // stdin / stdout / stderr struct _IO_FILE_plus { FILE file; // 對檔案的操作 const struct _IO_jump_t *vtable; }; #endif ``` - 所有 file stream 會被串在一個 linked list - next 在 `_IO_FILE` 中為 `_chain` - `fopen()` - ![](https://i.imgur.com/wbpFH6E.png) - 首先會 malloc 一段 memory 存 FILE struct - link 到存 FILE stream 的 linked list 內 - `_IO_list_all` 一開始會串著 sterr, stdout and stdin - 該填的填一填 `_flag`, `chain` and `vtable` 等... - 傳到 linked list (linked list 會先指到新增的, 此新增的 `chain` 在指向原本的 `chain`) - insert linked list node 的感覺 - 最新的會被 `_IO_list_all` 指到,緊接著是其他 fd 與 stderr, stdout, stdin (類似 LIFO ?) - `sys_open()` the file - `fread()` - ![](https://i.imgur.com/JNNzOIe.png) - 如果 stream buffer 為 NULL, 就 allocate 一塊給他 - 用 `vtable->_IO_file_xsgetn` 判斷 - 透過 `vtable->_IO_file_doallocate` allocate - 用 `vtable->_IO_file_underflow` 讀一大段到 stream buffer 上 - default 1 page - 利用 system call `sys_read()` 讀檔 - 再把 stream buffer 的 content copy 到 dest - `fwrite()` - ![](https://i.imgur.com/srd0VGL.png) - allocate buffer - 將 data 寫到 stream buffer 上 - `fclose()` - ![](https://i.imgur.com/mTk0pDo.png) - 從 linked list 中移除 (unlinked the FILE structure) - `flush()` and `release()` stream buffer - close file - release FILE structure - 統整 - FILE structure 在 `fopen()` 時自動 malloc 一個空間到 heap 上 - `_IO_new_file_init_internal` 會進行structure 初始化 - 每個 FILE structure 似乎都會有一組 buffer, 又稱作 file stream - file stream 負責接收 `sys_open()` 傳送的一大筆資料 - **此時 file stream 尚未分配到 mem** - `_IO_link_in` 將此 structure insert 至 `_IO_list_all` 的頭 - `_IO_list_all` 為 file_chain (linked list), 會將所有 FILE structure 串起來 - `sys_open()` 的結果為 fd, assign 至 `_fileno` - 在 `fread()` 時, 會先 `vtable->_IO_file_xsgetn` 看 file stream 是否為 NULL, 若是則利用 `vtable->doallocate` allocate 一塊 memory 給他 - 之後用 `vtable->_IO_file_underflow` 呼叫 `sys_read()`, 讀 `_fileno` 對應的檔案的 1 page 到 stream buffer 上 - 根據 user 呼叫 `fread()` 的參數給予對應大小的 response - `fclose()` 時會先把此 FILE structure 從 `_IO_list_all` 拔除, 清空 (flush and release) file stream 以及關閉檔案, 最後 `free()` 掉此 FILE structure - 其他 - 在 trace code 時會看到 debug symbol 有些為 `__GI__IO_file_XXX`,就是利用 vtable 來呼叫的 function - `_IO_buf_base` 為 buffer 開始,通常沒特別設定就會在 heap 上 - 真正在分配 `_IO_XXX_ptr or base or end` 等等是在 `_IO_new_file_underflow`,一開始除了 `_IO_buf_end`,其他全部都會等於 `_IO_buf_base` - ![](https://i.imgur.com/fIvi3ku.png) - 最後會 call read syscall,從 fd 讀 buffer size (st_blksize),並寫到 buffer 內 - `read_end` 指向透過 syscall read 讀到的結尾 - ![](https://i.imgur.com/IhU9nGs.png) - 最後寫到 dest 中,該輪完成 - ![](https://i.imgur.com/dHJnwm0.png) - 寫完之後 `read_ptr` 會 == `read_end` 代表讀完了 - 而後會將所有 ptr 等等都 reset 成 buffer base (`_IO_new_file_underflow`),再讀一次,會讀成功,但是只有 EOF (0xffffffff),而此時就會 return ```c fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base; fp->_IO_read_end = fp->_IO_buf_base; fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_buf_base; ``` - `fread` - ![](https://i.imgur.com/Rl9XkW4.png) - `fwrite` - `_IO_buf_base` 輸入輸出 buffer base - `_IO_buf_end` 輸入輸出 buffer end - `_IO_buf` 在 `_IO_doallocbuf` 會被 init - `_IO_write_base` 輸出 buffer base - `_IO_write_ptr` 輸出已使用到的位置 - `_IO_write_end` 輸出 buffer end - ![](https://i.imgur.com/9Jbdw6M.png) - `_IO_OVERFLOW` 能 fflush 所有的 - 在 `_IO_new_file_overflow` 內,會初始化沒有 write ptr 的 fp ```c if (f->_IO_write_base == NULL) { _IO_doallocbuf (f); _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base); } ``` - `_IO_setb` 是用來 set buffer ```c void _IO_setb (FILE *f, char *b, char *eb, int a) { if (f->_IO_buf_base && !(f->_flags & _IO_USER_BUF)) free (f->_IO_buf_base); f->_IO_buf_base = b; f->_IO_buf_end = eb; if (a) f->_flags &= ~_IO_USER_BUF; else f->_flags |= _IO_USER_BUF; } ``` - 之後 `_IO_write_base` == `_IO_write_ptr` == `_IO_buf_base`、`_IO_write_end` == `_IO_buf_end` - `_IO_OVERFLOW` flush buffer - 當下不會馬上寫到 file 中,而是等到 main 結束後,`_IO_cleanup` -> `_IO_flush_all_lockp` 才會寫回 - write 是 call `_IO_new_file_overflow`, read 是 call `_IO_new_file_underflow` #### arb write example ```c= #include <stdlib.h> int main() { char msg[100]; char *s = malloc(100); FILE *fp = fopen("./flag2.txt", "r"); fp->_flags &= ~4; // ~_IO_NO_READS fp->_IO_buf_base = msg; // write_start fp->_IO_buf_end = msg+100; // write_end fp->_fileno = 0; fread(s, 1, 6, fp); puts(msg); return 0; } ``` - `_IO_fread` 的 `bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/iofread.c#L30 - `_IO_sgetn` 的 `return _IO_XSGETN (fp, data, n);` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/genops.c#L464 - file operation 的 define 多都在 `libio/genops.c` - `_IO_file_xsgetn` 會 call `__underflow()`,如下 - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/fileops.c#L1359 - 因為 buffer size 有設,所以他會先檢查 FILE 的東西 ```c if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) break; ``` - `__underflow` 的 ` return _IO_UNDERFLOW (fp);` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/genops.c#L315 - 過程只有做一些 backup 檢查與其他的檢查 - `_IO_new_file_underflow` 的 `if (was_writing && _IO_switch_to_get_mode (fp))` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/fileops.c#L530 - 註解說明 `Flush unwritten characters.` - `_IO_switch_to_get_mode` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/genops.c#L174 - 在這邊會把 `_IO_read_base` 設為 `_IO_buf_base` - 回來 `_IO_new_file_underflow`,下面程式碼會把以下 ptr 都設為 `buf_base`,之後在 call `_IO_SYSREAD` ```c= fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_buf_base; ``` - `_IO_file_read` 作為 read 的 wrapper,原本要從 file 中讀 100 bytes (buffer 的關係) 寫到 buffer,但現在變成從 stdin 讀 (fileno = 0),寫到我們指定的 buffer 位置 - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/fileops.c#L1212 - ![](https://i.imgur.com/jrEjD5e.png) - 這邊 want 為我們傳入的 6,have 為實際從 stdin 輸入的大小 (原本應該是從 file 中讀 100 bytes 的), copy 完後就 return 回去了 ```c= have = fp->_IO_read_end - fp->_IO_read_ptr; if (want <= have){ memcpy (s, fp->_IO_read_ptr, want); fp->_IO_read_ptr += want; want = 0; } ``` #### arb read example ```c= #include <stdlib.h> int main() { char *msg = "hello world!"; char *s = malloc(100); read(0, s, 100); FILE *fp = fopen("./flag.txt", "r"); fp->_flags &= ~8; // ~_IO_NO_WRITES fp->_flags |= 0x800; // _IO_CURRENTLY_PUTTING fp->_flags |= 0x1000; // _IO_IS_APPENDIN fp->_IO_write_base = msg; // read_start fp->_IO_write_ptr = msg+6; // read_end fp->_IO_read_end = fp->_IO_write_base; fp->_fileno = 1; fwrite(s, 1, 100, fp); puts(s); return 0; } ``` - `_IO_fwrite` 的 `written = _IO_sputn (fp, (const char *) buf, request);` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/iofwrite.c#L30 - `_IO_new_file_xsputn` 的 `if (_IO_OVERFLOW (f, EOF) == EOF)` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/fileops.c#L1279 - `_IO_new_file_overflow` 的 `if (ch == EOF)` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/fileops.c#L806 - 此時 int ch 為 -1 - `f->_IO_write_ptr - f->_IO_write_base` 這邊為要先寫回 fd 的 size,而又因為改動 fd fileno 為 1,所以會把此 range 印到 1 (stdout) - `_IO_new_do_write` 的 `new_do_write` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/fileops.c#L491 - `new_do_write` 的 `count = _IO_SYSWRITE (fp, data, to_do);` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/fileops.c#L500 - `_IO_new_file_write` 的 `: write (f->_fileno, data, to_do));` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/fileops.c#L1255 - 即將把 write_base ~ write_ptr 的東西印到 stdout,並且在印完後會 reset buffer - ![](https://i.imgur.com/qxT8LsY.png) 最重要的是此 if condition,會先判斷有沒有資料要 flush,之後再做一次 `new_do_write()`,讀真正要讀的東西 https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/fileops.c#L1327 正常情況會跑 `_IO_default_xsputn`,把輸出寫到 buffer 內 #### 補充說明 使用到 stdout 的 function (puts, fwrite...) 可以讀東西到 buffer (任意寫),也能將 buffer 中的東西印出來 (任意讀) 任意讀的構造如上,控制 write / read 以及 fileno 任意寫的 example payload 如下,利用 `_IO_new_file_xsputn` 中判斷 `(f->_IO_write_end > f->_IO_write_ptr)`,若成立則代表輸出 buffer (write) 還能存東西,所以先把資料寫到裡面,但是要控制好 `read_end` 要等於 `write_base`,這樣 buffer 才不會重疊 ``` io_stdout_struct=IO_FILE_plus() flag=0 flag&=~8 flag|=0x800 flag|=0x8000 io_stdout_struct._flags=flag io_stdout_struct._IO_write_base=pro_base+elf.got['read'] io_stdout_struct._IO_read_end=io_stdout_struct._IO_write_base io_stdout_struct._IO_write_ptr=pro_base+elf.got['read']+8 io_stdout_struct._fileno=1 ``` 而為什麼要 0x8000 (`_IO_USER_LOCK`),因為如果沒有 `_IO_acquire_lock_clear_flags2`,則 process 會陷入 for loop https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/libioP.h#L872 https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/libio.h#L448 ``` # define _IO_funlockfile(_fp) \ if (((_fp)->_flags & _IO_USER_LOCK) == 0) _IO_funlockfile (_fp) ``` #### close trace - `_IO_new_fclose` call `_IO_un_link ((struct _IO_FILE_plus *) fp);` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/iofclose.c#L38 - `_IO_un_link` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/genops.c#L58 - `_IO_list_all = (struct _IO_FILE_plus *) _IO_list_all->file._chain;` 就是在 unlink FILE - 做完後會 return 回 `_IO_new_fclose`,並且 call ` _IO_file_close_it` 關閉他 - 可以知道,glibc 先把 FILE 從 list 移除後才 close - `_IO_new_fclose` 還會 call `_IO_new_file_close_it` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/fileops.c#L157 - 這邊可以看到,如果是 ~`_IO_NO_WRITES` (代表有寫) 的話,就會做 `_IO_do_flush`,把要 write 的值寫 ```c= if ((fp->_flags & _IO_NO_WRITES) == 0 && (fp->_flags & _IO_CURRENTLY_PUTTING) != 0) write_status = _IO_do_flush (fp); ``` - 然後會做 `? _IO_SYSCLOSE (fp) : 0);` 來 close fd - 之後一連串 `_IO_setX` 來清空 buffer - 裡面又有 `_IO_un_link` - `_IO_FINISH` - https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/fileops.c#L199 - 裡面確定是否 open,如果是的話要做 `_IO_do_flush (fp);`,並且關閉他 - 最後如果不是 stdin stdout stderr,就 free 掉 (stdin stdout stderr 預設就有 space,不是 malloc 出來的 - code ``` if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr) { fp->_IO_file_flags = 0; free(fp); } ``` --- - `_IO_setb (FILE *f, char *b, char *eb, int a)`:set buffer - `_IO_setg(fp, eb, g, eg)`:set read_XX (g 應該是指 gets) - code ``` #define _IO_setg(fp, eb, g, eg) ((fp)->_IO_read_base = (eb),\ (fp)->_IO_read_ptr = (g), (fp)->_IO_read_end = (eg)) ``` - `_IO_setp(__fp, __p, __ep)`:set write_XX (p 應該是指 print) - code ``` #define _IO_setp(__fp, __p, __ep) \ ((__fp)->_IO_write_base = (__fp)->_IO_write_ptr \ = __p, (__fp)->_IO_write_end = (__ep)) ``` #### use stdout to libc leak > 前提為 `sebvbuf(stdout, 0, 2, 0)` 想辦法把: - `stdout->_flags == 0xfbad1800` - `0xfbad0000` (file magic) - &= ~`_IO_NO_WRITES` == 0xfbad0000 - |= `_IO_CURRENTLY_PUTTING` (0x800) == `0xfbad0800` - |= `_IO_IS_APPENDING ` (0x1000) == `0xfbad1800` - `_IO_write_base` - 設為 `&(stdout->_flags)` 之類的 libc 位置 (需要比原本小,因為原本的 `_IO_write_base` == `_IO_write_ptr` == `_IO_write_end` 之後在 `puts` 時就會從 `&(stdout->_flags)` 開始噴,噴到 `_IO_write_end` #### use stdin to code execution > 前提為 `sebvbuf(stdin, 0, 2, 0)` 用 **unsorted bin attack 蓋掉 `_IO_buf_end`**,此時再寫就能從 `_IO_buf_ptr` 寫到 `_IO_buf_end`,也就是 `unsorted bin` (in main_arena),過程中就有 `__malloc_hook` 可以蓋 #### vtable hijack https://ray-cp.github.io/archivers/IO_FILE_vtable_hajack_and_fsop https://ray-cp.github.io/archivers/IO_FILE_vtable_check_and_bypass 透過修改 vtable ptr,指向可以控制的地方,然後因為 vtable ptr 指到的地方也是一堆 funcion ptr,所以我們可以透過修改對應使用到的 function ptr 來進行攻擊。 (glibc 2.24 前) glibc 2.24 後增加了以下 check,主要是 check vtable ptr 是不是在 glibc vtable range 中 ```c= uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; const char *ptr = (const char *) vtable; uintptr_t offset = ptr - __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) //检查vtable指针是否在glibc的vtable段中。 /* 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; ``` - `__start___libc_IO_vtables` 為 vtable range 的開始 - `__stop___libc_IO_vtables` 為 vtable range 的結束 #### fsop (studying) https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/genops.c#L58 glibc 中有個 function 叫 `_IO_flush_all_lockp`,主要負責 flush 所有 FILE,在程式結束之前會被 call 到 - 正常離開 (main return) ``` 0 _IO_flush_all_lockp 1 _IO_cleanup () 2 __run_exit_handlers 3 __GI_exit 4 __libc_start_main 5 _start () ``` - exit ``` #0 _IO_flush_all_lockp #1 _IO_cleanup () #2 __run_exit_handlers #3 __GI_exit #4 main () ``` - abort > GLIBC 2.27 the `abort()` function no longer calls `_IO_flush_all_lockp()` 攻擊方法為:偽造一個 fake FILE,並將 `_IO_list_all` 指到我們的 fake FILE,最後繞過以下檢查,使用 `_IO_OVERFLOW` 來 control flow ``` if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) ``` > glibc 2.23 舉 house of orange 為例子,將 `_IO_list_all` 蓋成 unsorted bin 後,smallbin 0x60 的位置剛好會是 `_chain`,而透過在此 chunk 構造好 data 與 vtable,在使用 vtable `_IO_OVERFLOW` 時會把 fp 當作第一個參數傳入,如果我們把 `_IO_OVERFLOW` 的位置寫成 system,fake chunk fp 一開始寫成 `/bin/sh\x00`,這樣在 call `_IO_OVERFLOW` 時就等於跑 `system(fp)`,getshell > glibc 2.24 後 利用 `_IO_str_jumps` 或 `_IO_wstr_jumps`,而兩者只差在一個是處理 wchar (寬字元) ```c= gef➤ p _IO_str_jumps $4 = { __dummy = 0x0, __dummy2 = 0x0, __finish = 0x7ffff7e57ed0 <_IO_str_finish>, __overflow = 0x7ffff7e57b30 <__GI__IO_str_overflow>, __underflow = 0x7ffff7e57ad0 <__GI__IO_str_underflow>, __uflow = 0x7ffff7e560d0 <__GI__IO_default_uflow>, __pbackfail = 0x7ffff7e57eb0 <__GI__IO_str_pbackfail>, __xsputn = 0x7ffff7e56130 <__GI__IO_default_xsputn>, __xsgetn = 0x7ffff7e56340 <__GI__IO_default_xsgetn>, __seekoff = 0x7ffff7e58030 <__GI__IO_str_seekoff>, __seekpos = 0x7ffff7e56780 <_IO_default_seekpos>, __setbuf = 0x7ffff7e56660 <_IO_default_setbuf>, __sync = 0x7ffff7e569f0 <_IO_default_sync>, __doallocate = 0x7ffff7e567f0 <__GI__IO_default_doallocate>, __read = 0x7ffff7e57970 <_IO_default_read>, __write = 0x7ffff7e57980 <_IO_default_write>, __seek = 0x7ffff7e57950 <_IO_default_seek>, __close = 0x7ffff7e569f0 <_IO_default_sync>, __stat = 0x7ffff7e57960 <_IO_default_stat>, __showmanyc = 0x7ffff7e57990 <_IO_default_showmanyc>, __imbue = 0x7ffff7e579a0 <_IO_default_imbue> } ``` 要打的地方在 `_IO_str_finish` ```c= void _IO_str_finish (_IO_FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //执行函数 fp->_IO_buf_base = NULL; _IO_default_finish (fp, 0); } ``` 直接 call `((_IO_strfile *) fp)->_s._free_buffer`,並把 `fp->_IO_buf_base` 當作參數 如果我們能讓 vtable == `_IO_str_jumps-8`,這樣對應到的 `__overflow` offset 就會是 `_IO_str_finish` ```c= gef➤ p _IO_file_jumps $7 = { __dummy = 0x0, __dummy2 = 0x0, __finish = 0x7ffff7e540d0 <_IO_new_file_finish>, __overflow = 0x7ffff7e54f00 <_IO_new_file_overflow>, __underflow = 0x7ffff7e54ba0 <_IO_new_file_underflow>, __uflow = 0x7ffff7e560d0 <__GI__IO_default_uflow>, __pbackfail = 0x7ffff7e57800 <__GI__IO_default_pbackfail>, __xsputn = 0x7ffff7e53750 <_IO_new_file_xsputn>, __xsgetn = 0x7ffff7e533c0 <__GI__IO_file_xsgetn>, __seekoff = 0x7ffff7e529e0 <_IO_new_file_seekoff>, __seekpos = 0x7ffff7e56780 <_IO_default_seekpos>, __setbuf = 0x7ffff7e526b0 <_IO_new_file_setbuf>, __sync = 0x7ffff7e52540 <_IO_new_file_sync>, __doallocate = 0x7ffff7e45df0 <__GI__IO_file_doallocate>, __read = 0x7ffff7e53720 <__GI__IO_file_read>, __write = 0x7ffff7e52fe0 <_IO_new_file_write>, __seek = 0x7ffff7e52780 <__GI__IO_file_seek>, __close = 0x7ffff7e526a0 <__GI__IO_file_close>, __stat = 0x7ffff7e52fc0 <__GI__IO_file_stat>, __showmanyc = 0x7ffff7e57990 <_IO_default_showmanyc>, __imbue = 0x7ffff7e579a0 <_IO_default_imbue> } ``` ### exploit - buffer overflow 蓋掉 fp - 會 crash 在 `_IO_acquire_lock(fp)`, 因為 FILE 內有 `*_lock`, 預防 multihread 有 RC 的情況 - 想辦法將其設為指向 0 的 address (offset 為 0x88) - 控 `_IO_FILE_plus.vtable`, 讓他指向 system - 最後再控制 call function 前的參數, 指到 `/bin/sh` ### FSOP - file-stream oriented programing - 控制 `_chain` and `_IO_list_all` - 好用的 function `_IO_flush_all_lockp` - main return, **abort** 等等時會 flsuh 所有 file stream - 判斷一些條件, 成立後會呼叫 `_IO_OVERFLOW` - 構造 linked list - e.g. house of orange - 用 unsorted bin attack 將 unsorted bin 的位置寫到 `_IO_list_all`, 同時也構造出 0x60 大小的 chunk 進入 small bin - unsorted bin attack - `malloc()` 時, 不管 unsorted bin 是否有剛好大小的 chunk, 他都會 unsorted bin 的 chunk 做 unlink - 剛好大小: 直接從 unsorted bin 移除給使用者 - 不是剛好: 從 unsorted bin 移除, 放到對應的 bin - 但這邊沒對 double linked list 做檢查 - 取出 unsorted bin 的最後一塊 chunk 做 victim, 然後將這塊 bk 指向的 chunk 的 fd 改成 unsorted bin 的位置 - 就是 linked list 的 delete node - 所以最後一塊 chunk 的 bk 改寫為"其他位置"後, 在 unlink 完 unsorted bin 會指向 "其他位置" 指到的 fd - 為很大的數字 (? - 通常會寫到 global_max_fast - 判斷是否為 fastbin chunk - 小於此值就不是 fastbin - 在配合 fastbin corruption attack - 如果 size 不對, 會把他放到 small bin - 要確保 `_IO_FILE_plus._flags` < 0 - 如何在沒有 `free()` 的情況下把 chunk 放入 small bin - 把可控已經 free 的 chunk 的 size 改成 0x60 - 在 `malloc()` 時, 會搜到此 chunk, 發現 size 不對後會放到 small bin (0x60) - 將 chunk (fd, bk) 的 address 改成任意我們想當作 chunk 的位置 - 下次再拿 chunk 時因為 chunk 不合法 (假設), 會觸發 `abort()` -> `_IO_flush_all_lockp()` - 會 call vtable 的某個 function, 並且將 FILE ptr 當作參數傳入 - ![](https://i.imgur.com/3ej72N6.png) - 如果我們將 bk 位置改成 `_IO_list_all` - 0x10 - `_IO_list_all` 因為 unlink unsorted bin 的機制會指向 unsorted_bin[0] (in main_arena) - 而此時因為 FILE 的機制 (`_IO_list_all`), main_arena 的 `smallbin[4]` (chunk size 為 0x60) 會指向我們的 chunk, 並把我們的 chunk 當作 FILE structure 看 - `_IO_list_all->_chain` 剛好為 smallbin[4] (0x60) - 需要滿足 `_IO_write_ptr` > `_IO_write_base` - 並在 abort 時, 假 FILE structure 也會被 `_IO_flush_all_lockp()` 影響 - 將 `_flags` 改成 '/bin/sh;' - vtable 的 function 改成 `system()` - 在 abort 時可以 trigger `system("/bin/sh")` - ![](https://i.imgur.com/iDiLBDE.png) - 新的 glibc 版本要求 vtable 要在 `_IO_vtable` section 之內 - 如果不是, 會進行第二段檢查 - for compatibilty - pointer guard 不怎麼可能繞過 - for shared library - 寫到 `_dl_open_hook` 也能繞, 但能寫到這, 也不必要寫到此 hook - FILE structure 是不是沒救了... 還有!! - 利用 stream buffer 跟 file descriptor - stdout 任意讀 - set `_fileno` to fd of stdout - set `_flag` & `~_IO_NO_WRITES` - set `_flag` |= `_O_CURRENTLY_PUTTING` - set `write_base` & `write_ptr` to mem you want to read - set `_IO_read_end` == `_IO_write_base` - 要避開一些會動到 stream buffer 的條件 - ![](https://i.imgur.com/qec8GYu.png) - stdin 任意寫 - set `_fileno` to fd of stdin - set `_flag` & `~_IO_NO_READS` - set `read_base` == `read_ptr` - set `buf_base` & `buf_ptr` to mem you want to read - `buf_end` - `buf_base` > size of fread - ![](https://i.imgur.com/UK9N57e.png) - GOT hijack - `__malloc_hook` / `__free_hook_` / `__realloc_hook_` - 如果沒有 file operation 可以用怎麼辦 - stdin / stdout / stderr 相關的 function 都可以 - put / printf - scanf / gets / fgets - 假設皆為 unbuffer - stdout - 用 fastbin attack - overwrite `_flags` - partial overwrite `_IO_write_base` ptr - partial overwrite unsorted bin ptr - 拿到 stdout 後,改寫 stdout 的 `_IO_write_base` (誤以為東西還沒輸出完) - 向 house of roman - stdin - unsorted bin attack 蓋 `_IO_buf_end` - if `scanf("%d", &var)` - `read(0, buf_base, sizeof(stdin buffer))` - 蓋掉 `__malloc_hook` - 用 `_IO_strfile_` == struct{ `_IO_streambuf`; `_IO_str_fields` } - field 內含兩個 function ptr - 剩下還有 `_IO_str_finish`, `_IO_str_jumps`, `_IO_wstr_finish` 等等 * `scanf("%d", &var)` 底層會 call `read(0, buf_base, sizeof(stdin buffer))` * `_IO_write_base` ~ `_IO_write_ptr` 內容的東西是要被寫得,寫去哪? `_fileno` * `_IO_buf_base` ~ `_IO_buf_end` 內容的東西是要被讀的,從哪讀? `_fileno` (1) ### file other trick - vtable 存的 function ptr 在 2.29 後可以寫 - vtable 本身 在 2.24 後有嚴格的檢查,基本上繞不太掉,沒辦法直接修改 可以控制 stdout 或是 stdin 等等 fp,可以嘗試改動 chain,改成 heap address 之類可以控制的地方,並偽造 fake `_IO_file_plus` ## heap > 為可以在 **runtime** 使用並釋放的 memory space, 是由 low address -> large address ### allocate heap memory region: mmap & brk ![](https://i.imgur.com/DuRJSBX.png =600x800) - brk - `void *sbrk(intptr_t increment)`: - `start_brk` 為 heap 起始, `brk` 為 heap 結束 - increment=0 時會返回當前 `brk` - `void brk(void *addr)`: - 傳入指定 `brk` address - mmap - `void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);`: - ex : `mmap(NULL, (size_t)(4*1024+1), PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)` - creates a new mapping in the virtual address space of the calling process. - 映射一塊 address 給 process 使用, 該塊 address 可能為 file 的 mapping, 也可能只是一塊 virtual address - addr=NULL: 讓 kernel 自己選 - prot: - READ 代表可讀 - WRITE 代表可寫 - flags: - MAP_PRIVATE 代表不被其他 process 共用 - MAP_ANONYMOUS 代表不屬於任何 file, initial with 0 - fd 需為 -1 - offset 為 0 **PS.** `getchar()`, `scanf()` 等等有關 input 的 function 皆會使用到 heap > The GNU C Library ("glibc", the usual C runtime on Ubuntu) allocates memory for internal use the first time stdout is used for anything ### 主要的兩個 function: malloc & free - `malloc()`: - first malloc - size >= 128KB: mmap --> sys_mmap - size < 128KB: brk --> sys_brk - 透過 brk, kernel 會給 glibc `132KB` 的 heap segment(rw), 並標示為`使用過`, 我們稱之 **main arena** - 示意 flow : 1. binary --要1KB--> glibc ----> kernel 2. binary ----> glibc --要memory--> kernel 3. binary ----> glibc <--給你**132KB**-- kernel 4. binary <--給你1KB-- glibc(共有132KB) <---- kernel - 對 binary 來說, `free()` 是釋放給 glibc - 對 glibc 來說, glibc 掌握 132KB memory, 控管 binary 的 memory - 對 kernel 來說, 他已經分配 132KB 給 glibc, 代表此 132KB 已被使用 - `malloc()` return 回來的 ptr 指向 **chunk**, 為 glibc 實作 memory management 的 data struct (header + data) - next malloc 1. if(size < 0x90), 會先去 fast bin 找有無 size 相符的 chunk, 有則回傳 2. 去 unsorted bin 找有無 size 相符的 chunk, 有則回傳 3. 無相符但有更大 size 的 chunk, 則切割後回傳, 剩餘的丟回 unsorted bin 4. 都沒有, 則從 top chunk 切並回傳 - `free()`: - 當非 fastbin size 的 chunk 被 free, **若與 top chunk 相接(在 top chunk 前面, chunk 後面是 top chunk)**, 則會被 merge - 當 will be freed chunk 前面的 chunk 是 no inuse(freed), 則會 trigger consolidate ```c= /* consolidate backward */ if(!prev_inuse(p)) { prevsize = p->prev_size; size += prevsize; // 自己 + 前一個 p = chunk_at_offset(p, -((long) prevsize)); // 將自己的 ptr 指到 p+offset 處 unlink(p, bck, fwd); } ``` 呼叫流程如下 - `malloc()` ----> `__lib_malloc()` ----> `_int_malloc()` - `free()` ----> `_int_free()` ### data structure * fast bin struct ```c typedef struct malloc_chunk *mfastbinptr; mfastbinptr fastbinsY[]; // Array of pointers to chunks ``` * unsorted, small and large bin struct ```c typedef struct malloc_chunk* mchunkptr; mchunkptr bins[]; // Array of pointers to chunks ``` * 各 thread arena 的 heap header ```c typedef struct _heap_info { mstate ar_ptr; /* Arena for this heap. */ struct _heap_info *prev; /* Previous heap. */ size_t size; /* Current size in bytes. */ size_t mprotect_size; /* Size in bytes that has been mprotected PROT_READ|PROT_WRITE. */ /* Make sure the following data is properly aligned, particularly that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of MALLOC_ALIGNMENT. */ char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK]; } heap_info; ``` * arena structure, e.g. main_arena ```c struct malloc_state { /* Serialize access. */ __libc_lock_define (, mutex); /* Flags (formerly in max_fast). */ int flags; /* Fastbins */ mfastbinptr fastbinsY[NFASTBINS]; /* Base of the topmost chunk -- not otherwise kept in a bin */ mchunkptr top; /* The remainder from the most recent split of a small request */ mchunkptr last_remainder; /* Normal bins packed as described above */ mchunkptr bins[NBINS * 2 - 2]; /* Bitmap of bins */ unsigned int binmap[BINMAPSIZE]; /* Linked list */ struct malloc_state *next; /* Linked list for free arenas. Access to this field is serialized by free_list_lock in arena.c. */ struct malloc_state *next_free; /* Number of threads attached to this arena. 0 if the arena is on the free list. Access to this field is serialized by free_list_lock in arena.c. */ INTERNAL_SIZE_T attached_threads; /* Memory allocated from the system in this arena. */ INTERNAL_SIZE_T system_mem; INTERNAL_SIZE_T max_system_mem; }; typedef struct malloc_state *mstate; ``` * chunk ```c struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; }; typedef struct malloc_chunk* mchunkptr; ``` * tcache & its entry ```c typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; // most 7 chunks tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct; typedef struct tcache_entry { struct tcache_entry *next; } tcache_entry; // point to next chunk "data" // fastbin fd point to next chunk "header" ``` #### detail ##### heap_info > thread arena 可以有多個 heap, 且每個 heap 都有自己的 header, 稱作 heap_info, 用來描述 heap 的 info - 由於 arena 受限於核心數量, 所以有些 thread 會 share arena, 而 arena 內可能包含不同 thread 的 heap, 所以用 heap_info 去描述 heap - 當 heap 不夠用時, kernel 會用 `mmap()` or `brk()` allocate memory (arena) 給 heap, 而此時該 thread 有多個 sub-heap (原 arena + 新 arena), 組成 heap - ![](https://i.imgur.com/UFQbhMW.png) - 如此圖 thread_arena 內部有兩個 `mmap()` 出的 heap, 且 heap2 的 `heap_info->prev` 指到 heap1 的 `heap_info->ar_ptr`, 而 heap1 的 `heap_info->ar_ptr` 指到 `malloc_state`, 此作法方便後續管理 * 而 main_arena 必定隸屬於 main_thread, 所以不需要 heap_info ![](https://i.imgur.com/XpIbIpc.png) ##### malloc_state > malloc_state 儲存 arena 的 info, 包含 bin, top chunk, last remainder chunk 等等資訊 (arena_header) - main thread 的 arena (malloc_state) 為 global, 在 libc 的 bss - other threads 的 arena (malloc_state) not global, 且 other threads arena 只能透過 `mmap()` create ### chunk - Chunk type - Allocated - ![](https://i.imgur.com/c6ob1mS.png) - Freed - ![](https://i.imgur.com/WOd9Upo.png) - Top - ![](https://i.imgur.com/1450Mrw.png) - 當緊鄰的 chunk 被 free, top chunk 會將其 merge (conlidate), 因此 top chunk 的 PREV_INUSE bit 永遠為 1 - Freed chunk - Fast bin - free 完後, 只會有 fd (single linked list) - default: 0x20 ~ 0x80 (7 個) - 備用到 0xb0 - LIFO - P (PREV_INUSE) 不會被清掉 - Small bin - 有 fd, bk (double linked list) - 0x20 ~ 0x3f0 (62 個) - FIFO - 一個大小一個 bin - Large bin - 有 fd, bk (double linked list) + fd_nextsize, bk_nextsize (上/下一個大小跟自己不一樣的 chunk 位置) - ![](https://i.imgur.com/negoJHB.png) - allocate 時, 採取 Best fit (滿足 chunk size 最近 size) - ![](https://i.imgur.com/DGPEedl.png) - Unsorted bin - 有 fd, bk (double linked list) - temporary cache (free 的 size 非 fast bin) - malloc 時會分配到對應 size 的 (small / large) chunk - ![](https://i.imgur.com/i8gDD1Y.png) - Tcache - 每個 thread 的 heap, 為了減少 lock 的次數與提升效能 - 只有 fd, 但另外有 key at bk 做 double free 的 check - 0x20 ~ 0x410 (64 個) - 7 個 entry per bin - LIFO - P 不會 unset - ![](https://i.imgur.com/0X1Z2Ro.png) - ![](https://i.imgur.com/ry4GtHN.png) - overlap bin size (fast / small) - 用來放切剩的大小剛好在 fast bin 的 size range 內 - e.g. 0x500 = 0x4c0 + 0x40, 0x40 會先被放到 "unsorted bin", 在回收時會被放到 "small bin", 而不會進入 "fast bin" - Consolidate - glibc 在 user malloc large bin size (>= 0x420), 會回收 + merge "fast bin" 的 chunk, 放入 "unsored bin" ##### large chunk 相同 index 下,排列有以下特點: - 大小從大到小 - 大小相同,則從 free 的時間 - 大小相同的 chunk,只有第一塊的 fd_nextsize 與 bk_nextsize 會指到其他地方,其他都是 0 - size 最大的 chunk bk_nextsize 指向最小的 chunk;size 最小的 fd_nextsize 指向最大的 chunk - fd_nextsize 指向 size 前面 (更小的) 的 linked list,bk_nextsize 指向 size 後面 (更大的) 的 - fd 指向後面時間才進來的 chunk (bk == NULL 為最大的 chunk) ###### example - code ```c= #include <stdio.h> #include <stdlib.h> int main() { void* arr[64]; arr[0] = malloc(0x470); arr[1] = malloc(0x10); arr[2] = malloc(0x470); arr[3] = malloc(0x10); arr[4] = malloc(0x470); arr[5] = malloc(0x10); arr[6] = malloc(0x480); arr[7] = malloc(0x10); arr[8] = malloc(0x480); arr[9] = malloc(0x10); arr[10] = malloc(0x480); arr[11] = malloc(0x10); arr[12] = malloc(0x490); arr[13] = malloc(0x10); arr[14] = malloc(0x490); arr[15] = malloc(0x10); arr[16] = malloc(0x490); arr[17] = malloc(0x10); for (int i = 0; i < 18; i++) { free(arr[i]); } malloc(0x600); return 0; } ``` - 結果 ![](https://i.imgur.com/g2GyLRW.png) ###### trace ```c= victim_index = largebin_index(size) bck = bin_at(av,victim_size) fwd = bck->fd fwd = bck bck = bck->bk victim->fd_nextsize = fwd->fd victim->bk_nextsize = fwd->fd->bk_nextsize victim->bk_nextsize->fd_nextsize = victim fwd->fd->bk_nextsize = victim victim->bk = bck victim->fd = fwd fwd->bk = victim bck->fd = victim ``` #### chunk detail 1. top: - 1~16 同 allocated - **P flag 恆為一**, 因為如果 free 掉**連續 memory 中 top chunk 的上個 chunk** 時, 若非 fastbin, 則該塊 chunk 會與 top chunk merge 2. allocated: - 1~8 bytes: - **連續 memory** 中上一塊: - 如果是 free chunk 則為**prev_size**, 為前一個 free chunk 的大小 - 如果是 allocated chunk 則為 **data**, 代表上一塊 allocated chunk 與當前 allocated chunk 有 8 bytes 重疊 - 9~16 bytes: - 因為 chunk 對齊 0x10, 所以**前 28 bits** 為 **chunk size** - **後 4 bits** 用來存其他資訊: 1. 沒用到 2. non_main_arena(N): 此 chunk 是否屬於 main_arena, 0 代表屬於 thread_arena 3. is_mmaped(M): chunk 是否透過 `mmap` 拿到 4. prev_inuse(P): **連續 memory 上個 chunk** 是否在使用中, 若為 1 則代表上個 chunk 為 allocated chunk, 正在被使用中 3. free: - 1~16 同 allocated - 因為被 free 掉, 代表用不到了, 所以 data 區可以存一些 meta-data(用來描述其他資料的資料) - 1~8 bytes (for data): - fd: 指向**同一 bin 的前一塊 chunk** (linked list), **非**連續 memory 的前一塊 - 9~16 bytes (for data): - bk: 指向**同一 bin 的後一塊 chunk** (linked list), **非**連續 memory 的後一塊 - 17~24 bytes (for data): - fd_nextsize: 指向前一塊 large chunk (不含 bin) - 25~32 bytes (for data): - bk_nextsize: 指向後一塊 large chunk (不含 bin) - 如果被 `free()` 的 chunk 為 bin 中的第一個 freed chunk, 則 `fd == bk == main_arena->bin`, 因為 `main_arena->bin` linked list 的頭尾為 `main_arena->bin` 自己的 chunk - ![](https://i.imgur.com/gjsGdxi.png) - ![](https://i.imgur.com/epEC4X8.png) 4. last remainder: - malloc 時, ptmalloc2 找到不到相符的 chunk, 只能從大塊的 chunk 切, 剩下的則是 last remainder chunk, 在 unsorted 內 #### bin detail - bins 根據 **sizes** 分成: 1. fast bin - chunk size < 0x90 bytes, 被 free 完會分到此 bin - fast bin 下面又根據 size, 再分成 0x20, 0x30, 0x40..., global_max_fast 為 0x80 - global_max_fast 初始值為 0, 第一次 `malloc()` 時才會被填入 ``` #define get_max_fast() global_max_fast ``` - **只用到 fb**, 且 NULL 結尾 (<---> small, large 都是以 bin_chunk 當開頭&結尾) - 為 LIFO, 所以從 fast bin 拿 chunk 時會先從第一個拿 - 原 chunk 在被 free 掉後, 若在 fast bin, 則**不會將下一塊 chunk 的 P 設成 0** - default_mxfast = 64 * SIZE_SZ / 4 - max_fast_size = 80 * SIZE_SZ / 4 - x86 - default = 0x40 - max = 0x50 - 大小為 0x10, 0x18..., 0x58 共 10 個 - x64 - default = 0x80 - max = 0xA0 - 大小為 0x20, 0x30..., 0xb0 共 10 個 2. small bin - chunk size < 0x400, **0x20~0x80 的 chunk 會根據機制放入 fast bin or small bin** - 其內部又分成 0x20~0x3f0 共 62 個 bin - circular doubly linked list - FIFO 3. large bin - chunk size > 0x400 - 在此 bin 的 free chunk 的 17~32 bytes (for data) 放了其他的 meta-data: - 17~24 bytes: fd_nextsize, linked list 中上個 chunk 的 size - 25~32 bytes: bk_nextsize, linked list 中下個 chunk 的 size - ![](https://i.imgur.com/xT6Jrh2.png =300x400) 4. unsorted bin - 若 chunk size > 0x80, 被 free 後會先進入 unsorted bin - circular doubly linked list - 若 trigger 到 `consolidate`, 會清空 unsorted bin, 將對應大小的 chunk 分配到 small & large bin - ![](https://i.imgur.com/SvmQD3N.png) * fast bin struct ```c typedef struct malloc_chunk *mfastbinptr; mfastbinptr fastbinsY[]; // Array of pointers to chunks ``` * unsorted, small and large bin struct ```c typedef struct malloc_chunk* mchunkptr; mchunkptr bins[]; // Array of pointers to chunks ``` #### tcache detail > glibc >= 2.26 後增加的機制 (ubuntu 17.10), 目的要提升 performance - https://medium.com/@ktecv2000/tcache-exploitation-871044f8b210 - fastbin 各種 size 前面都多了 7 個cache (ex. 0x30 free 7 次才會到 fastbin), 稱作 tcache - 每個 thread 都會有一個 - 共 64 個 bin (TCACHE_MAX_BINS) (**24 0x18 ~ 1032 0x408**) - 一個 bin 最多 7 個 chunk - 超過就直接放 unsorted bin - 會先從 tcache 拿, 等到空的才會去 fastbin 找, 而此時拿完第一個 fastbin 後, 會將剩下的 chunk 丟到 tcache, 因為是 LIFO, 所以: 1. tcache 為空, fastbin 內是 abcd 2. 取出 a, 將 bcd 以 reverse 的順序放入 tcache 3. tcache dcb, fastbin 為空 - secure - 沒有檢查 double free - `malloc()` 沒有檢查 size - tcache 的範圍涵蓋 small bin, 所以要透過 unsorted bin 去 leak libc, malloc size 必須大於 small bin - 如果有 UAF, 也可以透過 `malloc()` 0x100 以上的 chunk 7 次, 並且 free 7 次讓 cache 塞滿, 最後就可以進入 unsorted bin 並 leak 出 libc addr - 或是直接 allocte > 0x408 (1032) 的 chunk 讓 tcache 無法使用, 直接進入 unsorted bin - tcache 的 free 機制以及 malloc 機制十分不嚴謹, 我們任意構造一個大小非 0x50 的 chunk 也能放入 0x50 的 chunk 中並在 `malloc()` 時被取出 - 但假設此 chunk size 為 0x410, 被取出後會被當作是 0x410 的 chunk 看待 ### Trace Code - 取得 chunk size (mask & nomask) ```c /* Get size, ignoring use bits */ #define chunksize(p) (chunksize_nomask(p) & ~(SIZE_BITS)) /* Like chunksize, but do not mask SIZE_BITS. */ #define chunksize_nomask(p) ((p)->mchunk_size) ``` - 取得 next_chunk (當前 chunk address + chunk size) ```c /* Ptr to next physical malloc_chunk. */ #define next_chunk(p) ((mchunkptr)(((char *) (p)) + chunksize(p))) ``` - 取得 prev_chunk (當前 chunk address - prev_size) ```c /* Size of the chunk below P. Only valid if prev_inuse (P). */ #define prev_size(p) ((p)->mchunk_prev_size) /* Ptr to previous physical malloc_chunk. Only valid if prev_inuse (P). */ #define prev_chunk(p) ((mchunkptr)(((char *) (p)) - prev_size(p))) ``` - 查看 chunk 是否使用 (取得 next_chunk 的 mchunk_size 並 mask (PREV_INUSE flag)) ```c #define inuse(p) ((((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size) & PREV_INUSE) ``` - fastbin malloc ```c= if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ())) { idx = fastbin_index (nb); // 找對應的 bin (32:/8, 64:/16) mfastbinptr *fb = &fastbin (av, idx); mchunkptr pp; victim = *fb; // fastbin 第一個 ptr if (victim != NULL) // 若 bin 不為空 { if (SINGLE_THREAD_P) *fb = victim->fd; else REMOVE_FB (fb, pp, victim); if (__glibc_likely (victim != NULL)) { size_t victim_idx = fastbin_index (chunksize (victim)); if (__builtin_expect (victim_idx != idx, 0)) // chunk size 不 match malloc_printerr ("malloc(): memory corruption (fast)"); check_remalloced_chunk (av, victim, nb); #if USE_TCACHE /* While we're here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim; /* While bin not empty and tcache not full, copy chunks. */ while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = *fb) != NULL) { if (SINGLE_THREAD_P) *fb = tc_victim->fd; else { REMOVE_FB (fb, pp, tc_victim); if (__glibc_unlikely (tc_victim == NULL)) break; } tcache_put (tc_victim, tc_idx); } } #endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } } ``` ### Vulnerability 打 heap 的重點 - mess chunk metadata 讓 glibc 混亂 - UAF - double free - dangling ptr read/write - Heap overflow #### use-after-free > 全名為 use-after-free, free 完 ptr 後並沒有 `ptr = NULL;`, 導致原 ptr 仍指到 heap address, 又稱 dangling pointer - 由於 free 完後, free chunk 會放 meta-data (fd, bk), 所以可以做 `information leak` - double free 後, 該 pointer 指到的 chunk, 其 fd 的值會是自己, 而 heap 受到 aslr 保護 - malloc 出一個 free 掉後會到 unsorted bin 的 chunk, 因為 unsorted bin 的 fd & bk 會是 libc address(top chunk address, and main_arena 在 libc 的 bs #### off-by-one > 由於條件判斷錯誤, 導致在寫入時額外寫入 1 byte, 可能是任意字元, 可能是 null, 又稱 one byte overflow > 而蓋過的 byte, 在 heap 段若是 `prev_in_use` or `prev_size`, 則可能會出現漏洞 - 發生情況: - `strlen(buf)` 判斷長度並無判斷 `\x00`, 而 `strcpy(a, buf)` 時會把 `\x00` 也一起 copy, 導致蓋到下一個 byte - `for loop` 多一次, 寫到不該寫入的地方 主要為蓋 prev_inuse bit #### fastbin attack > fastbin 在檢查 double free 時, 只**檢查 linked list 第一個**是否為要 free 的 ptr, 可以用 `free(A); free(B); free(A);` bypass - **goal: 讓 in fastbin chunk->fd 指向我們要的位址, 再透過 `malloc()` 取得該位址的 ptr, 透過讀寫做 exploit** - e.g. `__malloc_hook` - 0x23 (0x13 for padding) - 在 double free A 後, 再次 malloc 時會得到 chunk A, 而此 chunk A 同時為 free chunk(in fastbin) 以及 allocated chunk - 可以藉此改掉 chunk A 在 free chunk 中的 fd 欄位, 而後 malloc 3 次後會得到我們改掉的 fake fd 所指到的位址+0x10 - 可以把 fake fd 改成 got address, 變成 hijack got; 把 fake fd 改成 stack, 變成 BOF 等等... - allocate large bin 會 trigger `malloc_consolidate()`, 將 fastbin 可以 merge 的 merge 後再放入 unsorted bin, 不能 merge 的直接放入, **所以可以 bypass double free** **但是第三次 `malloc()` 拿出的 fake_chunk 必須符合 fastbin check, chunk_size 需相符** 1. 找 stack 2. in 64, 可以找 GOT 沒 call 過的 function, 因為其開頭通常為 0x40 ====> 0x40 3. hook - offset(3) function ====> 0x7f - ![](https://i.imgur.com/RULXkhr.png) #### tcache attack - free 第一塊時, key 會被填上 tcache; free 第二塊時會檢查 `key (chunk+0x18) == tcache` - if true, 會 traverse 所有相同 size 的 chunk, 找是否有 chunk 跟你要 free 的 ptr 相同 chunk - if 有 => error - if 沒有 => 巧合 - tcache attack 的好處是, 完全不會檢查 malloc 的 size #### tcache stashing unlink - 某 size tcache bin 是空的, 但對應的 **smallbin** 有東西 - malloc 時 smallbin 拿 chunk => 剩下丟進 tcache, 並且不檢查 chunk size - 若剛好 bck 接著是 target, smallbin 在被 libc 整理後, target 會被寫入 smallbin 的位置 (libc) - 能達到在任意位置寫入 small bin address (很大的值) - 若需要把 unsorted bin 的 chunk 丟到 small bin 中, 只需要 malloc 比他更大的 size 即可 #### stack pivoting 這個攻擊手法必須要能在 stack 留下放 ROP chain 的 heap address,然後能寫到 realloc_hook 跟 malloc_hook 因為 malloc 跟 realloc 在執行前都會 push + sub rsp,所以很有可能會把 heap address 包在 function frame 之中,而此時假設跑 malloc,而此時剛好 rsp 放著 heap address,我們可以改寫 malloc_hook 成 realloc+6,realloc_hook 成 pop rsp ; ret,這樣 malloc 在 call malloc_hook 時,會 call realloc+6,+6 跟 + 0 相差會少 push 一個 register,而 realloc 在 call realloc_hook 時,會把 rsp 上的 value pop 回去,但是因為一開始少 push 一個,所以最後在 jmp 到 pop rsp ; ret 時,rsp 剛好會放 heap address,最後透過 pop rsp ; ret 就能跑 ROP 而此種攻擊方法也能用在 one_gadget,可以控制讓 rsp 符合 one_gadget 的條件 #### hook - `__free_hook` - 通常上面沒有合法的 size 可以用,要的話也在非常上面 (stdout 那邊),所以不太可能單獨使用 fastbin attack 來進行配合,通常可以使用 stashing (猜的、感覺可以)、unsorted bin attack 等等可以在某處寫 libc 位置的攻擊方法,在 free_hook 上面寫,這樣 fastbin attack 就可以拿到 free_hook - `__malloc_hook` - 因為 - 0x23 處會有 0x7f,通常配合 fastbin attack 來寫 one_gadget - trigger malloc 的方式有很多種,input 過多需要 buffer、output 過多需要 buffer、需要印 error msg 等等都有可能 (e.g. `printf("%10000c")`) - `__realloc_hook` - 緊接在 malloc_hook 後,能用來調整 one_gadget 的 stack layout,若 stack 有殘留 heap address,也能透過控制 malloc_hook => realloc => realloc_hook 過程中的 push pop,來做 stack pivoting to heap #### other heap trick - free unsorted bin 的 chunk 時, 會檢查上下是否為 freed, 若是, 則會 merge 成一塊大塊的 unsorted bin - any function about output, maybe malloc some spaces - e.g. double free -> printf -> malloc -> malloc_hook - one_gadget 可以嘗試 - 觸發 double free => 印東西出來 (`printf()`) => buffer 不夠大 => `malloc()` => malloc_hook - 從 main return => `__libc_start_main` 底層 - `malloc()` - main_arena 在 libc 的 bss 段 and `malloc - 0x10` - `calloc` skip 所有與 tcache 相關的東西 - tcache 的 fd, bk 是指向 chunk data, 其他的指向 chunk header - hook 系列 - `__free_hook` - `__malloc_hook` - `__realloc_hook` - 舉 `__free_hook` 為例, 若 chunk (ptr) 放的東西是 '/bin/sh', 且 `__free_hook` 內放 system addr, 在 call `free(ptr)` 時做的行為就是 `system(ptr)`, 也就是 `system("/bim/sh")` - main_arena 在 `malloc_hook` 下面 (+ 0x10),因此如果 malloc_hook 寫 one_gadget 都沒用的話,可以嘗試寫 top_chunk - 先在下方建立特定大小的 chunk size (e.g. 0x70),並在 0x70 fastbinY 的地方改成 chunk address - 找 chunk + 0x8 為夠大 size ,並且在 `free_hook` 上面的地方,之後將 top chunk 寫成此 chunk - 一直 malloc,直到要到 `free_hook`,寫 free_hook #### malloc trick - 當 malloc 的 size 超過 `mp_.mmap_threshold` (0x20000) 時,會使用 `mmap` 開一個新的 memory space 給 user,而這個位置**與 libc 的 offset 是固定的**,藉此可以 leak libc - 如果沒有 free 可以用,又需要 unsorted bin attack 的話,可以透過改寫 `top chunk`,讓 libc 在判斷一些條件後,把 top chunk 丟到 unsorted bin 中 - 在 libc2.23 的條件如下:[src](https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L2300) ``` assert ((old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)); ... /* If possible, release the rest. */ if (old_size >= MINSIZE) { _int_free (av, old_top, 1); } ``` - `(old_top == initial_top (av) && old_size == 0)`:不確定這段會不會過,initial_top 似乎沒法繞? - `(unsigned long) (old_size) >= MINSIZE`:old_size 是最 top chunk 的 size,至少要 >= 0x20 - `prev_inuse (old_top)`:prev_inuse bit 要設 - `(unsigned long) old_end & (pagesize - 1)) == 0)`:top chunk 加完 size 後的 address 需要與 page 對齊 --- 從 unsorted bin 切 chunk 出來時,剩下的 chunk (last remainder) fd bk 會指向 main_arena + 0x58,而拿到的 chunk 都會殘留**原本 chunk size 對應到的** large/small bin 的 address (in libc) ## other ### pic vs pie http://gcc.gnu.org/onlinedocs/gcc-4.8.0/gcc/Code-Gen-Options.html#Code-Gen-Options https://stackoverflow.com/questions/2463150/what-is-the-fpie-option-for-position-independent-executables-in-gcc-and-ld - pic 應該是強調 shared library - 產生的 code 並不會直接寫死 libc function,而是寫相對於下一個 intruction 的 offset。而要使用 libc function 時,會透過 dynamic loader 解析 GOT (lazy binding 等等),找出正確的 function 位置 - pie 可以讓 binary 本身不寫死 address - 讓 linker 能做 relocate,達到 executable file 的 ASLR - 但是感覺最後 pie 包含了 pic 的功能,而且還對 Local variable 有做優化 - ![](https://i.imgur.com/UfM5gBx.png) ### lazy binding & ret2dlresolve: > 當 resulting file call shared object 的 function 時, 才會去 so 內部找 function 實際位置並寫入 .got.plt (global offset table), 以減少 initial 大量延遲(一開始就要全找) 以及節省時間(有些根本不會被 call 到) - 步驟如下: 1. 在**第一次** call function 時, 會 `jmp` 至 func@plt, 而 func@plt 第一行即是 **`jmp` 至 func@got 內寫的值, 而此時為 func@plt+6**. 而後 func@plt+6 會執行 `push` 以及 `jmp`, 該 `push` 的值為**rel_offset(.rel.plt 的 index)**, `jmp` 到 **plt\[0\]** ![](https://i.imgur.com/vOcg7W3.png) 2. `0x8048380`, 也就是 plt\[0\] 會再 `push` 並 `jmp`, 此時 `push` 的值為 **\*link_map**, `jmp` 到 lib 中的 **<dl_runtime_resolve> (.got.plt 的 第三項, 0x8040a000(.got.plt) + 8)** function 的位置進行 binding. 即為: **`_dl_runtime_resolve(*link_map, rel_offset)`** ![](https://i.imgur.com/MYPNx60.png) ![](https://i.imgur.com/yHcTNSp.png) ![](https://i.imgur.com/68UDGP4.png) 3. `_dl_runtime_resolve` 會根據以下 pseudo code lookup function, 我們要做的事情就是: 1. build fake rel_entry 2. build fake sym_entry 3. puts "system\0" symbol string 4. calculate offset 5. build the ROP chain ```c= // Elf32_Word = uint32_t = long int = 8 bytes // Elf32_Half = uint16_t = 4 bytes // pseudo code _dl_runtime_resolve(struct link_map *l, Elf32_word reloc_offset) { Elf32_Rel *rel_entry = JMPREL(.rel.plt) + reloc_offset; Elf32_Sym *sym_entry = &SYMTAB[ ELF32_R_SYM( rel_entry->r_info ) ]; char *sym_name = sym_entry->st_name + STRTAB(.dynstr); _search_for_symbol_(l, sym_name); } ``` **trace** `_dl_runtime_resolve->_dl_fixup->_dl_lookup_symbol_x->do_lookup_x->check_match` - `_dl_runtime_resolve`: ![](https://i.imgur.com/W4pDv7F.png =600x800) - cfi(Call Frame Information) 開頭的 inst 與函數檢測有關, 即為 GNU Profiler - function 在做: 1. 保存 regs value 2. call `_dl_fixup` 3. 恢復 regs value 4. jmp 至 return value, 也就是 real function address - `_dl_fix_up`: ![](https://i.imgur.com/1fr8XyZ.png) ![](https://i.imgur.com/b01x9Ed.png) - 傳入 `link_map` 以及 `reloc_arg` 當參數, 前者為定值 .plt[1], 後者為 function 在 .rel.plt 的 offset - 根據傳入參數, 得到 reloc_struct(reloc) & symbol(\*sym) & 要被 reloc 的 addr(rel_addr) - call `_dl_lookup_symbol_x` - 最後結束時, call `elf_machine_fixup_plt` 將 got 寫入 real function address - `_dl_lookup_symbol_x`: - 在每個 scope 中, 用 `do_lookup_x` 找尋 symbol - 找到後, 標記 symbol 使用中, 並 return symbol & it's link_map - `do_lookup_x`: - traverse 所有 link_map, 找到後 assign 給 result - `check_match`: - 用 `strcmp` 比對 symbol name, 再比對 version ### 關閉NX ref: https://quentinmeffre.fr/pwn/2017/01/26/ret_to_stack.html 舉例來說 ``` // gcc main.c -fno-stack-protector -Wl,-z,relro,-z,now,-z,noexecstack -static int main() { char buf[64]; gets(buf); // Never use this function ! printf("%s", buf); return 0; } ``` - 簡單的 BOF - 沒有 canary - 沒有人家開的後門 - function沒有辦法 leak 出 glibc address - => ROP chain 的 gadget 明顯不夠 - 更新 `__stack_prot` - 原本 0x01000000 - 改成 0x7 (0x07000000 in x86 address) - call `_dl_make_stack_executable` - `_dl_make_stack_executable(void* address)` - 將 rdi 放入 `__libc_stack_end` address 當作參數 - 執行 `__mprotect`, 使其根據 `__stack_prot` - function info: `mprotect(const void *start, size_t len, int prot);` - prot - PROT_READ - R - RROT_WRITE - W - PROT_EXEC - X - PROT_NONE - cannot access - 需要將 prot 設為 7 (RWX) ### csu_init https://code.woboq.org/userspace/glibc/csu/elf-init.c.html#__libc_csu_init `__libc_csu_init` 是 `__libc_start_main` 時會使用到來初始化的 function,但是在某些條件下,他能夠做到 "控制 register 值" + "call function" + 在 stack 留下東西,並且沒有副作用 ``` __libc_csu_init { push r15 push r14 mov r15d, edi push r13 push r12 lea r12, __frame_dummy_init_array_entry push rbp lea rbp, __do_global_dtors_aux_fini_array_entry push rbx mov r14, rsi mov r13, rdx xor ebx, ebx sub rbp, r12 sub rsp, 8 sar rbp, 3 call _init_proc test rbp, rbp jz short loc_4005B6 nop dword ptr [rax+rax+00000000h] mov rdx, r13 mov rsi, r14 mov edi, r15d call [r12+rbx*8] add rbx, 1 cmp rbx, rbp jnz short loc_4005A0 add rsp, 8 pop rbx pop rbp pop r12 pop r13 pop r14 pop r15 retn ; } ``` 而我們需要的部分分成兩塊: 1. 任意控制 register value ``` pop rbx pop rbp pop r12 pop r13 pop r14 pop r15 retn ``` 2. call function 以及控制參數 `(r12)(rdi, rsi, rdx)` ``` mov rdx, r13 mov rsi, r14 mov edi, r15d call [r12+rbx*8] ... ``` 在設 `rbx = 0` and `rbp = 1` 的情況下,`cmp rbx, rbp` 會直接通過 這招真的很強 (X ### Function Residue > 這比較要講的是一個概念 push 會把某值從 regsiter 寫到 stack 上,pop 會把某值從 stack 拿到 regsiter 內,這一定大家都知道,但是這邊有一個關鍵:push 會把值洗掉,但是 **pop 拿了值不會清零** 因此執行完 libc function 後,很容易在 stack 殘留 libc address,但是要小心 `rsp` 與 `rbp` 的值,因為 libc function 可能都會使用到,如果離太近或是怎樣,可能會有**值被蓋掉的疑慮**,因此要多觀察、多 try #### Example of `gets` 利用不斷在 gets + stack pivoting,使留下的 `_IO_file_write` address 可以殘留在我們控制到的地方 (bss 等等),並藉著 `__libc_csu_init`,控制 rdi, rsi, rdx, r12,**執行 `_IO_new_file_write(stdin, got, 8)`** https://github.com/lattera/glibc/blob/master/libio/fileops.c#L1180 ```c _IO_new_file_write (FILE *f, const void *data, ssize_t n) { ssize_t to_do = n; ... __write (f->_fileno, data, to_do); // f->_fileno == *(fptr+0x70) } ``` 看得出來此 function 只需要用到 fileno,因此 stdin [:0x70] 可以任意值,只要 `*(fake_stdin+0x70)` 為 1 就好 ### Intel - Control-flow Enforcement Technology (CET) https://binpwn.com/papers/control-flow-enforcement-technology-preview.pdf https://www.linuxplumbersconf.org/event/2/contributions/147/attachments/72/83/CET-LPC-2018.pdf https://software.intel.com/content/www/us/en/develop/articles/emulating-applications-with-intel-sde-and-control-flow-enforcement-technology.html [debug about](https://software.intel.com/content/www/us/en/develop/articles/debugging-applications-with-intel-sde.html) CET - endbr64:End Branch 64 bit - 如果 CET 沒開就是 NOP - 避免掉 ROP - 能控制 control flow 的 instruction 有 - RET - JMP %rax - CALL %rax - 有兩個 component - Shadow Stack (SHSTK) - 基本上所有 program 都相容 (? - function prologue 時會將 return address 存在 shadow stack - function epilogue 時會把 shadow stack 的 address 取出並比較 - stack checks - `./sde -cet -- application` - Indirect Branch Tracking (IBT) - ENDBRANCH (endbr64) - indirect branch 為使用參數當作跳轉的位置,如 x64 的 `jmp rax` - 有執行到 endbr 的 address (jump target) 即為安全 - `endbr mark valid jump target addresses of indirect calls and jumps in the program` - 因此可以追蹤 indirect branch - 1 KB page - SSP (shadow stack pointer) - Enable CET:`gcc -fcf-protection ...`,但不知道怎樣才算有效 (? - Intel(R) SDE (Software Development Emulator) - stack unwind - function 會有 `__unwind {` 的 prefix - 代表在遇到 exception 時,會沿著 stack bt 往上找 exception handler - 此為一種強大的概念:Resource Acquisition Is Initialization (RAII) - resource 的有效期與持有 resource 的物件的生命期嚴格繫結,即由物件的建構函式完成 resource 的分配(取得),同時由解構函式完成 resource 的釋放 - 在這種情況下,只要物件能正確地解構,就不會出現 resource 泄露問題 - debug - `sde -debug -- yourapp` ### 隨手 note - ld 內的 `__libc_stack_end` 可以 leak 出 stack 的位置 - `scanf("%u")` 時可以輸入 `+`, `-` 特殊符號來 pass 這次的讀取 - 數字:讀近來 - 英文:直接讀到底 - `+ -`:pass 這次 - gdb 中找 fs 值 (TIB address) - `search -8 <canary>` - malloc.c 中的 `check_inuse_chunk` macro 只有在 MALLOC_DEBUG == 1 才會做事,而平常在檢查 inuse 的都會是 `inuse` macro ## browser https://www.youtube.com/watch?v=1eF7fMdVoOA&ab_channel=jwang https://www.youtube.com/watch?v=emt1yf2Fg9g&ab_channel=BlackHat https://saelo.github.io/presentations/blackhat_us_18_attacking_client_side_jit_compilers.pdf https://medium.com/walkout/%E6%B7%BA%E6%B7%A1-js-engine-%E6%A9%9F%E5%88%B6-77391b4dd3db - Chrome: V8 engine - Firefox: Spidermonkey - Safari: JavaScriptCore - Edge: hakraCore - jsengine object - 各大 implement 的方式都不一樣, 但是都會弄到 shape - object 在 new 時, 發現是新的, 會 create 兩個東西 - ![](https://i.imgur.com/QS9DNoh.png) - object 本身的 values (slot) - shape of object - 之後若其他變數 create object 時, 會去找有沒有一樣的 shape - 各 browser 稱呼,不過都是同個東西 - firefox: shape - v8: map - webkit: structure - edge: type - 改 shape 內 member 的 pointer - ![](https://i.imgur.com/AUob61p.png) - JIT-compiler (vanilla) - ![](https://i.imgur.com/oycmGRw.png) - Runtime - 執行快 overhead 多 - ![](https://i.imgur.com/3haaJAO.png) - 丟給 JIT compiler or Interpreter 的標準為:使用到的次數 - 常常跑到的 code 會丟給 JIT compiler (先編起來的效益較高) - JIT compiler 會需要把 bytecode 解析出的結果存起來,並且做一些 trace,因此 overhead 高 - multi level JIT (隨著執行次數增加而增高 level) - L1:no JIT (only interpreter) - L2:單純 compile,不優化 - L3:JIT with optimization - ![](https://i.imgur.com/PTMVKmm.png) - parser:產出 bytecode - runtime:runtime 時需要的東西 (object info ...) - JIT 的主要問題:type 資訊少,function 參數不知道怎麼處理,compiler 如何做 operation? - 直接找 shape structure -> type (但會有許多額外的 check) - `add(a: smi, b: smi)` - **trace 之前 function 的使用狀況** - **speculation** - 如果遇到不一樣的情況呢? - guard - 確保傳進的參數如果與之前 compile 完的 code 的 type 不一樣,仍不會出錯 - `if (check int fail) {jmp bailout}` - bailout 會去 interpreter or 在額外去 compile 對應 type 的 bytecode - optimization - 因為優化的關係, function 會有非預期的結果 (side effect) - redundancy elimination - eliminate duplicate guards (checks),刪除多餘的檢查 - 但是如果中間有 callback,則 "刪除多餘的檢查" 可能會讓出問題的地方沒被檢查到 - ![](https://i.imgur.com/Ls5gMVS.png) - CVE-2018-4233 - bound-check elimination - 攻擊手法 - 把 shape ptr 改掉,製造 type confusion ## exit hook `exit()` vs `atexit()`: - `int on_exit(void (*function)(int , void *), void *arg);` - 為 SunOS 的 function,並非 standard,所以不一定被其他平台支援 - 可以傳遞 argument - `int atexit(void (*function)(void));` - 為標準 exit hook - register a function to be called at normal process termination [__libc_start_main](https://elixir.bootlin.com/glibc/glibc-2.31/source/csu/libc-start.c#L129) -> [main function](https://elixir.bootlin.com/glibc/glibc-2.31/source/csu/libc-start.c#L339) -> [__run_exit_handler](https://elixir.bootlin.com/glibc/glibc-2.31/source/stdlib/exit.c#L38) -> [dl-fini](https://elixir.bootlin.com/glibc/latest/source/elf/dl-fini.c#L30) 其中在 `__libc_start_main` 中,有使用 `atexit()` 註冊一些 [exit function](https://elixir.bootlin.com/glibc/glibc-2.31/source/csu/libc-start.c#L237) 而 `__cxa_atexit()` 又跟 `atexit()` 差在哪裡呢? - `__cxa_atexit()` is not limited to 32 functions. - `__cxa_atexit()` will call the destructor of the static of a dynamic library when this dynamic library is unloaded before the program exits. control flow - `exit()` 會呼叫 `__run_exit_handler()` - `__run_exit_handler()` - cur 為 `(struct exit_function_list *) 0x7f1ff52229a0 <initial>` - exit_function_list ```c struct exit_function_list { struct exit_function_list *next; size_t idx; struct exit_function fns[32]; // functions }; ``` - exit_function ```c struct exit_function { /* `flavour' should be of type of the `enum' above but since we need this element in an atomic operation we have to use `long int'. */ long int flavor; union { void (*at) (void); struct { void (*fn) (int status, void *arg); void *arg; } on; struct { void (*fn) (void *arg, int status); void *arg; void *dso_handle; } cxa; } func; }; ``` - flavor 分 5 種 - ef_free: 0 - ef_us: 1 - ef_on: 2 - ef_at: 3 - ef_cxa: 4 - `PTR_DEMANGLE`: `ror 0x11 ; xor qword ptr fs:[0x30]` - 代表 MANGLE 時會先 rol 0x11,左位移 0x11,在 xor fs:[0x30] 一個 magic number - ex_cxa 對到的應該會是 `_dl_fini`,主要做的事情是 "call the destructors for all still loaded objects" - 必須要從 constructor 的頭開始 destruct - 最後會找 `_DYNAMIC`,`l_ld` 會指向 `_DYNAMIC` section,並且會呼叫 `l_ld->l_info[DT_FINI_ARRAY]`,會從 `_DYNAMIC` 找到 fini_array 的位置 - `#define DT_FINI_ARRAY 26 /* Array with addresses of fini fct */` - `(Elf64_Dyn *) 0x403e70` - 存放 0x403E18,也就是 fini 的開頭 - 正常來說在 ASLR 的情況下,l->l_addr 會放 ELF base,fini 會放 offset,之後會 call `(ELF_base + fini)[0]` 來呼叫 fini function - 所以要找 l_address offset 在 stack 中的位置也可以用此來判斷 - 而在 no ASLR 的情況下 fini 會直接是位置,l->l_addr 就會是 0 - 我們可以藉由改變 offset 的地方來控制要呼叫到的 function - 沒有變動的情況下,會去 call `__do_global_dtors_aux` - `__do_global_ctors_aux` and `__do_global_dtors_aux` for calling the constructors and destructors of these static objects - 這兩個 function 為 gcc 加上的,都是用來動態連結,在 main 之前載入 dynamic library 的變數之類的 - `__do_global_dtors_aux`:會呼叫到 `__cxa_finalize()` 來執行用 `atexit()` 註冊的 function (glibc 上面是這樣寫沒錯,但是實際上好像是 `_run_exit_handler`) - 此招可以配合 one_gadget,而 one_gadget 能夠符合的條件可以直接去 `execvpe` 找 - `RUN_HOOK (__libc_atexit, ());` 會呼叫 `__elf_set___libc_atexit_element__IO_cleanup__` 的 `_IO_cleanup` - `__elf_set___libc_atexit_element__IO_cleanup__` 可寫,所以也能將 one_gadget 寫在這 ## C++ ### how vector work ``` cpp template <class T, class A = std::allocator<T> > class vector { public: // public member functions private: T* data_; typename A::size_type capacity_; typename A::size_type size_; A allocator_; }; ``` vector => `[abi:cxx11]` 第一個 element => vector member (data ptr, data_size),且 vector 一開始的 size 為 0x80 (4 elements) - 如果 vector data size 小於 0x10,會直接在 vector 下面放資料 - 自己觀察 c++11 看起來像是 ``` struct vector { struct vector_element *data; void *what1; void *what2; } struct vector_element { char *data_ptr; int64_t data_size; union { char data[0x10]; { int64_t data_size; void *what3 = NULL; } } } ``` - vector 在 capacity 大於 75% 時就會 double its size ## glibc 2.32 機制 參考文章:https://medium.com/@ktecv2000/%E8%81%8A%E8%81%8Aglibc-2-32-malloc%E6%96%B0%E5%A2%9E%E7%9A%84%E4%BF%9D%E8%AD%B7%E6%A9%9F%E5%88%B6-safe-linking-9fb763466773 針對 tcache 與 fastbin 的 next pointer 做 XOR 的保護 - heap address xor - 會根據當前 address 與值做 xor,簡單來說用法就是 value = xor(value, &value >> 12) ``` c #define PROTECT_PTR(pos, ptr) \ ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr))) #define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr) ``` - align - xor 完 LSB 可以是 0x0 - 0xf,但是 xor 回去應該要是 0 才對 ``` c #define aligned_OK(m) (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0) #define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1) #define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \ ? __alignof__ (long double) : 2 * SIZE_SZ) ``` 所以在 heap 上做攻擊時,應該要先 leak heap address,才能通過 tcache 與 fastbin 中 address 的檢查