_ _
_ _ _|_| |_|_
___ ___ ___| | |___ ___ | | | |
| _| -_| .'| | | . | _| | | | |
|_| |___|__,|_|_|___|___| |_|_ _|_|
|_| |_|
2018-01-24
INSECURE REALLOC() USAGE
========================
Function signature of realloc() and excerpt of the linux man pages [1]:
void *realloc(void *ptr, size_t size);
"The realloc() function changes the size of the memory block pointed to by
ptr to size bytes. The contents will be unchanged in the range from the
start of the region up to the minimum of the old and new sizes. If the
new size is larger than the old size, the added memory will not be ini-
tialized. If ptr is NULL, then the call is equivalent to malloc(size),
for all values of size; if size is equal to zero, and ptr is not NULL,
then the call is equivalent to free(ptr). Unless ptr is NULL, it must
have been returned by an earlier call to malloc(), calloc() or realloc().
If the area pointed to was moved, a free(ptr) is done."
-
man7.org/linux/man-pages/man3/realloc.3.htmlA pretty important takeaway here is that the return value is super important, since realloc
may end up allocating new memory e.g. in case the old location of the memory wasn't ideal
for expansion. If that happens, the pointer you passed along points to unallocated memory,
since realloc will have free()d it and allocated new memory somewhere else and moved your
memory there.
So there's a couple of code patterns involving realloc() that may cause memory corruption:
Example 1:
1 char *ptr = malloc(512);
2 realloc(ptr, 1024);
3 free(ptr);
Obviously, if realloc moves memory around, the free on line 3 will end up being a double
free since you're free()ing memory which reallocate freed after moving the memory to a
new location in the heap.
Example 2:
1 char *ptr = malloc(512);
2 char *newptr = ptr;
3 newptr = realloc(newptr, 1024);
4 ptr = realloc(ptr, 2048);
This one may be harder to spot when auditing code, since the return value is apparently
handled it seems, but the two different pointers actually point to the same address in
memory. So that's also a double free.
Example 3:
1 char *ptr = malloc(512);
2 realloc(ptr, 2048);
3 strncpy(ptr, otherPtr, 512);
Use-after-free leading to out-of-bounds write if the attacker can control the
contents of the memory pointed to by otherPtr.
Obviously, attackers must be able to massage the heap somewhat to ensure viable
conditions for exploitation.
DETECTION
=========
Fuzzing for these issues is problematic, since the heap isn't necessarily clottered with
chunks in just the right layout for realloc to decide to move buffers around.
One solution presented here is to replace realloc() with a custom version which _always_
moves the memory around regardless of the heap layout. I did exactly that by forking
@Zardus' preeny and added a new preload library: crazyrealloc.c
github.com/magnusstubman/preeny/blob/master/src/crazyrealloc.c:
1 void *realloc(void *ptr, size_t size) {
2 void *r = original_realloc(ptr, size);
3 if (r != ptr) {
4 return r;
5 } else {
6 if (r) {
7 void *rm = malloc(size);
8 memcpy(rm, r, size);
9 free(r);
10 return rm;
11 } else {
12 return r;
13 }
14 }
15 }
Using it together with a sample test program results in:
test.c:
1 #include "stdio.h"
2 #include <stdlib.h>
3
4 int main()
5 {
6 char *s = malloc((size_t)10);
7 realloc(s, (size_t)15);
8 free(s);
9 }
Execution:
$ LD_PRELOAD=
crazyrealloc.so ./test
*** Error in `./realloc': double free or corruption (fasttop): 0x0000000001967010 ***
Aborted
If you use it and find bugs with it, let me know.
UPDATE
======
crazyrealloc.c was merged into preeny:
github.com/zardus/preeny