Intro
Hi there (again)! This series are going to an end as the next and feasible step is the widely known buffer overflow and its analysis in the heap and, I am not too convinced about it since the unsafe unlink method is long gone. But don’t be sad, today we are going for a bonus one! During the last post (double free attacks) one I stumbled across some weird behaviour that caught my attention by functions of the vfprintf.c family (for example printf or puts functions).
We will keep the level from last post and still use gdb but with the addition of pwndbg. This should be a short one, sorry if I dwell into too many details and fail to do so.
One last thing, this is by far, nothing that someone would be able to reproduce as the chances for this to happen on a real world scenario are close to zero, none, niks, nada. Other than that, I thought is a cool way to understand how we could be able to abuse other functions that also use the heap in such ways.
The vulnerability
Preface
I had a chat with a couple of people I look up to and admire for their skills and they provided very good points. One point in which they coincide is that the vulnerability here is the double free and that, by definition, the family of functions that use vfprintf.c is unsafe.
I am still undecided if the technique we are going to describe here is actually a vulnerability because, depending on the libc that our system is running, the code behaves very different. The “exploitable” case happens on libc 2.23 and libc 2.24. All other libc implementations are safe from this case. Also I haven’t reported this behaviour to the glibc maintainers and still doubting if doing so because the ways of linking different libc‘s to my code.
Then how do I know that it reliably works on libc 2.23 and libc 2.24 you might ask? Because I bugged several people and laptops :)
Last but not least everything in this blog post is 64bit oriented.
What
The weird thing I encountered while researching this, is that commonly such functions like printf
, used to use the .rodata
section to store the string and then send it to the stdout
stream but, for what I’ve seen, on libc
2.23 and 2.24 that is not the case and it uses the heap by allocating 1032+8
bytes and does not free the memory on exit. The cause of this might be performance but, in any case, there is a quite similar bug pointing to the usage of “printf” family and its memory usage.
For further reference and in our case, keep in mind that vfprintf.c contains code similar to the following pseudo-code:
... int vfprintf(args) { int string_malloced = 0; if (string_malloced == 0) { mem_for_str = malloc(0x410-8); } stream_from_memory(stdout, mem_for_str, args); return bytes_written; } ...
Basically, if there is no string malloc’d it will allocate a new string mem_for_str
of actual size in memory 1040 bytes (0x410) but, usable size of 1040-8 bytes (0x410-8). Again, remember that the 8 bytes are for the chunk size header. Then it will use that allocated memory to format the “format string” in args
, copy it into mem_for_str
and then feed it into the stdout
stream. If it is already malloc’d it will just reuse the mem_for_str
chunk.
As you can see there is no free(mem_for_str)
before returning the written bytes (return bytes_written
). This will leave a chunk allocated that is up to abuse with our double-free primitive. Mwahaha!!
When stars leave a trail in your memory
The scenario to happen here is quite the same as the one we described for the double-free but, in our case, the responsible of doing a malloc that is to be abused later, are the functions printf
or puts
.
... char *A, *B, *C; A = malloc(0x410-8); // (1) free(A); printf("BBBB"); // (2) free(A); // Double free B = malloc(0x200-8); C = malloc(0x200-8); printf("X"*0x208); // Not real C code (4) ...
I think it’s a good time to explain this through our old friends the ascii heap drawings =)
We start with the allocation at (1), nothing fancy about it.
+---HEAP GROWS UPWARDS | | +-+-+-+-+-+-+ | | CHUNK A | <-- Chunk A, size (0x410) | | | | | | | +-----------+ <-- A ends here. | | TOP | | | | V | | +-----------+
Then we free it so the TOP chunk takes that space and then we do a printf("BBBB")
.
+---HEAP GROWS UPWARDS | | +-+-+-+-+-+-+ | | PRINTF | <-- Old Chunk A position. | | 4242424242| <-- Contents of printf | | | | +-----------+ <-- printf chunk ends here | | TOP | | | | V | | +-----------+
Now if a double-free happens on old chunk A
we are going to effectively have a free()
on the chunk that printf
has allocated. At this point if we keep using the printf()
function, it is going to write to the TOP
chunk’s contents because it thinks that its chunk is still malloc’d (string_malloced == 1)
. But what would happen if we allocate two chunks in the free()
space of the old chunk A
which is, in turn, used by the printf
function?
+---HEAP GROWS UPWARDS | | +-+-+-+-+-+-+ | | CHUNK B | <-- Old printf chunk position. | +-----------+ | | CHUNK C | | +-----------+ <-- Old printf chunk ends here | | TOP | | | | V | | +-----------+
Hopefully you can clearly see the havoc that a long printf
could make here as it will overwrite chunk’s C
metadata (size headers and specific bits). In the pseudo-code snippet at (4) we write into chunk B
all the way into the headers of chunk C
by writting 0x200 (512) times the letter ‘X’.
The playground
Let’s get our brains messed with: we know we can overwrite chunks through printf/puts
if we have a double-free primitive, so our goal is going to be to call shellcode allocated in a chunk we control. This time we are going to show just two proof of concepts: One to show the behaviour in the debugger and the second one modifying last blog post’s jackpot code execution but, through this technique, and then jumping to a shellcode previously allocated in the stack. Let’s go!
The concept
We are going to use the file printf_double_free.c, which represents the When stars leave a trail in your memory section. Now, onto gdb+pwndbg as the code for this proof of concept has been already explained previously:
The exploitation
The grand finale is here! Get the file printf_fastbins_malloc_hook.c and go for code execution through the use of printf
but, before any crazy debugging sessions we are going to explain some caveats.
NX
Most likely, if you are here reading this you already know about NX
(or its Windows brother DEP
), if you don’t, no problem: Non-eXecutable is a bit that, if set, it will protect from the code execution to jump into the memory parts of the heap or stack – we can’t have a shellcode in the heap or the stack and execute it if this bit is set.
This is why, when compiling this proof of concept we need the “-z execstack
” flag (this is me being lazy and not wanting to craft a ROP chain). If you would be to exploit something through a technique similar to this you would have to either bypass NX through other ways or disable it through a ROP chain.
The shellcode
Now that we have our memory set as executable, I decided to set up a shellcode with the following command and a bit of help from the metasploit framework and python (you can copy paste from the video):
Let me explain what happened there: I generated the shellcode in raw format and straight fed it into the xxd
command that outputs the bytes from the shellcode in hexadecimal byte by byte. Then I removed the trailing newline characters with tr -d '\n'
to finally feed the hexadecimal formatted shellcode into python which will give me an escaped string version of our payload to inject it into our C code.
... memcpy(sc, "j;X\x99H\xbb/bin/sh\x00SH\x89\xe7h-c\x00\x00H\x89\xe6R\xe8\x08\x00\x00\x00/bin/sh\x00VWH\x89\xe6\x0f\x05", 47); ...
Rememberance
If you have skimmed through the code already, you will see that it is almost the same code as the double-free PoC which jumps to the jackpot function. The difference is the following:
The following is the code that used double free and memcpy
to write our rogue FD.
... puts("\n[+] free p5"); free(p5); puts("\n[+] allocate p6 with size 0x60"); char *p6 = malloc(0x60); // Takes the old address of p5 puts("\n[+] Double free p5"); free(p5); // double free p5 void *offset_byte_7f = (void *)malloc_hook-0x20-3; memcpy(p6, &offset_byte_7f, 0x8); ...
Current PoC code that uses “printf” to write our rogue FD:
... puts_heapless("\n[+] allocate p5 with size 0x60"); char *p5 = malloc(0x410-8); // Size of chunk for printf puts_heapless("\n[+] free p5"); free(p5); puts("This puts/printf will mess with p6 and p7"); // Takes the old address of p5 (1) puts_heapless("\n[+] Double free p5"); free(p5); // double free p5 puts_heapless("\n[+] allocate p6 with size 0x60"); char *p6 = malloc(0x60); free(p6); long int *offset_byte_7f = (long int *)malloc_hook-0x20-3; offset_byte_7f = (void*)offset_byte_7f+0xf5; printf(&offset_byte_7f); // (2) ...
The differences are the following:
– Usage of a helper function that doesn’t use the heap to print to stdout.
– The "puts"
at (1) will cause a new malloc that will take chunk p5
place
– Since the double free of p5
will free the chunk for puts
chunk, in this case we don’t need to write to a new chunk, we just need to do a puts
to place our rogue FD
pointer.
The execution
If you want this proof of concept to work, the first thing to do is compile it properly with the following command line:
gcc printf_fastbins_malloc_hook.c -o printf_fastbins_malloc_hook -ggdb -z execstack
After compiling, all we have to do is run it but, what would this PoC be without a debugging session to explain the nifty details?
Conclusions
We have managed to corrupt memory with a function from the “vfprintf” family. This leaves our imagination to think what other functions might be using the heap and leaving programmers unnoticed about such behaviour and, from an attacker perspective, the probability and scenarios of using any other functions like the one described here to corrupt and exploit heap can be on the increase.
That was all! Wish you heaps of happyness =)
Oh and! DO NOT hesitate on sending me an email at javier (at) the domain in the URL for any questions, inquiries, hatemail or wuddever!