Gallopsled / pwntools

CTF framework and exploit development library
http://pwntools.com
Other
12.05k stars 1.71k forks source link

Interact with a shell program #985

Closed Kyle-Kyle closed 7 years ago

Kyle-Kyle commented 7 years ago

It seems that pwntools can not help sometimes when dealing with a shell binary (need EOF to move on to next level) For example:

#include <stdio.h>
#include <unistd.h>

int main(){
    char str[200];
    str[100] = 0;
    printf("Loop begins\n");
    while(read(0, str, 100) > 0){ 
        printf("%s", str);
    }   
    printf("good\n");
    scanf("%s", str);
    printf("last instruction done\n");
    printf("last input: %s\n", str);
}

To get out of the loop, EOF is needed. And this program behaves as expected when run directly. However, as far as I know what I can do with pwntools is

  1. get out of the loop and miss the last input by closing the connection
  2. never get out of the loop

I tried to use PTY, but it seems not work for me.

zachriggle commented 7 years ago

You can, but you're doing that RCTF challenge wrong.

See tube.shutdown

Kyle-Kyle commented 7 years ago

Excuse me. I guess you misunderstand what I mean. It's supposed to be a PTY problem not a shutdown or stdin.close problem. Actually I have work the challenge out, but not in the way I have mentioned. But I just wonder why it does not work. If you compile the code and run it manually, after it receives EOF, it goes on and wait to receive your next input. I wonder whether I can accomplish it with pwntools. Because if you simply stdin.close, it will not receive any inputs anymore.

zachriggle commented 7 years ago

I might have misunderstood, there are a large number of IRC requests for a given challenge. In any case, you can send whatever PTY commands you want. You'll need to make stdin a PTY, and configure it. I recommend looking at "man stty" for more information. You want to send EOT, not EOF. In either case, it's not something the binary actually receives, but something handled in the kernel by the PTY emulation layer.

zachriggle commented 7 years ago

How to send true EOF in Pwntools to a local process

What you're asking for is surprisingly complex for a lot of reasons. To follow up a bit, you need to look at the following resources:

Ultimately, there is an easy way to get what you want, but it has consequences. In particular, all of the standard input space is mapped to TTY control codes. This means that when you send the byte \x04, the PTY will interpret this as EOF. This is the same as hitting Ctrl+D with a normal TTY (note that D is the 4th letter in the alphabet).

Let's take a look at a few variants. By default, when using pwntools, stdin is a pipe. All input bytes are sent directly and not touched by any PTY driver. stdout is a raw PTY, which is done because glibc will not buffer output if stdout is a PTY. The raw PTY is done to prevent any post-processing of the data (i.e. we get all bytes that are written, without newline transformations, etc.)

Note that we cannot get the terminal attributes, as stdin isn't a PTY.

>>> p = process('cat')
>>> termios.tcgetattr(p.stdin.fileno())
error: (25, 'Inappropriate ioctl for device')

We can make stdin be a PTY. This doesn't have much effect, as it's still a raw PTY. However, it will make isatty(stdin) return True, which means that most binaries will perform in interactive mode. As an example, try process('python') vs process('python', stdin=PTY).

If we examine the terminal control flags, note that not many flags are set, as the PTY that pwntools uses is raw by default. This prevents interpretation of input bytes, which is important for 99.999% of exploits.

