It’s been some years that operating systems have incorporated different protective techniques to defend against overflow attacks, and these techniques are usually very effective but they clearly cannot fully stop such attacks. In this post I target a vulnerable program, which suffers a memory leakage and this is exactly where leads to our successful exploitation and compromising the system.
One of the main common features among all classic overflow attacks is the ability of the attackers to guess the address of the stack frame or shared libraries’ functions before delivering a shellcode to a vulnerable application. After introduction of DEP (Data Execution Prevention) mechanism in operating systems and specifically adding the hardware support for such defensive measure, attackers could no longer load the shellcode in the stack and force the CPU to jump to it and then run it. Because in new computers, stack is normally (unless specified otherwise at compile time) loaded in Non-Executable pages, even if there is some valid instructions in stack and the execution flow gets redirected to it, the program stops in pursuit of addressing a general protection fault fired by the processor. Stack is simply no longer executable. Since hackers’ job is to find new attack scenarios after the previous one is deprecated, they devised another way to take over a vulnerable program using stack overflow bugs. Instead of injecting the shellcode into the stack, they used another form of redirection. This time instead of jumping onto the stack they force the processor to run one of the valid libc functions in the shared object file. These kinds of attacks were generally called return-to-libc (or return-to-kernel-text in kernel level attacks). This was a good progress in the parlance of exploitation, but soon got foiled after operating system designers started to add a new technology to their products called “Address Space Layout Randomization (ASLR)”. What hackers needed to have in return-to-libc attacks was the addresses of the desired locations in executable pages. Programs historically were used to run in similar addresses which didn’t change by re-running or even running them on a slightly different version of an OS. return-to-libc attacks could not be carried out if the attackers are not aware of the specific addresses needed. What ASLR does is to randomize both base address of a program image in memory and base addresses of dynamic objects that are loaded into the address space of each program. This made exploitation very difficult since without knowing the addresses, an intruder in most occasions seemed to not be able to do anything more useful than crashing the program by manipulating the stack frame. Despite other forms of dangers related to buffer overflow attacks – like changing local function variables – remained the same, arbitrary code execution effectively got extremely harder. Hackers – following their innate job – proposed many new ideas on how to reach code execution using overflow attacks with ASLR in place. First of all, from a theoretical perspective, we should inspect how exactly the feature is implemented in operating systems. If you consider a 48-bit address of a dynamic function in memory (read my previous posts if you don’t know why 48 bits) from what I’ve experienced with working and reversing these functions, you should expect the 32 middle bits of the address gets randomized each time the program starts.
Bruteforcing a 32 bit value is not hard but when it comes to finding virtual addresses for these attacks, the problem is that with each wrong guess you usually lose the program after a sudden crash. Even if the program restarts to the previous point, it receives new addresses thus rendering your bruteforce useless. That said, there are still situations that you can traverse through different addresses to find the correct one. We don’t wanna cover them here. Further, some vulnerabilities have been reported in behind the scene logic of “creating random addresses” or in other words in the randomness strength of ASLR algorithms. As an example you may be able to guess one program base address if you have a prior knowledge of virtual addresses in another program. Forgetting about all potential weaknesses regarding how ASLR works, let’s suppose that we cannot guess any address in a vulnerable program. Now what? If the program happens to inadvertently expose one of its variables or functions addresses, can we infer from that, address of another part of the program memory? Well it depends. Base address of dynamic objects and the base address of of the program image itself are randomized independently. So if we receive a virtual address from one known part of dynamic portion of the program for example, we can accurately calculate other parts of the dynamic object.
Case Study: A vulnerable program
I’ve prepared a program that has the problem of leaking its memory, while it has also a few overflow bugs in one of its functions. Since I didn’t have time to build a real commercial software to simulate the actual leakage bug in that, we suppose that this program somewhere in the course of its execution leaks the address of printf function. And I’ll show you, how we hack into the host computer just by knowing that very single address. You can see portions of our vulnerable function “receive_data” in the picture below.
As you can see there’s a predefined buffer of size 512 bytes in function. If we wanna overflow the function remotely we need to tamper with this variable in the stack frame. In two lines below that, there’s a call to scanf function that reads a name from the user. Let’s mix Local and Remote attacks together here. (Again the aim is to demonstrate an ASLR bypass when memory leakage occurs so I don’t take a huge time to turn this into a pure remote code execution attack). If we can access the buf_size variable through this scanf function, we’ll be able to force the recv function to read more than dictated in the program code from the remote party. So let’s start from there. We disassemble the binary and look at the respective machine instructions:
0x0000000000000ca1 <+326>: movl $0x200,-0x14(%rbp) <-- buf_size is here 0x0000000000000ca8 <+333>: lea 0x17a(%rip),%rdi # 0xe29 0x0000000000000caf <+340>: callq 0x920 <puts@plt> 0x0000000000000cb4 <+345>: lea -0x2da(%rbp),%rax <-- name is here 0x0000000000000cbb <+352>: mov %rax,%rsi 0x0000000000000cbe <+355>: lea 0x16a(%rip),%rdi # 0xe2f 0x0000000000000cc5 <+362>: mov $0x0,%eax 0x0000000000000cca <+367>: callq 0x9e0 <__isoc99_scanf@plt>
buf_size is stored 20 bytes before the function base pointer. Also, the “name” variable fortunately is stored in lower addresses. So we can reach buf_size by overwriting the name. Name is defined to be 10 bytes wide only. Up to this point we could understand how we can change the size argument of “recv” function. Now the question is how much exactly the size should be? Let’s continue our binary inspection:
0x0000000000000ccf <+372>: mov -0x14(%rbp),%eax 0x0000000000000cd2 <+375>: mov %eax,%esi 0x0000000000000cd4 <+377>: lea 0x157(%rip),%rdi # 0xe32 0x0000000000000cdb <+384>: mov $0x0,%eax 0x0000000000000ce0 <+389>: callq 0xa10 0x0000000000000ce5 <+394>: mov -0x14(%rbp),%eax <-- socket descriptor 0x0000000000000ce8 <+397>: movslq %eax,%rdx 0x0000000000000ceb <+400>: lea -0x2d0(%rbp),%rsi <-- buffer is here 0x0000000000000cf2 <+407>: mov -0x10(%rbp),%eax 0x0000000000000cf5 <+410>: mov $0x0,%ecx 0x0000000000000cfa <+415>: mov %eax,%edi 0x0000000000000cfc <+417>: callq 0x910 <recv@plt> 0x0000000000000d01 <+422>: mov %eax,-0x18(%rbp) 0x0000000000000d04 <+425>: cmpl $0x0,-0x18(%rbp)
Knowing the buffer is located at RSP-0x2D0, we can now start to specify the structure and size of our shellcode. Before that, let’s go back to the main advantage we are getting from this vulnerable program. As I said, the program leaks the address of printf function. Here we want to redirect the execution flow to “system” function that resides in libc. Since the addresses in libc library are relative to each other, having the address of a single known byte of it, is enough to infer other parts’ addresses. Now lets take a look at the dynamic symbol table of libc.so. If we couldn’t find our desired functions here, we continue our search by reading the debug and related symbol files and if that doesn’t work either we disassemble the library and sift through the machine instructions to find what we want.
Well well well! The dynamic symbol table of libc binary tends to submit what we’re looking for; helping us avoid taking further aggressive approaches! One important note here is that these addresses are heavily dependent on the exact version of the OS and libc that’s running on the target. In general you should simulate their environment to be able to build an accurate shellcode. However it’s better to mention here, technically speaking, we are not using a shellcode cause our injected input will not run but redirect the execution flow to another part of the program with existing code from shared libraries. That said let’s keep calling our payload a payload, not a shellcode.
You can see in the vulnerable code that, the leaked address is sent to the client using the same socket to receive the buffer.
Now Remember we have two payloads here, the first one to change the size argument received to “recv” function and the second one to change the return address located at the bottom of receive_data stack frame. If we need to change the value of RSP to something other than what is naturally set after each pop from stack frame, we’ll have to chain load the instructions in libc library. This is a much more advanced and much more time consuming technique and we don’t need it here. But that’s somehow the way to an ultimate overflow attack and is able to produce Turing complete sets of instructions. However we can compromise this vulnerable program without leveraging that. All we need to do here at this stage a careful construction of payloads.
You should take care that the only argument of “system” function is by convention passed to it through RDI register. Let’s take a look at the end of receive_data function:
0x0000000000000d0a <+431>: lea -0x2d0(%rbp),%rax 0x0000000000000d11 <+438>: mov %rax,%rsi 0x0000000000000d14 <+441>: lea 0x12b(%rip),%rdi # 0xe46 0x0000000000000d1b <+448>: mov $0x0,%eax 0x0000000000000d20 <+453>: callq 0xa10 0x0000000000000d25 <+458>: jmp 0xd45 <receive_data+490> 0x0000000000000d27 <+460>: mov -0x18(%rbp),%eax 0x0000000000000d2a <+463>: mov %eax,%edi 0x0000000000000d2c <+465>: callq 0x970 <gai_strerror@plt> 0x0000000000000d31 <+470>: mov %rax,%rsi 0x0000000000000d34 <+473>: lea 0x120(%rip),%rdi # 0xe5b 0x0000000000000d3b <+480>: mov $0x0,%eax 0x0000000000000d40 <+485>: callq 0xa10 0x0000000000000d45 <+490>: mov -0x10(%rbp),%eax 0x0000000000000d48 <+493>: mov %eax,%edi 0x0000000000000d4a <+495>: callq 0x990 <close@plt> 0x0000000000000d4f <+500>: mov -0xc(%rbp),%eax 0x0000000000000d52 <+503>: mov %eax,%edi 0x0000000000000d54 <+505>: callq 0x990 <close@plt> 0x0000000000000d59 <+510>: lea -0x2d0(%rbp),%rax <-- Address of buffer 0x0000000000000d60 <+517>: add $0xc8,%rax <-- 200 bytes Offset 0x0000000000000d66 <+523>: mov %rax,%rdi <-- Load into RDI 0x0000000000000d69 <+526>: callq 0xb50 0x0000000000000d6e <+531>: nop 0x0000000000000d6f <+532>: leaveq 0x0000000000000d70 <+533>: retq
Fortunately the last line in the function calls another function and passes the buffer to it. So the desired address is loaded into the RDI register and will not change during that function running. (We are not interested in what process_data function is doing, only remember RDI is unchanged in this function). So if the payload is correctly built and loaded into memory there shouldn’t be a problem running our command by redirecting the flow to “system” function.
Instead of “space” characters at the beginning of the packet, anything else can be used but I chose that because of the neutral role it plays when it’s inserted at the beginning of a shell command. But it doesn’t have any practical effect here since we are placing the shell command exactly after the initial buffer offset. One thing you should be careful about is that after redirecting the flow to another function the stack which contains your important payload parts, may get corrupted. So again, be careful in which parts exactly your placing important data in payload or shellcode. In some cases there’s no other way than revising the RSP register to prevent further instructions touching the valuable injected data.
Final Hit: The command
First, I should emphasize in this attack we’re limited in stack size because of the previous reason I just explained. We can here put a command of about 200 characters long. Since we aim to take over the system with a single shot, this command mus be smart. It depends on what exactly you want and how much creative you are but what I did was exporting my public ssh rsa key to the target’s “/root/.ssh” directory so that I can login to the system as super user. To to this I upload the ras key onto a server (here the target server itslef, but it can be anywhere else) then write “wget” command and by using a shell redirection, write the rsa key to the desired file.
Put It all Together: Deliver the Attack
Having formulated accurately the payloads and the command, now it’s time to carry out the actual attack against the target. Since we need to cope with some low level data payloads here, I’ve written this code to automate the attack stages.
- Receives the leaked memory address
- builds the payload according to the structure above
- calculates “system” function address
- sends the payload out to the target
Let’s run the server and then the exploit builder above:
server@localhost:/vuln$ ./build.input connected 7fef26327cf0 7fef26318490 Payload sent
If I’ve done everything correctly, I should now be able to login to the target:
client@localhost:~/exploit$ ssh email@example.com The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Wed Nov 22 09:17:02 2017 from 192.168.1.100 firstname.lastname@example.org:~#
Done. Target is under our control now.
When they started to tell in the news that some new defensive measures have been added to systems to mitigate overflow attacks, some got upset because their old exploits didn’t work anymore. However some others just took this as a new challenge to overcome. This is the nature of security. It’s an everlasting war between the two sides and in practice, with hard-working, witting, creativity and persistence, one can break any system. What breifly (I ommited some details and left them to the readers) explained here was only one way among many that you could follow to frusterate operating system security meausres and this shows how much you should be aware and how hard you should try if you are about to make any cyber inferastructure resist against various attacks. Programming bugs happen naturally and security holes are all over the place. It’s your job as a white hat to find them before they are misused and exploited to break into the systems.