2

I was trying to exploit a vulnerability in a ctf but I can not make fgets() reopen stdin to put my second stage ROP chain. I am using pwntool but the problem is more socket oriented.

I recreated the above situation. In a test program. The concept is simple. After overflowing the rip I return to main in order to give/change the input. The problem arises when pwntools/bash etc closes the second stdin.

Locally I can overcome the problem doing something like:

cat <(cat myinput.txt) - | ./test.txt

But how can I do this in a socket ? Using pwntools.interactive solves the problem but I want to send non printable characters. Here are some example screenshots of my ./test program

int main (){
    char mastr[150];
    printf("lol\n");

    fflush(stdout);
    fflush(stdin);
    fgets(&mastr,600,stdin);
    puts(mastr);

    return 0;
}

With pwntools.interactive() enter image description here

Without pwntools.interactive() enter image description here

What is the theory behind sockets and stdin ? I really do not know the correct terms to search it.

  • fgets performs buffered reading, so can read more than requested into the local buffer. – David Apr 5 '18 at 18:55
0

A note on fgets(): This is what I observed stepping through the debugger. When piped data is sent to fgets() it's buffered on the heap, then it's transferred to the stack one by one until

  1. A compare instruction returns true on a counter and the value that's passed in the RSI register to the fgets() function; which is the string length specified by the programmer.

  2. Or receipt of an 0x0a byte ("\n")

Once either of those are met, the function exits and STDIN is torn down. So it's buffered I/O and not actual I/O to a file descriptor that's used to communicate with the OS.

Anyways, to manage stdin like you're doing there with bash in Python, you can use Subprocess's Popen function to connect directly to the service, but there's a catch. Python blocks SIGPIPE by default, which prevents keeping the fd open for writing. You can overcome this using the signals module and creating a patch function that overrides Subprocesses configuration to allow SIGPIP:

import signals
def restore_signals():
    signals = ('SIGPIPE', 'SIGXFZ', 'SIGXFSZ')
    for sig in signals:
        if hasattr(signal, sig):
            signal.signal(getattr(signal, sig), signal.SIG_DFL)

Then, call your program directly (without piping) via a Subproccess shell passing the SIGPIP patch through preexec_fn arg. I also specified bash as the shell I wanted to use, but it's not required:

myproc = Popen("./test", 
                stdin=PIPE, 
                stdout=PIPE, 
                shell=True, 
                executable='/bin/bash', 
                preexec_fn=**restore_signals()**)

This will keep a pipe open and you'll just have to manually close it. I assume from an exploit dev standpoint, it won't really matter for you.

To send data, don't use the .communicate() method. You'll have to manually manage the connection sending your shellcode bytes like so:

#prep the receive buffer by clearing it first
myproc.stdout.flush()
#Send your shellcode where shellcode is a var holding your shellcode
myproc.stdin.write(shellcode+"\n")

Note the "\n" to simulate enter so fgets() knows to end the buffer.

Then, to read output, you probably won't want to use the .readline() method unless you know the output (like a banner) will end with a newline byte (0x0a / '\n'). You'll have to manually manage it using the .read() method.

The thing with .read() is you'll need to have some sense of the data that is returned. So if you're expecting a return of something like a memory leak or string and know the size, you're going to want to specify that so your script doesn't block while waiting for more data. I can't remember what the default buffer size on read is, but it won't return until that buffer is filled. .readline() will return on a 0x0a byte. So, if you're expecting a 10 byte string:

 #sleep 1 second after your shellcode send to give the process time
time.sleep(1)
output = myproc.stdout.read(10)

Keep in mind that if you're sending multiple lines, you're going to want to flush() the stdout buffer each time, or your response will read 10 bytes from the previously received data.

Repeat as necessary. Hope that helps.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.