├── README.md ├── a.out ├── demo.c └── demo.py /README.md: -------------------------------------------------------------------------------- 1 | # pwntools-glibc-buffering 2 | 3 | A common issue that people encounter when using Pwntools (or anything else, really) is that the C standard library (glibc, on Ubuntu) performs lots of buffering. This means that you may not see the results a your `printf` in a challenge binary. 4 | 5 | Glibc's buffering has three modes for stdout: 6 | 7 | - Fully buffered (stdout is a pipe) 8 | - Line buffered (stdout is a terminal) 9 | - Unbuffered (stdin and stdout are both terminals) 10 | 11 | This repository contains an example C program `demo.c` which demonstrates this issue. Of interest, directly calling `write` avoids any buffering. This allows us to see that the program has reached a certain state. Second, it makes use of `printf` which are generally fully-buffered, or line-buffered. Third, it makes use of one `printf` each which does/does not end with a newline (`"\n"`), which allows us to demonstrate the issues caused by buffering modes. 12 | 13 | Included is a sample script, `demo.py`, which demonstrates how the various buffering modes exhibit themselves. 14 | 15 | ## Both PTYs 16 | 17 | Here is the behavior one would see in an interactive terminal. Everything is immediately flushed to the screen, since it is expected that the input and output are both interactive. 18 | 19 | ``` 20 | $ python demo.py DEBUG STDIN=PTY STDOUT=PTY 21 | [x] Starting local process './a.out' 22 | [DEBUG] Sent 0x2 bytes: 23 | 'X\n' 24 | [DEBUG] Received 0x20 bytes: 25 | 'AHello\n' 26 | 'BCYou wrote: X\n' 27 | '\n' 28 | 'DEGoodbye' 29 | [DEBUG] Sent 0x2 bytes: 30 | 'Y\n' 31 | [+] Receiving all data: Done (16B) 32 | [*] Process './a.out' stopped with exit code 1 33 | [DEBUG] Received 0x10 bytes: 34 | 'FYou wrote: Y\n' 35 | '\n' 36 | 'G' 37 | ``` 38 | 39 | ## Stdout is a PTY 40 | 41 | Here is the default pwntools behavior. Notice that "F" precedes "Goodbye", and that the timeout is hit before "Goodbye" arrives. 42 | 43 | This is like `less` in the `cat foo | grep bar | less`. Note that `less` can actually still have input, despite `STDIN` being a pipe. It does this by accessing `/dev/tty` directly (or some analogue). However, results do not show up immediately in `less`, since `grep` may buffer them before sending it over the pipe. 44 | 45 | ``` 46 | $ python demo.py DEBUG STDOUT=PTY 47 | [+] Starting local process './a.out': Done 48 | [DEBUG] Sent 0x2 bytes: 49 | 'X\n' 50 | [DEBUG] Received 0x19 bytes: 51 | 'AHello\n' 52 | 'BCYou wrote: X\n' 53 | '\n' 54 | 'DE' 55 | [DEBUG] Sent 0x2 bytes: 56 | 'Y\n' 57 | [+] Receiving all data: Done (48B) 58 | [*] Process './a.out' stopped with exit code 1 59 | [DEBUG] Received 0x17 bytes: 60 | 'FGoodbyeYou wrote: Y\n' 61 | '\n' 62 | 'G' 63 | ``` 64 | 65 | ## Everything is a pipe 66 | 67 | This is how things work with the `subprocess` module by default, and old versions of Pwntools. 68 | 69 | It would be like `grep` in the shell command `cat foo | grep bar | less`. 70 | 71 | Note that `"Hello"` does not arrive until the buffers are flushed when the binary exits. 72 | 73 | ``` 74 | [+] Starting local process './a.out': Done 75 | [DEBUG] Sent 0x2 bytes: 76 | 'X\n' 77 | [DEBUG] Received 0x5 bytes: 78 | 'ABCDE' 79 | [DEBUG] Sent 0x2 bytes: 80 | 'Y\n' 81 | [+] Receiving all data: Done (48B) 82 | [*] Process './a.out' stopped with exit code 1 83 | [DEBUG] Received 0x2b bytes: 84 | 'FGHello\n' 85 | 'You wrote: X\n' 86 | '\n' 87 | 'GoodbyeYou wrote: Y\n' 88 | '\n' 89 | ``` 90 | -------------------------------------------------------------------------------- /a.out: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zachriggle/pwntools-glibc-buffering/339e4c533b73fafe5ac7903781f0e790ba7b0efb/a.out -------------------------------------------------------------------------------- /demo.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | char buffer[512]; 5 | write(1, "A", 1); 6 | printf("Hello\n"); 7 | write(1, "B", 1); 8 | fgets(buffer, sizeof buffer, stdin); 9 | write(1, "C", 1); 10 | printf("You wrote: %s\n", buffer); 11 | write(1, "D", 1); 12 | printf("Goodbye"); 13 | write(1, "E", 1); 14 | fgets(buffer, sizeof buffer, stdin); 15 | write(1, "F", 1); 16 | printf("You wrote: %s\n", buffer); 17 | write(1, "G", 1); 18 | } 19 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | if args['STDOUT'] == 'PTY': 4 | stdout = process.PTY 5 | else: 6 | stdout = subprocess.PIPE 7 | 8 | if args['STDIN'] == 'PTY': 9 | stdin = process.PTY 10 | else: 11 | stdin = subprocess.PIPE 12 | 13 | p = process('./a.out', stdout=stdout, stdin=stdin) 14 | p.sendline("X") 15 | data = p.recvuntil('Goodbye', timeout=3) 16 | p.sendline("Y") 17 | p.recvall() 18 | --------------------------------------------------------------------------------