Plugin Post: Robust Process Scanner
It's pretty well known, in memory forensics circles, that there are two common ways of finding processes in memory images: list-walking, which traverses the kernel's linked list of process data structures, and scanning, which does a sweep over memory, looking for byte patterns that match the data found in a process data structure.
Having two different ways of finding processes can be very handy, especially when we suspect that someone may be trying to hide processes. One common way of hiding processes in Windows is called DKOM (Direct Kernel Object Manipulation); this technique works by just unlinking the process you want to hide from the kernel's list, like so:
This makes it invisible from programs such as the task manager, as well as memory forensic tools that use list-walking (including Volatility's pslist). However, such hidden processes can still be found by scanning memory using a signature for the process data structure; this is what psscan2 does.
Unfortunately, it's been known since at least 2007 (as mentioned in AAron Walters and Nick Petroni's Blackhat DC talk, and more recently in a presentation by Jesse Kornblum) that even signature scans can be evaded by crafty attackers. Signatures typically rely on "magic" values found in the process data structure. For example, in Windows XP, process data structures always begin with "\x03\x00\x1b\x00", which makes it pretty easy to find them in memory images.
But is that magic value really essential to the correct functioning of a process in Windows? What if an attacker just overwrites those four bytes with zeroes? As it turns out, Windows will be perfectly happy to keep running the process! At the same time, it will be completely hidden from existing forensic tools. What's more, as I demonstrated in my paper for CCS 2009 (Robust Signatures for Kernel Data Structures), around 51 fields in the process data structure can be manipulated by attackers in this way – including nearly all of the fields currently used to find processes.
So what's a forensic analyst to do? Luckily, there are some parts of the process data structure that are hard for an attacker to mess with without causing one of these:
So if we can build a signature based on these fields, we can find processes that existing signature scanners might miss.
And that's just what I've done. Here, for your consideration and consumption, is the creatively-named psscan3 (just drop it into the memory_plugins directory of Volatility 1.3.2). It uses a only fields that have been identified as "robust" to locate processes in Windows memory. It's a bit slower than the existing scanners, right now, because it's checking for more things.
If you want to try it out, you might also want to download this sample memory image, which has a hidden process at offset 0x01a4bc20. In Volatility, pslist, psscan, and psscan2 all miss the process, but psscan3 detects it, as shown in this exciting screenshot (click to enlarge; the windows show, from left to right, psscan, psscan2, and psscan3) [EDIT: Blogger is for some reason refusing to link to the larger size; click here to view it]:
If you'd like a copy of the rootkit that hid this process (which is based on the FU Rootkit), send me an e-mail (but be warned that I probably won't be able to dig up the source until this fall).
Comments
$ python volatility psscan3 -f /g/ds_fuzz_hidden_proc.img
YARA is not installed, see http://code.google.com/p/yara-project/
c:\Python26\lib\site-packages\Crypto\Hash\MD5.py:6: DeprecationWarning: the md5
module is deprecated; use hashlib instead
from md5 import *
PID PPID Time created Time exited Offset PDB
Remarks
------ ------ ------------------------ ------------------------ ---------- -----
----- ----------------
Scanner () on Offset 0 Error: pop from empt
y list
Traceback (most recent call last):
File "volatility", line 219, in
main()
File "volatility", line 215, in main
command.execute()
File "c:\Volatility\Volatility\memory_plugins/psscan3.py", line 169, in execut
e
scan_addr_space(search_space,scanners)
File "c:\Volatility\Volatility\forensics\win32\scan2.py", line 218, in scan_ad
dr_space
o.process(chunk,as_offset+poffset, metadata=metadata)
File "c:\Volatility\Volatility\forensics\win32\scan2.py", line 148, in process
self.process_buffer(buf,self.offset,metadata)
File "c:\Volatility\Volatility\memory_plugins/psscan3.py", line 53, in process
_buffer
match_count = self.check_addr(buf, i)
File "c:\Volatility\Volatility\memory_plugins/psscan3.py", line 39, in check_a
ddr
val = func(buff,found)
File "c:\Volatility\Volatility\memory_plugins/psscan3.py", line 99, in check_v
adroot
val = read_obj_from_buf(buf, types, field, found)
File "c:\Volatility\Volatility\forensics\object.py", line 250, in read_obj_fro
m_buf
(offset, current_type) = get_obj_offset(data_types,member_list)
File "c:\Volatility\Volatility\forensics\object.py", line 204, in get_obj_offs
et
current_type = member_list.pop()
IndexError: pop from empty list
any ideas??
What version of Volatility are you using? I've tested the plugin with the most recent stable version of Volatility (1.3.2), which you can get by doing:
svn checkout http://volatility.googlecode.com/svn/tags/Volatility-1.3.2
I am curious if by adding these new checks for process discovery do you no longer identify some processes that have exited, which may violate some of the assumptions or rules because the OS has freed or zeroed out those parts of the EPROCESS?
Can you try using this with a clean copy of Volatility 1.3.2 (i.e., check out the source to a new directory, and then drop psscan3 in memory_plugins), and let me know if the problem persists?
Thanks,
Brendan
Great point! This is one of the things we mention in the paper. The signature used in the scanner I posted was trained on live processes. When a process exits, one of the things that gets zeroed out is the ObjectTable member, which is used in this signature. You can actually just comment out the check:
self.add_constraint(self.check_object_table)
This doesn't introduce any false positives, but it does allow exited processes to be found. And the whole thing remains as robust as before :)
spaz:memory_plugins larry$ ls
example1.py example2.py example3.py example4.py
example1.pyc example2.pyc example3.pyc example4.pyc
spaz:memory_plugins larry$ cp ../../Volatility-1.3_Beta/memory_plugins/psscan3.py .
spaz:memory_plugins larry$ cd ..
spaz:Volatility-1.3.2 larry$ python volatility psscan3Traceback (most recent call last):
File "volatility", line 219, in
main()
File "volatility", line 215, in main
command.execute()
File "/Volumes/Shared/vola/Volatility-1.3.2/memory_plugins/psscan3.py", line 164, in execute
space = FileAddressSpace(self.opts.filename)
File "/Volumes/Shared/vola/Volatility-1.3.2/forensics/addrspace.py", line 44, in __init__
self.fhandle = open(fname, mode)
TypeError: coercing to Unicode: need string or buffer, NoneType found