SBN

Zero-Day Snafus — Hunting Memory Allocation Bugs

Zero-Day Snafus — Hunting Memory Allocation Bugs

Preface

Languages like C/C++ come with the whole “allocation party” of malloc, calloc, zalloc, realloc and their specialized versions kmalloc etc. For example, malloc has a signature void *malloc(size_t size) which means one can request an arbitrary number of bytes from the heap and the function returns a pointer to start working on. The memory should then later be freed with a free(). These functions remain a quite decent point of interest for hackers to exploit applications even in 2019 – case in point, the recent double-free bug in WhatsApp which I shall discuss in a follow-up post.

So, I recently had a chat with Alexei who pointed me to his blog where he presents a pretty cool Ghidra based script to discover common malloc bugs. I got inspired by that and cooked up a few simple queries with a tool called Ocular that can help in discovering such issues a bit faster. Ocular and its Open Source cousin Joern were developed by our team at ShiftLeft. This will also be an opportunity to learn how Ocular and Joern work and understand their inner workings. If you don’t like security, you can at least learn Scala with these tools – as I did as well 😉

malloc() Havoc

So, coming back to the malloc() drama, here are a few cases where seemingly valid use of malloc can go really wrong:

  • Buffer Overflow: It may be possible that the size parameter of malloc is computed by some other external functions. For example, as Alexei mentioned in his post, here is a scenario where the size is returned from another function:
int getNumber() {
int number = atoi("8");
number = number + 10;
return number;
}
void *scenario3() {
int a = getNumber();
void *p = malloc(a);
return p;
}

