|
4 | 4 |
|
5 | 5 | ## Basic Information |
6 | 6 |
|
7 | | -For more information about what is a Tcache bin check this page: |
8 | | - |
| 7 | +For more information about what a Tcache bin is, check this page: |
9 | 8 |
|
10 | 9 | {{#ref}} |
11 | 10 | bins-and-memory-allocations.md |
12 | 11 | {{#endref}} |
13 | 12 |
|
14 | | -First of all, note that the Tcache was introduced in Glibc version 2.26. |
| 13 | +The **Tcache attack** (also known as **Tcache poisoning**) is the tcache equivalent of a fast-bin attack: the attacker corrupts the `next` pointer stored in a freed tcache entry so that a later `malloc()` returns an **attacker-chosen address**. |
| 14 | + |
| 15 | +This primitive is only useful if the attacker can first **write into a freed chunk**. Common ways of getting that primitive are explained here: |
| 16 | + |
| 17 | +{{#ref}} |
| 18 | +overwriting-a-freed-chunk.md |
| 19 | +{{#endref}} |
| 20 | + |
| 21 | +Historically, this was introduced together with tcache in **glibc 2.26** and was initially very easy to exploit. Modern glibc added several checks, so the attack is still relevant, but it now depends a lot more on the target version and the primitive you start from. |
| 22 | + |
| 23 | +### Modern constraints |
| 24 | + |
| 25 | +- **glibc 2.29+** added stronger tcache **double-free detection** using the `key` field stored inside a freed `tcache_entry`. A plain `free(A); free(B); free(A);` usually aborts now unless the `key` check is bypassed or the chunk is reintroduced through another path. |
| 26 | +- **glibc 2.32+** added **safe-linking** to singly-linked allocator lists (`tcache` and `fastbins`). The stored `next` pointer is no longer raw, but mangled as: |
| 27 | + |
| 28 | +```c |
| 29 | +stored_next = target ^ (chunk_addr >> 12) |
| 30 | +``` |
| 31 | + |
| 32 | + Therefore, a modern tcache poisoning usually needs either: |
| 33 | + - A **heap leak** to compute the protected pointer correctly, or |
| 34 | + - A different primitive that bypasses/abuses safe-linking instead of forging the pointer directly. |
| 35 | +- **Alignment checks** also became stricter, which is why a bad poisoned pointer now tends to crash with **`malloc(): unaligned tcache chunk detected`**. |
| 36 | +- **glibc 2.34+** removed the old malloc hooks from the active API, so classic end goals such as overwriting `__malloc_hook` / `__free_hook` should be treated as **version-specific legacy targets**, not as the default modern outcome. |
| 37 | + |
| 38 | +In practice, the modern end goal is usually one of these: |
| 39 | + |
| 40 | +- Return a chunk on top of another **heap object** to get arbitrary read/write. |
| 41 | +- Return a chunk on a **writable global / application structure** and corrupt a code pointer later used by the program. |
| 42 | +- Return a chunk on top of a structure later abused for **FSOP**, **ROP**, or another post-write primitive. |
| 43 | + |
| 44 | +### What a poisoned tcache entry looks like |
| 45 | + |
| 46 | +A freed tcache chunk is reused as a `tcache_entry`: |
| 47 | + |
| 48 | +```c |
| 49 | +struct tcache_entry { |
| 50 | + struct tcache_entry *next; |
| 51 | + uintptr_t key; |
| 52 | +}; |
| 53 | +``` |
| 54 | + |
| 55 | +So, after freeing a chunk, the first qword of its user data becomes the singly-linked-list pointer and the next qword is used for the double-free check. If the vulnerability lets you overwrite either of them, you can often turn it into: |
| 56 | + |
| 57 | +- **Arbitrary allocation** by corrupting `next` |
| 58 | +- **Double-free bypass** by corrupting `key` |
15 | 59 |
|
16 | | -The **Tcache attack** (also known as **Tcache poisoning**) proposed in the [**guyinatuxido page**](https://guyinatuxedo.github.io/29-tcache/tcache_explanation/index.html) is very similar to the fast bin attack where the goal is to overwrite the pointer to the next chunk in the bin inside a freed chunk to an arbitrary address so later it's possible to **allocate that specific address and potentially overwrite pointes**. |
| 60 | +### Basic modern poisoning flow |
17 | 61 |
|
18 | | -However, nowadays, if you run the mentioned code you will get the error: **`malloc(): unaligned tcache chunk detected`**. So, it's needed to write as address in the new pointer an aligned address (or execute enough times the binary so the written address is actually aligned). |
| 62 | +1. Obtain a primitive that lets you **edit a freed chunk**. |
| 63 | +2. Free a chunk of the target tcache size. |
| 64 | +3. Leak or infer the freed chunk address if safe-linking is enabled. |
| 65 | +4. Overwrite its `next` pointer with the **mangled** version of the target address. |
| 66 | +5. Allocate once to consume the corrupted entry. |
| 67 | +6. Allocate again to get a chunk returned at the chosen target. |
| 68 | + |
| 69 | +For glibc 2.32+ the forged value is typically: |
| 70 | + |
| 71 | +```c |
| 72 | +fake_next = target ^ (victim_chunk_addr >> 12) |
| 73 | +``` |
| 74 | + |
| 75 | +If the returned pointer is not aligned to the allocator expectations, `malloc()` will usually abort before you get control. |
19 | 76 |
|
20 | 77 | ### Tcache indexes attack |
21 | 78 |
|
22 | | -Usually it's possible to find at the beginning of the heap a chunk containing the **amount of chunks per index** inside the tcache and the address to the **head chunk of each tcache index**. If for some reason it's possible to modify this information, it would be possible to **make the head chunk of some index point to a desired address** (like `__malloc_hook`) to then allocated a chunk of the size of the index and overwrite the contents of `__malloc_hook` in this case. |
| 79 | +Usually it's possible to find at the beginning of the heap a chunk containing the **amount of chunks per index** inside the tcache and the address to the **head chunk of each tcache index**. If for some reason it's possible to modify this information, it would be possible to **make the head chunk of some index point to a desired address** so then allocating a chunk of the corresponding size returns that controlled address. |
| 80 | + |
| 81 | +This is powerful because it doesn't just poison one freed entry: it corrupts the **per-thread tcache metadata** itself, which can let you pivot several size classes at once. |
| 82 | + |
| 83 | +### Modern variants worth knowing |
| 84 | + |
| 85 | +#### House of Botcake |
| 86 | + |
| 87 | +This is the standard modern answer when the old tcache double-free no longer works. The idea is to use **overlap/consolidation with the unsorted bin** so the victim chunk ends up both reachable from tcache and part of a larger freed region. After regaining access to overlapping memory, you poison the tcache entry as usual. |
| 88 | + |
| 89 | +This is especially useful on modern glibc when you have: |
| 90 | + |
| 91 | +- A way to trigger consolidation |
| 92 | +- A double-free-like primitive that is blocked by tcache checks |
| 93 | +- Enough heap control to recover the victim chunk and rewrite its mangled `next` |
| 94 | + |
| 95 | +#### Tcache stashing unlink attack |
| 96 | + |
| 97 | +This is not a direct "overwrite `next` inside a freed tcache chunk" attack, but it is a very relevant **tcache-focused arbitrary-allocation primitive**. It abuses the path where glibc moves chunks from a **small bin into tcache**. If you can corrupt the small-bin metadata (typically `bk`) before that transfer, glibc can end up **stashing a fake chunk into tcache**, after which a normal `malloc()` returns it. |
| 98 | + |
| 99 | +This is useful when a challenge gives you stronger control over **small-bin metadata** than over a freed tcache entry itself. |
| 100 | + |
| 101 | +#### Safe-linking bypasses |
| 102 | + |
| 103 | +On glibc 2.32+ the core problem is not "how do I overwrite `next`?" but "how do I produce a valid protected pointer?" Common answers are: |
| 104 | + |
| 105 | +- Leak the heap by printing or reading a **freed tcache chunk**. |
| 106 | +- Use an overlap/UAF to recover the heap base and then encode the poisoned pointer correctly. |
| 107 | +- Abuse a primitive that effectively applies the protection logic **twice** or that corrupts the **tcache metadata** rather than a single entry. |
23 | 108 |
|
24 | 109 | ## Examples |
25 | 110 |
|
26 | 111 | - CTF [https://guyinatuxedo.github.io/29-tcache/dcquals19_babyheap/index.html](https://guyinatuxedo.github.io/29-tcache/dcquals19_babyheap/index.html) |
27 | 112 | - **Libc info leak**: It's possible to fill the tcaches, add a chunk into the unsorted list, empty the tcache and **re-allocate the chunk from the unsorted bin** only overwriting the first 8B, leaving the **second address to libc from the chunk intact so we can read it**. |
28 | 113 | - **Tcache attack**: The binary is vulnerable a 1B heap overflow. This will be abuse to change the **size header** of an allocated chunk making it bigger. Then, this chunk will be **freed**, adding it to the tcache of chunks of the fake size. Then, we will allocate a chunk with the faked size, and the previous chunk will be **returned knowing that this chunk was actually smaller** and this grants up the opportunity to **overwrite the next chunk in memory**.\ |
29 | | - We will abuse this to **overwrite the next chunk's FD pointer** to point to **`malloc_hook`**, so then its possible to alloc 2 pointers: first the legit pointer we just modified, and then the second allocation will return a chunk in **`malloc_hook`** that it's possible to abuse to write a **one gadget**. |
| 114 | + We will abuse this to **overwrite the next chunk's FD pointer** to point to a sensitive target, so then later allocations return a controlled pointer and give an arbitrary write primitive. |
30 | 115 | - CTF [https://guyinatuxedo.github.io/29-tcache/plaid19_cpp/index.html](https://guyinatuxedo.github.io/29-tcache/plaid19_cpp/index.html) |
31 | 116 | - **Libc info leak**: There is a use after free and a double free. In this writeup the author leaked an address of libc by readnig the address of a chunk placed in a small bin (like leaking it from the unsorted bin but from the small one) |
32 | 117 | - **Tcache attack**: A Tcache is performed via a **double free**. The same chunk is freed twice, so inside the Tcache the chunk will point to itself. Then, it's allocated, its FD pointer is modified to point to the **free hook** and then it's allocated again so the next chunk in the list is going to be in the free hook. Then, this is also allocated and it's possible to write a the address of `system` here so when a malloc containing `"/bin/sh"` is freed we get a shell. |
| 118 | + - This is still a good **historical** example, but remember that the easy version of this attack does **not** generalise to glibc `2.32+` / `2.34+` without accounting for safe-linking and hook removal. |
33 | 119 | - CTF [https://guyinatuxedo.github.io/44-more_tcache/csaw19_popping_caps0/index.html](https://guyinatuxedo.github.io/44-more_tcache/csaw19_popping_caps0/index.html) |
34 | 120 | - The main vuln here is the capacity to `free` any address in the heap by indicating its offset |
35 | | - - **Tcache indexes attack**: It's possible to allocate and free a chunk of a size that when stored inside the tcache chunk (the chunk with the info of the tcache bins) will generate an **address with the value 0x100**. This is because the tcache stores the amount of chunks on each bin in different bytes, therefore one chunk in one specific index generates the value 0x100. |
36 | | - - Then, this value looks like there is a chunk of size 0x100. Allowing to abuse it by `free` this address. This will **add that address to the index of chunks of size 0x100 in the tcache**. |
37 | | - - Then, **allocating** a chunk of size **0x100**, the previous address will be returned as a chunk, allowing to overwrite other tcache indexes.\ |
38 | | - For example putting the address of malloc hook in one of them and allocating a chunk of the size of that index will grant a chunk in calloc hook, which allows for writing a one gadget to get a s shell. |
| 121 | + - **Tcache indexes attack**: It's possible to allocate and free a chunk of a size that when stored inside the tcache chunk (the chunk with the info of the tcache bins) will generate an **address with the value `0x100`**. This is because the tcache stores the amount of chunks on each bin in different bytes, therefore one chunk in one specific index generates the value `0x100`. |
| 122 | + - Then, this value looks like there is a chunk of size `0x100`, allowing the attacker to `free` this address. |
| 123 | + - Then, **allocating** a chunk of size **`0x100`**, the previous address will be returned as a chunk, allowing to overwrite other tcache indexes. |
39 | 124 | - CTF [https://guyinatuxedo.github.io/44-more_tcache/csaw19_popping_caps1/index.html](https://guyinatuxedo.github.io/44-more_tcache/csaw19_popping_caps1/index.html) |
40 | 125 | - Same vulnerability as before with one extra restriction |
41 | | - - **Tcache indexes attack**: Similar attack to the previous one but using less steps by **freeing the chunk that contains the tcache info** so it's address is added to the tcache index of its size so it's possible to allocate that size and get the tcache chunk info as a chunk, which allows to add free hook as the address of one index, alloc it, and write a one gadget on it. |
| 126 | + - **Tcache indexes attack**: Similar attack to the previous one but using less steps by **freeing the chunk that contains the tcache info** so its address is added to the tcache index of its size. Then, allocating that size returns the **tcache metadata chunk itself**, which allows poisoning other indexes. |
42 | 127 | - [**Math Door. HTB Cyber Apocalypse CTF 2023**](https://7rocky.github.io/en/ctf/other/htb-cyber-apocalypse/math-door/) |
43 | 128 | - **Write After Free** to add a number to the `fd` pointer. |
44 | 129 | - A lot of **heap feng-shui** is needed in this challenge. The writeup shows how **controlling the head of the Tcache** free-list is pretty handy. |
45 | 130 | - **Glibc leak** through `stdout` (FSOP). |
46 | 131 | - **Tcache poisoning** to get an arbitrary write primitive. |
| 132 | +- [**mailman. ImaginaryCTF 2023**](https://sekai.team/blog/imaginary-ctf-2023/mailman) |
| 133 | + - Modern **glibc 2.35** challenge. |
| 134 | + - The exploit chain uses a **heap leak** to defeat safe-linking, then **House of Botcake** to create the overlap needed for a modern tcache poisoning. |
| 135 | + - Good example of using tcache poisoning as a step towards **FSOP/ROP**, not just a hook overwrite. |
| 136 | +- [**catastrophe. DiceCTF 2022**](https://ret2school.github.io/post/catastrophe/) |
| 137 | + - Modern **glibc 2.35** challenge. |
| 138 | + - Leak a heap pointer by reading a **freed tcache entry**, encode the poisoned pointer correctly, then use **House of Botcake** to obtain the arbitrary write needed for the rest of the chain. |
47 | 139 |
|
48 | | -{{#include ../../banners/hacktricks-training.md}} |
| 140 | +## References |
49 | 141 |
|
| 142 | +- [https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/](https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/) |
| 143 | +- [https://github.com/shellphish/how2heap](https://github.com/shellphish/how2heap) |
50 | 144 |
|
| 145 | +{{#include ../../banners/hacktricks-training.md}} |
0 commit comments