-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improper handling for double free in large chunks #12
Comments
As far as I can tell, this is not a bug in DieHard. This code confuses corruption due to double-frees with aliasing when DieHard (legally and correctly) returns the same memory address after it has been freed. DieHard correctly ignores calls to free already-freed large memory: see https://github.com/emeryberger/DieHard/blob/master/src/include/largeheap.h#L29. DieHard allocates large objects via |
Hi, Emery. As so far as I believe, a secure allocator should properly maintain the state of freed pointer (aka dangling pointer). In the theoretical case, we can think an ideal allocator that has a hash table that maps a pointer and its state (e.g., allocated or free). Every malloc() call, we register an allocated state in the hash table, and every free() call we mark free in the state. In this model, this ideal allocator detect double free using a recorded state, and the secure allocator should be indistinguishable from this ideal allocator (at least for double free). So if p[2] and p[4] are same, this would be totally acceptable because the pointer triggering double free is perfectly valid. i.e., free(p[2]) == free(p[4] <- this is valid). But, as you can see in the assertion, p[2] and p[4] are different pointers, which mean that freeing p[2] should be prevented because it is a dangling pointer according to our previous model. I didn't look at the detail of this implementation, but it looks like the hash table (https://github.com/emeryberger/DieHard/blob/master/src/include/largeheap.h#L29) seems doing exactly same thing that I described in the ideal allocator, but I don't know why this double free is allowed. I need to take a look the code more carefully. |
By your logic, in a secure allocator, a freed object would never be recycled? I of course disagree. The only issue here is that, unlike for small objects in DieHard/er, large objects are immediately reused (depending on the OS), where as small objects are probabilistically highly unlikely to be reused immediately. But reuse is inevitable and required in a practical memory allocator, else it would leak memory. |
Surely not. Recycling is required. So this is totally fine
But the issue here is that
|
So I think it is better to be forceful (i.e., abort if get() returns false) to prevent this kind of case. |
There is no reason whatsoever for |
If According to our previous discussion, you said "OK, I had a chance to look at this. Now I see what is going on. What is happening is pretty straightforward. Large objects (>64K) are just allocated and freed via mmap (DieHard(er) effectively is doing nothing but acting as a wrapper to mmap/munmap, except for tracking sizes). Your program is unmapping a large chunk, and it's getting recycled by the OS. I could indeed track freed chunks to make sure this doesn't happen twice, probably just by overloading how I manage sizes. I will take a look at this." So I thought that you also agree with this problem. |
First, with respect to my previous statement, I was wrong. DieHard/er does not in fact try to unmap things twice, and per GitHub blame, that logic has been in place for a long time (at least 8 years). Second, I hope you will agree that:
Given all these things, it's not at all clear what you behavior you consider to be a bug, per se. |
I totally agree with your second part. But, the current issue is not related to reclaiming, but the validity (and its checking) of a pointer to be freed. A secure allocator (or other allocator) need to prevent use of a dangling pointer (i.e., double free). For example, ptmalloc2, which follows your second part, still can detect double free in the PoC. If I run the PoC with ptmalloc2, it will give me an error |
I think you agree with that a secure allocator should not allow to free an invalid object. Then what is the invalid object? In my understanding, the invalid object is the object that is not returned by the allocator or an object that is freed. It is not dependent whether its backed memory is allocated (or reclaimed) because we agree that freeing a middle pointer e.g. |
An invalid object is one that was never returned by |
An object that has been freed is not an invalid object, as long as it was generated by |
I also do not necessarily agree that |
IIRC DieHard/er treats such calls to |
I also think that an object, which is freed, should be treated as invalid. Then, do you think freed object should be also freeable? I guess not. Here what I mean a valid or an invalid object is related to free(). The valid object means that it is fine to be freed, and the invalid one is not.
Yes. I think ignoring seems to be fine, too. I found that DieHarder's decision for double free is quite generous. But I still believe this is problematic because the PoC breaks an important invariant in the allocator for security --- allocated (or live) memory should not be overlapped. BTW, thank you for your time. |
That invariant is decidedly not being broken (unless something is seriously wrong). Allocated memory definitely does not overlap: that's a key correctness requirement of any allocator, secure or otherwise. |
Actually, in the PoC, p[6] is not p[4], which means that p[6] is live. But, the next allocated memory p[7] is same with p[6], which mean they are overlapped.
|
No, DieHard/er is not returning overlapped memory. Yes, To make clear what is happening, I instrumented DieHard to print out the addresses of all allocated "large" memory during execution of your PoC code. Remember that under the covers, all of these
I hope this helps. All that is happening is that a chunk of memory starting at the same address is being repeatedly allocated and freed. |
No. I modified the PoC for printing out allocated memory.
This is how I got the result. $ cat /etc/issue
Ubuntu 18.04 LTS \n \lx86_64 GNU/Linux
$ gcc -o poc poc.c
$ LD_PRELOAD=$(pwd)/DieHard/src/dieharder.so ./poc
p[1] = 0x7ff1f0b09000
p[2] = 0x7ff1f0aee000
p[4] = 0x7ff1f0acf000
p[6] = 0x7ff1f0aee000
p[7] = 0x7ff1f0aee000 As you can see, p[6] is not alias of p[4] in Ubuntu 18.04. |
This is PoC found for Mac OS. It can be reproduced as follows:
As you can see, p[1] is not overlapped with p[3], which is live memory. Thus, p[3] should be live and will be overlapped with newly allocated memory p[4]. |
I will look at this later but to be clear, |
Ok. Let me know after you look this issue. As so far as I know, |
FYI, this is poc annoated with its syscall. I think the problem is that DieHarder calls
|
Emery,
The secure behavior of an allocator is to recognize the state of p[1] (i.e., already freed) and prevent it from being munmap(). What we found here is that, the underlying memory layout of alloced/freed chunks due to this particular sequence of alloc/free calls used in this PoC has the security implication. It means that the following malloc() returns an overlapping chunk, which an attacker can leverage to craft the original memory region. This is indeed a standard, yet easier-kind exploitation technique (i.e., much lesser complex than typical heap exploitation techniques in ptmalloc). In real-world applications, say web browsers, this behavior can be easily leveraged to achieve a full RCE. Note that, we are also planning to create a CTF problem based on this technique to exploit a DieHarder-protected program. |
Hi, emery. Sorry for very very late investigation of this problem. Here is the root cause of this issue. Here is more simpler and intuitive poc to demonstrate this issue. #ifndef __linux__
#error "Only for Linux"
#endif
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>
#include <sys/mman.h>
int main() {
char* p1 = malloc(65536 + 0x1000);
char* p2 = p1 + 0x1000;
free(p2); // This is considered as a valid pointer!
char* p3 = malloc(65536);
assert(p2 == p3);
} According to the POSIX spec, freeing an invalid pointer is actually undefined behavior. So as an allocator, it is also ok to work like this. However, in general, modern allocators (e.g., scudo, freeguard, ..) prohibited this. Similarly, in dieharder, small allocations do not have such behaviors. May I ask you why you choose this kind of design for large allocation? Best, |
Hi. Emery.
This is the issue for Dieharder that we discussed.
I am making this issue to keep track it for further discussion.
I also attached the PoC that you further minimized.
Thank you.
The text was updated successfully, but these errors were encountered: