printf tips

May 2017

Recently I was solving a challenge from the wargame which involved a string format vulnerability. I did know how to identify this particular type of vulnerability, but I had never actually exploited one before. As a result, I ran into some issues while solving the challenge.

In this article I want to share some of the lessons I learned and share some tips about the printf family of functions. These tips can be very useful when exploiting string format vulnerabilities.

The %n, %hn and %hhn specifiers

The printf family of functions implement the %n, %hn and %hhn specifiers. They require an address to an int, short int and char. Interestingly, the documentation here and here states that the all integers have to be signed. I ran some tests and apparently only the %n specifier is limited to signed int values. The %hn and %hhn specifiers will happily write unsigned values. Some example code for testing this is provided here.

What is also important to know is that printf functions implement a width (sub?)specifier which allows setting the minimum number of characters to be printed. If the value to be printed is shorter than the width, then the value is padded with spaces. When combined with %n, width specifiers enable us to print more characters without actually having them present in the format string. This can be handy when the length of the format string is limited.

Some example code (with additional comments and explanations) for writing arbitrary values to an integer, using the %n, %hn and %hhn specifiers is provided here.

Positional specifiers

The specs give information about the standard syntax for specifying the position of the argument which needs to be printed. The syntax is of the form %n$ where n is the number of the argument to be printed, starting from 1.

The specs also mention that the syntax needs to be consistent, namely if %10$ appears in the format string, then all numbers from %1$ to %9$ should also be present. For some reason, this is not enforced at runtime and printf will happily accept an "invalid" format string. The behavior is not defined, but I was able to deduce some rules through testing.

In the case of the amd64 architecture, the first 6 arguments are in registers and the rest of the arguments are pushed to the stack (more info here). Since each register is 8 bytes, pushes and pops also default to 8 bytes. As a result, for each number, one only needs to "discard" the arguments in registers, multiply by 8 and add the value of the stack pointer before the call to printf (almost).

This feature can be useful when we want to use an address which is higher up the stack and do not want to use other specifiers to consume stack values. Same as before, this is handy especially when the length of the format string is limited.

Some example code (again with additional comments and explanations) for using positional specifiers with %n is provided here.

PS: Initially I was planning to make this article much longer, but then I decided to make the example .c files available so that people can test things out for themselves. There are more comments in the code to help you understand each item.

PPS: Kudos to Alex Reece for his article which prompted me to run some experiments of my own and write this article with my own findings.