Exploring stack memory with printfs
- Michele Iarossi
- May 21, 2023
- 4 min read
Updated: Apr 25, 2024
Understanding program execution and memory management is essential for software developers and security professionals. One crucial area of interest is the stack memory, which plays a fundamental role in function calls, local variable storage, and control flow. In this blog post, we will explore how printf() statements can be used to investigate the stack memory.
Stack memory
Before diving into how print statements can be leveraged for exploring stack memory with printfs, let's review the basics of stack memory. The stack is a region of memory that grows and shrinks as functions are called and return. It is organised in a last-in-first-out (LIFO) manner and contains stack frames, each associated with a function call. Stack frames, also called procedure frames or activation records, consist of local variables, function arguments, return addresses, and other bookkeeping information.
Using printfs for viewing stack memory
Print statements are commonly used for debugging and tracing program execution. By strategically placing print statements in your code, you can output specific values during runtime, but they can also introduce security vulnerabilities if used improperly, allowing an attacker to inspect stack-related information such as the content of local variables.
So called format string vulnerabilities occur when the format string argument in a printf() statement is controlled by an attacker. If an attacker can manipulate the format string, they can read or even modify arbitrary memory locations, potentially leading to remote code execution or data corruption.
Even though such vulnerabilities are nowadays well known, i.e. attempts to write memory locations via printf() should be intercepted by the operating system, dumping the content of the stack is still possible by exploiting printf() undefined behaviour when there are insufficient arguments for the provided format string.
Consider the following trivial test() function:
/* vulnerable function */
void test(const char* f, int a, int b, int c, int d) {
char *format;
char password[] = "secure_password";
format = (char *)malloc(strlen(f)+1);
if ( format && f )
strcpy(format,f);
else {
/* error */
printf("\nInvalid input arguments!\n");
exit(1);
}
/* format string is not sanitised! */
printf(format,a,b,c,d);
}
where test() is called in a main() function like this:
int main(int argc, const char * argv[]) {
/* call test function */
test(argv[1],4,5,6,7);
return 0;
}
The test() function stores a secure password in its local variable password and accepts from the command line a format string for the printf() call without sanitising it.
If an attacker provides this input as the desired format:
0x%08x|0x%08x|0x%08x|0x%08x|
0x%016llx|0x%016llx|0x%016llx|0x%016llx|0x%016llx|
0x%016llx|0x%016llx|0x%016llx|0x%016llx|0x%016llx|0x%016llx|
then the following stack content is revealed:
0x00000004|0x00000005|0x00000006|0x00000007|
0x00007ff850bff940|
0x0000600003000090|
0x0000000600000007|
0x0000000400000005|
0x00007ff7bfeff708|
0x705f657275636573|
0x0064726f77737361|
0x00007ff7bfeff0b0|
0x9f5776b2e2e700e4|
0x00007ff7bfeff1b0|
0x0000000100003dc5|
Program ended with exit code: 0
How does this work? The point here is that the output function printf() uses an internal variable to identify the location of the next argument. At the beginning, this pointer refers to the first argument (the variable a having value 4). Each 0x%08x in the format string reads a value it interprets as an int from the location identified by the argument pointer. The first four integers correspond to the four arguments to the printf() function, i.e. the values 4, 5, 6, and 7.
Then what happens next is that printf() displays the stack frame for the currently executing function (including the return address and arguments for the currently executing function). Each 0x%016llx causes printf() to move sequentially through the stack frame. By using this technique, it is possible to expose the content of the stack memory. An attacker can use this data to determine offsets and other information about the program to further exploit this or other vulnerabilities.
We can help us decode the information above by printing also the addresses of the local format variable and of the main() function:
main = 0x100003d20
format = 0x600003000090
We see that the value
0x0000600003000090|
corresponds to the address of the input format string stored in format. Next we find the test() input arguments 4, 5, 6, and 7:
0x0000000600000007|
0x0000000400000005|
We can read out the content of the password local variable by converting the following hexadecimal values to the corresponding ASCII characters:
0x705f657275636573| -> p_eruces
0x0064726f77737361| -> drowssa
Finally, by comparing the main() function address:
main = 0x100003d20
with the last value:
0x0000000100003dc5|
we can deduct that this last value represents the return address to the main() function() from the test() function.
Mitigating printf-related security flaws:
To minimise the security risks associated with printf() statements, consider the following best practices:
Validate and sanitise input: Ensure that user-controlled input passed to printf() is properly validated and sanitised to prevent format string vulnerabilities and unexpected behaviour.
Avoid printing sensitive information: Take caution when printing sensitive data. Ensure that debugging statements containing sensitive information are removed or properly secured in production environments.
Use secure alternatives: Consider using secure logging mechanisms or libraries specifically designed for logging, which provide better control over what information is logged and where it is stored.
Limit debugging code: Remove or disable debugging code that utilises printf() statements before deploying the application to production, as it can introduce unnecessary risks.
Employ static code analysis and security tools: Regularly scan your codebase using static code analysis tools or security scanners to identify potential printf-related vulnerabilities and apply appropriate fixes.
Conclusion
While printf statements are valuable for debugging and tracing program execution, it is crucial to be aware of the security risks they introduce. By following best practices, carefully validating input, and considering security implications, you can minimise the potential vulnerabilities associated with printf statements and ensure the robustness and security of your software.
Finally, the following reference has been consulted for creating this post:
Comments