In this case, while the source of malloc‘s size argument is simply atoi(), that may not always be the case. What if the value of integer (number + 10) overflowed and became much smaller than what was subsequently required (by memcpy for example)? It may lead to a buffer overflow when accessing or writing to it.

  • Zero Allocation: As indicated in Julien’s remarkable work (https://openwall.info/wiki/_media/people/jvanegue/files/woot10.pdf) providing zero as size parameter, while valid, can cause out-of-bounds errors. This is quite possible in cases where the size is determined after some arithmetic operation (such as a multiplication operation)
void *scenario2(int y) {
int z = 10;
void *p = malloc(y * z);
return p;
}

What if supposedly externally controlled y evaluates to zero? In this case, malloc may return NULL pointer, but it's up to the user to make sure that there are NULL checks before using the allocated memory.

  • Intra-Chunk Heap Overflow: One of my favorite heap exploits that I have seen multiple times in the wild and have been a victim of myself, is a case wherein a given chunk of allocated memory, you accidentally overwrite one section while operating on another unrelated one. An example taken from Chris Evans’ Blog explains it quite well:
struct goaty { char name[8]; int should_run_calc; };
int main(int argc, const char* argv[]) {
struct goaty* g = malloc(sizeof(struct goaty));
g->should_run_calc = 0;
strcpy(g->name, "projectzero");
if (g->should_run_calc) execl("/bin/gnome-calculator", 0);
}
  • UAF, Memory Leaks: These are quite common as well — forgetting to free() allocated memory within loop constructs could lead to leaks which can in certain cases be used to laterally cause malicious crashes or generic performance degradation. Another case is remembering to free memory, but trying to use it later on, causing use-after-free bugs. While not having a high reproducibility in exploits, this can still be caused when free is close to malloc and we try to reallocate (which generally returns the same or a nearby address) thus allowing us to access previously freed memory.

In this blog, we will attempt to cover the first two cases where we use Ocular to sanitize the malloc's size argument and see if they could eventually lead to buffer overflows or zero allocation bugs.

Ocular

Ocular allows us to first represent code (C/C++/Java/Scala/C# etc.) into a graph called Code Property Graph — CPG (it's like a mix of AST, control flow and data flow graphs). I call it half-a-compiler. We take in source code (C/C++/C#) or bytecode (Java) and compile it down to an IR. This IR is basically the graph we have (CPG). Instead of compiling it down further, we load it up in memory and allow questions to be asked to this IR to asses data leaking between functions, data-flow analysis, ensuring variables in critical sections are used properly, detecting buffer overflows, UAFs etc.

And since its a graph, well, the queries are pretty interesting and are 100% Scala and just like GDB or Radare, can be written on a special Ocular Shell. For example, you could say,

“Hey Ocular, list all functions in the source code that have “alloc” in their name and give me the name of its parameter”

This would get translated on Ocular Shell as:

ocular> cpg.method.name(".*alloc.*").parameter.name.l
res1: List[String] = List("a")

You could really go crazy here — for example, here is me creating a graph of the code and listing all methods in the code in less than a minute:

Detecting Allocation Bugs with Ocular/Joern

Let's level up a bit and try to make some simple queries specific to malloc() now. Consider the following piece of code. You could save it and play with it in Ocular or its Open Source brother Joern

#include <stdio.h>
#include <stdlib.h>
int getNumber() {
int number = atoi("8");
number = number + 10;
return number;
}
void *scenario1(int x) {
void *p = malloc(x);
return p;
}
void *scenario2(int y) {
int z = 10;
void *p = malloc(y * z);
return p;
}
void *scenario3() {
int a = getNumber();
void *p = malloc(a);
return p;
}

In the code above, let's identify the call-sites of malloc listing the filename and line-numbers. We can formulate it in the following query on the Ocular shell:

Ocular Query:

ocular> cpg.method.callOut.name("malloc").map(x => (x.location.filename, x.lineNumber.get)).l

Result:

List[(String, Integer)] = List(
("../../Projects/tarpitc/src/alloc/allocation.c", 23),
("../../Projects/tarpitc/src/alloc/allocation.c", 17),
("../../Projects/tarpitc/src/alloc/allocation.c", 11)
)

In the sample code, a clear indicator of zero allocation that can happen is scenario2 or scenario3 where arithmetic operations are happening in the data flow leading up to the parameter of malloc call-site. So let's try to formulate a query that lists the data flows with a source as parameters from the “scenario” methods and sinks as all malloc call-sites. We then find all the flows and filter the ones which have arithmetic operations on the data in the flow. This would be a clear indicator of the possibility of zero or incorrect allocation.

Ocular Query:

ocular> val sink = cpg.method.callOut.name("malloc").argument
ocular> var source = cpg.method.name(".*scenario.*").parameter
ocular> sink.reachableBy(source).flows.passes(".*multiplication.*").p

Result:

In the query above, we created local variables on Ocular shell called source and sink. The language is scala but as you can see is pretty verbose so I don’t have to explain you much, but still, for the sake of completeness, this is how we can explain the first statement in the Ocular query in English:

To identify the sink, find all call-sites (callOut) for all methods in the graph (cpg) with name as malloc and mark their arguments as sink.

In the code above, these will be x, (y * z) and a. You get the point 😉

Pretty cool, but you would say that this is trivial. Since we are explicitly marking a source method as a scenario. Lets level-up a bit now. What if we don’t want to go through all methods and then find if they are vulnerable. What if we could go from any arbitrary call site as a source to the malloc call site as a sink trying to find the data flow on which arithmetic operations are done? We can formulate a query in English where we define a source by first going through all call-sites of all methods, filtering OUT the ones having malloc (sink of interest) and any operation (not interesting), and then making the source as the return (methodReturn) of the actual methods of the callsites. In his case, these are ‘atoi’ and ‘getnumber’. Then find data-flows from these sources to the malloc callsite argument as a sink that has an arithmetic operation on the data in the flow. Sounds convoluted, but maybe Ocular Query can help explain this more programmatically:

Ocular Query:

ocular> val sink = cpg.method.callOut.name("malloc").argument
ocular> var source = cpg.method.callOut.nameNot(".*(<operator>|malloc).*").calledMethod.methodReturn
ocular> sink.reachableBy(source).flows.passes(".*(multiplication|addition).*").p

Result:

If this is not cool, then I’m outta here. I do not usually endorse technology strongly — because one day we all will die and all this will be someone else’s problem, but if security has to be done properly, this is how you have to do it. You can’t clean-up a flooded basement by pumping out water with buckets while the water drips from the ceiling. And taking hacksurance is the worst way to cop-out in my opinion

You have to dig deep and replace the leaking pipes to stop the flood.

In the next blog, I will show how we can make a Double Free Detector in just three lines of Scala with Ocular/Joern.

Originally posted by our Staff Scientist, Suchakra Sharma on https://suchakra.wordpress.com/2019/10/07/zero-day-snafus-hunting-memory-allocation-bugs/. You can follow him @tuxology on Twitter.


Zero-Day Snafus — Hunting Memory Allocation Bugs was originally published in ShiftLeft Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.


*** This is a Security Bloggers Network syndicated blog from ShiftLeft Blog - Medium authored by Suchakra Sharma. Read the original post at: https://blog.shiftleft.io/zero-day-snafus-hunting-memory-allocation-bugs-797e214fab6c?source=rss----86a4f941c7da---4