Intro
This post will try to do a small introduction to the QL language using real-world vulnerabilities that I found in the past, and it will end with a small challenge using QL.
A few months ago, I heard of Semmle QL for the first time, what they do is perform multiple code analysis techniques against source code, and dump these results into a database. Then using the QL language, you can query this data to perform variant analysis.
But what is variant analysis, and why it places a significant role in security? Variant analysis is the process of using known vulnerability as a start point to find similar problems in the code.
Multiple different techniques could be used to perform variant analysis, and I think that I can’t do a better job than Semmle explaining them, so here you go with the first reference.
Now that you know a little bit of the theory, let’s get our hands dirty digging into some QL queries to see what can we do.
From my experience with QL, the best way to learn is from practical examples, so during this blogpost, I will use a query that I created to discover two bugs in the barebox bootloader.
Previous inspiration
The idea of looking into bootloaders comes from a recent blogpost from Fermin J. Serna CTO of Semmle in this blogpost he talks about different vulnerabilities that they found in the u-boot bootloader. I was curious about the attack surface in this kind of software, so I search for another ‘big’ bootloader to play with, and I ended with Barebox.
But, what’s a boot loader? A Bootloader is a piece of code that runs before any operating system is running. Depending on your configuration the operative system could be loaded from multiple sources, a hard drive, a network resource etc.
One of the problems that Fermin and his team identified in u-boot is that the developers where reading uint32 values from the network and then using this values as the size in-memory operations to perform further network reads. Since network resources could be controlled by an attacker, this implies an important security risk.
Finding the bugs
With this in mind, we will be focusing on the load from a network resource. The first step will be identify how the developers of this boot loader perform network reads. Taking a quick look at the code, we could found the following functions declared in /include/net.h
So it seems that we now know what functions the developers are using to read things like uint32 and uint64, let’s jump into QL to search where these functions are used.
First steps with QL
There are multiple options on how to use QL, you can download pre-generated snapshots from LGTM and then query them locally using an eclipse plugin, or you can query them directly in LGTM. From my experience the first one is the most powerful (and fastest in terms of performance); also it has some extra additions like the possibility of seeing the paths of calls between functions and so on, but in this post, we will be using LGTM for the sake of simplicity.
You can access to a query console on LGTM with the Barebox project already loaded clicking here.
So let’s dig into QL. A QL query is defined by a Select clause which specifies what the result of the query should be. Then you can add more complexity to the query with other clauses like a From to perform variable declarations or Where statements where you can apply logical formulas.
Identifying function calls
One of the most powerful things of QL are the builtin libraries for different languages, in this builtin libraries we can find classes, predicates, or multiple other handy things. For example, we are now going to use the FunctionCall class. This class itself also contain multiple predicates like getTarget to get an instance of the function class that this call is for, or inherited predicates from the Call class like getArgument to get the different arguments of this call.
So with these basic concepts, we can define a FunctionCall called fc in the From clause, then obtain the name of the target function of this function call and compare it against the string “net_read_uint32“. Finally, we just need to print the fc variable in the Select statement.
And with this simple query we can get all the calls to “net_read_uint32” function, but reviewing all of the 103 results manually would be really tedious, so let’s keep digging into QL features.
Custom classes
Another thing that the Query languages allow us is define our own classes, with powerful things like inheritance. This step is something that is not really necessary, but helpful in case you wanted to build more complex queries.
As you can see, it is as simple as defining a new class that extends the FunctionCall class, then we perform some “filters” in the constructor filtering only FunctionCalls to memcpy functions.
Getting call parameters
Now that we have all the calls, we can get more details about them. For example, the parameters, if you recall from the beginning of the article, we are looking for untrusted data that could end in the size parameter of a memcpy. To access the parameters of a FunctionCall instance, we can use one of his predefined predicates, getArgument. this predicate will receive a number as an argument (the position of the desired argument) and then it will return an object of type Expr, since the argument can be a hardcoded string, an arithmetic operation, a variable and so on.
As you can see, now our query return not only the call to the memcpy but also the second parameter of this call.
So at this point, we know how to locate the origin of the data “net_read_uint32” calls and the juicy destination of this data, memcpy operations. But how can we determine if there is any connection between them?
Data Flow and Taint Tracking
To do this we can use another of the powerful QL libraries, in this case the DataFlow analysis one, more specifically the TainTracking module.
There are different ways to use this module, from just using it straight into the Where statement as described here, or something more configurable, creating a custom class that extends the TainTracking module.
import semmle.code.cpp.dataflow.TaintTracking class MyTaintTrackingConfiguration extends TaintTracking::Configuration { MyTaintTrackingConfiguration() { this = "MyTaintTrackingConfiguration" } override predicate isSource(DataFlow::Node source) { ... } override predicate isSink(DataFlow::Node sink) { ... } }
As you can see in the previous snippet the TaintTracking module have two required predicates isSource, used to define where taint may flow from, and isSink that defines where taint may flow to. When this is defined, the newly created configuration can be used with the predicate hasFlow.
This will leave us with just 6 results an amount small enough to discard false positives manually (If I recall correctly there were 7 when I did this, but LGTM is now using a more recent version with the patch for the CVEs already applied).
The bugs
This is not perfect, and we can still have false positives (Indeed all of them are since as I mentioned before LGTM is using the latest commit from the GitHub repo that contains all the fixes), but the amount of results to manually verify is much smaller. So let’s dig it into some of them.
In this example we have a read from network using net_read_uint32 on line 313, this value is then assigned to a variable called count and later into the program flow the variable will be used in a memcpy to copy into the pointer name (previously defined in the code as char name[256];) but if you notice in line 314 there is a size check against the count parameter that will end in a bailout if this value is greater than 255. Making this operation safe.
Let’s check now a vulnerable example.
In this case, we see a read from the network on line 516 using the net_read_uint32 function, this read is then assigned to the variable rlen that will be later used in line 528 as the size parameter of a memcpy without any intermediate checks.
Apart from this one, there was another part of the code affected by the same issue. This issues were reported to the project maintainer and fixed within three days with the corresponding patches.
One week later Mitre assigned the CVEs CVE-2019-15937 and CVE-2019-15938
Future Steps
Now that you have some insights about how to use QL and hopefully you are interested in trying it out, I will give you a query idea that should be easy to implement with the things learned in this blogpost and a bit of research.
Imagine the following code snippet (This was found using QL in a real-world sandboxing solution, unfortunately fortunately, it was not exploitable):
assert(dlen <= longest_path_elt);
memcpy(scratch, dir, dlen);
The problem with asserts is that normally assert calls are removed during compilation time if the build is marked as production via for example the NDEBUG macro. So if this is the case, the size check will no longer be there in the final binary, creating a potential memory corruption.
With this in mind my challenge for you is, try to build a QL query to detect variables used in an assert call (remember that assert is a macro, not a function) that will be later used as the size parameter in a memcpy call.
With the things learned in this post and some digging into the QL documentation, this should be doable, so good luck!
Finally, I would like to thank Nico Waisman for is amazing help and also recommend his twitter account. He posts QL snippets to detect new and old vulnerabilities, and from my short experience with QL this was one of the most useful things to learn. Since not only you will find real-world use cases, but also because he uses classes and predicates that you don’t found without really digging into the documentation.
Send me the assert query to hector @ the domain of this website if you manage to create it to make me happy! (I will also try to hook you up with a Sensepost t-shirt if you are the first one).