The Rocky Road To Pwn - Part One
- Part One: Format Print Basic and Elementary Format Print Attack
- Part Two: Relocation(ASLR) and Data Manipulation With Format Print
- Part Three: A real CTF challenge
Welcome to the first part of the Pwn trilogy. Our journey begins with exploring the glibc implementation of the format print function printf from the C standard library. This will pave the way for direct and indirect memory reading, which is instrumental for information leaking. In the upcoming parts of this trilogy, we will delve deeper into the format print function, demonstrate how to manipulate data with format printing and explain how it facilitates remote code execution (RCE). Additionally, we will discuss a system-level countermeasure to binary exploitation (Pwn) and how to bypass it. Finally, we will demonstrate the application of these skills in a real CTF challenge.
Format print basic
In C programming, the format print function is a crucial tool for output operations, enabling developers to present information to users in a structured and readable format. Its versatility allows for printing various data types, making it an indispensable asset for debugging, user interaction, and data presentation in software development. However, this power retains pricy vulnerabilities, particularly in format string attacks. These vulnerabilities, if not understood and addressed, can be exploited to compromise the security of the software.
To understand how the format string attack works, we can start by looking at the position-parameterized placeholder arguments, which are in the form of %m$p, where m represents the position parameter of the placeholder and p represents the actual conversion specifier. To further explore the relationship between placeholders and the arguments of the format print function, let’s consider an example: printf("%d %c %x", 1, 121, 65);.
1 | [skid.t@BlackArch workspace]$ cat poc.c |
In the example mentioned, the print function displays 1 y 41 using the format string %d %c %x. Here, %d corresponds to the first argument after the format string, 1. %c is for the second argument, which has the 121—the ASCII code for the letter y. %x refers to the third argument with the value 65, which is 0x41 in hexadecimal.
1 | [skid.t@BlackArch workspace]$ cat poc.c |
Let’s update the format string using positional parameters. Now, the %2$d in the format string corresponds to the second argument after the format string, with a value of 121. The %3$c corresponds to the third argument with a value of 65, the ASCII code for A; the same goes for the last one.
Even though the program’s output aligns with the position parameters of each placeholder in the format string, we still need to determine how the format print function handles the position-parameterized placeholders. There are two hypotheses: it either rearranges the arguments during compile time or reads them out of order during runtime. To reveal the secret, we can run the program with a debugger and set a breakpoint before the format print function is called. The debugger helps us gain a lower-system view of how things work.
1 | Breakpoint 1, 0x000055555555515b in main () at poc.c:3 |
We run and debug the program on a Linux-based x86_64 system. In the debugging session, we notice that the RDI register contains the pointer to the format string, the RSI register contains the first argument after the format string, the RDX register contains the second argument, and the RCX register contains the third argument. This adheres to the Linux x86_64 userspace function calling convention. As a result, the format print function resolves out-of-order placeholders during runtime since the arguments are not rearranged.
We also dereference the format string pointer in the debugger because the value of RDI is just a pointer, and the address referred to by the pointer is the actual beginning of the format string. Consider the s conversion specifier from the format print function manual. If we arrange a placeholder with the s conversion specifier targeting the RDI register, the format print function will print the format string itself. For a program under a debugger, we could simply alter the value in the RSI register, making it identical to the RDI register. This way, we could use a simple format string with %s as its first placeholder to print the format string itself. We won’t provide an example here but will discuss more in the indirect reading section of the format print attack.
Format print attack
The format print attack(aka. format string attack) is a classical memory corruption attack without typical out-of-bounds buffer overflowing. It comes with the variadic function design of the format print function printf in the C standard library. The format print function takes its first argument as a format string containing placeholders and replaces them with the actual values during runtime. Thanks to the out-of-order placeholder resolution and built-in debug functionalities, user-controllable format strings provide an exploitable vulnerability for arbitrary memory dump or corruption.
The format print function always requires a format string as its first argument. Controlling the format string by a legitimate user or a malicious attacker can lead to a format print attack. We have categorized format print attack techniques into three categories: direct memory reading, indirect memory reading, and indirect memory writing. These categories are based on the behaviour of the format print function. Direct memory reading can read at most eight octlets in existing stack frames, while indirect memory reading and writing theoretically enable access to arbitrary addresses. At the end of the last section, we utilize the s conversion specifier to echo the format string. The indirect memory reading takes a similar idea. But before we dive into the indirect memory reading, let’s start with the direct memory reading.
Direct memory reading
Direct memory reading is a technique that allows an external entity to exploit the format print function. If the external entity controls the format string provided to the format print function, it can provide a format string that contains too many or incorrectly positioned placeholders. In this case, the function attempts to convert the values in the corresponding memory location to a printable value, replacing the placeholder.
Most operating systems and architectures allow arguments to be passed on the stack using the procedure calling convention. Therefore, an external entity could create a specific format string that manipulates the format print function. The function would interpret an address on the stack as an argument associated with a placeholder in the format string. This would result in the function trying to convert the values in the corresponding memory location to a printable value, replacing the placeholder.
Since procedure call parameters on the stack can only appear on the caller’s stack frame, direct memory reading can only be applied to addresses above the current stack pointer. Additionally, direct memory reading usually extends beyond the current stack frame, covering most existing stack frames. The following example provides a demonstration of direct memory reading.
1 | [skid.t@BlackArch workspace]$ cat poc.c |
In this example, the program creates a stack variable called flag and sets its value to my secret flag. It then uses the format print function with a long format string containing placeholders but no corresponding arguments. When the program is executed, it prints six hexadecimal numbers, each corresponding to a placeholder in the format string.
For example, let’s take 0000000000000000, which corresponds to the placeholder %6$016llX. Here, 6 indicates that this placeholder takes the value of the sixth argument after the format string from the format print function. 016 is the field width specifier, indicating that the resulting hexadecimal number contains 16 characters with leading zeros. Lastly, llX specifies that the argument is a long long integer presented in hexadecimal. On a Linux-based x86_64 system, long long integers are 64 bits long.
Recall the Linux x86_64 userspace function calling convention; the RDI register is for the first argument in a function call. In the case of the format print function, it’s always a pointer to the format string. The first six arguments are passed by registers, and then arguments are passed on the stack. Given that the sixth argument after the format string is the seventh argument in a format print function call, %6$016llX decodes the first stack argument and displays it as a hexadecimal number.
According to the calling convention, the first stack argument is the 8-byte value pointed to by the stack pointer before the function call. It’s also the adjacent 8-byte value to the return address of the callee function’s stack frame. Therefore, the long format string prints six subsequent 8-byte values from the caller function’s stack frame in ascending order. We can verify this by examining the stack frame in the debugger.
1 | Breakpoint 1, 0x000055555555518b in main () at poc.c:5 |
When using the Gnu debugger (GDB), we can use a similar format specifier to the memory dump command. This command displays the six consecutive octlets from the stack pointer (RSP) in hexadecimal format. The content shown by the debugger is the same as the values from the format print function.
We must consider endianness to further decode the hexadecimal number into byte sequences. On x86_64 machines, integers are stored in little-endian. Therefore, to reveal the 8-byte sequence represented by 0x7263657320796d40, we split the integer into bytes and reverse it: 0x40 0x6d 0x79 0x20 0x73 0x65 0x63 0x72. With the debugger, we don’t need to do this manually, as there’s another format specifier for the memory dump command that displays the memory chunk byte-by-byte.
1 | (gdb) x/48bx $rsp |
Remember that the flag variable is located on the stack, specifically within the stack frame of the main function. Consequently, the program may have inadvertently exposed the flag through the format print function. Given this, it’s crucial to analyze the stack dump for the flag.
Stack dump interpretation
The example program dumped a memory chunk from its runtime stack. We must analyze the memory dump to check if it contains the flag string. To understand the structure of the stack frame, we can use static analysis by disassembling the main function to gain more insights.
1 | (gdb) disassemble main |
The disassembled main function reveals that stack variables use the base pointer (RBP) relative addressing. This is common on x86_64 machines because the base pointer (RBP) is more stable than the stack pointer (RSP) during a procedure call. We will delve into this in more detail later. The disassembled instructions indicate that the flag text is stored on the stack at RBP-23 (0x17), occupying a 15-byte space. Following that is the stack canary at RBP-8, which occupies 8 bytes. Then, the saved base pointer value from the previous stack frame, an octlet, follows the stack canary. Finally, the 8-byte return address is at the bottom of the current stack frame.
In the debugging session, it was discovered that the stack pointer (RSP) is located at 0x7ffffffe2d0. With this information, we can reconstruct the structure of the stack frame.
1 | Stack Pointer(RSP): ┌───────────────────────┐ |
The stack pointer (RSP) is 32 bytes (0x20 in hexadecimal) from the base pointer (RBP). The flag variable is located at 0x7fffffffe2d9. Following that, the stack canary is at 0x7fffffffe2e8, the value of the saved RBP pointed to by the base pointer is at 0x7fffffffe2f0, and the return address is at 0x7fffffffe2f8.
We have the addresses and offsets of each variable. With the stack dump, we can easily reconstruct the flag string my secret flag from the leaked byte sequence: 0x6d 0x79 0x20 0x73 0x65 0x63 0x72 0x65 0x74 0x20 0x66 0x6c 0x61 0x67 0x00. These values come from the second and third hexadecimal numbers printed by the program.
In real-world scenarios, the flag string may contain sensitive data, such as user credentials, that should not be displayed. Furthermore, a malicious actor may provide a format string instead of a hardcoded one. The provided format string typically contains too many or incorrectly positioned placeholders, which can result in data leakage. As the format print function reads the corresponding data and directly prints it without pointer dereferences, the attack technique goes by direct memory reading.
Exploring direct memory reading is a valuable path to delve into binary exploration. In this section, we extracted a secret flag from the victim process’s stack frame. As mentioned, the direct memory reading technique only works for addresses higher than the current stack pointer (RSP). To go beyond this, we must introduce the indirect memory reading technique. Additionally, it’s important to practice reading disassembled functions and reconstructing data structures from memory dumps.
Indirect memory reading
Indirect memory reading enhances memory dumping by targeting arbitrary addresses. Unlike direct memory reading, which reads the value on the stack and prints it directly, indirect memory reading fetches the value on the stack, dereferences it as if it is a pointer, and prints out data at the designated address.
In a previous example, we modified the RSI register using a debugger and printed the format string. The concept of indirect memory reading is similar but involves using values on the stack instead of modifying register values. In this case, the RSI register is linked to a placeholder with the s conversion specifier. The s conversion specifier treats the associated argument as a string pointer, dereferences it, and prints both printable and non-printable bytes until a null byte is encountered. Once again, we’ll use the following example to better understand the s conversion specifier at a lower-system level.
1 | [skid.t@BlackArch workspace]$ cat poc.c |
In this example, the string “motto” is stored as a global static constant outside the runtime call stack. When the printf function attempts to print the content of the “motto” string, it needs to access the provided pointer that contains the string’s address. Additionally, the source code is compiled with GCC built-in functions disabled. This means that built-in function optimizations will not replace printf with puts even if dynamic formatting at runtime is unnecessary. As with previous cases, it is essential to disassemble the main function with objdump to view its underlying details at a lower level.
1 | [skid.t@BlackArch workspace]$ objdump --disassemble=main ./poc |
The main function has a short paragraph of instructions that the disassembler created. These instructions load the addresses of the “motto” string and the format string into parameter correspondent registers before calling the printf procedure. The addresses of both strings are not hardcoded as specific values, but instead, they are an offset towards the instruction pointer register (RIP). This happens because the compiler uses position-independent code (PIC), which enables program segments to be relocated into arbitrary memory chunks instead of a predefined specific address, as requested by the address space layout randomization (ASLR). ASLR is a countermeasure against adversaries who aim to gain control over the program flow.
Although the disassembler has already commented on the designated position of the corresponding addresses of the strings, it’s still interesting to understand how those numbers come out. If you add 0xecc directly to the address of the lea instruction 0x113d, you may encounter a weird number 0x2009, 7 bytes ahead of the assembler-suggested address. This happens because the address arithmetic occurs after the fetch stage of the CPU instruction cycle. Therefore, the correct RIP value for the arithmetic is the address of the next instruction, 0x1144, which leads to the correct address, 0x2010 or 8208 in decimal. Scrutinizing the binary with the corresponding offset in hexadecimal format or attaching a debugger to inspect the actual layout of a loaded executable in the real system can help verify the address.
1 | Breakpoint 1, 0x0000555555555156 in main () at poc.c:6 |
The program runs on a Linux-based x86_64 system with ASLR enabled. ASLR randomizes the memory location of the program’s sections, including the main function and the lea instructions. After relocation, the main function is moved to 0x555555555139, and the two lea instructions are relocated to 0x55555555513d and 0x555555555147, respectively. Despite the randomization, the relative offsets between the instructions, the motto, and the format strings remain consistent. We confirmed this consistency using the debugger’s built-in arithmetic system.
The s conversion specifier in printf treats the specified argument as a pointer to a character string (char *). It dereferences the pointer and then replaces the corresponding placeholder with the binary content of the character string. The binary content of the character string consists of printable and non-printable bytes until a null byte is encountered. Although limiting the number of bytes to print in the resulting string is possible, there’s no straightforward way to encode the byte stream into decimal or hexadecimal format. The concept of indirect memory reading combines the s conversion specifier and position parameters to read data from an arbitrary address.
Understanding how the %s placeholder works at a lower level makes it easier to comprehend position-parameterized placeholders. Position parameterized placeholders in externally controlled format strings are crucial for indirect memory reading. To better understand this concept, consider the following C example: it sets up a scenario where the program prints the address of a global static constant named secret_motto, reads an externally controlled format string from the standard input, and processes it. This example is a valuable practice for the techniques we have just learned.
1 |
|
Unlike previous examples, the program prints the address of the motto instead of loading it onto the stack frame. Also, because the first printf is far from the second printf and there are multiple procedure calls in between, the preservation of arguments passed by registers is not guaranteed. However, the user input buffer resides on the call stack, allowing us to inject the motto’s address into the stack at runtime. This makes it possible to perform an indirect memory reading with a position-parameterized placeholder. We can begin by disassembling the main function and reconstructing the stack frame.
1 | [skid.t@BlackArch workspace]$ gcc -O0 -g poc.c -o poc |
The main function in assembly is more complex than in previous examples, but we can defer most details for analysis purposes. The user input buffer is located on the stack and is directly pointed to by the stack pointer RSP. One notable aspect of the disassembly is the significant increase in the stack frame, which is indicated by the considerable decrement of the stack pointer RSP in the middle of the function. This happens because alloca allocates the buffer for user input within the current stack frame, while malloc allocates memory from the heap. The delayed allocation of the input buffer helps to align the start of the buffer with the stack pointer RSP.
The user input buffer begins at the stack pointer RSP, and the program reveals the address of the motto. This makes it easier to create a format string payload that prints the secret motto. Since the program uses fgets to read user input, we can inject raw bytes into the buffer without worrying about null and escape characters. To exploit this, a simple approach for the format string is to encode the motto’s address into a little-endian octlet and append it with the suffix %6$s. Theoretically, the format print function should display the little-endian octlet in raw binary and then print the motto. The suffix %6$s instructs the format print function to dereference the first octlet on the stack and print the string at that address. However, we don’t live in a perfect world.
The fgets function does not distinguish null bytes and places all input content into the stack buffer. However, the format print function only processes part of the format string. Precisely, it only echoes the first six bytes of the little-endian octlet. After carefully reading the manual of the format print function, we found it reasonable, as the function treats the format string as a null-terminating string. Therefore, the format print function only processes the portion of the format until the first null byte is present. Based on this, we must adjust the format string payload so that the placeholder suffix goes before the little-endian octlet.
We switched the position of the placeholder suffix and the little-endian octlet to prevent truncation. Remember that the positional parameter in a placeholder specifies the procedure call argument for the format print function. The x86_64 calling convention requires all stack arguments to be octlets. Therefore, the little-endian octlet must be aligned to the stack pointer (RSP) by eight. We must pad the placeholder with arbitrary non-null characters to meet the alignment requirement. We choose to use the space character for padding. Additionally, we need to update the position parameter from 6 to 7 in the format placeholder, as the little-endian octlet is now one octlet away from the stack pointer (RSP). In the end, we have %7$s followed by the raw bytes of the address of the motto string.
One crucial detail remains: the format string payload depends on the randomly generated address of the motto, which occurs and leeks during runtime. As a result, we cannot prepare the payload in advance. Since the payload contains non-printable bytes, we cannot simply enter it like a regular command line program. To address this issue, we used a bash trick. We redirected the standard input from the current console to a named pipe called payload, then input the assembled format string payload into the named pipe file. Additionally, we utilized the bash built-in command printf to construct the payload, and we had to include an extra escape sequence % to prevent interpretation errors.
1 | [skid.t@BlackArch workspace]$ mkfifo payload |
Here, we have a program that prints a motto. In this example, we have exploited a vulnerable program to reveal a global static string that should have been kept secret. The exploitation involves using the indirect memory reading technique and some bash tricks. The indirect memory reading technique does not require attaching a debugger to the vulnerable program or altering its register value. Instead, it loads the target address on the stack and associates it with the s conversion specifier using a positional parameter. The indirect memory reading approach offers a valuable opportunity to access specific memory areas within a computer system.
This is the part one of the Pwn trilogy. Here, we cover the basics of the format print function, from an overview to a lower-level system aspect. We also discuss the format string attack, focusing on two detailed approaches: direct and indirect memory reading, both on how to leak information. Part two will explore how to alter existing data and take over the victim program’s control flow, leading to remote code execution (RCE). Additionally, we will examine countermeasures against modern binary exploitation and how to bypass them. Finally, part three will involve practically applying all the techniques in an actual Capture The Flag (CTF) challenge. We hope you enjoy them.