--- name: exploiting-integer-overflow-vulnerabilities description: Methodology for finding and exploiting integer overflow, underflow, truncation, and signedness bugs in native code during authorized engagements, focusing on how wrapped arithmetic in size/length calculations leads to undersized allocations, oversized copies, length-check bypasses, and downstream heap/stack overflows. domain: cybersecurity subdomain: binary-exploitation tags: - binary-exploitation - integer-overflow - exploit-development version: '1.0' author: xalgorix license: Apache-2.0 --- # Exploiting Integer Overflow Vulnerabilities ## When to Use - During authorized assessments of native code (C/C++/Rust/Go) where attacker-controlled integers feed size, length, count, index, or offset calculations. - When an allocation size is computed from arithmetic (`count * elem_size`, `len - header`, `size + padding`) that can wrap, producing an **undersized buffer** later written with the logical (un-wrapped) size — a classic integer-overflow-to-heap-overflow chain. - When a length/bounds check uses a narrow type (e.g. 1-byte size field) that can be overflowed to bypass validation while the real copy uses a larger value. - When signed/unsigned confusion causes a negative input to be treated as a huge positive size. ## Critical: Concepts/Steps Most Often Missed - **The overflow itself is rarely the impact — find the dangerous downstream use.** An int wrap is only exploitable when the wrapped value sizes an allocation/copy/index. Always trace the tainted integer to a `malloc`, `memcpy`, `read`, loop bound, or array index. - **Truncation across type widths.** `uint32_t product = (uint32_t)(count * elem_size)` can wrap to a tiny value while a separate `size_t to_copy = count * elem_size` keeps the full value — allocation is small, copy is huge. Watch every cast to a narrower type. - **Underflow on subtraction is just as dangerous.** `size_t payload = total_len - HEADER` with `total_len < HEADER` underflows to a near-`SIZE_MAX` value, turning a "shrink" into an unbounded copy. - **Signedness.** A signed `int` read with `scanf("%d")` then cast to `unsigned` makes negatives become large positives, defeating `if (x > LIMIT)` checks. - **Alignment/rounding re-introduces overflow after an initial guard.** Custom allocators that pass an initial wrap check then do `total += (8 - total) % total` can wrap again for values near `SIZE_MAX`, yielding a tiny chunk (e.g. CVE-2025-54957). - **Go/Rust wrap silently in release/normal builds.** Go has no overflow panic by default; instrument with go-panikint; Rust wraps in release unless `overflowing_*`/checked arithmetic is used. ### How to CONFIRM Instrument and observe the wrap directly. In C, compile a probe with `-fsanitize=integer`/UBSan (`-fsanitize=signed-integer-overflow,unsigned-integer-overflow`) to get a runtime report at the exact operation. In a debugger, set a breakpoint after the size computation and inspect the register/variable: confirm the allocation size is small (e.g. 32) while the copy length is huge (e.g. `0xffffffffffffff f8`). The exploit is confirmed when the undersized buffer's neighbor (e.g. `is_admin` flag, chunk metadata) is overwritten — verify the target field changed and the gated/privileged path executes. ## Workflow ### Step 1: Map Attacker-Controlled Integers to Size/Index Sinks ```bash # Find suspicious arithmetic feeding allocations/copies grep -nE "malloc|calloc|memcpy|memmove|alloca|\\bread\\b" ./src/*.c # Look for: size = a*b; size = a+b; size = a-b; (uint32_t)(...); short/int casts ``` Reverse the size computation and the subsequent use; note the type widths at each step (8/16/32/64-bit, signed vs. unsigned). ### Step 2: Compute the Wrapping Input ```c // 8-bit wrap: 255 + 1 == 0 // 32-bit truncation: (uint32_t)(0x100000000 * 1) == 0 -> alloc tiny, copy huge // underflow: (size_t)(8 - 16) == 0xfffffffffffffff8 ``` Reference max values: `uint8_t` 255, `uint16_t` 65535, `uint32_t` 4294967295, `uint64_t` 18446744073709551615. Pick the input that drives the allocation size small while the copy/loop length stays large (or vice-versa). ### Step 3: Trigger the Undersized Allocation + Oversized Write ```python from pwn import * io = process('./int_ovf_heap_priv') # count=2^32 truncates to 0 -> alloc32 = 32, but copy uses full size_t product io.sendlineafter(b"Entry count: ", b"4294967296") io.sendlineafter(b"Entry size: ", b"1") io.recvuntil(b">> Send bundle payload on stdin (EOF to finish)...") # offset_to_sess = 48; overflow 48 bytes then flip is_admin payload = b"A"*48 + p32(1) io.send(payload) io.shutdown("send") # deliver EOF to stop the read loop print(io.recvall(timeout=5).decode(errors="ignore")) # expect admin + FLAG ``` The underflow variant: send `Total packet length: 8` (< HEADER 16) so `payload_len = 8 - 16` underflows to a huge value, letting a bounded reader walk past the 24-byte gap and flip `is_admin`. ### Step 4: Escalate the Downstream Corruption Depending on what the undersized buffer is adjacent to: - **Data-oriented**: flip an adjacent flag/struct field (e.g. `is_admin`) — no control-flow hijack needed (PAC/CFI-safe). - **Heap metadata**: overwrite the next chunk's size/`fd` and pivot to a heap-exploitation primitive (tcache/fastbin). - **Stack**: an undersized stack buffer + oversized copy becomes a stack overflow (offset/length-check bypass) — chain to ret2win/ROP. - **Length-check bypass**: a 1-byte size field overflowed so a 260-byte input passes a "length 4" check, then overflows. ## Key Concepts | Concept | Description | |---------|-------------| | **Overflow (wrap)** | Result exceeds type max and wraps to a small value (e.g. `255+1 -> 0`). | | **Underflow** | Unsigned subtraction below 0 wraps to near `SIZE_MAX` (e.g. `8-16 -> 0xff...f8`). | | **Truncation** | Storing a wide value in a narrower type drops high bits (`(uint32_t)0x100000000 -> 0`). | | **Signedness confusion** | A signed negative reinterpreted as a large unsigned, bypassing `> LIMIT` checks. | | **Undersized allocation** | Wrapped size allocates a tiny buffer while a copy uses the full logical size. | | **Rounding re-wrap** | Alignment math (`total += (8-total)%total`) wraps again after an initial guard. | | **Length-check bypass** | Overflowing a small size field so an oversized input passes validation. | ## Tools & Systems | Tool | Purpose | |------|---------| | **UBSan / -fsanitize=integer** | Pinpoints the exact overflowing operation at runtime in C/C++ probes. | | **gdb + pwndbg/GEF** | Inspect computed sizes vs. copy lengths, watch the adjacent target field change. | | **pwntools** | Drive prompts, send wrapping inputs, `p32`/`p64`, `shutdown('send')` for EOF, process/remote IO. | | **z3** | Solve for inputs satisfying modular-arithmetic constraints (e.g. `a*x mod 2^64 == b`). | | **go-panikint** | Forked Go toolchain that panics on wrap; surfaces silent Go overflow in fuzzing/CI. | | **radare2 / Ghidra** | Reverse size computations, identify type widths and the dangerous sink. | | **AFL++/libFuzzer** | Fuzz size/offset parsers to discover wrap-triggering inputs. | ## Common Scenarios ### Scenario 1: Multiply truncation -> heap overflow `count*elem_size` truncated to 32 bits allocates 32 bytes, but the copy uses the full 64-bit product, overflowing into an adjacent `session` struct and flipping `is_admin` to gain privileges. ### Scenario 2: Subtraction underflow -> unbounded copy `payload_len = total_len - HEADER` with `total_len < HEADER` underflows; a bounded reader still writes far past the small buffer, overwriting a neighboring flag. ### Scenario 3: 1-byte length field bypass Only 1 byte stores a password length; overflowing it makes the program believe the length is 4 while it is actually 260, bypassing the bounds check and overflowing the stack buffer (chain to ret2win). ### Scenario 4: Allocator rounding re-wrap (CVE-2025-54957) A custom bump allocator passes an initial overflow guard, then `total += (8 - total) % total` wraps for values near `SIZE_MAX`, returning a tiny chunk; the caller writes `payload_length` attacker bytes past it in a deterministic per-frame slab. ## Output Format ``` ## Integer Overflow Finding **Vulnerability**: Integer overflow/truncation -> undersized allocation -> heap overflow (CWE-190 / CWE-680) **Severity**: Critical (privilege escalation / RCE) — depends on adjacent target **Binary**: ./bundle_importer (parsing of count/elem_size) ### Root Cause uint32_t product = (uint32_t)(count * elem_size); // truncates size_t to_copy = count * elem_size; // full value used in copy Input count=2^32, elem_size=1 -> alloc=32, copy=4294967296. ### Trigger & Proof Sent 48 'A' + p32(1); overwrote session.is_admin (offset 48); privileged path executed (FLAG printed). ### Impact Heap buffer overflow corrupting an adjacent privilege flag -> authorization bypass. ### Recommendation 1. Use checked/saturating arithmetic (__builtin_mul_overflow, ckd_add) for all size math. 2. Validate count/size ranges before allocation; reject when product exceeds SIZE_MAX or a sane cap. 3. Use the SAME size value for allocation and copy; avoid narrowing casts in size paths. 4. Compile with -fsanitize=integer in CI and fuzz size/offset parsers. ```