Friday, May 15, 2015

Using mprotect(.., .., PROT_NONE) on Linux

After deciding to revisit some old code of mine (ok, very old), I realized that there was something different about how Linux was allocating pages of data I wanted to hide.   At first, I was glad that I couldn't see the data using yarascan, but then I realized that I was unable to access the memory regions at all in linux_volshell to verify that they were, in fact, obfuscated. So I decided to take a look at using a smaller, stripped down program. Below is one such example, comments are included to explain what is happening:

int main( int argc, char *argv[]){ // pid: the process ID of this process // so we can print it out int pid; pid = getpid(); //size: an integer to hold the current page size int size; size = getpagesize(); //[1] create two pointers in order to allocate //memory regions char *buffer; char *buffer2; //unprotected buffer: //allocate memory using mmap() buffer2 = (caddr_t) mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, 0,0); //[2] put some characters in the allocated memory //we're setting these characters one at a time in order //to avoid our strings being detected from the binary itself buffer2[0] = 'n'; buffer2[1] = 'o'; buffer2[2] = 't'; buffer2[3] = ' '; buffer2[4] = 'h'; buffer2[5] = 'e'; buffer2[6] = 'r'; buffer2[7] = 'e'; //protected buffer: //allocate memory with mmap() like before buffer = (caddr_t) mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, 0,0); //[2] put some characters in the allocated memory //we're setting these characters one at a time in order //to avoid our strings being detected from the binary itself buffer[0] = 'f'; buffer[1] = 'i'; buffer[2] = 'n'; buffer[3] = 'd'; buffer[4] = ' '; buffer[5] = 'm'; buffer[6] = 'e'; //[3] protect the page with PROT_NONE: mprotect(buffer, size, PROT_NONE); //[4] print PID and buffer addresses: printf("PID %d\n", pid); printf("buffer at %p\n", buffer); printf("buffer2 at %p\n", buffer2); //spin until killed so that we know it's in memory: while(1); return 0; }
Now we'll just compile the above program and run it. In short, the above program [1] creates two buffers, [2] places characters in these buffers, [3] then calls mprotect() on one of them with PROT_NONE; [4] the program then prints out its process ID and the virtual addresses of the aforementioned buffers:

$ ./victim PID 29620 buffer at 0x7f2bec9a1000 buffer2 at 0x7f2bec9a2000
Let's set up a config file so that we don't have to type as much:

$ cat linux.config [DEFAULT] LOCATION=file:///Path/to/Virtual%20Machine/Linux%20Mint%20Cinnamon/Mint%2064-bit-d583834d.vmem PROFILE=LinuxLinuxMintCinnamonx64x64 PID="29620"
If we try to search for the strings we placed in the buffers we get:

$ python vol.py --conf-file=linux.config linux_yarascan -Y "not here" Volatility Foundation Volatility Framework 2.4 Task: victim pid 29620 rule r1 addr 0x7f2bec9a2000 0x7f2bec9a2000 6e 6f 74 20 68 65 72 65 00 00 00 00 00 00 00 00 not.here........ 0x7f2bec9a2010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x7f2bec9a2020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ [snip] $ python vol.py --conf-file=linux.config linux_yarascan -Y "find me" Volatility Foundation Volatility Framework 2.4

Well that was disappointing, we couldn't find the "find me" string. What you would expect, is to be able to access the memory contents using Volatility. Let's use the linux_volshell plugin to explore the victim process' memory and see if we can access the memory addresses (given to us from the program at run time) directly. First we'll examine the contents at 0x7f2bec9a2000:

$ python vol.py --conf-file=linux.config linux_volshell Volatility Foundation Volatility Framework 2.4 >>> Current context: process victim, pid=29620 DTB=0x3cfb1000 >>> db(0x7f2bec9a2000) 0x7f2bec9a2000 6e 6f 74 20 68 65 72 65 00 00 00 00 00 00 00 00 not.here........ 0x7f2bec9a2010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x7f2bec9a2020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x7f2bec9a2030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x7f2bec9a2040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x7f2bec9a2050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x7f2bec9a2060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x7f2bec9a2070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
As we see, we have the contents that we'd expect. Now to try to access the other memory location:

>>> db(0x7f2bec9a1000) Memory unreadable at 7f2bec9a1000
We see that we've been rejected. After a bit of investigation, we see that the point of failure is in the entry_present() function in the amd64.py address space. In order to find the value of the entry that failed, we'll print it out. First we'll add a print statement to entry_present():

def entry_present(self, entry): if entry: if (entry & 1): return True arch = self.profile.metadata.get('os', 'Unknown').lower() # The page is in transition and not a prototype. # Thus, we will treat it as present. if arch == "windows" and ((entry & (1 << 11)) and not (entry & (1 << 10))): return True # we want a valid entry that hasn't been found valid: print hex(entry) return False

Now let's see what the entry is:
$ python vol.py --conf-file=linux.config linux_yarascan -Y "find me" Volatility Foundation Volatility Framework 2.4 0x2681d160
So let's look a little closer at the page table entry:

>>> print "{0:b}".format(0x2681d160) 100110100000011101000101100000
The way we read this is from right to left.  The entry_present() function checks the 0th bit to see if the entry is present. In this case the bit is not set (0); therefore this function returns False.

At this point, I decided to take a look at the Linux source code to see how mprotect() is actually implemented. As it turns out, the _PAGE_PRESENT bit is cleared when mprotect(...PROT_NONE) is called on a page and the _PAGE_PROTNONE bit is set [3]. Looking at how _PAGE_PROTNONE is defined [4][5][6] we'll see that it's actually equivalent to the global bit (8th bit) [1][2]. So let's look at our page table entry again, we'll notice that the 8th bit is indeed 1:

>>> print "{0:b}".format(0x2681d160) 100110100000011101000101100000


So let's patch the entry_present() function with our findings:

def entry_present(self, entry): if entry: if (entry & 1): return True arch = self.profile.metadata.get('os', 'Unknown').lower() # The page is in transition and not a prototype. # Thus, we will treat it as present. if arch == "windows" and ((entry & (1 << 11)) and not (entry & (1 << 10))): return True # Linux pages that have had mprotect(...PROT_NONE) called on them # have the present bit cleared and global bit set if arch == "linux" and ((entry & (1 << 8))): return True return False


Let's see if we get anything back:
$ python vol.py --conf-file=linux.config linux_yarascan -Y "find me" Volatility Foundation Volatility Framework 2.4 Task: victim pid 29620 rule r1 addr 0x7f2bec9a1000 0x7f2bec9a1000 66 69 6e 64 20 6d 65 00 00 00 00 00 00 00 00 00 find.me......... 0x7f2bec9a1010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x7f2bec9a1020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ [snip]

Success! So now we're able to see the "forbidden" string that we couldn't access before.  This goes to show that sometimes you have to dig a little into "why" something is not working as expected. In this case, we lucked out and had source code to examine, but sometimes things are not as easy as all that. Both intel.py and amd64.py have been patched in order to accommodate memory sections that have had mprotect() called on them with PROT_NONE.

References

[1] AMD64 Architecture Programmer's Manual Volume 2: System Programming http://developer.amd.com/wordpress/media/2012/10/24593_APM_v21.pdf

1 comment:

  1. I love that you linked to Dr. Baker's pages, but I'm sad the links are broken :( Stupid internet.

    Great post! :)

    ReplyDelete