Friday, May 15, 2015

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:

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:

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:

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

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

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.