This post is partially an analysis of a double-free vulnerability (CVE-2019-11932) in an image processing library used by WhatsApp and partially a reference for on-device harness development when fuzzing native libraries on Android. I first learned of this vulnerability by reading a blog post from Awakened, the researcher who disclosed the issue. The author did not elaborate on how this issue was found, and I wanted to understand how hard it would be to rediscover the bug. As we will see, the vulnerability itself is fairly shallow and is easy to reproduce by fuzzing the vulnerable library with AFL++.
This CVE is particularly interesting because the vulnerable library code (android-gif-drawable < v1.2.18) could be triggered remotely by sending someone a malformed GIF file. This primitive was not perfect as it relied on the target taking some manual actions, like opening the WhatsApp image gallery. Additionally, this vulnerability would only be part of a larger component chain that would include additional vulnerabilities, for example, to perform information leaks and to escalate privileges. Still, these types of vulnerabilities are rare and expensive because of the potential human intelligence value they provide. This case also illustrates why it is so important that applications audit the libraries they include in their code base. Large enterprises should perhaps do more to contribute to and improve the security of Open-Source Software (OSS) they employ in their products. A more recent, analogous example resulted in the disclosure of five vulnerabilities in libxml2.
Based on Awakened’s vulnerability writeup, I focused my efforts on the GIF decoding routine. A GIF file is structured as a header and logical screen descriptor followed by a stream of records for each frame. These records consist of an image descriptor (width, height, position and palette), optional extension blocks (transparency, delays, etc), and compressed pixel data. In decoding.c there is a function, DDGifSlurp, which walks the GIF record streams and builds up per-frame metadata. If decode=true, it extracts raw per-frame pixels. Normally, frames are the same size. This makes sense because when you look at a GIF, you see a series of frames playing in a loop. When frames are the same size, the function will keep reusing the allocation it has created to store the buffer (rasterBits). However, the function does handle cases where the frames are of a different size by calling reallocarray to allocate a new buffer. The realloc function is a combination of free and malloc. If no size is provided, it simply frees the pointer.
Commit df309bb - decoding.c here
If we imagine the first frame has some normal dimensions,40*10, a buffer of 400 bytes is allocated. The second frame has some malformed dimensions, 0*20. In this case, the following is true:
When reallocarray is called, the allocation size is calculated as 0*20=0; this causes rasterBits to be free'd. If the third frame has similarly malformed dimensions, it frees the same pointer again, resulting in a double-free.
Symbols are important; they make it easier to interpret what a piece of code is doing. If you analyze libraries that were extracted from an Android Package Kit (APK), they will most likely be stripped of symbols. In our particular case, for android-gif-drawable, this is not an issue because we have access to the source. However, if you need to reverse engineer a closed-source binary, you should at least apply the Java Native Interface (JNI) types to make the research process more straightforward. There is a post by @Ch0pin you can read here to give you some more background. In my case, I am using Binary Ninja, and I found a working type header file that can be imported here.
To understand how to apply the types, you can search the decompiled APK for native declarations. In the screenshot below, we can see some of the android-gif-drawable declarations from the APK in JEB.
Take getFrameDuration as an example:
Here, J translates to jlong and I translates to jint. Notice that the function also has a return type of jint. If we combine these values with the standard calling convention for native JNI invocation, we end up with:
You can apply some automation to this process (using androguard, JEB API, etc). With proper type mappings you can programmatically walk every class and apply all identified types to the library you are analyzing.
Some engineering is required to parse the APK, extract the call definitions and apply them in your preferred decompiler. This effort is worth it because it reduces the amount of manual labor, and it can give you a high-level overview of native library use in the APK as a whole.
The first thing I did (since the library is open-source) was build my own version of android-gif-drawable from the v1.2.17 release package using the Android NDK. Then, I reviewed what exports were available in the binary:
This is helpful information because we know we can call DDGifSlurp directly, and we can also see the set of JNI-exported functions. If we look at DDGifSlurp again, we see that the first argument is a pointer to some complex type, GifInfo.
Commit df309bb - gif.h here
We could manually create a fake GifInfo object; however, the object is quite big and is itself a composite of other complex types (like GifFileType). Instead, it makes more sense to investigate the other native functions to see how GifInfo objects are usually created. We can quickly find some potential candidates.
Commit df309bb - gif.c here
Of these, the byte variants seem to have the least overhead; in particular, openByteArray only requires us to create a jbyteArray object, which we can do easily in C.
Note that the GifInfo object itself is created by createGifInfo.
Commit df309bb - init.c here
In the above code snippet, you can see that the initialization function also calls DDGifSlurp, but it is not able to trigger the vulnerable code because decode=false. Setting this flag to false triggers the isInitialPass case within DDGifSlurp, which only records per-frame metadata without parsing the frames.
At this point, we have a pretty good understanding of how to call the vulnerable code path, and we can put together a series of calls to reach the function we want to fuzz.
However, we are missing two elements here. First, if we create thousands of these call chains, we will run out of memory and crash our harness, so we need to make sure to free any resources we create. To achieve this, we can use another of the JNI-exported functions.
Commit df309bb - dispose.c here
The second missing element is much less obvious. When DDGifSlurp initializes the GIF, it walks the list of frames, modifying the GifInfo object as it goes. Before we can process the GIF again, we need to rewind it to its initial state. Doing so resets our position in the ByteArrayContainer to the start position and resets some properties of the GifInfo object, as can be seen below.
Commit df309bb - controle.c here
Our final call chain looks like this:
We can create a test binary that will take a GIF from disk and pass it through our call chain. Notice that we include a jenv header file (sourced from this Quarkslab post), and we also include the gif header directly from the android-gif-drawable library itself.
Typically, for fuzzing, we would be in one of three scenarios:
In our case, openByteArray has a pretty straightforward prototype, so we are in this second category, where we are able to create the function arguments from C without additional dependencies.
The code above will read an image from disk, initialize the Java Virtual Machine (JVM), create a jbyteArray and pass the image through our call chain. At the end, we print some properties from the GifInfo object so we can get some metadata and confirm the code ran to completion without error. Later, we can use this test binary to debug any crashes we may find.
With the difficult part behind us, we can create a fuzzing harness by wrapping the test code in some AFL++ boilerplate. In main, we initialize the Java VM and then create a function which takes a byte array as input. This function, fuzz_one_input, will perform all the actions necessary to step through our call chain once with the input provided. We will use Frida to hook this function so AFL can pass inputs to it and collect coverage.
The small Frida script below lets AFL++ pass inputs to the harness. It injects a tiny C-hook that copies each AFL test case straight into the function input buffer, tells AFL++ exactly where to restart on each iteration, and leverages Frida's instrumentation to collect coverage.
Finally, we can kick off fuzzing on the phone.
I let the fuzzer run for about 7 hours before ending the run. We can see from the AFL output below, we executed over 200M test cases and recorded 29.1k crashes, of which 42 were saved. AFL applies some heuristics based on signal type, faulting address and edges in the coverage map to determine if a crash is sufficiently interesting to keep. This doesn't mean that each of these crashes is unique.
If we want to triage crashes, we can augment the vulnerable library by adding some additional print statements that will give us more insights into what happens inside the DDGifSlurp decode condition.
For our convenience, we can also recompile test_DDGifSlurp with ASan, which will give us more verbose information on what went wrong at runtime without necessarily having to dive into LLBD immediately.
There are a few variations of this vulnerability that can be triggered depending on the size and composition of the GIF frames.
In this example, we can see a variation that is almost identical to the one Awakened has in their blog post.
Parsers are obviously tricky to get right, and it is easy to make mistakes or have mismatched assumptions about what data the parser will process. This vulnerability was very easy to rediscover using our harness. In fact, the very first crash was reported just a few minutes after the start of the run.
In cases where these types of libraries are included in highly security-critical applications, like messaging apps, they should definitely be subjected to extensive manual and automated testing. If application engineers don't validate library code, it's clear that researchers will (and they may or may not report their findings, no judgments).
This bug was reported and fixed in 2019, but I was curious and did some investigation into the issue history of the repository. To my surprise, I found an issue from 2016 that was closed due to inactivity, which almost certainly relates to this same vulnerability.
The user reports a crash from Java_pl_droidsonroids_gif_GifInfoHandle_renderFrame, which is the typical way the library uses the vulnerable DDGifSlurp call. In our harness, we do not call this function because:
These actions are compute intensive if we perform them thousands of times per second, and we don’t need them to exercise the vulnerable function.
I expect many researchers in the vulnerability research (VR) community are monitoring open and closed GitHub issues from open-source libraries that are loaded by sensitive applications. Given how high-profile WhatsApp is as a target, it’s doubtful that I am the first to look at this specific issue, and others, in the android-gif-drawable library. I would not be surprised at all if this bug was known before 2019.
1. How a double-free bug in WhatsApp turns to RCE - here
2. Patched GIF Processing Vuln Still Affects Mobile Apps - here
3. Android greybox fuzzing with AFL++ Frida mode - here
4. Fuzzing Redux, leveraging AFL++ Frida-Mode on Android native libraries - here
5. android-gif-drawable - here