Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/binary-exploitation/libc-heap/heap-overflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,43 @@ Source-side checks such as `count <= remaining_input` are **not enough**. The pa

This pattern is especially relevant in **games, importers, asset packs, media parsers, and mod/plugin ecosystems**, where a “passive” file can trigger complex stateful parsing and attacker-controlled heap shaping.<sup>[[2]](#references)</sup>

### Canonicalization output overflowing fixed-stride object pools

Do not size a destination from the raw XML alone: **canonicalization can produce a different-length serialization**. In a NetScaler SAML path, exclusive C14N canonicalized attacker-controlled `ds:SignedInfo` into a fixed-size network-buffer object before authentication completed. Most signature fields were constrained, but `ec:InclusiveNamespaces PrefixList` accepted a long list when every space-separated prefix was unique, making it a useful expansion surface. The patched implementation rejects a computed canonical form of `0x1001` bytes or more.<sup>[[10]](#references)[[11]](#references)</sup>

A compact way to generate uniquely identifiable C14N input is:<sup>[[10]](#references)</sup>

```python
prefixes = " ".join(f"N{i}" for i in range(2000))
inclusive_ns = (
'<ec:InclusiveNamespaces '
'xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" '
f'PrefixList="{prefixes}"/>'
)
```

The important exploitation pattern is **delayed cross-object corruption**. The canonicalized data started at `+0x180` in a fixed-stride `0x980`-byte packet-buffer pool; a linear overflow entered the next object's header and reached a type field, a data pointer at `+0x50`, and a freelist link at `+0x60`. The fault occurred only after the poisoned object was recycled and packet-send code trusted those fields, so a crash in `memcpy` was far removed from the original C14N write. On FreeBSD, the resulting invalid access may appear as `SIGBUS` rather than `SIGSEGV`.<sup>[[10]](#references)</sup>

For this kind of pool allocator, use the overflow length as a grooming parameter and map the victim header systematically:<sup>[[10]](#references)</sup>

1. Fill the input with fixed-width, self-describing tokens such as `N%07d` or `M0000000`, then inspect corrupted qwords to recover the exact source offset controlling each field.
2. Sweep lengths while breaking on both allocation and the later consumer; the length can select the recycled victim as well as the deepest overwritten field.
3. Preserve fields needed to reach the dangerous use. For example, set a damaged freelist link to a known valid object instead of leaving marker bytes there.
4. Trace the corrupted pointer through all arithmetic immediately before its use rather than assuming the stored value becomes the final pointer unchanged.

In the demonstrated consumer, the overwritten neighbor supplied the destination, the original object supplied attacker-controlled canonicalized bytes, and packet metadata supplied the copy length:<sup>[[10]](#references)</sup>

```c
memcpy(*(victim + 0x50),
*(source + 0x50),
*(source + 0xE0) - *(source + 0x50));
```

The implementation also performed `destination = stored_pointer - packet_length`. Therefore, targeting address `X` required storing `X + packet_length`, illustrating why controlled pointer arithmetic must be inverted when building a write-what-where primitive. In a non-PIE process without ASLR, that write can target a frequently executed global callback; if attacker data is at a predictable executable heap address, replacing the callback with that address can yield direct shellcode execution without an information leak or ROP.<sup>[[10]](#references)</sup>

> [!NOTE]
> If the corrupted process cannot safely continue, execute persistence or cleanup logic **before** the inevitable fault. In the NetScaler chain, shellcode replaced fatal-signal handlers with `SIG_IGN` via FreeBSD `sigaction`, preventing the handlers from asking the supervisor to reboot the appliance. The supervisor then respawned only the packet engine, preserving files written before the crash. This behavior is supervisor-specific and should be verified by tracing the process-to-watchdog notification path.<sup>[[10]](#references)</sup>

### Real-World Example: CVE-2025-40597 – Misusing `__sprintf_chk`

In SonicWall SMA100 firmware 10.2.1.15 the reverse-proxy module `mod_httprp.so` allocates an **0x80-byte** heap chunk and then concatenates several strings into it with `__sprintf_chk`:<sup>[[1]](#references)</sup>
Expand Down Expand Up @@ -120,5 +157,7 @@ Practical exploitation would require **heap grooming** to place a controllable o
- [7] [8kSec – ARM64 Reversing and Exploitation Part 1: ARM Instruction Set & Simple Heap Overflow](https://8ksec.io/arm64-reversing-and-exploitation-part-1-arm-instruction-set-simple-heap-overflow/)
- [8] [7rocky – Auth-or-out. Hack The Box](https://7rocky.github.io/en/ctf/htb-challenges/pwn/auth-or-out/)
- [9] [MITRE CWE-122 – Heap-based Buffer Overflow](https://cwe.mitre.org/data/definitions/122.html)
- [10] [watchTowr Labs – You’re Back In The Room: Citrix NetScaler pre-authentication RCE](https://labs.watchtowr.com/youre-back-in-the-room-citrix-netscaler-pre-auth-rce-cve-2026-8452/)
- [11] [Citrix Security Bulletin CTX696604](https://support.citrix.com/support-home/kbsearch/article?articleNumber=CTX696604)

{{#include ../../banners/hacktricks-training.md}}