Format Strings

The printf family of functions accept a format string as an argument which may be used for formatting the list of arguments passed to the function.

int printf(const char *restrict format, ...);

It is unsafe to use user-supplied input as the format string because there is a directive (more below) that can write an integer to a memory location. Further, the format string can expose any data that is on the stack.

Reference: man 3 printf

Specification

The overall syntax of a conversion specification is:

%[$][flags][width][.precision][length modifier]conversion

Positional argument

Represented as $ above, this option represents the nth argument passed to printf (1-indexed).

printf("%2$d %1$d", 10, 20)
// Output: 20 10

Flags

FlagDescription
0Zero pad the value
-Left adjust to the field boundary
<space>A blank should be left before a positive number
+Always use a sign (+/-) before a number
'Group output with thousands’ grouping characters (if locale information has any)

Width

Specifying a minimum field width will pad with spaces (by default) on either the left or right of the value. Instead of a decimal digit, a * or *m$ will specify that the field width is given in the next or mth argument respectively.

Precision

Precision represents the minimum number of decimal digits to display for floating point numbers. Similar to width, * or *m$ may be used to indicate the precision is given in an argument.

Length modifier

ModifierDescription
hhchar / unsigned char
hshort / unsigned short
llong / unsigned long
lllong long / unsigned long long
qsynonymous to ll
Llong double
jintmax_t / uintmax_t
zsize_t / ssize_t
tptrdiff_t

Conversion specifier

The type of conversion to apply to the argument.

SpecifierDescription
d, isigned decimal notation
ounsigned octal notation
uunsigned decimal notation
x, Xunsigned hexadecimal notation (lower / upper cased)
e, Edecimal exponent notation [-]d.ddde±dd
f, Fdecimal notation [-]ddd.ddd
g, Gautomatically choose e or f
a, Ahexadecimal decimal notation [-]0xh.hhhhp±d
cchar representation
sstring representation
phexadecimal representation of a pointer
nwrite the number of characters written so far to an argument
%literal %

Danger of format specifiers

If you didn’t catch it, %n will write the number of written characters so far to a pointer (passed as an argument).

int num;
printf("hello%n", &num);
// num == 5

This, combined with the variable argument list printf uses means an attacker can overwrite arbitrary bytes in memory (e.g. pointers on the stack). If the input buffer also exists on the stack, the attacker can supply the address they want to overwrite as well.

#include <stdio.h>

int main() {
    int secret = 0xdeadbeef;
    int *x = &secret;
    printf("%-1337x%6$n");
    // secret == 1337
    printf("\n%d\n", secret);
}
$ gcc -m32 test.c -o test && ./test