Integer Truncation Exploitation: 64-to-32-Bit Narrowing, Size Check Bypass and Memory Corruption
Theory
Why This Matters
Integer truncation — assigning a wider type into a narrower variable — silently discards the upper bits and produces a value the programmer did not intend. CVE-2018-6789 (Exim integer truncation to heap overflow) allowed pre-auth remote code execution affecting millions of mail servers. NICE K0168 and T0028 both require recognising how type mismatches in size computations create exploitable gaps between a check and the subsequent use. On 64-bit systems, the most common pattern is using int or unsigned int for a length derived from a 64-bit strlen or recv, then using the truncated value as a copy or allocation size.
Core Concept
Truncation occurs when a value of type T is assigned to a narrower type U. The high bits that do not fit into U are discarded silently — no exception, no compiler warning at default settings.
size_t big = 0x100000041ULL; // 64-bit value: 4294967361
int small = (int)big; // truncated to 32-bit: 0x41 = 65
The canonical vulnerability pattern:
// user sends a 64-bit length field
uint64_t user_len = read_uint64(sock);
// programmer truncates to int, believing "large values will be negative and caught"
int len = (int)user_len; // user_len = 0x100000010 -> len = 0x10 = 16
if (len < 0 || len > MAX_BUF) { // check passes: 16 < MAX_BUF
return ERROR;
}
char *buf = malloc(len); // allocates 16 bytes
recv(sock, buf, user_len); // copies 0x100000010 bytes — OVERFLOW
The check on len passes because truncation made the value small. But recv uses the original 64-bit user_len as the byte count, so it overflows the 16-byte allocation.
int len = strlen(buf) for huge buffers: strlen returns size_t (64-bit on 64-bit platforms). If a buffer is longer than INT_MAX (2147483647 bytes), assigning the result to int wraps to a negative value. A subsequent check if (len > MAX) may still pass if MAX is an int and len is negative — because -1 < MAX is true. Then memcpy(dst, src, len) with len cast to size_t copies SIZE_MAX bytes.
Unsigned size check bypass by wrapping to small:
unsigned int count = user_input; // e.g. 0xFFFFFFFD
unsigned int total = count + 3; // wraps: 0xFFFFFFFD + 3 = 0x00000000
if (total > MAX_ITEMS) return; // 0 < MAX_ITEMS: check passes
char *arr = malloc(total * 4); // malloc(0) — returns non-NULL small buffer
// subsequent fill of `count` elements overflows
Technical Deep-Dive
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
// Vulnerable pattern: size_t -> int truncation
char *copy_user_data(const char *src) {
int len = strlen(src); // strlen returns size_t; truncated to int
char *dst = malloc(len + 1); // if len was negative due to truncation, malloc(negative+1) = malloc(0) or wraps
memcpy(dst, src, len);
dst[len] = ' ';
return dst;
}
// Safe: keep size_t throughout
char *copy_user_data_safe(const char *src) {
size_t len = strlen(src);
if (len >= SIZE_MAX - 1) return NULL;
char *dst = malloc(len + 1);
if (!dst) return NULL;
memcpy(dst, src, len);
dst[len] = ' ';
return dst;
}
Demonstrating the unsigned wrap-to-small bypass in Python:
import ctypes
# Simulate 32-bit unsigned arithmetic
count = ctypes.c_uint32(0xFFFFFFFD).value
total = ctypes.c_uint32(count + 3).value
print(f"count = {count:#x}, total = {total:#x}")
# count = 0xfffffffd, total = 0x0
# if total < MAX: check passes; malloc(0 * 4) = malloc(0) -- minimal allocation
# but memcpy uses count (0xFFFFFFFD) elements -- massive overflow
Identifying truncation in Ghidra decompiler output:
// Ghidra will show explicit casts in decompiled C:
iVar1 = (int)strlen(user_buf); // <- red flag: size_t cast to int
if (iVar1 < 0x100) { // signed check on truncated size
malloc((long)iVar1 + 1); // cast back to long -- original value gone
}
The cast (long)iVar1 after truncation does not recover the original 64-bit value — it sign-extends the 32-bit int, which may be negative if the original was > INT_MAX.
Reverse Engineering Methodology
- In Ghidra, enable the "Data Type Propagation" analysis pass. Look for
(int)casts applied to results of string or I/O functions (strlen,read,recv,fread) — these are truncation points. - Trace every user-controlled length value through type assignments. Any narrowing assignment (64-bit result into 32-bit variable) is a candidate.
- Check whether the narrowed value is used in both a conditional check and a copy/allocation. The check uses the truncated value; the copy may use the original.
- Search for mismatched types in function call sites:
recv(fd, buf, signed_int_var)passessigned_int_varassize_t— if negative, it becomes a very large count.
Common Reversing Errors
- Trusting the decompiler's type inference: Ghidra and IDA sometimes infer types incorrectly and show clean
size_tusage when the assembly is actually doing 32-bit arithmetic. Always cross-check the decompiled output against the actual instruction widths (DWORD PTRvsQWORD PTR). - Assuming the check variable and the copy variable are the same: a subtle bug has the check use
len(truncated) and the copy useuser_len(original). These may be in different stack slots despite looking similar in decompilation. - Missing the sign extension on function call: passing a negative
intas asize_targument sign-extends to a huge unsigned value. This is legal C but almost always a bug. Look for function signatures that takesize_tcalled withintlocals. - Overlooking intermediate arithmetic:
len / 2after truncation may still be positive while the original was huge; the division does not undo the overflow — it just shifts the exploitable range.
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.