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 points 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 from 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 in our team ShiftLeft, which has one of its feet deep in the no-so-shady Berlin hacker scene (the other feet is ground firmly in the super-shady Silicon Valley circles). 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 – like 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 function. 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 more smaller than what was subsequently required (by memcpy for example)? It may lead to a buffer overflow when the accessing or writing to it.

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 its upto the user to make sure that there are NULL checks before using the allocated memory.

  • Intra-Chunk Heap Overflow: One of my favorite heap exploit that I have seen multiple times in the wild and have been victim of myself, is a case where in 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 uses 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 same or 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 as Code Property Graph – CPG (its 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.

CPG-layers-flows

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:

list-methods

 

Detecting Allocation Bugs with Ocular/Joern

Lets 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, lets 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 upto the parameter of malloc call-site. So lets try to formulate a query which 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 an arithmetic operations on the data in the flow. This would be a clear indicator of 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:

scenario1

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 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 source to malloc call site as 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 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 sink which have any 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:

scenario3

If this in 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 im my opinion :-/  You have dig deep and replace the leaking pipes to stop the flood.

Get down in the code and fix yo’ s**t.

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

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s