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.
Instrucciones de origen · Vista previa de solo lectura
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.
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 /UBSan
() 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. ). The exploit is confirmed when the undersized
buffer's neighbor (e.g. flag, chunk metadata) is overwritten — verify the target field changed and the
gated/privileged path executes.
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
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 loopprint(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.
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.
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).
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.