Using Printf in Assembly Leads to Empty Output When Piping, But Works on the Terminal

Using printf in assembly leads to empty output when piping, but works on the terminal

Use call exit instead of a raw _exit syscall after using stdio functions like printf. This flushes stdio buffers (write system call) before making an exit_group system call).

(Or if your program defines a main instead of _start, returning from main is equivalent to calling exit. You can't ret from _start.) Calling fflush(NULL) should also work.


As Michael explained, it is OK to link the C-library dynamically. This is also how it is introduced in the "Programming bottom up" book (see chapter 8).

However it is important to call exit from the C-library in order to end the program and not to bypass it, which was what I wrongly did by calling exit-syscall. As hinted by Michael, exit does a lot of clean up like flushing streams.

That is what happened: As explained here, the C-library buffers the the standard streams as follows:

  1. No buffering for standard error.
  2. If standard out/in is a terminal, it is line-buffered.
  3. If standard out/in is a not a terminal, it is fully-buffered and thus flush is needed before a raw exit system call.

Which case applies is decided when printf is called for the first time for a stream.

So if printf_try is called directly in the terminal, the output of the program can be seen because hello has \n at the end (which triggers the flush in the line-buffered mode) and it is a terminal, also the 2. case.

Calling printf_try via $(./printf_try) means that the stdout is no longer a terminal (actually I don't know whether is is a temp file or a memory file) and thus the 3. case is in effect - there is need for an explicit flush i.e. call to C-exit.

Printf call from assembly do not print to stdout

If you want to use C library functions you shouldn't use _start, but good old main. If you steal _start from libc, it won't be able to perform several setup and cleanup operations, such as flushing the open C files.

This is visible in this case only when writing to file, as when writing to a tty stdout is line-buffered by default, so it's flushed immediately. When redirecting to file, instead, the data is just copied in a temporary buffer, to be flushed when it's full enough, or at termination - but the C runtime cleanup functions aren't ever called in your case, so the data is never actually passed to the OS.

Another possibility is to exit using the exit libc function (extern exit at the beginning and call exit at the end of the execution), which should handle the cleanup, but I'm not sure if stuff from libc is even required to work fine if it doesn't get the chance to be initialized in first place should be safe on Linux, see @PeterCordes comment and thanks to @Kirill_Zaitsev for trying it.

ASM printf: no output if string doesn't include \n newline

The exit syscall (equivalent to _exit in C) doesn't flush the stdout buffer.

Outputting a newline causes a flush on line-buffered streams, which stdout will be if it is pointed to a terminal.

If you're willing to call printf in libc, you shouldn't feel bad about calling exit the same way. Having an int $0x80 in your program doesn't make you a bare-metal badass.

At minimum you need to push stdout;call fflush before exiting. Or push $0;call fflush. (fflush(NULL) flushes all output streams)

Why didn't printf print out the string in this buffer overflow?

stdout defaults to line-buffered, and the string doesn't end with a newline. If you changed it to puts("Pwned!!"); then stdout would get flushed before puts returns.

But with printf, the data is just sitting there in a stdio buffer until something else prints a newline, or until fflush(stdout). exit() or cleanly returning from main will cause fflush, but segfaulting will kill the process without ever making a system call to hand that I/O data to the OS.

This is exactly the same problem as Using printf in assembly leads to an empty ouput except that case was using an _exit(2) system call instead of segfaulting.


If the goal is to force you to get win() called without breaking later execution, that's another level of challenge.

But if win() is supposed to represent something like a successful ROP attack that calls system() or execve with "/bin/sh" then win() is not well-written. execve will happen on the spot, not at some later time.

Printf without newline in assembly

fflush() flushes buffered output in line or full-buffered stdio streams:

extern fflush
...
xor edi, edi ; RDI = 0
call fflush ; fflush(NULL) flushes all streams
...

Alternatively, mov rdi, [stdout] / call fflush also works to flush only that stream. (Use default rel for efficient RIP-relative addressing, and you'll need extern stdout as well.)

Why can't I redirect the output of my binary to a file?

The correct way of exiting the program was to use a call to exit libc function instead of the raw syscall

How register offset affects printing

This code looks like it's set up for you to store the string bytes into buffer, relative to RSI, so the %s conversion in the format string will print it. But with the string in .data instead of .rodata where you should put read-only data, yes you can overwrite bytes of the format string at runtime.

When you overwrite the \n, printf doesn't flush the output buffer because stdout is line buffered. You exit with sys_exit (direct system call) instead of call exit or returning from main, leaving the data un-printed. See Using printf in assembly leads to an empty ouput

You can use ltrace ./my_program to see the library function calls it makes.



Related Topics



Leave a reply



Submit