>>> p = process('cat', stdin=PTY)
>>> pprint(termios.tcgetattr(p.stdin.fileno()))
[0,     # iflag == 0
 4,     # oflag == ONLCR
 191,   # cflag ...
 2608,  # lflag == ECHOE | ECHOK | ECHOCTL | ECHOKE

We can make it be a non-RAW pty. Note that ICANON is set in the lflags, so the terminal behaves canonically (e.g. Ctrl+D would end input, Ctrl+C kills the process, etc.). This is only desired in 0.00001% of exploits that would use pwntools, as it becomes impossible to send many bytes.

>>> p = process('cat', stdin=PTY, raw=False)
>>> pprint(termios.tcgetattr(p.stdin.fileno()))
[1280, # iflag == INLCR | IXON
 5,    # oflag == OPOST | ONLCR
 191,  # cflag ...
 35387, # lflag == ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHOCTL | ECHOKE | IEXTEN
 15,
 15,
 ['\x03', # VINTR == 0 / CINTR == 3 / Ctrl+C
  '\x1c',
  '\x7f',
  '\x15',
  '\x04', # VEOF == 4 / CEOF == 4 / Ctrl+D
  ...

In thie scenario, we can send most standard control codes. For example, sending '\x03' will terminate the process with SIGINT. Sending '\x04' will send EOF. Sending '\x08' is a backspace. However, it's now actually impossible to send these bytes to the target process -- they are intercepted by the PTY driver!

So, how do you get to be able to really send EOF? You can use stdin=PTY, raw=False, as long as you don't actually need '\x08' or '\x03' etc. in your exploit. Then you send the appropriate character, which is canonically '\x04'. You can re-map all of these, but I'm not going to dive into that.

As an added bonus, because it's behaving as a canonical TTY, all input is echoed.

Just fucking show me, Zach

Fine.

int main() {
    char buffer[32];
    while(1) {
        int n = read(0, buffer, sizeof(buffer));
        dprintf(1, "XXX"); // <-- To demonstrate echo
        dprintf(1, "Got %#x bytes\n", n); // <-- To show return value
        if(n > 0)
            write(1, buffer, n); // <-- To echo contents
    }
}

Here's a basic example. We'll just send "Hello" and then a non-printing character '\x7f' (chosen by fair dice roll, no bamboozles).

from pwn import *
context.log_level = 'debug'

p = process('./a.out', stdin=PTY, raw=False)
p.sendline("Hello\x7f")
p.clean()

What do you think will be printed? Common sense says something like:

XXXGot 0x7 bytes
Hello

However, what we actually get is different for several reasons. The debug output looks like:

[+] Starting local process './a.out': pid 22089
[DEBUG] Sent 0x7 bytes:
    00000000  48 65 6c 6c  6f 7f 0a                               │Hell│o··│
    00000007
[DEBUG] Received 0x22 bytes:
    00000000  48 65 6c 6c  6f 08 20 08  0d 0a 58 58  58 47 6f 74  │Hell│o· ·│··XX│XGot│
    00000010  20 30 78 35  20 62 79 74  65 73 0d 0a  48 65 6c 6c  │ 0x5│ byt│es··│Hell│
    00000020  0d 0a                                               │··│
    00000022
[*] Stopped process './a.out' (pid 22089)

What the fuck? Why did we get "Hello" twice? Any why are there carriage returns? Why did the first '\x7f' get turned into '\x08\x20\x08? Why is the second "Hello" truncated? Why did the process only receive 5 bytes!?!? Welcome to the world of terminals! Here there be dragons. . Read the resources that I linked to at the top, and you'll learn more about terminal emulation than you ever cared about.

The example that you care about is basically this:

import tty
from pwn import *
context.log_level='debug'

p = process('./a.out', stdin=PTY, raw=False)
p.send(chr(tty.CEOF))
p.clean()
p.sendline("Wow!")
p.clean()

Which yields the long-sought-after result (or at least something close)...

[+] Starting local process './a.out': pid 29142
[DEBUG] Sent 0x1 bytes:
    '\x04' * 0x1
[DEBUG] Received 0x10 bytes:
    'XXXGot 0 bytes\r\n'
[DEBUG] Sent 0x5 bytes:
    'Wow!\n'
[DEBUG] Received 0x1e bytes:
    'Wow!\r\n'
    'XXXGot 0x5 bytes\r\n'
    'Wow!\r\n'
[*] Stopped process './a.out' (pid 29142)
05lover commented 5 years ago

Hi zachrihhle, thanks you for your excellent answer. Is there any way to send a real 'EOF' to a remote program ?

05lover commented 5 years ago

And then get shell and interactive with the remote program?

ghost commented 3 years ago

I have the same question as @05lover.