# Some Research about the Linux Heap ###### tags: `Linux` `security` `project` <!-- ## 全文連結 > [2022 NCU ADL CTF project2 - 第-1組](https://hackmd.io/@cy023/ByqYfv2vo) --> ## References > [Heap Exploitation](https://guyinatuxedo.github.io/25-heap/index.html) [heap-exploitation](https://heap-exploitation.dhavalkapil.com/) [MallocInternals - glibc wiki](https://sourceware.org/glibc/wiki/MallocInternals) [Heap Exploit 學習筆記](https://medium.com/@b3rm1nG/heap-exploit-%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98-d724d0afa59b) [Bins and Chunks - heap-exploitation](https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/bins_chunks#unsorted-bin) [House of Force - heap-exploitation](https://heap-exploitation.dhavalkapil.com/attacks/house_of_force) [TCACHE exploitation](https://medium.com/@b3rm1nG/tcache-exploitation-871044f8b210 ) [how2heap](https://github.com/shellphish/how2heap) [opp556687 - Linux heap exploit](https://hackmd.io/@opp556687/BkAm4kiyD) ## Terminologies in the linux heap ### arena 每一個 heap 都屬於一個 arena,一個 arena 可能包含多個 references to heaps 以及 heaps 裡的 linked lists of chunks,也稱為 free lists。被指派至某個 arena 的 thread 就會從此 arena 的 free lists 裡動態配置記憶體。 main arena 使用程式一開始就有的 heap (right after .bss),其餘的則透過 `mmap()` 來分配記憶體給他們的 heap(s)。 ### heap 動態分配的 memory (eg: `malloc()` / `new`),為一連續的虛擬記憶體區塊,被細分為許多的 chunks ### chunk 一塊可以被 allocated, freed 或是與鄰近 chunks 結合起來的記憶體區塊,minimum size = `4*sizeof(void*)` (32 bytes in x86_64) ```c struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk, if it is free. */ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if this chunk is 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 this chunk is free. */ struct malloc_chunk* bk_nextsize; }; typedef struct malloc_chunk* mchunkptr; ``` Free chunks 會以 `fd`, `bk` 串成一個 doubly linked list,allocated chunks 則不會用到這兩個指標,`malloc()` 會回傳原本 `fd` 的位址 Chunk size 恆為 8 的倍數,因此原本 size field 的 3 LSBs 恆為 0不需要儲存,可以當成 flags 來用,其中最後一個位元表示 previous chunk is in use (by the application) x86_64 系統而言,chunk header 占了 0x10 (16) bytes,因此 data (`malloc()` return 的位址) 是從 `(void *) mchunkptr + 0x10` 的地方開始儲存 ![image alt](https://miro.medium.com/max/640/1*CjulobLkVT16622tKU5bhA.webp) ### bin free (available) chunks 會根據大小以不同的串列連結起來,以便 library 可以快速的找到適合的 chunk 以滿足需求,bins 指的就是這些串列 - fast bins: 較小 (64-bit 系統而言貌似是指 size <= 160 bytes, **MAX_FAST_SIZE: 160** in 64-bit system) 的 chunks 會被存在 fast bins 裡面,也就是一個元素為 singly-linked list 的陣列,每個 list/bin 負責維護固定大小的 chunks。被加入 fast bins 的 chunks 不會被與鄰近的 chunk 合併 - unsorted bin: heap 裡僅有一個 unsorted bin,原先屬於 small bins, large bins 的 chunks 被釋放後會被放至此處。之後在 `malloc` 中這些 chunks 才會被排序並被移動至 small, large bins - small bins: 類似 fast bins 但 small bins chunks 在被加入到 small bins 時會先與鄰近 chunks 結合以合併 (coalesce) 成更大的 chunks,small bins 為雙向鏈結 - large bins: 當一個 chunk 所在的 bin (就是 large bin) 包含了不同大小的 chunks 時 (**MIN_LARGE_SIZE: 1024** in 64-bit system),此 chunk 就被稱為 "large" chunk。與 small bins 一樣,chunks 在被加入到 large bins 時會先與鄰近 chunks 結合以合併 (coalesce) 成更大的 chunks,large bins 為雙向鏈結 - top chunk: 每個 arena 都會紀錄一個 "top" chunk,通常為最大的 available chunk。此為**鄰近 unmapped memory 的 chunk,也就表示其位於 arena 當中最上方 (記憶體位址最大)** 且 top chunk 不屬於任何 bin。**House of force** 攻擊就是透過修改 top chunk 的內容以做到隨機任意記憶體讀寫 ([opp556687 - Linux heap exploit#House-of-Force](https://hackmd.io/@opp556687/BkAm4kiyD#House-of-Force)) ### Thread Local Cache (**tcache**) `malloc` 能夠知道有多個 threads 的存在,但也就僅此而已。`malloc` 中並沒有針對多執行緒的優化,它認定 kernel 會以有效率的方法來處理這些議題 每個執行緒會有一個 thread-local variable ([> Thread-Local Storage](https://web.mit.edu/rhel-doc/3/rhel-gcc-en-3/thread-local.html)) 來表示它最後使用的 arena,若該 arena 正在被使用的話則當另一執行緒需要使用該 arena 時 (為避免 [race condition](https://en.wikipedia.org/wiki/Race_condition)) 此執行緒會被 blocked 直到該 arena 變成 available。 每個執行緒還會有一個稱為 **tcache** 的快取,其中包含了一些不需要 lock 任何 arena 就能直接存取的 chunks,他們以一個元素為 singly-linked list 的陣列來儲存 (像是 fastbins),每個元素連結了單一大小的 chunks,也就是陣列的 index 表示 chunks size。`free()` 的時候會先看看 tcache 有無空間存放目標 chunk,沒有的話才會放到其他 bins ```c /* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache. */ typedef struct tcache_entry { struct tcache_entry *next; } tcache_entry; /* There is one of these for each thread, which contains the per-thread cache (hence "tcache_perthread_struct"). Keeping overall size low is mildly important. Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons. */ typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct; ``` - `TCACHE_MAX_BINS` 應該是 64 - `tcache_entry` 為單向鏈結串列的節點,`next` 指向下一塊 chunk 的 payload - `tcache_perthread_struct` 用來維護 thread local cache - `counts` 表示 bin 裡面的 chunks counts,同個 bin 裡面最多只能放 7 塊,超過的話會被放到 fastbins, unsorted bin 裡面 - `entries` 相當於 tcache 裡面的 bins,也就是共有 64 個 bin --- > 底下演算法主要來自 [MallocInternals - glibc wiki](https://sourceware.org/glibc/wiki/MallocInternals) ## Malloc Algorithm (abstract explanation) - 如果在 tcache 中可以找到合適的 chunk 就將其回傳 - 如果要求的空間夠大則使用 `mmap()` 以直接向 OS 要求記憶體空間。此空間大小的門檻是動態決定的,且這樣分配的空間數量是有限制的 - 如果與要求空間大小適合的 fastbin 裡找的到 chunk 就使用它。若有其他的 availbable chunks 就將他們填到 tcache 裡面 - 如果與要求空間大小適合的 smallbin 裡找的到 chunk 就使用它。若有其他的 availbable chunks 就將他們填到 tcache 裡面 - 如果要求的空間夠大 (>= 1024 in 64-bit system) 則將 fastbins 的全部 chunks 移至 unsorted bin 並進行合併 (coalesce) - 開始將 chunks 由 unsorted bin 移到 small / large bins 並進行合併 (coalesce),在過程中如果有發現大小合適的 chunk 就使用它 - 接著搜尋適合的 large bin,沒發現夠大的 chunk 就著搜尋更大的 large bins 直至發現一塊夠大的 chunk - 如果 fastbins 裡面還有 chunks (在要求的空間較小 (<= 1024 in 64-bit system) 時會發生) 就進行合併 (consolidate) 並重複前兩步 - 分離一部分的 top chunk (?) > 不確定這邊是甚麼意思,**原文 `Split off part of the "top" chunk, possibly enlarging "top" beforehand.`** > > [Core Functions - heap-exploitation](https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/core_functions#void-_int_malloc-mstate-av-size_t-bytes) 根據上面的文章,這邊應該是指如果到最後還沒有回傳適合的 chunk,會在最後把 top chunk 分為兩塊,remainder chunk 成為新的 top chunk 而另一塊將被回傳給使用者 ## Free Algorithm (abstract explanation) 實際上的釋放 (freeing) 一塊記憶體並不是將其還給 OS,`free()` 事實上是將 a chunk of memory 標記為 "free to be reused",在 OS 看來此空間依舊屬於該 process。 如果 "top" chunk 足夠大的話,其一部分的記憶體區塊可能會被 unmapped 並還給 OS。 - 如果 tcache 有足夠空間便將 chunk (`free()` 的目標) 存在 tcache 並 return - 如果 chunk 夠小的話則將其放在適合的 fastbins 裡面 (該 chunk 不會被合併 (coalesce)) - 如果該 chunk 之前是透過 `mmap()` 配置的話即將其 `munmap()` - 確認該 chunk 是否與另一 available chunk 相鄰,是的話就將它們合併 (coalesce) - 如果合併後的 chunk 不是 top chunk 的話將其放到 unsorted bin - 如果該 chunk 夠大就將任何的 fastbins 合併 (coalesce) (?),再確認 top chunk 是否足夠大,能夠將其一部分的記憶體區塊 unmapped 並還給 OS > 這邊也不確定是甚麼意思,**原文 `coalesce any fastbins`** ## Linux Heap Exploitation ### double free - 即對同一塊 chunk 進行兩次 free 的呼叫,會使 bin 裡面出現兩塊同樣的 chunk,一般而言 glibc 會檢查但僅會檢查上一次 free 的對象與這次是否相同,而不會對整個 bin 甚至是所有 free lists 的 chunks 都進行檢查 - 因此可以在 double free 中間穿插對另一塊 chunk 的 `free()` 來通過上述檢查,使同一個 bin 中有兩塊同樣的 free chunks ### House of Force (top chunk related) 1. 根據上方 malloc algorithm 可知如果到最後還沒有回傳適合的 chunk,會在最後把 top chunk 分為兩塊,remainder chunk 成為新的 top chunk 而另一塊將被回傳給使用者 2. 在此前提下若能透過 overflow 來控制 top chunk size 為一個非常大的數字 (64 個 1, 0xff...f),就能不透過 `mmap()` 來分配空間 3. 透過 malloc 分配一個接近任意大小的 chunk (`malloc(evil_size)`),能使我們得到一塊緊鄰目標 (接近任意位址) 的空間 把這個大小定義為 evil_size,新的 top chunk 位址、舊的 top chunk 位址分別定義為 new_top、old_top,目標位址定義為 dest,可以由以下式子得出 evil_size 的值 (**2*sizeof(long) == 0x10** 為 chunk header size) 1. new_top = old_top + evil_size + 2*sizeof(long) 2. evil_size + 2*sizeof(long) = new_top - old_top 3. evil_size = new_top - old_top - 2*sizeof(long) 4. evil_size = dest - 2\*sizeof(long) - old_top - 2*sizeof(long) 5. evil_size = dest - old_top - 4*sizeof(long) Reference: [shellphish/how2heap/house_of_force](https://github.com/shellphish/how2heap/blob/master/glibc_2.27/house_of_force.c) 4. 最後我們就能讓 `malloc()` 回傳一個接近任意的位址,也就能達到接近任意位址的讀寫 - CTF: HITCON-Training-Lab11 (解法1 bamboobox1.py) - [Write up](https://github.com/sShaAanGg/ADC-projects/tree/main/project2/house-of-force) ### fastbin attack 1. 在 tcache 已滿的情況下,最後一個 `free()` 的目標 chunk 會被放到對應大小的 fastbin 的最前面 2. 如果連續呼叫 `free()` 並傳入同一塊 chunk 的話,在第二次的時候由於同樣的 chunk 還是該 fastbin 的第一個節點因此會偵測到有 double free 的出現,程式會被終止執行 3. 假如我們先 malloc 3 塊大小為 8 的 chunk,就可以利用上述兩點,先 free chunk 1 接著 free chunk 2 再 free chunk 1,這樣就會使存著 chunk 大小為 8 的 fastbin 前三個 available chunks 變成 chunk 1, chunk 2, chunk 1 4. 現在呼叫 `malloc(8)` 兩次會產生一個奇特的狀況,即我們擁有 chunk1 的讀寫存取權限而 chunk1 卻也同時位於對應的 fastbin 的 head,此時只要修改 chunk1 的前 8 bytes (free chunk 中的 `fd` 欄位) 為我們想要偽造的 chunk address 再呼叫 malloc(8) 即可拿到一塊偽造的 chunk,但為通過對 chunk size 的檢查,需要先把偽造的 chunk 的 size 也設定為 8