This report can be read both on this site, and as its original report form. It is highly recommended that you read the original report form instead because it is better formatted.
The utilization of the insecure gets() function resulted in a buffer overflow vulnerability. This security hole was exploited to execute arbitrary code despite the enabled NX bit via a return-to-libc attack. The deprecated gets() function should be replaced with the more secure fgets() alternative to prevent the attack mentioned in this report. It is highly suggested that the process running on port 31568 be terminated as soon as possible until the remediations outlined in the Conclusion section are followed.
Attack Narrative
The destination and port on which this binary is running were given:
Destination
Port
mc.ax
31568
Additionally, the source code and libc file used by the binary were provided.
Binary Analysis
Source Code
At the top of the file, 32 bytes were allocated to the your_reassuring_and_comforting_we_will_arrive_safely_in_libc buffer (for the sake of shortening this name, this buffer is called input_buffer throughout this report). After initializing input_buffer, a series of three setbuf() calls are run, a function which controls the way a stream is buffered. Additionally, this function can control the size of the buffer, however due to the fact that the buffer argument is NULL, the stream is unbuffered[1].
Following the setbuf() calls is a succession of three puts() calls before the insecure gets() function is run with input_buffer as the argument. After providing an input, the printf() function is called which prints out the address of printf in memory.
Behavior
Looking at the security of the binary with the checksec command, it is found that the NX bit of the binary is enabled:
Note that this binary is 64-bit in little endianness
Therefore, the RIP cannot simply be overwritten to point to shellcode. However, the instruction pointer can easily be overwritten due to the lack of a stack canary. Furthermore, there is no PIE (Position Independent Executables) which means that the libc base can be calculated by finding the offset of the executables (this is examined in detail in the Finding Base Libc Address section). This element is essential to the success of return-to-libc attacks.
When executing the binary, user input can be provided after the “safely?” string:
Only after providing an input, the binary printed out the address of printf(). However, this address changes with each new execution of the binary:
The printf() address is shown in red
This change is evidence of the existence of ASLR (address space layout randomization), which is a security feature to help prevent memory corruption vulnerabilities. Therefore, the base address of libc cannot be easily calculated by subtracting the address of printf in the previous execution of the binary by the printf symbol in the given libc file.
Exploit Construction
GDB
Thus, to correctly calculate the libc base address, it is essential to overwrite RIP to point to main() so that the binary allows us to input a second payload (this time with the knowledge of the printf address). First, the offset of RIP must be calculated:
The overall exploit can be summarized into two waves: wave one consists of repeating the main function and retrieving the printf address, and wave two consists of calling the libc system() function with /bin/sh as the argument.
PwnTools
Rerunning main()
Using pwntools[2], a python library made for facilitating the process of writing binary exploits, we can create a program (which was named poc.py) to exploit the program:
The code shown above first starts a local process for the binary. Afterwards, the payload is sent and an interactive instance is called to the process:
The main function was successfully called again. Now the printf function address can be retrieved to find the base libc address for the second wave of the exploit.
Finding Base Libc Address
With the knowledge of where the printf function is in memory, the base address of libc can be calculated, and therefore the address of system() and location of the /bin/sh string can be determined by adding their offsets to the base libc address:
Observe the following addresses denoted in red when executing poc.py:
The exact address of system() could be calculated due to the known offset of this function being 0x449c0 (which is 0x7fe5b91ec150 - 0x7fe5b91a7790). Note that this works because PIE is disabled.
Building system(“/bin/sh”)
After discovering the addresses of the system() function and /bin/sh string, it follows that the system(“/bin/sh”) call must be built and executed using the aforementioned addresses. To do so, it is important to be able to control the RDI register which is used to pass parameters into functions. The RDI, RSI, RDX, and RCX registers are all used for that purpose, but they function via a hierarchical basis, in which the parameter passed into a function follows that particular order[3]:
parameter1 corresponds to RDI, parameter2 corresponds to RSI, and so on.
Therefore, to pass /bin/sh to system(), it is important to pop the RDI register and pass in the desired parameter value. Using the ROPgadget –binary ret2the-unknown command, ROP gadgets that perform the desired pop operation can be found with their respective locations in memory:
0x00000000004012a3 : pop rdi ; ret
Exploit
Using this gadget, the system() call will contain /bin/sh as its argument, and a shell will be returned:
Wave 1
Running the exploit results in the successful return of a shell:
Conclusion
The insecure gets() function should never be used due to its lack of boundary checks on user input. This can result in the overwriting of memory that can lead to arbitrary code execution. ASLR and enabling the NX bit are not adequate in the prevention of binary exploitation (however they do help mitigate vulnerabilities). The following remediations should be strongly considered to prevent the attack outlined in this report:
Replace the gets() function with fgets()
The latter performs boundary checks on user input which mitigates buffer overflow attacks
Implement PIE
Return-to-libc attacks worked by calculating the addresses of the system function and base libc address based on their known offers
By enabling PIE, the known offsets between executables cannot be predicted as they change with each new runtime process
Implement a stack canary
The stack canary will mitigate buffer overflow attacks by protecting the return pointer
Port 31568 should be closed immediately until the current binary is replaced with a more secure version that follows the aforementioned remediations.
scanf() is a function that is widely used in C programs. This binary, which is seemingly secure, made subtle but dangerous programming mistakes that resulted in a security hole through which a user can manipulate memory. Since this binary is dynamically linked, overwriting the GOT entry subsequently forces the program to jump to memory of the attacker’s choice when the manipulated function pointer gets called.
This box is a great introduction to the exploitation of a web server. It involves exploiting a web service through an LFI vulnerability and upgrading that to an RCE exploit via log poisoning. The method of escalating to root privileges is also instructive.
This challenge was about binary exploitation. There were a total of nine binaries which increased in difficulty after each exploit. Common binary exploitation techniques are discussed in this report including ret2libc, shellcode injection, format string exploitation, among others.