Try   HackMD

PWN cheatsheet

tags: pwn 👻

info

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

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

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
    
    1. 想控 rax
    ​​​​jmp rax 一定會有 (jop)
    ​​​​gets/fgets 會讓 rax 變成 rdi
    ​​​​strcpy/strncpy (strncpy, rdx 設 0,完全不 crash,但還是會有一樣效果)
    ​​​​alarm call 第二次時回傳上剩餘等待的時間 (unsigned int)
    
    1. 控制 rcx
    ​​​​不常用
    ​​​​通常會 function call (strcpy ecx = 輸入字串)
    ​​​​syscall 完後 rcx == rip
    
    1. 寫 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 為 
  • %n, %hn 預設為將之前印出來 character 數量,寫到對應 argument ptr 指到的位置,而如果沒有指定 k$,可以在一次 fmt 之內按照順序修改多次

    • k$ 會在 fmt 前將 ptr 指到的 address 存來,因此不能動態改變
  • printf("%*");* 代表取對應函式參數的值

  • 用來計算總共寫多少次的 function

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
      ​​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: _filenosystem() open 產生
    • _IO_FILE_plus
      ​​​​​​​​// 平常在使用的 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()
      • 首先會 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()
      • 如果 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()
      • allocate buffer
      • 將 data 寫到 stream buffer 上
    • fclose()
      • 從 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
      • 最後會 call read syscall,從 fd 讀 buffer size (st_blksize),並寫到 buffer 內
        • read_end 指向透過 syscall read 讀到的結尾
      • 最後寫到 dest 中,該輪完成
        • 寫完之後 read_ptr 會 == read_end 代表讀完了
      • 而後會將所有 ptr 等等都 reset 成 buffer base (_IO_new_file_underflow),再讀一次,會讀成功,但是只有 EOF (0xffffffff),而此時就會 return
      ​​​​​​​​  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
      • 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
        • _IO_OVERFLOW 能 fflush 所有的
        • _IO_new_file_overflow 內,會初始化沒有 write ptr 的 fp
          ​​​​​​​​​​​​​​​​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
          ​​​​​​​​​​​​​​​​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

#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; }

arb read example

#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; }

最重要的是此 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);
  • _IO_un_link
  • 做完後會 return 回 _IO_new_fclose,並且 call _IO_file_close_it 關閉他
    • 可以知道,glibc 先把 FILE 從 list 移除後才 close
  • _IO_new_fclose 還會 call _IO_new_file_close_it
    ​​​​ 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
  • 最後如果不是 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 中

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 (寬字元)

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

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

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 當作參數傳入
    • 如果我們將 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")
    • 新的 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 的條件
      • 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
      • 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

  • 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
      ​​​​​​​​/* 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
typedef struct malloc_chunk *mfastbinptr;
mfastbinptr fastbinsY[]; // Array of pointers to chunks
  • unsorted, small and large bin struct
typedef struct malloc_chunk* mchunkptr;
mchunkptr bins[]; // Array of pointers to chunks
  • 各 thread arena 的 heap header
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
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
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
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
    • 如此圖 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
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
    • Freed
    • Top
      • 當緊鄰的 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 位置)
      • allocate 時, 採取 Best fit (滿足 chunk size 最近 size)
    • Unsorted bin
      • 有 fd, bk (double linked list)
      • temporary cache (free 的 size 非 fast bin)
      • malloc 時會分配到對應 size 的 (small / large) chunk
    • Tcache
      • 每個 thread 的 heap, 為了減少 lock 的次數與提升效能
      • 只有 fd, 但另外有 key at bk 做 double free 的 check
      • 0x20 ~ 0x410 (64 個)
      • 7 個 entry per bin
      • LIFO
      • P 不會 unset
  • 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
#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; }
  • 結果
trace
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 bitschunk size
      • 後 4 bits 用來存其他資訊:
        1. 沒用到
        2. non_main_arena(N): 此 chunk 是否屬於 main_arena, 0 代表屬於 thread_arena
        3. is_mmaped(M): chunk 是否透過 mmap 拿到
        4. prev_inuse§: 連續 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
  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
    4. unsorted bin
      • 若 chunk size > 0x80, 被 free 後會先進入 unsorted bin
      • circular doubly linked list
      • 若 trigger 到 consolidate, 會清空 unsorted bin, 將對應大小的 chunk 分配到 small & large bin
  • fast bin struct
typedef struct malloc_chunk *mfastbinptr;
mfastbinptr fastbinsY[]; // Array of pointers to chunks
  • unsorted, small and large bin struct
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)
    ​​​​/* 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)
    ​​​​/* Ptr to next physical malloc_chunk. */
    ​​​​#define next_chunk(p) ((mchunkptr)(((char *) (p)) + chunksize(p)))
    
  • 取得 prev_chunk (當前 chunk address - prev_size)
    ​​​​/* 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))
    ​​​​#define inuse(p)
    ​​​​    ((((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size) & PREV_INUSE)
    
  • fastbin malloc
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

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
  • 某 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
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 有做優化

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), jmpplt[0]
    2. 0x8048380, 也就是 plt[0] 會再 pushjmp, 此時 push 的值為 *link_map, jmp 到 lib 中的 <dl_runtime_resolve> (.got.plt 的 第三項, 0x8040a000(.got.plt) + 8) function 的位置進行 binding.
      即為: _dl_runtime_resolve(*link_map, rel_offset)


    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
// 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:
    • 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:

    • 傳入 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
  1. 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,但是要小心 rsprbp 的值,因為 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

_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

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® 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/淺淡-js-engine-機制-77391b4dd3db

  • Chrome: V8 engine
  • Firefox: Spidermonkey
  • Safari: JavaScriptCore
  • Edge: hakraCore
  • jsengine object
    • 各大 implement 的方式都不一樣, 但是都會弄到 shape
    • object 在 new 時, 發現是新的, 會 create 兩個東西
      • object 本身的 values (slot)
      • shape of object
        • 之後若其他變數 create object 時, 會去找有沒有一樣的 shape
        • 各 browser 稱呼,不過都是同個東西
          • firefox: shape
          • v8: map
          • webkit: structure
          • edge: type
    • 改 shape 內 member 的 pointer
    • JIT-compiler (vanilla)
      • Runtime
      • 執行快 overhead 多
        • 丟給 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
      • 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,則 "刪除多餘的檢查" 可能會讓出問題的地方沒被檢查到
        • 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 -> main function -> __run_exit_handler -> dl-fini

其中在 __libc_start_main 中,有使用 atexit() 註冊一些 exit function

__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
      ​​​​​​​​struct exit_function_list
      ​​​​​​​​{
      ​​​​​​​​  struct exit_function_list *next;
      ​​​​​​​​  size_t idx;
      ​​​​​​​​  struct exit_function fns[32]; // functions
      ​​​​​​​​};
      
    • exit_function
      ​​​​​​​​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
    • 最後會找 _DYNAMICl_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

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/聊聊glibc-2-32-malloc新增的保護機制-safe-linking-9fb763466773

針對 tcache 與 fastbin 的 next pointer 做 XOR 的保護

  • heap address xor
    • 會根據當前 address 與值做 xor,簡單來說用法就是 value = xor(value, &value >> 12)
    ​​​​#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 才對
    ​​​​#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 的檢查