|
9 | 9 |
|
10 | 10 | **`atexit()`** is a function to which **other functions are passed as parameters.** These **functions** will be **executed** when executing an **`exit()`** or the **return** of the **main**.\ |
11 | 11 | If you can **modify** the **address** of any of these **functions** to point to a shellcode for example, you will **gain control** of the **process**, but this is currently more complicated.\ |
12 | | -Currently the **addresses to the functions** to be executed are **hidden** behind several structures and finally the address to which it points are not the addresses of the functions, but are **encrypted with XOR** and displacements with a **random key**. So currently this attack vector is **not very useful at least on x86** and **x64_86**.\ |
13 | | -The **encryption function** is **`PTR_MANGLE`**. **Other architectures** such as m68k, mips32, mips64, aarch64, arm, hppa... **do not implement the encryption** function because it **returns the same** as it received as input. So these architectures would be attackable by this vector. |
| 12 | +Currently the **addresses to the functions** to be executed are **hidden** behind several structures and finally the address to which it points are not the addresses of the functions, but are **encrypted with XOR** and displacements with a **random key**. So currently this attack vector is **not very useful at least on x86** and **x86_64**.\ |
| 13 | +The **encryption function** is **`PTR_MANGLE`**. However, the exact implementation is **architecture- and glibc-version-dependent**: the generic glibc header is a no-op, but modern arch-specific implementations exist for important targets such as **x86_64** and **aarch64**. Therefore, **do not assume** that a non-x86 target automatically has **unmangled** exit pointers; check the target build's `pointer_guard.h` / `PTR_MANGLE` macros first. |
14 | 14 |
|
15 | 15 | You can find an in depth explanation on how this works in [https://m101.github.io/binholic/2017/05/20/notes-on-abusing-exit-handlers.html](https://m101.github.io/binholic/2017/05/20/notes-on-abusing-exit-handlers.html) |
16 | 16 |
|
@@ -124,7 +124,7 @@ __call_tls_dtors (void) |
124 | 124 | For each registered function in **`tls_dtor_list`**, it'll demangle the pointer from **`cur->func`** and call it with the argument **`cur->obj`**. |
125 | 125 |
|
126 | 126 | Using the **`tls`** function from this [**fork of GEF**](https://github.com/bata24/gef), it's possible to see that actually the **`dtor_list`** is very **close** to the **stack canary** and **PTR_MANGLE cookie**. So, with an overflow on it's it would be possible to **overwrite** the **cookie** and the **stack canary**.\ |
127 | | -Overwriting the PTR_MANGLE cookie, it would be possible to **bypass the `PTR_DEMANLE` function** by setting it to 0x00, will mean that the **`xor`** used to get the real address is just the address configured. Then, by writing on the **`dtor_list`** it's possible **chain several functions** with the function **address** and it's **argument.** |
| 127 | +Overwriting the PTR_MANGLE cookie, it would be possible to **bypass the `PTR_DEMANGLE` function** by setting it to 0x00, will mean that the **`xor`** used to get the real address is just the address configured. Then, by writing on the **`dtor_list`** it's possible **chain several functions** with the function **address** and it's **argument.** |
128 | 128 |
|
129 | 129 | Finally notice that the stored pointer is not only going to be xored with the cookie but also rotated 17 bits: |
130 | 130 |
|
@@ -235,6 +235,66 @@ It's possible to check the **`initial` structure** in a debugging session with G |
235 | 235 | To abuse this you need either to **leak or erase the `PTR_MANGLE`cookie** and then overwrite a `cxa` entry in initial with `system('/bin/sh')`.\ |
236 | 236 | You can find an example of this in the [**original blog post about the technique**](https://github.com/nobodyisnobody/docs/blob/main/code.execution.on.last.libc/README.md#6---code-execution-via-other-mangled-pointers-in-initial-structure). |
237 | 237 |
|
| 238 | +## Practical `__exit_funcs` / `initial` forgery on modern glibc |
| 239 | +
|
| 240 | +Since classic hooks such as `__free_hook` disappeared from normal modern-glibc targets, a very common follow-up to an arbitrary write is to **forge a fake `struct exit_function_list`** and then repoint **`__exit_funcs`** to it. This is basically the most practical version of abusing `__run_exit_handlers()` on recent glibc releases. |
| 241 | +
|
| 242 | +The important points are: |
| 243 | +
|
| 244 | +- `exit()` is just a wrapper around **`__run_exit_handlers(status, &__exit_funcs, true, true)`**, so corrupting `__exit_funcs` is often enough to win when the program returns from `main` or calls `exit()`. |
| 245 | +- On a lot of targets the statically allocated `initial` list already contains one `ef_cxa` entry for **`_dl_fini`**. That gives you a **known plaintext** pointer inside the exit list. |
| 246 | +- If you can leak the **mangled** `_dl_fini` pointer and also know the **real** `_dl_fini` address, you can recover the pointer-guard cookie and then mangle any function you want. For a refresher on pointer guard, check [**this page**](../common-binary-protections-and-bypasses/libc-protections.md#pointer-guard). |
| 247 | +
|
| 248 | +On **x86_64**, the formulas are usually: |
| 249 | +
|
| 250 | +```python |
| 251 | +rol = lambda x, n: ((x << n) | (x >> (64 - n))) & ((1 << 64) - 1) |
| 252 | +ror = lambda x, n: ((x >> n) | (x << (64 - n))) & ((1 << 64) - 1) |
| 253 | +ptr_guard = ror(enc_dl_fini, 0x11) ^ real_dl_fini |
| 254 | +enc_target = rol(real_target ^ ptr_guard, 0x11) |
| 255 | +``` |
| 256 | + |
| 257 | +With that, a forged exit list usually looks like: |
| 258 | + |
| 259 | +```c |
| 260 | +struct exit_function_list { |
| 261 | + struct exit_function_list *next; |
| 262 | + size_t idx; |
| 263 | + struct exit_function fns[32]; |
| 264 | +}; |
| 265 | + |
| 266 | +// Minimal fake list |
| 267 | +next = NULL; |
| 268 | +idx = 1; |
| 269 | +fns[0].flavor = ef_cxa; |
| 270 | +fns[0].func.cxa.fn = mangled(system); |
| 271 | +fns[0].func.cxa.arg = binsh_ptr; |
| 272 | +``` |
| 273 | + |
| 274 | +Then, overwrite `__exit_funcs` so it points to the fake list and make the program terminate via `exit()` or `return` from `main`. Remember that **`idx` is consumed backwards**, so if you forge **several** entries they will run in **reverse order**. |
| 275 | + |
| 276 | +### Practical notes |
| 277 | + |
| 278 | +- If you only have a **write** primitive but no read, another option is to **overwrite the pointer-guard cookie in TLS** with `0x0` and store `rol(target, 0x11)` as the encrypted pointer. This works because demangling becomes just `ror(ptr, 0x11)`. |
| 279 | +- If you already have a **libc leak**, the encrypted `_dl_fini` entry is often enough to recover the cookie because **`ld.so` is commonly adjacent to libc** in memory and `_dl_fini` sits at a fixed offset inside `ld.so` for the target build. |
| 280 | +- This technique is useless if the target terminates via **`_exit()`** or crashes before returning to the normal exit path. In those cases prefer other post-write targets such as [**`.fini_array` / `.dtors`**](www2exec-.dtors-and-.fini_array.md) or non-exit-time pivots. |
| 281 | + |
| 282 | +### Quick debugging checklist |
| 283 | + |
| 284 | +```gdb |
| 285 | +b __run_exit_handlers |
| 286 | +b __call_tls_dtors |
| 287 | +x/10gx __exit_funcs |
| 288 | +x/gx $fs_base+0x30 |
| 289 | +``` |
| 290 | + |
| 291 | +If `__exit_funcs` is already corrupted, inspect the first entry and verify: `flavor`, `idx`, the mangled function pointer, and whether the program will really reach `exit()` instead of `_exit()`. |
| 292 | + |
| 293 | +## References |
| 294 | + |
| 295 | +- [Code execution part 1: from exit to system](https://blog.rop.la/en/exploiting/2024/06/11/code-exec-part1-from-exit-to-system.html) |
| 296 | +- [Dead or Alive - forging custom exit handlers on modern glibc](https://draksec.cz/blog/htb-uni-ctf-24/dead-or-alive/) |
| 297 | + |
238 | 298 | {{#include ../../banners/hacktricks-training.md}} |
239 | 299 |
|
240 | 300 |
|
|
0 commit comments