Local, automatic variables can assume unexpected values if they are used before they are initialized. C99 specifies "If an object that has automatic storage duration is not initialized explicitly, its value is indeterminate" [[ISO/IEC 9899-1999]]. In practice, this value defaults to whichever values are currently stored in stack memory. While uninitialized memory often contains zero, this is not guaranteed. Consequently, uninitialized memory can cause a program to behave in an unpredictable or unplanned manner and may provide an avenue for attack. Most compilers warn about uninitialized variables, but these can be ignored by the programmer.
Non-Compliant Code Example
In this example, two functions are called consecutively. The first function, func1(...)
, is passed an integer entered by a user. That integer is stored in variable i
for the duration of the function. The second function, func2()
, declares a local integer variable j
. j
is not initialized before being checked against a constant value, CONDITION_CHECK
. Because j
is uninitialized, it assumes whatever value is at that location in the stack, in this case the value of i
from func1()
. As a result, if the user entered 42, the condition statement if (j == CONDITION_CHECK)
succeeds.
#define CONDITION_CHECK 42 void func1(int arg) { int i = arg; } void func2(void) { int j; if (j == CONDITION_CHECK) puts("Condition passed!!\n"); else puts("ERROR: Condition failed\n"); } ... func1(i); /* the value of i originates from an untrusted source */ func2(); ...
Compliant Solution
The local, automatic variable j
should be initialized to a default value.
#define CONDITION_CHECK 42 void func1(int arg) { int i = arg; } void func2(void) { int j = 0; /* initialize j to 0 */ if (j == CONDITION_CHECK) puts("Condition passed!!\n"); else puts("ERROR: Condition failed\n"); } ... func1(i); /* the value of i originates from an untrusted source */ func2(); ...
Non-Compliant Code Example
In this example derived from mercy, the programmer mistakenly fails to set the local variable mesg
to the msg
argument in the log_error
function. When the sprintf()
call dereferences the mesg
pointer, it actually dereferences the address that was supplied in the username
buffer, which in this case is the address of "password". The sprintf()
call copies all of the data supplied in "password" until a NULL byte is reached. Because the "password" buffer is larger than buffer
, a buffer overflow occurs.
int do_auth(void) { char username[MAX_USER], password[MAX_PASS]; puts("Please enter your username: "); fgets(username, MAX_USER, stdin); puts("Please enter your password: "); fgets(password, MAX_PASS, stdin); if (!strcmp(username, "user") && !strcmp(password, "password")) { return 0; } return -1; } void log_error(char *msg) { char *err, *mesg; char buffer[24]; sprintf(buffer, "Error: %s", mesg); printf("%s\n", buffer); } int main(void) { if (do_auth() == -1) { log_error(ERR_CRITIC | ERR_AUTH, "Unable to login"); } return 0; }
Compliant Solution
In the compliant solution, mesg
is initialized to msg
as shown below.
void log_error(char *msg) { char *mesg = msg; char buffer[24]; sprintf(buffer, "Error: %s", mesg); printf("%s\n", buffer); }
This solution is compliant provided that the null-terminated byte string referenced by msg
is 17 bytes or less, including the null terminator. A much simpler, less error prone, and better performing solution is shown below:
void log_error(char *msg) { printf("Error: %s\n", msg); } ... log_error("Unable to login"); ...
Priority: P2 Level: L3
Referencing uninitialized variables are relatively unlikely to result in an exploitable vulnerability because most compilers provide warnings when an uninitialized variable is referenced and most programmers take these warnings seriously.
Component |
Value |
---|---|
Severity |
1 (low) |
Likelihood |
1 (unlikely) |
Remediation cost |
2 (medium) |
References
- mercy
- ISO/IEC 9899-1999 Section 6.7.8 Initialization