# 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 的檢查