Thursday, October 13, 2011

Ain't Nuthin But a K(Timer) Thing, Baby

Last Volatility Friday, I hinted that kernel timers objects (KTIMER) can be enumerated in physical memory dumps to help with analyzing ZeroAccess, Rustock, and other rootkits. This week, we'll discuss a bit more about what exactly timers are used for, how they are installed, and how to detect them - both the wrong way(s) and the right way(s). Volatility is the only memory forensics framework giving you ability to work with kernel timers, which is just another reason why I think this tool is unique and exciting.

What are Kernel Timers?

Timer Objects and DPCs are most often used by malware for synchronization and notification. A rootkit driver may create a timer to simply be notified when a given time elapses. If you think this is similar to just calling Sleep(), then you're right. However, calling Sleep() puts your thread to sleep and prevents it from performing other actions while you wait. Also, Sleep() doesn't create any interesting forensic artifacts, so its a good thing when malware uses timers instead. But functionality-wise, you can also create timers that reset after expiring. In other words, instead of just being notified once, you can be notified on a periodic basis. Maybe the rootkit wants to check if a DNS host name resolves every 5 minutes, or poll a given registry key for changes every 2 seconds. Timers are great for these types of tasks.

When you create a timer, you can supply a DPC routine (Deferred Procedure Call) - otherwise known as a callback function. When the timer expires, the system calls your callback function. The address of your callback is stored in the KTIMER structure along with information on when (and how often) to execute the callback. And now you should understand why kernel timers are such useful artifacts for malware analysis. Rootkits will load drivers in kernel memory, but try hard to stay undetected. But their use of timers gives us a clear shot directly to where the rootkit is hiding in memory. All we need to do is find the KTIMERs.

ETIMER, KTIMER and Windows API

You may be familiar with the fact that every executive process (EPROCESS) has a nested KPROCESS. Every executive thread (ETHREAD) has a nested KTHREAD. The same is true for timers. Every ETIMER has a nested KTIMER. We need to figure out the difference between these structures and then determine how to find the right ones. Let's take a look at them in Windbg:
kd> dt _ETIMER
nt!_ETIMER
+0x000 KeTimer : _KTIMER
+0x028 TimerApc : _KAPC
+0x058 TimerDpc : _KDPC
+0x078 ActiveTimerListEntry : _LIST_ENTRY
+0x080 Lock : Uint4B
+0x084 Period : Int4B
+0x088 ApcAssociated : UChar
+0x089 WakeTimer : UChar
+0x08c WakeTimerListEntry : _LIST_ENTRY

kd> dt _KTIMER
ntdll!_KTIMER
+0x000 Header : _DISPATCHER_HEADER
+0x010 DueTime : _ULARGE_INTEGER
+0x018 TimerListEntry : _LIST_ENTRY
+0x020 Dpc : Ptr32 _KDPC
+0x024 Period : Int4B
KTIMER.DueTime and KTIMER.Period store information on when the timer expires. KTIMER.Dpc is a KDPC whose DeferredRoutine points to the registered callback function. As you can see, KTIMER.TimerListEntry is a doubly-linked list that ties the various KTIMERs together. So as we are initially working through this, you may think that by finding all ETIMERs, you'll find all KTIMERs. Right? Keep reading to find out why that's dead wrong.

So which API functions do you use to work with timers? The following Native APIs are implemented in ntdll.dll and call through to the kernel via SSDT. The handles returned by NtCreateTimer and NtOpenTimer are for ETIMER objects, and can be seen in the process's handle table.
  • NtCreateTimer
  • NtOpenTimer
  • NtCancelTimer
  • NtQueryTimer
  • NtSetTimer

The following APIs implemented in ntoskrnl.exe work directly with KTIMER objects:

  • KeInitializeTimerEx
  • KeClearTimer
  • KeCancelTimer
  • KeSetTimer
  • KeCheckForTimer

If open ETIMERs can be seen in a process's handle table, then we can use Volatility's handles command to view them. Just pass the "-t Timer" parameters like this:

$ python vol.py -f mem.dmp handles -t Timer
Volatile Systems Volatility Framework 2.1_alpha
Offset(V) Pid Type Details
0x81fd5638 636 Timer ''
0x820d1a78 636 Timer ''
0x8217fcc0 636 Timer 'userenv: refresh timer for 636:1516'
0x81e406c8 636 Timer 'userenv: refresh timer for 636:1892'
0x823092c0 680 Timer ''
0x81f46200 680 Timer ''
0x8228c608 692 Timer ''
0x81e4f148 692 Timer ''
[...]

We make several observations from this output. First, timers set by usermode APIs can optionally have names (set with the ObjectAttributes parameter to NtCreateTimer). Second, and more important to our topic at hand is that none of the timers shown belong to kernel drivers (by "belong" I mean that none of the callbacks point to kernel mode functions). And this brings us to our next big discussion. How do we find timers with callbacks in kernel mode? A few options were considered:

1. ETHREAD.ActiveTimerListHead is a LIST_ENTRY of ETIMERs. However, it is only for active timers. Not what we want.

2. KTHREAD.Timer is a KTIMER, but again, not the same as the ones we want (you can walk the KTHREAD.Timer.TimerListEntry and see that there aren't many entries).

3. The nt!KiTimerTableListHead global symbol in ntoskrnl.exe. This is a promising possibility, but unfortunately Microsoft has not been keeping it very consistent across operating system. The KiTimerTableListHead points to a different type of data on 2000, XP, 2003, 2008, and Vista. Sadly, it doesn't even exist on Windows 7.

4. The ETIMER objects in process handle tables. Not what we want (as previously described) because it doesn't contain timers with kernel callbacks.

5. Pool Scanning. Some RE shows that ETIMERs exist in non-paged pools roughly 0x98 bytes in size, with the tag Tim\xE5. If we can find these pools, and thus all ETIMERs, then we'll find all KTIMERs, right?

Here we are again back at the question we've asked twice already. The answer is NO. Although ETIMER contains a nested KTIMER, it is possible to create a timer without allocation of a new ETIMER. For example, the KTIMER can be a variable in a driver's .data section. This is indicated by the fact that API functions such as KeInitializeTimer require the caller to allocate storage for the KTIMER. For example:

KTIMER Timer;
KeInitializeTimer(&Timer);

This is in fact how many drivers allocate storage for the timers they use. A real example can be seen in srv.sys:

.data:0001F990 ; struct _KTIMER ScavengerTimer
.data:0001F990 _ScavengerTimer
So the significance to this is - scanning for ETIMER objects and trying to reference the nested KTIMER will fail to locate the KTIMER used by srv.sys. KTIMERs don't need to exist in dynamically allocated pools at all.

The Best of the Worst

Despite KiTimerTableListHead changing so much across OS versions, its probably our best bet. On Windows 2000, KiTimerTableListHead was an array of 128 LIST_ENTRY structures for KTIMERs. For example it may have looked like this:
#define TIMER_TABLE_SIZE 128
#define LIST_ENTRY KiTimerTableListHead[TIMER_TABLE_SIZE];
On Windows XP SP0-SP3 x86 and Windows 2003 SP0, Microsoft expanded the array to 258 LIST_ENTRYs.
#define TIMER_TABLE_SIZE 256
#define LIST_ENTRY KiTimerTableListHead[TIMER_TABLE_SIZE];
On Windows XP x64, Windows 2003 SP1-SP2, and Vista SP0-SP2, Microsoft not only expanded it, but changed the type of data it pointed to. It was changed to 512 KTIMER_TABLE_ENTRY structures.
#define TIMER_TABLE_SIZE 512

typedef struct _KTIMER_TABLE_ENTRY {
LIST_ENTRY Entry;
ULARGE_INTEGER Time;
} KTIMER_TABLE_ENTRY, *PKTIMER_TABLE_ENTRY;

#define KTIMER_TABLE_ENTRY KiTimerTableListHead[TIMER_TABLE_SIZE];
Starting with Windows 7, there is no more KiTimerTableListHead. The KTIMERs can be found by walking a list pointed to by the KPCR (KPCR.PrcbData.TimerTable.TimerEntries). Credits for finding the Windows 7 list goes to Matt Suiche who wrote a Windbg plugin supporting Windows XP and Windows 7.

So despite the variance among data structures, its annoying but certainly not impossible to write a Volatility plugin that supports XP, 2003, 2008, Vista, and 7. In fact, it can be done in less than 150 lines of Python, including comments, and without relying on PDB support! The plugin has been commited to malware.py and will be considered one of the new plugins fully released with the 2.1 alpha version of the malware plugins later this year.

Malware Analysis with Kernel Timers

Here's the moment we've been waiting for! You may recall a snippet of last week's post about detecting ZeroAccess due to its use of timers with a callback function in an unknown region of kernel memory.

$ python vol.py -f zeroaccess2.vmem timers
Volatile Systems Volatility Framework 2.1_alpha
Offset DueTime Period(ms) Signaled Routine Module
0x805598e0 0x00000084:0xce8b961c 1000 Yes 0x80523dee ntoskrnl.exe
0x820a1e08 0x00000084:0xdf3c0c1c 30000 Yes 0xb2d2a385 afd.sys
0x81ebf0b8 0x00000084:0xce951f84 0 - 0xf89c23f0 TDI.SYS
[snip]
0x81dbeb78 0x00000131:0x2e896402 0 - 0xf83faf6f NDIS.sys
0x81e8b4f0 0x00000131:0x2e896402 0 - 0xf83faf6f NDIS.sys
0x81eb8e28 0x00000084:0xe5855f6a 0 - 0x80534e48 ntoskrnl.exe
0xb20bbbb0 0x00000084:0xd4de72d2 60000 Yes 0xb20b5990 UNKNOWN
0x8210d910 0x80000000:0x0a7efa36 0 - 0x80534e48 ntoskrnl.exe
0x82274190 0x80000000:0x711befba 0 - 0x80534e48 ntoskrnl.exe
0x81de9690 0x80000000:0x0d0c3e8a 0 - 0x80534e48 ntoskrnl.exe

Beautiful! The UNKNOWN indicates that the memory can't be attributed to any legitimate kernel driver. How about a Rustock.C variant?

$ python volatility.py timers -f rustock-c.vmem 
Volatile Systems Volatility Framework 1.4_rc1
Offset DueTime Period(ms) Signaled Routine Module
0xf730a790 0x00000000:0x6db0f0b4 0 - 0xf72fb385 srv.sys
0x80558a40 0x00000000:0x68f10168 1000 Yes 0x80523026 ntoskrnl.exe
0x80559160 0x00000000:0x695c4b3a 0 - 0x80526bac ntoskrnl.exe
0x820822e4 0x00000000:0xa2a56bb0 150000 Yes 0x81c1642f UNKNOWN
0xf842f150 0x00000000:0xb5cb4e80 0 - 0xf841473e Ntfs.sys
0xf70d00e0 0x00000000:0x81eb644c 0 - 0xf70c18de HTTP.sys
0xf70cd808 0x00000000:0x81eb644c 60000 Yes 0xf70b6202 HTTP.sys
0x81e57fb0 0x00000000:0x6a4f7b16 30000 Yes 0xf7b62385 afd.sys
0x81f5f8d4 0x00000000:0x6a517bc8 3435 Yes 0x81c1642f UNKNOWN
0x82055218 0x00000000:0x6cb1d516 10000 Yes 0xf8a126c4 watchdog.sys
0x82022530 0x00000000:0x6cb1d516 10000 Yes 0xf8a126c4 watchdog.sys
0x82007270 0x80000000:0x139ab60a 0 - 0x80534016 ntoskrnl.exe
0x82041b40 0x00000098:0x9f1d5f32 0 - 0xf83fafdf NDIS.sys
0x8207acc0 0x80000000:0x0f13ff2e 0 - 0x80534016 ntoskrnl.exe
0x81f7eaf4 0x00000000:0x6d0082b0 20000 Yes 0x81c1642f UNKNOWN
0x82035308 0x00000000:0x74442ce8 60000 Yes 0xf83fb72c NDIS.sys
0x81f2f158 0x80000000:0x520f0f9e 0 - 0x80534016 ntoskrnl.exe

Ahah! Rustock registers three different timers, pointing to the same unknown region of memory, but that expire at different time periods.

What Can't You Do with Volatility?

With a bit of research, a little coding, and some malware samples to test with, there isn't much you can't do with Volatility. Timers with callbacks pointing to unknown kernel memory regions are important artifacts that you don't want to miss. Please join me in welcoming the timers plugin to the malware forensic space!

ZeroAccess, Volatility, and Kernel Timers

As today is Volatility Friday, we'll discuss how to hunt ZeroAccess in memory by following the lead of several others and then writing our own custom plugin. I first want to recognize the work done on this topic by Frank Boldewin, Giuseppe Bonfa, and Marco Giuliani. They are the ones doing the reverse engineering here - I am just translating their findings and showing how to do things "the Volatility way."

The First Sample: e6823f932c2b40d2025d78bb78d64458

This is the same sample Frank used in his analysis (see link above) so you will want to read that post first. Frank's course of actions to find ZeroAccess were:

1) Find the DEVICE_OBJECT that starts with ACPI#PNP[...]
2) Find the DRIVER_OBJECT to which the device belongs (note the driver is unnamed)
3) Take the DriverStart and DriverSize members of the DRIVER_OBJECT to dump the rootkit's kernel code
4) Search for threads executing in the same space as the rootkit's kernel code
5) Find the process with a fake usermode ADS name (i.e. a colon ':' in the name)
6) Dump the malicious process

Okay so let's get to work with Volatility! We'll follow nearly the same order of operations that Frank used, however to start instead of looking in the \global?? directory for the DEVICE_OBJECT, we'll just use the devicetree plugin.
$ python vol.py -f zeroaccess1.vmem devicetree
Volatile Systems Volatility Framework 2.1_alpha
DRV 0x022dba18 '\\Driver\\NetBT'
---| DEV 0x81dc7d30 NetBT_Tcpip_{9B5A1EAD-7852-455F-9740-5E1FCD05D812} FILE_DEVICE_NETWORK
---| DEV 0x82251ca0 NetBt_Wins_Export FILE_DEVICE_NETWORK
---| DEV 0x81eb3030 NetbiosSmb FILE_DEVICE_NETWORK
DRV 0x022e1c08 '\\FileSystem\\MRxDAV'
---| DEV 0x81caca58 WebDavRedirector FILE_DEVICE_NETWORK_FILE_SYSTEM
DRV 0x022e3880
---| DEV 0x81f3ee48 ACPI#PNP0303#2&da1a3ff&0 FILE_DEVICE_UNKNOWN
DRV 0x022f9f38 '\\Driver\\intelppm'
---| DEV 0x81c8cdd8 (unnamed) FILE_DEVICE_UNKNOWN
There's a unnamed DRIVER_OBJECT at physical offset 0x022e3880 that has a DEVICE_OBJECT at virtual address 0x81f3ee48 with name ACPI#PNP[...]. That seems to correlate with Frank's findings, so it feels like we're on the right track. The next thing I want to do is figure out the DriverStart and DriverSize of the DRIVER_OBJECT. One could easily modify the devicetree plugin to print this information (we'll show how to do it programatically in a minute) but for the time being I'll just use volshell. The values displayed will be in decimal.
$ python vol.py -f zeroaccess1.vmem volshell
Volatile Systems Volatility Framework 2.1_alpha
Current context: process System, pid=4, ppid=0 DTB=0x319000
To get help, type 'hh()'

In [1]: dt("_DEVICE_OBJECT", 0x81f3ee48)
[CType _DEVICE_OBJECT] @ 0x81F3EE48
0x0 : Type 3
0x2 : Size 184
0x4 : ReferenceCount 3
0x8 : DriverObject 2181970048

In [2]: dt("_DRIVER_OBJECT", 2181970048)
[CType _DRIVER_OBJECT] @ 0x820E3880
0x0 : Type 4
0x2 : Size 168
0x4 : DeviceObject 2180247112
0x8 : Flags 18
0xc : DriverStart 2987057152
0x10 : DriverSize 126976
0x14 : DriverSection 2180702128
0x18 : DriverExtension 2181970216

In [3]: hex(2987057152)
Out[3]: '0xb20ae000'

In [4]: hex(126976)
Out[4]: '0x1f000'

Now that we know the driver's kernel code exists at 0xb20ae000, we can dump it with moddump.
$ python vol.py -f  zeroaccess1.vmem moddump -o 0xb20ae000 -D zeroaccess/
Volatile Systems Volatility Framework 2.1_alpha
Dumping Unknown, Base: b20ae000 output: driver.b20ae000.sys

$ strings zeroaccess/driver.b20ae000.sys
!This program cannot be run in DOS mode.
X6Rich
.text
`.rdata
@.data
.reloc

PVVV
Ul;Upw
!This program cannot be run in DOS mode.
Rich
.text

RSA1
A8C,
!This program cannot be run in DOS mode.
Rich
.text
PPPTh
ZwCreateFile
ZwTestAlert
ntdll.dll
ExitProcess
KERNEL32.dll
1234567890123456789012345678901234567890123456789012345678901234

CryptAcquireContextW
CryptImportKey
CryptCreateHash
CryptHashData
CryptVerifySignatureW
CryptDestroyHash
CryptReleaseContext
MD5Init
MD5Update

ntp2.usno.navy.mil
ntp.adc.am
tock.usask.ca
ntp.crifo.org
ntp1.arnes.si
ntp.ucsd.edu
ntp.duckcorp.org
wwv.nist.gov
clock.isc.org

Based on the "This program cannot[...]" strings, the driver has multiple embedded PE files, so you can already begin to expect code injection or at least a user mode presence. Hold that thought.

The 4th step in Frank's analysis was to check for suspicious threads based on their starting or current IP location. You can do this easily with the threads plugin by filtering for orphan threads (-F OrphanThread).
$ python vol.py -f  zeroaccess1.vmem threads -F OrphanThread
Volatile Systems Volatility Framework 2.1_alpha
------
ETHREAD: 0x81c6b4a0 Pid: 4 Tid: 908
Tags: OrphanThread,SystemThread
Created: 2011-10-13 14:26:18
Exited: -
Owning Process: 0x823c8830 'System'
Attached Process: 0x823c8830 'System'
State: Waiting:WrQueue
BasePriority: 0x8
Priority: 0x8
TEB: 0x00000000
StartAddress: 0xb20b6105
ServiceTable: 0x80552fa0
[0] 0x80501b8c
[1] -
[2] -
[3] -
Win32Thread: 0x00000000
CrossThreadFlags: PS_CROSS_THREAD_FLAGS_SYSTEM
b20b6105: 58 POP EAX
b20b6106: 870424 XCHG [ESP], EAX
b20b6109: ffd0 CALL EAX
b20b610b: 8b0df8bb0bb2 MOV ECX, [0xb20bbbf8]
b20b6111: ff2500920bb2 JMP DWORD [0xb20b9200]
b20b6117: 64a118000000 MOV EAX, [FS:0x18]
As you can see, this thread's StartAddress is 0xb20b6105 - well within range of the ZeroAccess driver. By using the callbacks plugin, you can also see there's a suspicious IoRegisterShutdownNotification routine pointing at approximately the same range in kernel memory.
$ python vol.py -f  zeroaccess1.vmem callbacks
Volatile Systems Volatility Framework 2.1_alpha
Type Callback Owner
PsSetCreateProcessNotifyRoutine 0xf87ad194 vmci.sys
KeBugCheckCallbackListHead 0xf83e65ef NDIS.sys (Ndis miniport)
KeBugCheckCallbackListHead 0x806d77cc hal.dll (ACPI 1.0 - APIC platform UP)
KeRegisterBugCheckReasonCallback 0xf8b7aab8 mssmbios.sys (SMBiosData)
KeRegisterBugCheckReasonCallback 0xf8b7aa70 mssmbios.sys (SMBiosRegistry)
KeRegisterBugCheckReasonCallback 0xf8b7aa28 mssmbios.sys (SMBiosDataACPI)
KeRegisterBugCheckReasonCallback 0xf82e01be USBPORT.SYS (USBPORT)
KeRegisterBugCheckReasonCallback 0xf82e011e USBPORT.SYS (USBPORT)
KeRegisterBugCheckReasonCallback 0xf82f7522 VIDEOPRT.SYS (Videoprt)
IoRegisterShutdownNotification 0xf88ddc74 Cdfs.SYS (\FileSystem\Cdfs)
IoRegisterShutdownNotification 0xf8bb05be Fs_Rec.SYS (\FileSystem\Fs_Rec)
IoRegisterShutdownNotification 0xf8303c6a VIDEOPRT.SYS (\Driver\VgaSave)
IoRegisterShutdownNotification 0xb2d108fa vmhgfs.sys (\FileSystem\vmhgfs)
IoRegisterShutdownNotification 0xf8bb05be Fs_Rec.SYS (\FileSystem\Fs_Rec)
IoRegisterShutdownNotification 0xf8303c6a VIDEOPRT.SYS (\Driver\mnmdd)
IoRegisterShutdownNotification 0xf8303c6a VIDEOPRT.SYS (\Driver\vmx_svga)
IoRegisterShutdownNotification 0xf8303c6a VIDEOPRT.SYS (\Driver\RDPCDD)
IoRegisterShutdownNotification 0xb20b49d0
IoRegisterShutdownNotification 0xf86aa73a MountMgr.sys (\Driver\MountMgr)
IoRegisterShutdownNotification 0xf8bb05be Fs_Rec.SYS (\FileSystem\Fs_Rec)
Next we'll try and locate ZeroAccess in user mode based on Frank's findings. At first I didn't understand what "Fake Usermode ADS" meant, but when I used the pslist and dlllist commands it all started to make sense. The image name from pslist comes from EPROCESS.ImageFileName (in kernel memory) and the info from dlllist comes from the PEB (in user memory). In normal cases, output from the two plugins will match, but in this case you can see the discrepancy:
$ python vol.py -f zeroaccess1.vmem pslist
Volatile Systems Volatility Framework 2.1_alpha
Offset(V) Name PID PPID Thds Hnds Time
---------- -------------------- ------ ------ ------ ------ -------------------
0x823c8830 System 4 0 57 398 1970-01-01 00:00:00
0x820df020 smss.exe 376 4 3 19 2010-10-29 17:08:53
0x821a2da0 csrss.exe 600 376 11 342 2010-10-29 17:08:54
[snip]
0x820d4b28 3418651033 1148 668 1 5 2011-10-13 14:26:18
0x81c6ada0 cmd.exe 1244 1664 0 ------ 2011-10-13 14:37:07

$ python vol.py -f zeroaccess1.vmem dlllist -p 1148
Volatile Systems Volatility Framework 2.1_alpha
************************************************************************
3418651033 pid: 1148
Command line : 3418651033:1720073012.exe
Service Pack 3

Base Size Path
0x00400000 0x000330 C:\WINDOWS\3418651033:1720073012.exe
0x7c900000 0x0af000 C:\WINDOWS\system32\ntdll.dll
0x7c800000 0x0f6000 C:\WINDOWS\system32\kernel32.dll
Kernel memory says the image name is 3418651033 but user memory says its 3418651033:1720073012.exe. Either way, you can dump it with procexedump.
$ python vol.py -f  zeroaccess1.vmem procexedump -p 1148 -D zeroaccess/
Volatile Systems Volatility Framework 2.1_alpha
************************************************************************
Dumping 3418651033, pid: 1148 output: executable.1148.exe

$ strings zeroaccess/executable.1148.exe
!This program cannot be run in DOS mode.
Rich
.text
PPPTh
ZwCreateFile
ZwTestAlert
ntdll.dll
ExitProcess
KERNEL32.dll
If you recall the embedded PE strings we viewed from the extracted kernel driver, you'll recognize that this is one of them. One down, one to go. How can we locate the other one - with our only bit of knowledge being that we expect it to be somewhere in user mode memory? How about malfind?
$ python vol.py -f zeroaccess1.vmem malfind -D zeroaccess/
Process: services.exe Pid 668
VAD: 0x950000-0x954fff Vad PAGE_EXECUTE_READWRITE
0x00950000 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 MZ..............
0x00950010 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0x00950020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x00950030 00 00 00 00 00 00 00 00 00 00 00 00 d0 00 00 00 ................
0x00950040 0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68 ........!..L.!Th
0x00950050 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f is.program.canno
0x00950060 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20 t.be.run.in.DOS.
0x00950070 6d 6f 64 65 2e 0d 0d 0a 24 00 00 00 00 00 00 00 mode....$.......
Dumped to services.exe.82073020.950000.954fff.dmp

$ strings zeroaccess/services.exe.82073020.950000.954fff.dmp
!This program cannot be run in DOS mode.
Rich
.text
`.rdata
@.data

ZwCreateFile
ZwTestAlert
ntdll.dll
ExitProcess
KERNEL32.dll
1234567890123456789012345678901234567890123456789012345678901234
RtlImageDirectoryEntryToData
RtlAddressInSectionTable
RtlImageNtHeader
ZwOpenFile
Bingo! There's the other embedded PE that the kernel driver must inject into services.exe.

Building a Custom ZeroAccess Volatility Plugin

I'm sure many readers are wondering, how hard is it to automate the actions that we've done. Well, you're in luck...its not too difficult at all. In fact, I've written a sample plugin to assist with your understanding of the Volatility API. Let's get started. First define a class with the name of your plugin. Mine will be "ZeroAccess" and it will inherit from DriverScan (since we want to scan for DRIVER_OBJECTs) and ProcExeDump (since we want to rebuild/dump PE files).
class ZeroAccess(filescan.DriverScan, procdump.ProcExeDump):
"""Dump the ZeroAccess Driver and Files"""

def __init__(self, config, *args):
# remove all of procdump's options except DUMP_DIR
procdump.ProcExeDump.__init__(self, config, *args)
config.remove_option("UNSAFE")
config.remove_option("PID")
config.remove_option("OFFSET")
Then we add a function to the class that can handle the rebuilding of PE files - most of which was taken from the dlldump source code.
    def dump_pe(self, space, start, file_name):
"""Dump a PE file to disk"""

if not self._config.DUMP_DIR:
return

full_path = os.path.join(self._config.DUMP_DIR, file_name)
sys.stdout.write("Dumping PE: {0}\n".format(full_path))
of = open(full_path, 'wb')

try:
for chunk in self.get_image(sys.stdout, space, start):
offset, code = chunk
of.seek(offset)
of.write(code)
except ValueError:
pass

of.close()
Most of the heavy lifting will be done in the calculate function. I won't describe each step individually like I did in the Abstract Memory Analysis: Zeus Encryption Keys post, because its already pretty well commented and we described all the steps above.
    def calculate(self):

space = utils.load_as(self._config)

# enumerate system threads (0x00000010 = PS_CROSS_THREAD_FLAGS_SYSTEM)
system_threads = [t for t in modscan.ThrdScan(self._config).calculate() if t.CrossThreadFlags & 0x00000010]

# find and dump the malicious kernel driver
for item in filescan.DriverScan(self._config).calculate():

# unpack the parameters
(object, driver, extension, object_name) = item

# looking for unnamed driver objects
if driver.DriverName.Length != 0:
continue

# the first and only device should be ACPI#PNP[...]
device = obj.Object("_DEVICE_OBJECT",
offset = driver.DeviceObject, vm = space)

# get the device's object header
object = obj.Object("_OBJECT_HEADER", \
offset = device.obj_offset - \
device.obj_vm.profile.get_obj_offset("_OBJECT_HEADER", "Body"), \
vm = space)

object.kas = space
device_name = object.get_object_name()

# did we find zeroaccess?
if not str(device_name).startswith("ACPI#PNP"):
continue

sys.stdout.write("DriverObject: {0:#x}\n".format(device.DriverObject))
sys.stdout.write(" DriverStart: {0:#x}\n".format(driver.DriverStart))
sys.stdout.write(" DriverSize: {0:#x}\n".format(driver.DriverSize))
sys.stdout.write("DeviceObject: {0:#x} {1}\n".format(device.obj_offset, device_name))

# dump the driver
file_name = "Driver.{0:#x}.sys".format(driver.DriverStart)
self.dump_pe(space, driver.DriverStart, file_name)

# now what we know the memory range, look for bad threads
for thread in system_threads:

if thread.StartAddress > driver.DriverStart and \
thread.StartAddress < driver.DriverStart + driver.DriverSize:

sys.stdout.write("Bad Thread: {0:#x} Tid {1}\n".format(\
thread.obj_offset,
thread.Cid.UniqueThread))

# now find and dump the fake usermode ADS process
for proc in tasks.pslist(space):

for dll in proc.Peb.Ldr.InLoadOrderModuleList.list_of_type(\
"_LDR_DATA_TABLE_ENTRY", "InLoadOrderLinks"):

# look for the ADS name
if str(dll.BaseDllName).find(":") != -1:

sys.stdout.write("Fake ADS EXE: {0} pid {1} base {2:#x}\n".format(\
proc.ImageFileName,
proc.UniqueProcessId,
dll.DllBase))

file_name = "Dll.{0:#x}.{1:#x}.dll".format(proc.obj_offset, dll.DllBase)
self.dump_pe(proc.get_process_address_space(), dll.DllBase, file_name)
Testing the ZeroAccess Volatility Plugin

Here is some example output from our new plugin:
$ python vol.py -f  zeroaccess1.vmem zeroaccess -D zeroaccess/
Volatile Systems Volatility Framework 2.1_alpha
DriverObject: 0x820e3880
DriverStart: 0xb20ae000
DriverSize: 0x1f000
DeviceObject: 0x81f3ee48 ACPI#PNP0303#2&da1a3ff&0
Dumping PE: zeroaccess/Driver.0xb20ae000.sys
Bad Thread: 0x1e6b4a0 Tid 908
Fake ADS EXE: 3418651033 pid 1148 base 0x400000
Dumping PE: zeroaccess/Dll.0x820d4b28.0x400000.dll

$ ls zeroaccess/
Dll.0x820d4b28.0x400000.dll Driver.0xb20ae000.sys
That's it folks! In just a small Python script, we identified the malicious DRIVER_OBJECT, the malicious DEVICE_OBJECT, the Fake Usermode ADS, the "stealth" ETHREAD, and extracted a few of the PE files. Now grab IDA Pro and enjoy reversing the unpacked binaries.

The Second Sample: 505b071b3eead7ded32a766c98efb6ef

Realizing that there are thousands of ZeroAccess variants floating around, I wanted to try and duplicate the findings on at least one other sample. Here's the output of our new plugin on the next sample we tried:
$ python vol.py -f  zeroaccess2.vmem zeroaccess -D zeroaccess/
Volatile Systems Volatility Framework 2.1_alpha
DriverObject: 0x81f523f0
DriverStart: 0xb2158000
DriverSize: 0xd000
DeviceObject: 0x81fc2370 ACPI#PNP0303#2&da1a3ff&0
Dumping PE: zeroaccess/Driver.0xb2158000.sys
As you can see, a few of the artifacts were found and extracted, but not all of them. It turns out this sample is quite different than the first. For one, it doesn't use the Fake Usermode ADS. Instead, you can spot the malicious process because its mapped from a suspicious namespace:
$ python vol.py -f zeroaccess2.vmem dlllist -p 1136
************************************************************************
svchost.exe pid: 1136
Command line : "\\.\globalroot\Device\svchost.exe\svchost.exe"
Service Pack 3

Base Size Path
0x00400000 0x0002d0 \\.\globalroot\Device\svchost.exe\svchost.exe
0x7c900000 0x0af000 C:\WINDOWS\system32\ntdll.dll
0x7c800000 0x0f6000 C:\WINDOWS\system32\kernel32.dll
What is \\.\globalroot\Device\svchost.exe? Surely that device doesn't exist on a clean system. Let's check with devicetree again and see if anything looks different than the first ZeroAccess sample:
$ python vol.py -f zeroaccess2.vmem devicetree
DRV 0x01e109b8 '\\Driver\\03621276'
---| DEV 0x820ec030 svchost.exe FILE_DEVICE_DISK

DRV 0x01df6718 '\\Driver\\kmixer'
---| DEV 0x825347d0 (unnamed) FILE_DEVICE_KS
DRV 0x0214f4c8 '\\Driver\\03621275'
---| DEV 0x81fe8030 (unnamed) FILE_DEVICE_ACPI
DRV 0x021523f0
---| DEV 0x81fc2370 ACPI#PNP0303#2&da1a3ff&0 FILE_DEVICE_UNKNOWN
I've highlighted almost all of the output in red, because...well, almost all if it is malicious. This variant of ZeroAccess creates 3 DRIVER_OBJECTs and 3 DEVICE_OBJECTs. The patterns can be expressed as the following:

1) a DRIVER_OBJECT with all numbers in the name (03621276) with a DEVICE_OBJECT named svchost.exe and type FILE_DEVICE_DISK

2) a DRIVER_OBJECT with all numbers in the name (03621275) with an unnamed DEVICE_OBJECT and type FILE_DEVICE_ACPI

3) an unnamed DRIVER_OBJECT with DEVICE_OBJECT named ACPI#PNP[...] (this is the one we already explored)

Modifying our new plugin to handle these additional cases is trivial, thus I'll leave it as an exercise to the reader. However, the one thing you may run into is that the DriverStart and DriverSize for the first two DRIVER_OBJECTs has been zeroed out:
$ python vol.py -f zeroaccess2.vmem volshell
Volatile Systems Volatility Framework 2.1_alpha
Current context: process System, pid=4, ppid=0 DTB=0x319000
To get help, type 'hh()'

In [1]: dt("_DEVICE_OBJECT", 0x820ec030)
[CType _DEVICE_OBJECT] @ 0x820EC030
0x0 : Type 3
0x2 : Size 184
0x4 : ReferenceCount 2
0x8 : DriverObject 2176911800

In [2]: dt("_DRIVER_OBJECT", 2176911800)
[CType _DRIVER_OBJECT] @ 0x81C109B8
0x0 : Type 4
0x2 : Size 168
0x4 : DeviceObject 2182004784
0x8 : Flags 4
0xc : DriverStart 0
0x10 : DriverSize 0
0x14 : DriverSection 0
0x18 : DriverExtension 2176911968
0x1c : DriverName \Driver\03621276
0x24 : HardwareDatabase 0
0x28 : FastIoDispatch 0
0x2c : DriverInit 2176398992

In [3]: hex(2176398992)
Out[3]: '0x81b93690'
So although the DriverStart and DriverSize members are zero, making it difficult to dump the driver's code, we can still get a hint of where the code exists because the DriverInit value is not zero. We're looking around kernel addresses 0x81b93690. Cross reference that with orphan threads:
$ python vol.py -f  zeroaccess2.vmem threads -F OrphanThread
Volatile Systems Volatility Framework 2.1_alpha
------
ETHREAD: 0x81fa4bc8 Pid: 4 Tid: 416
Tags: OrphanThread,SystemThread
Created: 2011-10-13 04:29:10
Exited: -
Owning Process: 0x823c8830 'System'
Attached Process: 0x823c8830 'System'
State: Waiting:DelayExecution
BasePriority: 0x8
Priority: 0x8
TEB: 0x00000000
StartAddress: 0x81b9a505
ServiceTable: 0x80552fa0
[0] 0x80501b8c
[1] -
[2] -
[3] -
Win32Thread: 0x00000000
CrossThreadFlags: PS_CROSS_THREAD_FLAGS_SYSTEM
81b9a505: 58 POP EAX
81b9a506: 870424 XCHG [ESP], EAX
81b9a509: ffd0 CALL EAX
81b9a50b: 8b0d8cf7b981 MOV ECX, [0x81b9f78c]
81b9a511: ff25a4cdb981 JMP DWORD [0x81b9cda4]
81b9a517: 64a118000000 MOV EAX, [FS:0x18]
The last thing I wanted to point out is the callbacks output. This version of ZeroAccess not only installs the IoRegisterShutdownNotification, but also a CmRegisterCallback - for monitoring or preventing access to certain registry keys.
$ python vol.py -f  zeroaccess2.vmem callbacks
Volatile Systems Volatility Framework 2.1_alpha
Type Callback Owner
PsSetCreateProcessNotifyRoutine 0xf87ad194 vmci.sys
KeBugCheckCallbackListHead 0xf83e65ef NDIS.sys (Ndis miniport)
KeBugCheckCallbackListHead 0x806d77cc hal.dll (ACPI 1.0 - APIC platform UP)
KeRegisterBugCheckReasonCallback 0xf8b7aab8 mssmbios.sys (SMBiosData)
KeRegisterBugCheckReasonCallback 0xf8b7aa70 mssmbios.sys (SMBiosRegistry)
IoRegisterShutdownNotification 0x81b934e0 UNKNOWN (\Driver\03621276)
[snip]
IoRegisterShutdownNotification 0xf8bb05be Fs_Rec.SYS (\FileSystem\Fs_Rec)
IoRegisterShutdownNotification 0xf853c2be ftdisk.sys (\Driver\Ftdisk)
IoRegisterShutdownNotification 0x805cdef4 ntoskrnl.exe (\FileSystem\RAW)
IoRegisterShutdownNotification 0xf83d98f1 Mup.sys (\FileSystem\Mup)
IoRegisterShutdownNotification 0x805f5d66 ntoskrnl.exe (\Driver\WMIxWDM)
CmRegisterCallback 0x81b92d60 UNKNOWN (--)
ZeroAccess Rogue Kernel Timers

In early January 2011, when Frank Boldewin and I were working on finding artifacts of Rustock.B and Rustuck.C in memory, he suggested that a plugin for analyzing kernel timers be added to Volatility. I drafted up a copy and then forgot about it until he reminded me this week, and guess what - just in time to be useful. I will go into depth on kernel timers next Volatility Friday, but for now you can see yet another strong factor in memory that indicates something suspicious is going on.
$ python vol.py -f zeroaccess2.vmem timers
Volatile Systems Volatility Framework 2.1_alpha
Offset DueTime Period(ms) Signaled Routine Module
0x805598e0 0x00000084:0xce8b961c 1000 Yes 0x80523dee ntoskrnl.exe
0x820a1e08 0x00000084:0xdf3c0c1c 30000 Yes 0xb2d2a385 afd.sys
0x81ebf0b8 0x00000084:0xce951f84 0 - 0xf89c23f0 TDI.SYS
0x8055b200 0x00000086:0x1c631c38 0 - 0x80534a2a ntoskrnl.exe
0xf842f270 0x00000084:0xd0fea092 0 - 0xf84111b4 Ntfs.sys
0xb27f3990 0x00000084:0xd36a83fa 0 - 0xb27e4385 srv.sys
0x805516d0 0x00000084:0xda9d7dbc 60000 Yes 0x804f3eae ntoskrnl.exe
0x80550ce0 0x00000084:0xd11d9f24 0 - 0x8053b8fc ntoskrnl.exe
0x80551800 0x00000084:0xcebda77e 1000 Yes 0x804f3716 ntoskrnl.exe
0x82255720 0x80000084:0xb14adbda 0 - 0x80534e48 ntoskrnl.exe
[snip]
0x81dbeb78 0x00000131:0x2e896402 0 - 0xf83faf6f NDIS.sys
0x81e8b4f0 0x00000131:0x2e896402 0 - 0xf83faf6f NDIS.sys
0x81eb8e28 0x00000084:0xe5855f6a 0 - 0x80534e48 ntoskrnl.exe
0xb20bbbb0 0x00000084:0xd4de72d2 60000 Yes 0xb20b5990 UNKNOWN
0x8210d910 0x80000000:0x0a7efa36 0 - 0x80534e48 ntoskrnl.exe
0x82274190 0x80000000:0x711befba 0 - 0x80534e48 ntoskrnl.exe
0x81de9690 0x80000000:0x0d0c3e8a 0 - 0x80534e48 ntoskrnl.exe
Conclusion and Source Code

Thanks again to Frank, Guiseppe, and Marco for sharing the results of their analysis and ultimately making this blog post possible (without doing my own RE).

Download the ZeroAccess Volatility Plugin here. Play with it, learn from it, enoy it!

Sunday, September 25, 2011

Abstract Memory Analysis: Zeus Encryption Keys

Community Momentum is Rising!

The amount of research pouring out of the Volatility community recently has been very exciting. Over the past few weeks, we've seen Russ McRee of HolisticInfoSec.org / toolsmith write about Memory Analysis with DumpIt and Volatility. Frank Boldewin published a CSI:Internet article titled A Trip Into RAM that focused on detecting Spyeye. @bradarndt wrote a lengthy Zeus Analysis in Volatility 2.0 just days after releasing a new plugin called Vscan that automatically submits files extracted from memory to online virus scanners. evild3ad also wrote a piece on Zeus just days after providing a nice installation tutorial for Volatility 2.0 on Ubuntu.

Last week, Evilcry (@Blackmond_) updated his Carberp Reverse Engineering post to show some of the many ways to detect Carberp in memory...my favorite being the detection of NT syscall hooks. Several of our primary developers have launched the 64-bit branch of Volatility in preparation for the 2.1 release. @gleeda just published her Timeliner, Registry API, Event log plugins and corresponding documentation (also makes a great tutorial on writing your own Volatility plugins).

To keep the momentum going, I'll discuss one of the more abstract ways that you can leverage Volatility.
We'll take a look at how to locate and extract Zeus's RC4 encryption keys, then use the keys to decrypt other configuration data found in memory. The plugins we'll discuss were written well over a year ago and kept private due to sensitivity of the key recovery methods. It wasn't exactly a secret previously (ThreatExpert disclosed some juicy details back in May) but it also wasn't common sense knowledge. Earlier today, TrustDefender announced their finding of Zeus Trojan Update - new variants based on leaked zeus source code. The new versions reportedly use AES instead of RC4 and intentionally try to clear memory of the encryption keys. That means for certain we're not disclosing anything the attackers don't already know.

Zeus Versions

The earliest variants of Zeus (circa 2006) encrypted configurations using a very simple algorithm that I described in the original report called [Prg] Malware Case Study. We won't discuss these versions anymore - they don't use encryption keys so there's nothing fun to try and recover.

Near the end of 2008 (I'm going to say at approximately version 1.2.0), Zeus started using unique 256-byte RC4 keys embedded in each binary to decrypt configurations. It prevented analysts from using the key from one sample for decrypting the configuration used by another sample. I designed an unpacker-like tool that ran on live VMs and could extract the key without infecting the system (thus you could loop through hundreds of samples without reverting). Then you could decrypt configurations in a brute-force like manner, by cycling through all of the extracted keys until one "unlocked" it.

The sample I'll be using that represents all Zeus versions after 1.2.0 but before 2.x is MD5
b5709cd23d8ad91d79062ad63e8516ed.

The present day Zeus variants (2.x) still use RC4 encryption, but many also have a hardware-locking feature so that a sample recovered from one machine won't run on another machine (such as a sandbox or analysis system). These variants hide data in randomly generated registry keys and directories on the file system. They also use a separate 256-byte RC4 key for the configuration and for the stolen data files.

The sample I'll be using that represents Zeus versions 2.x is MD5
b93fb47257f052f6bc99f0b96b718cd4.

Recovering keys for Zeus versions > 1.20 and < 2.0

These variants of Zeus define a data structure with the 256-byte RC4 key at offset 0x2A. The challenge is finding those exact 256 bytes in a memory dump that is potentially several gigabytes large. Talk about needle in a haystack, right? One of the more obvious solutions is look for patterns among the other structure members and develop a scanner with constraints. For example, if the member at offset 0 was always 0x12345678 and member at offset 4 was always between 0x30 and 0x45, then you have some criteria to start searching. However, that's not the road I took.

While reversing some samples, I noticed a pointer to the base of the structure can be found at one of the most unexpected locations. As shown in the image below, after the Zeus "body" is injected into a process and fully unpacks itself, the pointer is in the Import Address Table (IAT) directly after the ws2_32!closesocket entry.

Whether this was done on purpose by the Zeus authors or accidentally is beyond me. I don't know how things like this happen by accident, nor do I know any reasons to do intentionally. However, since hundreds of thousands of samples were created using the same Zeus builder, they all share the same secret.

Now that we know what we're looking for, let's discuss how to find it. Here is a brief description of the steps:

1. Find Zeus. We'll cycle through the active process list. For each process, we'll traverse the VAD tree. For every memory segment, we'll check if there is injected code (i.e. a private, virtually-allocated region with VadS tag and no file mapping). This is all rather simple, since its exactly how malfind has located Zeus infections for years.

2. Find closesocket in the IAT. This step sounds straight-forward, but its not. Due to Zeus' packing and obfuscation, the names of imported functions are not always available. Thus, we can't parse the IAT looking for "closesocket" as normal PE header tools do. Instead, we leverage Volatility's ImpScan plugin, which scans for calls to imported functions using a "reverse lookup" kind of algorithm (described fully in Malware Analyst's Cookbook Recipe 16-8: Scanning for Imported Functions with Impscan).

3. Find the structure pointer. As previously described, the pointer is the next 4 bytes after the closesocket entry. So we dereference those 4 bytes as a pointer, add 0x2A to reach the RC4 key structure member, and read 256-bytes.

4. Write the plugin. Now we know what we need to do. Let's do it! We'll need to import volatility.obj for access to the Object class, volatility.win32.tasks for access to process enumeration, volatility.utils for access to the Hexdump function, and volatility.plugins.malware for the ability to inherit from the ImpScan plugin.
import volatility.obj as obj
import volatility.utils as utils
import volatility.win32.tasks as tasks
import volatility.plugins.malware as malware
Now we define a class that inherits from malware.ImpScan. The name of our class (ZeusScan1) will automatically become the name of the command that you type, for example "python vol.py zeusscan1"
class ZeusScan1(malware.ImpScan):
"Scan for and dump Zeus RC4 keys"

def __init__(self, config, *args):
malware.ImpScan.__init__(self, config, *args)

A majority of the work happens in the calculate function. As described above, we begin by iterating through active processes. For each process, we acquire an address space (an object referring to the process's private virtual memory) and also enumerate the loaded DLLs which we'll need to locate ws2_32.dll.
    def calculate(self):
addr_space = malware.get_malware_space(self._config)

RC4_KEYSIZE = 0x102

# cycle through the active processes
for p in self.filter_tasks(tasks.pslist(addr_space)):

# get the process address space
ps_ad = p.get_process_address_space()
if ps_ad == None:
continue

# enumerate DLLs (inherited from DllList)
mods = self.list_modules(p
Next we traverse the VAD segments and filter by private allocations with no file mapping and 'MZ' at the base. Any segments that satisfy this criteria are almost certainly injected Zeus binaries.
           # traverse the VAD
for vad in p.VadRoot.traverse():

if vad == None:
continue

# only looking for short VADs (non-memory mapped)
if (vad.Tag != "VadS"):
continue

# only looking for private, virtually-alocated memory
if vad.u.VadFlags.PrivateMemory == 0:
continue

# find the start and end range
start = vad.get_start()
end = vad.get_end()
data = vad.get_data()

# check for PE headers at the base
if data[0:2] != 'MZ':
continue
Now comes Step 2. We need to find where closesocket is located in process memory. This is done by first locating ws2_32.dll and then calling getprocaddress() - an extension to the module that parses its EAT and returns the requested function's virtual address.
               # locate the winsock2 module
winsock = malware.find_module_by_name(mods, "ws2_32.dll")

if winsock == None:
continue

# resolve the address of closesocket export
closesocket = winsock.getprocaddress("closesocket")

if closesocket == None:
continue
The next thing we do is use call_scan which is inherited from malware.ImpScan. It is a generator that yields tuples in the following order: address of the CALL instruction, constant (IAT entry address), and call destination (the API function address). To clear up any confusion around these values, let's go over a quick example. Say you have the following code in a disassembly ".text:00120408 CALL DWORD PTR:[0010022c] ; kernel32.CreateFileW".

In the example, 00120408 is the address of the CALL instruction. 0010022c is the constant. If you dereference the constant, you'll find a DWORD sized value that points to kernel32.CreateFileW. So what we're doing is scanning for CALLs that lead to ws2_32!closesocket. We're taking the constant, adding 4 bytes, and then using the DWORD at that location as a pointer to the structure that contains the RC4 key. Without further ado, here's the rest of the code:
               # scan for calls to imported functions (inherited from ImpScan)
calls = list(self.call_scan(ps_ad, data, start))

for (addr_of_call, const, call_dest) in calls:

if call_dest != closesocket:
continue

# read the DWORD directly after closesocket
struct_base = obj.Object('Pointer', offset = const + 4, vm = ps_ad)

# to be valid, it must point within the vad segment
if (struct_base < start) or (struct_base > (start + end)):
continue

# grab the key data
key = ps_ad.read(struct_base + 0x2a, RC4_KEYSIZE)

# greg's sanity check
if len(key) != RC4_KEYSIZE or key[-2:] != "\x00\x00":
continue

yield p, struct_base, key
We yield 3 values from the calculate function. These 3 values are automatically passed to render_text if you're using text-mode output. Thus we also must define a render_text method in our plugin.
    def render_text(self, outfd, data):

for p, struct_base, key in data:
hex = "\n".join(["{0:#010x} {1:<48} {2}".format(struct_base + 0x2a + o, h, ''.join(c)) for o, h, c in utils.Hexdump(key)])
outfd.write("Process: {0} {1}\n".format(p.UniqueProcessId, p.ImageFileName))
outfd.write(hex)
outfd.write("\n")
5. Test the plugin. When you run zeusscan1 against a memory dump infected with one of the these Zeus samples, you'll see output like this:
$ python vol.py -f XPSP3-0e4e1fd4.vmem zeusscan1
Volatile Systems Volatility Framework 2.1_alpha

Process: 624 winlogon.exe
0x00ac602a 76 e6 ce 65 3e d9 5e 0c 0a 8f 14 81 f6 dc ee 2f v..e>.^......../
0x00ac603a bd 1a 10 0d 98 63 61 34 9d 8e 75 1c b6 b8 b7 56 .....ca4..u....V
0x00ac604a 4a bb d1 99 37 a2 40 4f e0 77 80 52 3f db c4 73 J...7.@O.w.R?..s
0x00ac605a 69 21 96 eb 0b 28 7f 90 d2 e5 46 d0 39 2a 51 43 i!...(....F.9*QC
0x00ac606a ca 1e a0 62 8c fe c8 15 78 8b 9b d4 2d a4 da ef ...b....x...-...
0x00ac607a 47 66 b1 ec 93 c1 87 e3 a9 b9 4c 9f 6a 86 20 d3 Gf........L.j...
0x00ac608a a5 8d df 72 3b 44 e1 cb 85 92 04 74 f2 84 33 a6 ...r;D.....t..3.
0x00ac609a f3 ae 2b c5 94 f5 7c 50 c9 1b de 83 ad cc 68 60 ..+...|P......h`
0x00ac60aa c2 6e 09 c7 a7 16 fa d6 9e bf b2 ab 32 ed 3c 36 .n..........2.<6
0x00ac60ba 2e b3 91 0e 05 08 af f1 c6 49 82 95 dd ac 5f 3d .........I...._=
0x00ac60ca 29 e8 a8 bc 88 9c 24 c3 17 79 6c f9 fc 1d b4 19 ).....$..yl.....
0x00ac60da b5 f0 89 ff c0 03 d8 48 57 d5 e2 67 35 7b f4 a1 .......HW..g5{..
0x00ac60ea 3a 30 02 be 42 cd e4 31 fb 7d 4d 4b 7a 26 5a 45 :0..B..1.}MKz&ZE
0x00ac60fa 38 64 55 13 54 ba 00 8a f8 07 ea 27 71 e9 53 cf 8dU.T......'q.S.
0x00ac610a f7 70 23 58 06 01 5d 6d e7 4e 5b 22 12 6f 97 aa .p#X..]m.N[".o..
0x00ac611a 59 11 5c d7 25 41 18 7e 2c fd b0 a3 0f 6b 9a 1f Y.\.%A.~,....k..
0x00ac612a 00 00 ..

Process: 668 services.exe
0x0075602a 76 e6 ce 65 3e d9 5e 0c 0a 8f 14 81 f6 dc ee 2f v..e>.^......../
0x0075603a bd 1a 10 0d 98 63 61 34 9d 8e 75 1c b6 b8 b7 56 .....ca4..u....V
0x0075604a 4a bb d1 99 37 a2 40 4f e0 77 80 52 3f db c4 73 J...7.@O.w.R?..s
0x0075605a 69 21 96 eb 0b 28 7f 90 d2 e5 46 d0 39 2a 51 43 i!...(....F.9*QC
0x0075606a ca 1e a0 62 8c fe c8 15 78 8b 9b d4 2d a4 da ef ...b....x...-...
0x0075607a 47 66 b1 ec 93 c1 87 e3 a9 b9 4c 9f 6a 86 20 d3 Gf........L.j...
0x0075608a a5 8d df 72 3b 44 e1 cb 85 92 04 74 f2 84 33 a6 ...r;D.....t..3.
0x0075609a f3 ae 2b c5 94 f5 7c 50 c9 1b de 83 ad cc 68 60 ..+...|P......h`
0x007560aa c2 6e 09 c7 a7 16 fa d6 9e bf b2 ab 32 ed 3c 36 .n..........2.<6
0x007560ba 2e b3 91 0e 05 08 af f1 c6 49 82 95 dd ac 5f 3d .........I...._=
0x007560ca 29 e8 a8 bc 88 9c 24 c3 17 79 6c f9 fc 1d b4 19 ).....$..yl.....
0x007560da b5 f0 89 ff c0 03 d8 48 57 d5 e2 67 35 7b f4 a1 .......HW..g5{..
0x007560ea 3a 30 02 be 42 cd e4 31 fb 7d 4d 4b 7a 26 5a 45 :0..B..1.}MKz&ZE
0x007560fa 38 64 55 13 54 ba 00 8a f8 07 ea 27 71 e9 53 cf 8dU.T......'q.S.
0x0075610a f7 70 23 58 06 01 5d 6d e7 4e 5b 22 12 6f 97 aa .p#X..]m.N[".o..
0x0075611a 59 11 5c d7 25 41 18 7e 2c fd b0 a3 0f 6b 9a 1f Y.\.%A.~,....k..
0x0075612a 00 00 ..
And there you have it. The plugin prints a hexdump of the 256-byte RC4 key unique to the Zeus variant that has infected the machine. Since its the same version of Zeus injected into winlogon.exe and services.exe, the keys you see are going to match.

Recovering keys for Zeus versions >= 2.0

Welcome to the real fun. I'm not going to dive deep into the details of recovering RC4 keys from these versions, because Sergei
The function named "signature_function" can be expressed using wildcards like this:
PUSH 102h
LEA EAX, [ESP+????????]
PUSH EAX
LEA EAX, [ESP+??]
PUSH EAX
CALL ???????? ; custom_memcpy
MOV EAX, 1E6h
PUSH EAX
PUSH OFFSET ???????? ; real_rc4_key
There may be variations due to compiler differences, so we also should look for this:
PUSH 102h
LEA EAX, [EBP-????????]
PUSH EAX
LEA EAX, [EBP-????????]
PUSH EAX
CALL ???????? ; custom_memcpy
MOV EAX, 1E6h
PUSH EAX
PUSH OFFSET ???????? ; real_rc4_key
As for the function named "decode_config_data", it can be expressed like this:
PUSH ESI
MOV EDX, ????0000 ; config size (immediate)
PUSH EDX
PUSH OFFSET ???????? ; config_data
PUSH EAX
CALL ???????? ; custom_memcpy
MOV ESI, ???????? ; last_section_rva
MOV ECX, ???????? ; imagebase
There are several variations of these patterns, once again, due to compiler differences and usage of general purpose registers. But its nothing that Yara rules can't handle. By converting the above instructions into opcodes, we have the following signatures:
zeus_key_sigs = {
'namespace1':'rule z1 {strings: $a = {56 BA ?? ?? 00 00 52 68 ?? ?? ?? ?? 50 E8 ?? ?? ?? ?? 8B 35 ?? ?? ?? ?? 8B 0D ?? ?? ?? ??} condition: $a}',
'namespace5':'rule z5 {strings: $a = {56 BA ?? ?? 00 00 52 68 ?? ?? ?? ?? 50 E8 ?? ?? ?? ?? 8B 0D ?? ?? ?? ?? 03 0D ?? ?? ?? ??} condition: $a}',
'namespace2':'rule z2 {strings: $a = {55 8B EC 51 A1 ?? ?? ?? ?? 8B 0D ?? ?? ?? ?? 56 8D 34 01 A1 ?? ?? ?? ?? 8B 0D ?? ?? ?? ??} condition: $a}',
'namespace3':'rule z3 {strings: $a = {68 02 01 00 00 8D 84 24 ?? ?? ?? ?? 50 8D 44 24 ?? 50 E8 ?? ?? ?? ?? B8 E6 01 00 00 50 68 ?? ?? ?? ??} condition: $a}',
'namespace4':'rule z4 {strings: $a = {68 02 01 00 00 8D 85 ?? ?? ?? ?? 50 8D 85 ?? ?? ?? ?? 50 E8 ?? ?? ?? ?? B8 E6 01 00 00 50 68 ?? ?? ?? ??} condition: $a}'
}
For plugin testing purposes, I created a memory dump infected with 4 different 2.x samples. But wait a minute, doesn't Zeus check for mutexes and back off if a system is already infected? Well, yes, it does...but I paused each sample in a debugger after it unpacked and before it could exit (that's easier than creating 4 separate memory dumps).

Let's give her a whirl! You should see the name of the process housing the injected Zeus code, along with the URL that Zeus contacts for an update, the local system's unique identifier (computer name plus some random characters), the XOR keys, the randomly generated registry keys and value names that Zeus uses for storing data, and the randomly generated file paths and file names for Zeus executables and stolen data files. Oh, and the two RC4 keys ;-)
$ python vol.py -f zeus2x4.vmem zeusscan2
--------------------------------------------------
Process: wuauclt.exe
Pid: 940
Address: 0xD80000
URL: http://193.43.134.14/eu2.bin
Identifier: JASONRESACC69_7875768F16073AAF
Mutant key: 0x17703072
XOR key: 0x2006B8FE
Registry: HKEY_CURRENT_USER\SOFTWARE\Microsoft\Izozo
Value 1: Kealtuuxd
Value 2: Yrdii
Value 3: Kebooqu
Executable: Obyt\ihah.exe
Data file: Ebupzu\uzugl.dat

Config RC4 Key:
0x00000000 4a ba 2c 63 eb 7c fc 45 c4 f3 b6 2d 31 29 21 2e J.,c.|.E...-1)!.
0x00000010 53 0f 3f ef 9a 2a f8 82 96 6b e1 a2 3b 5f 34 fd S.?..*...k..;_4.
0x00000020 a6 02 cc 39 0b 16 40 33 1f a1 dc af 93 9b 5b 94 ...9..@3......[.
0x00000030 68 62 84 46 ca 64 8d 43 13 d4 d9 72 00 5c 2b bc hb.F.d.C...r..+.
0x00000040 f6 d7 88 91 24 9f bd 1e 7a 07 c5 6e 1a 4e 90 92 ....$...z..n.N..
0x00000050 c1 42 0c 75 47 3a 9e 1d c2 ec 0d ed b8 71 b4 ab .B.uG:.......q..
0x00000060 e6 5d e3 14 48 b9 e9 e8 b2 10 ee f4 e2 2f a4 09 .]..H......../..
0x00000070 54 b7 95 be 50 99 8b 87 8f 37 9d fa f2 d5 b1 18 T...P....7......
0x00000080 01 db 3c cf aa 70 e5 15 9c 5a 26 27 de da d8 d6 ..<..p...Z&'....
0x00000090 59 a8 1b 30 cd 6c 78 c0 e7 c6 81 22 86 17 38 a7 Y..0.lx...."..8.
0x000000a0 df 41 ad 4d 44 11 76 a3 52 a9 b3 6d 51 05 c9 b5 .A.MD.v.R..mQ...
0x000000b0 85 49 77 c7 23 f7 3e 8a 03 69 ac 3d 4c 89 ff 58 .Iw.#.>..i.=L..X
0x000000c0 dd 57 5e 97 98 f1 65 c3 7d f0 e0 20 e4 25 7e 7b .W^...e.}.. .%~{
0x000000d0 b0 06 4b a5 c8 80 f9 f5 55 1c 7f 83 73 d1 66 fe ..K.....U...s.f.
0x000000e0 8c 28 19 4f 60 36 0a 8e ce ae fb 0e 74 35 79 56 .(.O`6......t5yV
0x000000f0 a0 08 ea bb 67 d3 d0 6a 12 6f 32 bf d2 04 cb 61 ....g..j.o2....a
0x00000100 00 00 ..

Credential RC4 Key:
0x00000000 6f e4 94 f2 f1 5e 5c c1 8c e8 66 c5 13 2a 23 39 o....^....f..*#9
0x00000010 84 36 6a 83 b2 55 6c 11 5a f3 b6 20 07 6d ba de .6j..Ul.Z.. .m..
0x00000020 52 8e 34 bf 8a 05 0f 64 35 29 cb 5f ff 00 87 fc R.4....d5)._....
0x00000030 b5 5b 67 b8 eb 1a 0e 1f 32 ae 54 3a 88 ed c3 51 .[g.....2.T:...Q
0x00000040 40 14 3e 53 dc 7c a7 0b 79 26 e5 45 99 7d 1c d0 @.>S.|..y&.E.}..
0x00000050 90 8f 80 95 71 58 41 5d f9 af 9e a1 6e ef 25 4e ....qXA]....n.%N
0x00000060 48 2d b1 bd 33 ab d3 b7 4d 10 7e 44 65 7b cd 2f H-..3...M.~De{./
0x00000070 ea 3f 2c ce 9a 9d db 31 b0 69 cf f7 e7 a6 82 a4 .?,....1.i......
0x00000080 ad a3 30 9b 76 f0 f5 ac c2 fb 8b 4f fe 8d a8 04 ..0.v......O....
0x00000090 86 a0 50 4c 4b e2 ec 60 e6 dd c6 42 cc 6b 89 57 ..PLK..`...B.k.W
0x000000a0 d1 d8 78 4a 1d d7 9f e0 7a 75 e3 7f a2 77 85 2b ..xJ....zu...w.+
0x000000b0 59 16 d6 d4 f4 93 ee 9c d2 03 be 2e 06 1b 56 70 Y.............Vp
0x000000c0 d5 73 ca f8 fd 12 37 49 98 46 0d bb 96 c9 18 b9 .s....7I.F......
0x000000d0 81 74 a9 3c 21 c4 da 38 0c 1e 27 0a c7 15 47 68 .t.ji--..8..'...G
0x000000e0 bc f6 91 fa 72 3d 01 e9 22 e1 09 c8 19 c0 aa b3 ....r=..".......
0x000000f0 b4 08 17 3b 61 92 02 63 43 62 d9 df 97 24 28 a5 ...;a..cCb...$(.
0x00000100 00 00 ..
I'll show a preview of the other variants installed on zeus2x4.vmem, but withhold the keys for the sake of brevity.
--------------------------------------------------
Process: nifek_locked.ex
Pid: 2204
Address: 0x400000
URL: http://zephehooqu.ru/bin/koethood.bin
Identifier: OREO_7875768FD1A71CE9
Mutant key: 0x895B26B0
XOR key: 0x739A94C5
Registry: HKEY_CURRENT_USER\SOFTWARE\Microsoft\Opiss
Value 1: Ypzoarfu
Value 2: Omviacy
Value 3: Otedhay
Executable: Pese\nifek.exe
Data file: Iqpa\ytzea.dat

--------------------------------------------------
Process: vaelh.exe
Pid: 952
Address: 0x400000
URL: http://nahwgwwergwyt.com/gamer/eqtewttetwq.img
Identifier: ZESRA037219_7875768F7BDEA03C
Mutant key: 0x90B18B41
XOR key: 0x569E6C1F
Registry: HKEY_CURRENT_USER\SOFTWARE\Microsoft\Koti
Value 1: Imwiase
Value 2: Qaofl
Value 3: Izsuuqex
Executable: Ammaax\vaelh.exe
Data file: Xuuf\otsip.dat

--------------------------------------------------
Process: anaxu.exe
Pid: 3508
Address: 0x400000
URL: http://basicasco.ru/otp/zero.doc
Identifier: PENNY_74DEB1E33B0FB384
Mutant key: 0x2CB173C4
XOR key: 0xD60F77DA
Registry: HKEY_CURRENT_USER\SOFTWARE\Microsoft\Otve
Value 1: Puyqo
Value 2: Yhahezf
Value 3: Obivq
Executable: Wuebx\anaxu.exe
Data file: Neme\wuwe.dat
Conclusion

This is just one example of taking memory analysis to the next level and I hope it reminds everyone that Volatility isn't just a tool for carving Windows data structures and finding evidence to correlate with disk, registry, and network artifacts. Volatility is a powerful, flexible tool limited only by your own creativity. Every Friday (or every day, depending on how much trouble you want to get into with your boss) we invite you to stop what you're doing and join the fun of pushing the limits of memory analysis. What do you want to do next?

View the zeusscan1 source code here.
View the zeusscan2 source code here.
To use the plugins, you need Yara, the 2.1 alpha branch of Volatility, and malware.py.

Until the next Volatility Friday....

Tuesday, September 20, 2011

Detecting Stealth ADS with The Sleuth Kit (TSK)

Exploit Monday has an interesting article on Stealth Alternate Data Streams and Other ADS Weirdness - read it if you haven't already. The author points out some ways to disrupt traditional stream detection tools. Since I created my own stream detection tool (see Chapter 10 of Malware Analyst's Cookbook), I was interested to see how it would fare. The tool is built on The Sleuth Kit API, thus it reads directly from the MFT instead of relying on Windows API functions.

Here is a basic listing of my C: drive contents:
C:\>dir
Volume in drive C has no label.
Volume Serial Number is 400B-1E43

Directory of C:\

08/22/2010 01:36 PM 0 AUTOEXEC.BAT
08/22/2010 01:36 PM 0 CONFIG.SYS
08/22/2010 01:38 PM dir Documents and Settings
06/03/2011 02:58 PM dir Program Files
09/20/2011 10:50 AM dir WINDOWS
Now I'll create a few of the special streams.
C:\>echo hi > \\?\C:\NUL
C:\>echo hi > \\?\C:\NUL:hidden

C:\>echo hi > test.txt
C:\>echo hi > test.txt:^G^G^G
Sanity check on the drive contents (we should see two new files):
C:\>dir
Volume in drive C has no label.
Volume Serial Number is 400B-1E43

Directory of C:\

08/22/2010 01:36 PM 0 AUTOEXEC.BAT
08/22/2010 01:36 PM 0 CONFIG.SYS
08/22/2010 01:38 PM dir Documents and Settings
09/20/2011 10:57 AM 5 NUL
06/03/2011 02:58 PM dir Program Files
09/20/2011 11:21 AM 5 test.txt
09/20/2011 10:50 AM dir WINDOWS
Now let's see how we do! I'll use tsk-xview.exe (note you also need offreg.dll).
Z:\tools>tsk-xview.exe -r -v

[INFO] High-level enumeration. Please wait.
[INFO] Found 95773 files and dirs
[INFO] Opened \\.\PhysicalDrive0
[INFO] Partition NTFS (0x07) at sector 56
[INFO] Low-level enumeration. Please wait.
[STREAM] C:/NUL:hidden
Inode: 12991-128-3
Size: 8
SIA Created: Tue Sep 20 10:56:14 2011
SIA File Modified: Tue Sep 20 10:57:27 2011
SIA MFT Modified: Tue Sep 20 10:57:27 2011
SIA Accessed: Tue Sep 20 10:57:27 2011
FNI Created: Tue Sep 20 10:56:14 2011
FNI File Modified: Tue Sep 20 10:56:14 2011
FNI MFT Modified: Tue Sep 20 10:56:14 2011
FNI Accessed: Tue Sep 20 10:56:14 2011
[STREAM] C:/test.txt:^^^
Inode: 12994-128-6
Size: 5
SIA Created: Tue Sep 20 10:58:25 2011
SIA File Modified: Tue Sep 20 11:21:17 2011
SIA MFT Modified: Tue Sep 20 11:21:17 2011
SIA Accessed: Tue Sep 20 11:21:17 2011
FNI Created: Tue Sep 20 10:58:25 2011
FNI File Modified: Tue Sep 20 10:58:25 2011
FNI MFT Modified: Tue Sep 20 10:58:25 2011
FNI Accessed: Tue Sep 20 10:58:25 2011
It detected both streams perfectly, albeit displaying the file name as ^^^ instead of ^G^G^G (it could be more console-friendly and print those chars as hex values).

Friday, June 3, 2011

Stuxnet's Footprint in Memory with Volatility 2.0

In this blog post, we'll examine Stuxnet's footprint in memory using Volatility 2.0. A talk was given at Open Memory Forensics Workshop on this topic (see the online Prezi) and the details will be shared here for anyone who missed it.

I picked this topic for two reasons. First, Stuxnet modifies an infected system in such ways that are perfect for showing off many of the new capabilities in Volatility 2.0. We won't cover all of Volatility's commands (for example you won't see idt, gdt, ssdt), because Stunet doesn't mess with those areas of the system, but you'll get a good summary. Second, although many people understand technical malware descriptions, not many people have the "glue" knowledge to translate artifacts that they read about into Volatility commands. Sometimes you can capably determine if a system is infected by hunting for the artifacts eluded to in reports. Thus, many of the artifacts we'll be hunting come from direct quotes in the following articles:

* [1] Mark Russinovich's Analyzing a Stuxnet Infection with the Sysinternals Tools, Part I
* [2] Mark Russinovich's Analyzing a Stuxnet Infection with the Sysinternals Tools, Part II
* [3] Mark Russinovich's Analyzing a Stuxnet Infection with the Sysinternals Tools, Part III
* [4] Symantec's W32.Stuxnet Dossier
* [5] Amr Thabet's MrxCls - Stuxnet Loader Driver
* [6] ESET's Stuxnet Under The Microscope

Getting Started

The memory image we'll be working with is available here. The MD5 of the Stuxnet sample is 74ddc49a7c121a61b8d06c03f92d0c13 (link to ThreatExpert).

Since I plan to run several commands on the same memory image, I'll start by setting environment variables for the file name and profile.

$ export VOLATILITY_LOCATION=file:///memory/stuxnet.vmem
$ export VOLATILITY_PROFILE=WinXPSP3x86

Then I made sure to grab the latest malware.py source code and placed it in Volatility's plugins directory.

Artifact 1: Extra lsass.exe
"a normal Windows XP installation has just one instance of Lsass.exe that the Winlogon process creates when the system boots (Wininit creates it on Windows Vista and higher). The process tree reveals that the two new Lsass.exe instances were both created by Services.exe...the Service Control Manager, which implies that Stuxnet somehow got its code into the Services.exe process." [1]
Based on this statement, you could use the pslist, psscan, or pstree commands. Pslist walks the doubly-linked list of EPROCESS structures starting from PsActiveProcessHead. Psscan uses pool tag scanning. Since we have no reason to believe that Stuxnet uses DKOM for process hiding, I won't use psscan. Pstree inherits from pslist (see the Volatility 2.0 Inheritance Graph) and is probably best since it shows a visual parent/child relationship.
$ ./vol.py pstree
Volatile Systems Volatility Framework 2.0
Name Pid PPid Thds Hnds Time
0x823C8830:System 4 0 59 403 1970-01-01 00:00:00
. 0x820DF020:smss.exe 376 4 3 19 2010-10-29 17:08:53
.. 0x821A2DA0:csrss.exe 600 376 11 395 2010-10-29 17:08:54
.. 0x81DA5650:winlogon.exe 624 376 19 570 2010-10-29 17:08:54
... 0x82073020:services.exe 668 624 21 431 2010-10-29 17:08:54
.... 0x81FE52D0:vmtoolsd.exe 1664 668 5 284 2010-10-29 17:09:05
..... 0x81C0CDA0:cmd.exe 968 1664 0 ------ 2011-06-03 04:31:35
...... 0x81F14938:ipconfig.exe 304 968 0 ------ 2011-06-03 04:31:35
.... 0x822843E8:svchost.exe 1032 668 61 1169 2010-10-29 17:08:55
..... 0x822B9A10:wuauclt.exe 976 1032 3 133 2010-10-29 17:12:03
..... 0x820ECC10:wscntfy.exe 2040 1032 1 28 2010-10-29 17:11:49
.... 0x81E61DA0:svchost.exe 940 668 13 312 2010-10-29 17:08:55
.... 0x81DB8DA0:svchost.exe 856 668 17 193 2010-10-29 17:08:55
..... 0x81FA5390:wmiprvse.exe 1872 856 5 134 2011-06-03 04:25:58
.... 0x821A0568:VMUpgradeHelper 1816 668 3 96 2010-10-29 17:09:08
.... 0x81FEE8B0:spoolsv.exe 1412 668 10 118 2010-10-29 17:08:56
.... 0x81FF7020:svchost.exe 1200 668 14 197 2010-10-29 17:08:55
.... 0x81C47C00:lsass.exe 1928 668 4 65 2011-06-03 04:26:55
.... 0x81E18B28:svchost.exe 1080 668 5 80 2010-10-29 17:08:55
.... 0x8205ADA0:alg.exe 188 668 6 107 2010-10-29 17:09:09
.... 0x823315D8:vmacthlp.exe 844 668 1 25 2010-10-29 17:08:55
.... 0x81E0EDA0:jqs.exe 1580 668 5 148 2010-10-29 17:09:05
.... 0x81C498C8:lsass.exe 868 668 2 23 2011-06-03 04:26:55
.... 0x82279998:imapi.exe 756 668 4 116 2010-10-29 17:11:54
... 0x81E70020:lsass.exe 680 624 19 342 2010-10-29 17:08:54
As you can see, the two lsass.exe processes that started on 2011-06-03 have a parent pid of 668, which belongs to services.exe. However the real lsass.exe (pid 680) has a parent pid of 624 which belongs to winlogon.exe. Given the method used to start the two malicious lsass.exe processes, their SIDs also match the legit copy...which you can verify with getsids.
$ ./vol.py getsids -p 680,868,1928
Volatile Systems Volatility Framework 2.0
lsass.exe (680): S-1-5-18 (Local System)
lsass.exe (680): S-1-5-32-544 (Administrators)
lsass.exe (680): S-1-1-0 (Everyone)
lsass.exe (680): S-1-5-11 (Authenticated Users)

lsass.exe (868): S-1-5-18 (Local System)
lsass.exe (868): S-1-5-32-544 (Administrators)
lsass.exe (868): S-1-1-0 (Everyone)
lsass.exe (868): S-1-5-11 (Authenticated Users)

lsass.exe (1928): S-1-5-18 (Local System)
lsass.exe (1928): S-1-5-32-544 (Administrators)
lsass.exe (1928): S-1-1-0 (Everyone)
lsass.exe (1928): S-1-5-11 (Authenticated Users)
Artifact 2: Process Priority
"...some Windows system processes (such as the Session Manager, service controller, and local security authentication server) have a base process priority slightly higher than the default for the Normal class (8)." - Windows Internals 5th Edition pg. 395
In other words, the legit local security authentication server (lsass.exe) will have a higher priority than normal processes, including those created by Stuxnet. The process base priority is stored in EPROCESS.Pcb.BasePriority. Although there isn't necessarily a plugin already written to extract the BasePriority field, the data is very easy to access in Volatility, as opposed to some GUI tools which only show you select fields from EPROCESS. For example, just use a little volshell scripting.
$ ./vol.py volshell 
Volatile Systems Volatility Framework 2.0
Current context: process System, pid=4, ppid=0 DTB=0x319000
Leopard libedit detected.
Welcome to volshell! Current memory image is:
file:////memory/stuxnet.vmem
To get help, type 'hh()'

In [1]: for proc in win32.tasks.pslist(self.addrspace):
....: if proc.UniqueProcessId in (680, 868, 1928):
....: print "Pid: {0} Priority: {1}".format(proc.UniqueProcessId, proc.Pcb.BasePriority)
....:
Pid: 680 Priority: 9
Pid: 868 Priority: 8
Pid: 1928 Priority: 8

As you can see, the BasePriority of the legit lsass.exe (pid 680) is 9, whereas the ones created by Stuxnet are 8. It is possible to change the priority by using SetPriorityClass, but Stuxnet doesn't bother to do so. Also, since the base priority of threads is inherited from the base priority of the process which owns the thread (unless SetThreadPriority is called), then the differences should be visible using the threads command.

Take a look at a thread owned by the legit lsass.exe (Tid 1768) and a thread owned by a malicious lsass.exe (Tid 764).
$ ./vol.py threads 
[snip]
------
ETHREAD: 0x822722d0 Pid: 680 Tid: 1768
Tags: HookedSSDT
Created: 2010-10-29 17:09:05
Exited: -
Owning Process: 0x81e70020 'lsass.exe'
Attached Process: 0x81e70020 'lsass.exe'
State: Waiting:UserRequest
BasePriority: 0x9
Priority: 0x9
TEB: 0x7ffa0000
StartAddress: 0x7c8106e9
ServiceTable: 0x80552fa0
[0] 0x80501b8c
[0x19] NtClose 0xb240f80e PROCMON20.SYS
[0x29] NtCreateKey 0xb240f604 PROCMON20.SYS
[0x3f] NtDeleteKey 0xb240f4ac PROCMON20.SYS
[0x41] NtDeleteValueKey 0xb240f4f2 PROCMON20.SYS
[0x47] NtEnumerateKey 0xb240f3f2 PROCMON20.SYS
[0x49] NtEnumerateValueKey 0xb240f34e PROCMON20.SYS
[0x4f] NtFlushKey 0xb240f446 PROCMON20.SYS
[0x62] NtLoadKey 0xb240f972 PROCMON20.SYS
[0x77] NtOpenKey 0xb240f7d0 PROCMON20.SYS
[0xa0] NtQueryKey 0xb240f03e PROCMON20.SYS
[0xb1] NtQueryValueKey 0xb240f166 PROCMON20.SYS
[0xf7] NtSetValueKey 0xb240f28a PROCMON20.SYS
[0x107] NtUnloadKey 0xb240fac2 PROCMON20.SYS
[1] -
[2] -
[3] -
Win32Thread: 0x00000000
CrossThreadFlags:

------
ETHREAD: 0x81f44730 Pid: 1928 Tid: 764
Tags: HookedSSDT
Created: 2011-06-03 04:26:55
Exited: -
Owning Process: 0x81c47c00 'lsass.exe'
Attached Process: 0x81c47c00 'lsass.exe'
State: Waiting:UserRequest
BasePriority: 0x8
Priority: 0x8
TEB: 0x7ffdb000
StartAddress: 0x7c8106e9
ServiceTable: 0x80552f60
[0] 0x80501b8c
[0x19] NtClose 0xb240f80e PROCMON20.SYS
[0x29] NtCreateKey 0xb240f604 PROCMON20.SYS
[0x3f] NtDeleteKey 0xb240f4ac PROCMON20.SYS
[0x41] NtDeleteValueKey 0xb240f4f2 PROCMON20.SYS
[0x47] NtEnumerateKey 0xb240f3f2 PROCMON20.SYS
[0x49] NtEnumerateValueKey 0xb240f34e PROCMON20.SYS
[0x4f] NtFlushKey 0xb240f446 PROCMON20.SYS
[0x62] NtLoadKey 0xb240f972 PROCMON20.SYS
[0x77] NtOpenKey 0xb240f7d0 PROCMON20.SYS
[0xa0] NtQueryKey 0xb240f03e PROCMON20.SYS
[0xb1] NtQueryValueKey 0xb240f166 PROCMON20.SYS
[0xf7] NtSetValueKey 0xb240f28a PROCMON20.SYS
[0x107] NtUnloadKey 0xb240fac2 PROCMON20.SYS
[1] 0xbf999b80
[2] -
[3] -
Win32Thread: 0xe126ceb0
CrossThreadFlags:
The BasePriority 0x9 you see for Tid 1768 is because the parent process (legit lsass.exe) has BasePriority 0x9 (slightly above normal). The BasePriority 0x8 you see for Tid 764 is because the parent process (Stuxnet lsass.exe) has BasePriority 0x8 (Normal).

This isn't a strong artifact, since threads can dynamically change priority, but its an artifact nonetheless.

Lastly, it is worth noting that Stuxnet's kernel driver injects DLLs into processes by using the KeStackAttachProcess API. So if you happen to dump memory during one of the injection procedures, you'll also see in the threads output that the owning process is different from the attached process.

Artifact 3: Too Few DLLs
"...besides running as children of Services.exe, another suspicious characteristic of the two superfluous processes is the fact that they have very few DLLs loaded..." [1]
Based on this statement, you can use dlllist with the -p parameter to focus only on certain processes. In this case, we're interested in comparing pids 680 (legit), 868 (bad), and 1928 (bad).
$ ./vol.py dlllist -p 680,868,1928
Volatile Systems Volatility Framework 2.0
************************************************************************
lsass.exe pid: 680
Command line : -
Service Pack 3

Base Size Path
0x01000000 0x006000
0x7c900000 0x0af000 C:\WINDOWS\system32\ntdll.dll
0x7c800000 0x0f6000 C:\WINDOWS\system32\kernel32.dll
0x77dd0000 0x09b000 C:\WINDOWS\system32\ADVAPI32.dll
0x77e70000 0x092000 C:\WINDOWS\system32\RPCRT4.dll
0x77fe0000 0x011000 C:\WINDOWS\system32\Secur32.dll
0x75730000 0x0b5000 C:\WINDOWS\system32\LSASRV.dll
[snip - about 60 others]

************************************************************************
lsass.exe pid: 868
Command line : "C:\WINDOWS\\system32\\lsass.exe"
Service Pack 3

Base Size Path
0x01000000 0x006000 C:\WINDOWS\system32\lsass.exe
0x7c900000 0x0af000 C:\WINDOWS\system32\ntdll.dll
0x7c800000 0x0f6000 C:\WINDOWS\system32\kernel32.dll
0x77dd0000 0x09b000 C:\WINDOWS\system32\ADVAPI32.dll
0x77e70000 0x092000 C:\WINDOWS\system32\RPCRT4.dll
0x77fe0000 0x011000 C:\WINDOWS\system32\Secur32.dll
0x7e410000 0x091000 C:\WINDOWS\system32\USER32.dll
0x77f10000 0x049000 C:\WINDOWS\system32\GDI32.dll
************************************************************************
lsass.exe pid: 1928
Command line : "C:\WINDOWS\\system32\\lsass.exe"
Service Pack 3

Base Size Path
0x01000000 0x006000 C:\WINDOWS\system32\lsass.exe
0x7c900000 0x0af000 C:\WINDOWS\system32\ntdll.dll
0x7c800000 0x0f6000 C:\WINDOWS\system32\kernel32.dll
0x77dd0000 0x09b000 C:\WINDOWS\system32\ADVAPI32.dll
0x77e70000 0x092000 C:\WINDOWS\system32\RPCRT4.dll
0x77fe0000 0x011000 C:\WINDOWS\system32\Secur32.dll
0x7e410000 0x091000 C:\WINDOWS\system32\USER32.dll
0x77f10000 0x049000 C:\WINDOWS\system32\GDI32.dll
0x00870000 0x138000 C:\WINDOWS\system32\KERNEL32.DLL.ASLR.0360b7ab
[snip - about 20 others]
This output supports Mark's findings that the malicious lsass.exe processes have far fewer DLLs loaded than the legit copy. Its worth mentioning that the malicious processes also have less open files and registry keys - which you can verify with the handles command (we'll use this command later).

Artifact 4: Injected Code
"No non-Microsoft DLLs show up in the loaded-module lists for Services.exe, Lsass.exe or Explorer.exe, so they are probably hosting injected executable code. [....] Sure enough, the legitimate Lsass has no executable data regions, but both new Lsass processes have regions with Execute and Write permissions in their address spaces at the same location and same size." [1]
Based on this statement, you can use malfind to automatically locate and extract the injected executable code.
$ ./vol.py malfind -D out 
Volatile Systems Volatility Framework 2.0
Name Pid Start End Tag Hits Protect
lsass.exe 868 0x00080000 0x000F9FFF Vad 0 6 (MM_EXECUTE_READWRITE)
Dumped to: out/lsass.exe.1e498c8.00080000-000f9fff.dmp
0x00080000 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 MZ..............
0x00080010 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0x00080020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x00080030 00 00 00 00 00 00 00 00 00 00 00 00 08 01 00 00 ................
0x00080040 0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68 ........!..L.!Th
0x00080050 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f is program canno
0x00080060 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20 t be run in DOS
0x00080070 6d 6f 64 65 2e 0d 0d 0a 24 00 00 00 00 00 00 00 mode....$.......

lsass.exe 1928 0x00080000 0x000F9FFF Vad 0 6 (MM_EXECUTE_READWRITE)
Dumped to: out/lsass.exe.1e47c00.00080000-000f9fff.dmp
0x00080000 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 MZ..............
0x00080010 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0x00080020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x00080030 00 00 00 00 00 00 00 00 00 00 00 00 08 01 00 00 ................
0x00080040 0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68 ........!..L.!Th
0x00080050 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f is program canno
0x00080060 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20 t be run in DOS
0x00080070 6d 6f 64 65 2e 0d 0d 0a 24 00 00 00 00 00 00 00 mode....$.......
Malfind located two suspicious regions, at 0x80000 base address in both processes. They have MM_EXECUTE_READWRITE permissions and start with MZ, meaning a PE is likely stored here. My usual next step is to try and find out where the injected code came from. Is it mapped to some file on disk? You can use vadinfo to see more granular details about the memory segment:
$ ./vol.py vadinfo -p 868 
[snip]

VAD node @822e7e70 Start 00080000 End 000f9fff Tag Vad
Flags:
Commit Charge: 0 Protection: 6
ControlArea @81de9890 Segment e2b7dbf0
Dereference list: Flink 00000000, Blink 00000000
NumberOfSectionReferences: 0 NumberOfPfnReferences: 0
NumberOfMappedViews: 1 NumberOfUserReferences: 1
WaitingForDeletion Event: 00000000
Flags: Commit, HadUserReference
FileObject: none
First prototype PTE: e2b7dc30 Last contiguous PTE: e2b7dff8
Flags2: Inherit
You can tell the memory isn't backed by a file because the FileObject pointer is none/NULL. It would be backed by a file if the PE was loaded via LoadLibrary. More on this in the next artifact.

Artifact 5: Hidden DLLs
"Stuxnet calls LoadLibrary with a specially crafted file name that does not exist on disk and normally causes LoadLibrary to fail. However, W32.Stuxnet has hooked Ntdll.dll to monitor for requests to load specially crafted file names. These specially crafted filenames are mapped to another location instead—a location specified by W32.Stuxnet. That location is generally an area in memory where a .dll file has been decrypted and stored by the threat previously. The filenames used have the pattern of KERNEL32.DLL.ASLR.[HEXADECIMAL]..." [4]
Based on this statement, you can use the dlllist command to view the evidence. Even though the file doesn't exist on disk *and* the malware hooks the DLL loading APIs, the file name will still end up in the PEB lists.
$ ./vol.py dlllist | grep ASLR
Volatile Systems Volatility Framework 2.0
Base Size Path
0x013f0000 0x138000 C:\WINDOWS\system32\KERNEL32.DLL.ASLR.0360c5e2
0x00d00000 0x138000 C:\WINDOWS\system32\KERNEL32.DLL.ASLR.0360c8ee
0x00870000 0x138000 C:\WINDOWS\system32\KERNEL32.DLL.ASLR.0360b7ab
For a different perspective, you can use the ldrmodules plugin. This is useful if you don't preemptively know to search for "ASLR" or any predictable file name. This plugin compares the PE files in memory with mapped files and the 3 DLL lists in the PEB. This is one of my favorite artifacts, because Stuxnet actually uses 3 different types of code injection - all of which are visible (and distinguishable from each other) by using a simpe command:
$ ./vol.py ldrmodules -p 1928
Volatile Systems Volatility Framework 2.0
Pid Process Base InLoad InInit InMem Path
1928 lsass.exe 0x00080000 0 0 0 -
1928 lsass.exe 0x7C900000 1 1 1 \WINDOWS\system32\ntdll.dll
1928 lsass.exe 0x773D0000 1 1 1 \WINDOWS\WinSxS\x86_Microsoft.Windows.Common-Controls_6595b64144ccf1df_6.0.2600.5512_x-ww_35d4ce83\comctl32.dll
1928 lsass.exe 0x77F60000 1 1 1 \WINDOWS\system32\shlwapi.dll
1928 lsass.exe 0x771B0000 1 1 1 \WINDOWS\system32\wininet.dll
1928 lsass.exe 0x77A80000 1 1 1 \WINDOWS\system32\crypt32.dll
1928 lsass.exe 0x77FE0000 1 1 1 \WINDOWS\system32\secur32.dll
1928 lsass.exe 0x77C00000 1 1 1 \WINDOWS\system32\version.dll
1928 lsass.exe 0x01000000 1 0 1 -
1928 lsass.exe 0x5B860000 1 1 1 \WINDOWS\system32\netapi32.dll
1928 lsass.exe 0x77E70000 1 1 1 \WINDOWS\system32\rpcrt4.dll
1928 lsass.exe 0x71AB0000 1 1 1 \WINDOWS\system32\ws2_32.dll
1928 lsass.exe 0x71AD0000 1 1 1 \WINDOWS\system32\wsock32.dll
1928 lsass.exe 0x774E0000 1 1 1 \WINDOWS\system32\ole32.dll
1928 lsass.exe 0x7E410000 1 1 1 \WINDOWS\system32\user32.dll
1928 lsass.exe 0x77F10000 1 1 1 \WINDOWS\system32\gdi32.dll
1928 lsass.exe 0x77120000 1 1 1 \WINDOWS\system32\oleaut32.dll
1928 lsass.exe 0x76D60000 1 1 1 \WINDOWS\system32\iphlpapi.dll
1928 lsass.exe 0x769C0000 1 1 1 \WINDOWS\system32\userenv.dll
1928 lsass.exe 0x7C800000 1 1 1 \WINDOWS\system32\kernel32.dll
1928 lsass.exe 0x76BF0000 1 1 1 \WINDOWS\system32\psapi.dll
1928 lsass.exe 0x77C10000 1 1 1 \WINDOWS\system32\msvcrt.dll
1928 lsass.exe 0x77DD0000 1 1 1 \WINDOWS\system32\advapi32.dll
1928 lsass.exe 0x7C9C0000 1 1 1 \WINDOWS\system32\shell32.dll
1928 lsass.exe 0x00870000 1 1 1 -
1928 lsass.exe 0x76F20000 1 1 1 \WINDOWS\system32\dnsapi.dll
1928 lsass.exe 0x5D090000 1 1 1 \WINDOWS\system32\comctl32.dll
1928 lsass.exe 0x71AA0000 1 1 1 \WINDOWS\system32\ws2help.dll
1928 lsass.exe 0x77B20000 1 1 1 \WINDOWS\system32\msasn1.dll
The three lines in red are either suspicious because an entry is missing from one of the 3 PEB lists or because the path name is blank. From top to bottom, you first see the PE at 0x80000 which we have already identified as injected code in artifact 4. It wasn't loaded with LoadLibrary so its not in any of the 3 PEB lists. Its not backed by a file, so the path is blank. Most likely, this is code injected via VirtualAlloc(Ex) and WriteProcessMemory.

The PE at 0x01000000 is interesting, because there is an entry for it in 2 of the 3 DLL lists (missing from the Init list), yet it isn't backed by a file. In normal cases, a program's main module (the .exe) will be backed by a file. So the question becomes: is 0x01000000 the ImageBase of the main module? To see, you can run ldrmodules using the -v (verbose) flag.
$ ./vol.py ldrmodules -p 1928 -v 
Volatile Systems Volatility Framework 2.0
Pid Process Base InLoad InInit InMem Path
1928 lsass.exe 0x00080000 0 0 0 -
1928 lsass.exe 0x01000000 1 0 1 -
Load Path: C:\WINDOWS\system32\lsass.exe : lsass.exe
Mem Path: C:\WINDOWS\system32\lsass.exe : lsass.exe
1928 lsass.exe 0x00870000 1 1 1 -
Load Path: C:\WINDOWS\system32\KERNEL32.DLL.ASLR.0360b7ab : KERNEL32.DLL.ASLR.0360b7ab
Init Path: C:\WINDOWS\system32\KERNEL32.DLL.ASLR.0360b7ab : KERNEL32.DLL.ASLR.0360b7ab
Mem Path: C:\WINDOWS\system32\KERNEL32.DLL.ASLR.0360b7ab : KERNEL32.DLL.ASLR.0360b7ab
This confirms that 0x01000000 is the ImageBase for the main module, C:\WINDOWS\system32\lsass.exe. So then why is the path blank, indicating that the memory isn't backed by a file? We may be looking at a hollow process situation. You can confirm by using procexedump or procmemdump to extract whatever exists in this memory region and compare it with the legit lsass.exe.
$ ./vol.py procexedump -p 680,868,1928 -D out 
Volatile Systems Volatility Framework 2.0
************************************************************************
Dumping lsass.exe, pid: 680 output: executable.680.exe
************************************************************************
Dumping lsass.exe, pid: 868 output: executable.868.exe
************************************************************************
Dumping lsass.exe, pid: 1928 output: executable.1928.exe
Here are the strings (ANSI only) from the legit lsass.exe:
$ strings out/executable.680.exe 
!This program cannot be run in DOS mode.
Rich
.text
`.data
.rsrc
ADVAPI32.dll
KERNEL32.dll
NTDLL.DLL
LSASRV.dll
SAMSRV.dll
Here are the strings from one of the malicious lsass.exe:
$ strings out/executable.868.exe 
!This program cannot be run in DOS mode.
Rich
.verif
.text
.bin
.reloc
ZwMapViewOfSection
ZwCreateSection

ZwOpenFile
ZwClose

ZwQueryAttributesFile
ZwQuerySection
TerminateProcess
GetCurrentProcess
CloseHandle
WaitForSingleObject
OpenProcess
ExitProcess
CreateThread
SetUnhandledExceptionFilter
SetErrorMode
KERNEL32.dll
AdjustTokenPrivileges
LookupPrivilegeValueW
OpenProcessToken
ADVAPI32.dll
VirtualProtect
GetModuleHandleW
GetCurrentThreadId
GetTickCount
lstrcpyW
lstrlenW
GetProcAddress
wsprintfW
USER32.dll
As you can see, the content of the alleged lsass.exe binary is clearly different. This is in fact the effect of process hollowing - the second type of code injection used by Stuxnet. The names of the APIs in red are the ones hooked by the malware (see Artifact 6) and the others are probably from the file's Import Address Table.

Lastly, the PE at 0x00870000 named KERNEL32.DLL.ASLR.0360b7ab also is not backed by a file on disk, but its in all 3 DLL lists. That's because this is the "hidden" DLL that is never written to disk (an attempt to evade antivirus). That is the third type of code injection / DLL hiding implemented by Stuxnet.

Artifact 6: Hooked APIs
"The functions hooked for this purpose in Ntdll.dll are:

* ZwMapViewOfSection
* ZwCreateSection
* ZwOpenFile
* ZwCloseFile
* ZwQueryAttributesFile
* ZwQuerySection" [4]
Based on this statement, you can use the apihooks plugin to detect what Symantec is describing.
$ ./vol.py apihooks 
Volatile Systems Volatility Framework 2.0
Name Type Target Value
services.exe[668] syscall ntdll.dll!NtClose[0x7c90cfd0] 0x7c900050 MOV EDX, 0x7c900050 (UNKNOWN)
services.exe[668] syscall ntdll.dll!NtCreateSection[0x7c90d160] 0x7c900048 MOV EDX, 0x7c900048 (UNKNOWN)
services.exe[668] syscall ntdll.dll!NtMapViewOfSection[0x7c90d500] 0x7c900044 MOV EDX, 0x7c900044 (UNKNOWN)
services.exe[668] syscall ntdll.dll!NtOpenFile[0x7c90d580] 0x7c90004c MOV EDX, 0x7c90004c (UNKNOWN)
services.exe[668] syscall ntdll.dll!NtQueryAttributesFile[0x7c90d6f0] 0x7c900054 MOV EDX, 0x7c900054 (UNKNOWN)
services.exe[668] syscall ntdll.dll!NtQuerySection[0x7c90d8b0] 0x7c900058 MOV EDX, 0x7c900058 (UNKNOWN)
services.exe[668] syscall ntdll.dll!ZwClose[0x7c90cfd0] 0x7c900050 MOV EDX, 0x7c900050 (UNKNOWN)
services.exe[668] syscall ntdll.dll!ZwCreateSection[0x7c90d160] 0x7c900048 MOV EDX, 0x7c900048 (UNKNOWN)
services.exe[668] syscall ntdll.dll!ZwMapViewOfSection[0x7c90d500] 0x7c900044 MOV EDX, 0x7c900044 (UNKNOWN)
services.exe[668] syscall ntdll.dll!ZwOpenFile[0x7c90d580] 0x7c90004c MOV EDX, 0x7c90004c (UNKNOWN)
services.exe[668] syscall ntdll.dll!ZwQueryAttributesFile[0x7c90d6f0] 0x7c900054 MOV EDX, 0x7c900054 (UNKNOWN)
services.exe[668] syscall ntdll.dll!ZwQuerySection[0x7c90d8b0] 0x7c900058 MOV EDX, 0x7c900058 (UNKNOWN)

[snip]
If there are 6 hooked APIs, why are there 12 lines of output? Since Ntdll.dll exports each function twice (one for Nt* and one for Zw*), the apihooks plugin shows them both. Also, it is apparent that Stuxnet uses the "syscall" hooking technique similar to Carberp instead of the more common Inline/IAT/EAT hooking. To interactively explore code around the hook address, use the volshell command. This time we'll use it to follow the flow of execution when a hooked API is called.

First you need to break into the shell and switch into the context of a process that has been hooked. Then navigate to the hooked API. I'll start with ZwClose which is at 0x7C90cfd0.
$ ./vol.py volshell
Volatile Systems Volatility Framework 2.0
Current context: process System, pid=4, ppid=0 DTB=0x319000
Welcome to volshell! Current memory image is:
file:///memory/stuxnet.vmem
To get help, type 'hh()'
>>> cc(pid=668)
Current context: process services.exe, pid=668, ppid=624 DTB=0xa940080
>>> dis(0x7c90cfd0)

0x7c90cfd0 b819000000 MOV EAX, 0x19
0x7c90cfd5 ba5000907c MOV EDX, 0x7c900050
0x7c90cfda ffd2 CALL EDX
0x7c90cfdc c20400 RET 0x4
0x7c90cfdf 90 NOP
0x7c90cfe0 b81a000000 MOV EAX, 0x1a
0x7c90cfe5 ba0003fe7f MOV EDX, 0x7ffe0300
0x7c90cfea ff12 CALL DWORD [EDX]
0x7c90cfec c20c00 RET 0xc
0x7c90cfef 90 NOP
[snip]
When comparing the instructions in red (which belong to ZwClose) with the instructions in purple (which belong to an API not hooked by Stuxnet, you see that a different value is placed in EDX. The clean API calls the DWORD at 0x7ffe0300 (see the system call dispatcher on x86). The hooked API calls 0x7c900050. So that is our next hop when following the rootkit, and what we see next will be interesting.
>>> dis(0x7c900050)
0x7c900050 b203 MOV DL, 0x3
0x7c900052 eb08 JMP 0x7c90005c
0x7c900054 b204 MOV DL, 0x4
0x7c900056 eb04 JMP 0x7c90005c
0x7c900058 b205 MOV DL, 0x5
0x7c90005a eb00 JMP 0x7c90005c
0x7c90005c 52 PUSH EDX
0x7c90005d e804000000 CALL 0x7c900066
0x7c900062 f20094005aff2269 ADD [EAX+EAX+0x6922ff5a], DL
0x7c90006a 6e OUTS DX, [ESI]
0x7c90006b 20444f53 AND [EDI+ECX*2+0x53], AL
0x7c90006f 206d6f AND [EBP+0x6f], CH
0x7c900072 64652e0d0d0a2400 OR EAX, 0x240a0d
0x7c90007a 0000 ADD [EAX], AL
0x7c90007c 0000 ADD [EAX], AL
0x7c90007e 0000 ADD [EAX], AL
At 0x7c90005d there is a CALL to 0x7c900066. But according to the disassembly, 0x7c900066 is in the middle of the instruction that starts at 0x7c900062. Its possible that this is an anti-disassembling trick (see the Anti-Disassembling section of MindshaRE: Anti-Reversing Techniques) but either way, all we have to do is re-align the disassembly engine by telling it to start at 0x7c900066.
>>> dis(0x7c900066)
0x7c900066 5a POP EDX
0x7c900067 ff22 JMP DWORD [EDX]
Ah, that's better! So when the CALL at 0x7c90005d is executed, its return address (0x7c900062) is pushed onto the stack. The POP EDX instruction at 0x7c900066 then removes that value from the stack and places it in EDX. At 0x7c900067, EDX is dereferenced and called. So the pointer being dereferenced is 0x7c900062. The 4 bytes at that address are highlighted in red above - f2009400. Given endiannes, this is actually 0x009400F2 - our official next hop in following the rootkit.
>>> dis(0x009400F2)
0x9400f2 5a POP EDX
0x9400f3 84d2 TEST DL, DL
0x9400f5 7425 JZ 0x94011c
0x9400f7 feca DEC DL
0x9400f9 0f8482000000 JZ 0x940181
0x9400ff feca DEC DL
0x940101 0f84bb000000 JZ 0x9401c2
0x940107 feca DEC DL
0x940109 0f84fe000000 JZ 0x94020d
0x94010f feca DEC DL
0x940111 0f8440010000 JZ 0x940257
0x940117 e98c010000 JMP 0x9402a8
0x94011c e8f9010000 CALL 0x94031a

[snip]
0x9401ad 8d542408 LEA EDX, [ESP+0x8]
0x9401b1 cd2e INT 0x2e
By analyzing the code at this location, you can begin to understand the exact purpose of the hook. However, from Artifact 5 we already know the purpose is to support loading DLLs that don't exist on disk.

The instructions in red show how the malware eventually calls the requested system service. It uses the IDT instead of the SSDT. Although Windows itself doesn't use the IDT for system service dispatching anymore (that stopped with Windows 2000), the IDT was still kept around for backward capability. This begs the question...are any other DLLs on my system still using the IDT? How unique is the byte pattern for the instructions in red?

To find out, you can search process memory for 8D 54 24 ?? CD 2E where ?? is a wildcard. In the output below, two memory regions tested positive. 0x00940000 shouldn't be surprising since that's the source of our signature. But 0x013F0000 also contains the pattern.
$ ./vol.py malfind -p 668 -D out -Y "{8D 54 24 ?? CD 2E}" 
Volatile Systems Volatility Framework 2.0
Name Pid Start End Tag Hits Protect
services.exe 668 0x00940000 0x00940FFF Vad 1 6 (MM_EXECUTE_READWRITE)
Dumped to: out/services.exe.2273020.00940000-00940fff.dmp

YARA rule: z1
Hit: 8d542408cd2e
0x00940145 8d 54 24 08 cd 2e eb 0c 5a 8d 54 24 08 64 ff 15 .T$.....Z.T$.d..
0x00940155 c0 00 00 00 85 c0 75 23 e8 b8 01 00 00 85 d2 74 ......u#.......t

Hit: 8d542408cd2e
0x009401ad 8d 54 24 08 cd 2e eb 0c 5a 8d 54 24 08 64 ff 15 .T$.....Z.T$.d..
0x009401bd c0 00 00 00 c3 e8 53 01 00 00 85 d2 74 20 50 57 ......S.....t PW

Hit: 8d542408cd2e
0x009401f8 8d 54 24 08 cd 2e eb 0c 5a 8d 54 24 08 64 ff 15 .T$.....Z.T$.d..
0x00940208 c0 00 00 00 c3 81 7c 24 08 ae 82 19 ae 75 03 33 ......|$.....u.3

Hit: 8d542408cd2e
0x00940242 8d 54 24 08 cd 2e eb 0c 5a 8d 54 24 08 64 ff 15 .T$.....Z.T$.d..
0x00940252 c0 00 00 00 c3 e8 be 00 00 00 85 d2 74 26 50 52 ............t&PR

Hit: 8d542408cd2e
0x00940293 8d 54 24 08 cd 2e eb 0c 5a 8d 54 24 08 64 ff 15 .T$.....Z.T$.d..
0x009402a3 c0 00 00 00 c3 e8 6d 00 00 00 85 d2 52 74 45 8b ......m.....RtE.

Hit: 8d542408cd2e
0x00940305 8d 54 24 08 cd 2e eb 0c 5a 8d 54 24 08 64 ff 15 .T$.....Z.T$.d..
0x00940315 c0 00 00 00 c3 50 56 57 51 52 83 ec 1c 8b c4 6a .....PVWQR.....j

services.exe 668 0x013F0000 0x01527FFF Vad 1 6 (MM_EXECUTE_READWRITE)
Dumped to: out/services.exe.2273020.013f0000-01527fff.dmp
YARA rule: z1
Hit: 8d542408cd2e
0x0144782e 8d 54 24 08 cd 2e eb 0c 5a 8d 54 24 08 64 ff 15 .T$.....Z.T$.d..
0x0144783e c0 00 00 00 85 c0 75 23 e8 b8 01 00 00 85 d2 74 ......u#.......t

Hit: 8d542408cd2e
0x01447896 8d 54 24 08 cd 2e eb 0c 5a 8d 54 24 08 64 ff 15 .T$.....Z.T$.d..
0x014478a6 c0 00 00 00 c3 e8 53 01 00 00 85 d2 74 20 50 57 ......S.....t PW

Hit: 8d542408cd2e
0x014478e1 8d 54 24 08 cd 2e eb 0c 5a 8d 54 24 08 64 ff 15 .T$.....Z.T$.d..
0x014478f1 c0 00 00 00 c3 81 7c 24 08 ae 82 19 ae 75 03 33 ......|$.....u.3

Hit: 8d542408cd2e
0x0144792b 8d 54 24 08 cd 2e eb 0c 5a 8d 54 24 08 64 ff 15 .T$.....Z.T$.d..
0x0144793b c0 00 00 00 c3 e8 be 00 00 00 85 d2 74 26 50 52 ............t&PR

Hit: 8d542408cd2e
0x0144797c 8d 54 24 08 cd 2e eb 0c 5a 8d 54 24 08 64 ff 15 .T$.....Z.T$.d..
0x0144798c c0 00 00 00 c3 e8 6d 00 00 00 85 d2 52 74 45 8b ......m.....RtE.

Hit: 8d542408cd2e
0x014479ee 8d 54 24 08 cd 2e eb 0c 5a 8d 54 24 08 64 ff 15 .T$.....Z.T$.d..
0x014479fe c0 00 00 00 c3 50 56 57 51 52 83 ec 1c 8b c4 6a .....PVWQR.....j
As you can see, both memory regions have 6 hits, which coincides with the 6 hooked APIs. But why are there two memory regions in the first place? Well, just guessing here but based on the relative size (140KB to 1KB), the 0x013F0000 region contains the code responsible for installing the API hooks. It allocated and copied a smaller 1KB chunk of itself to 0x00940000.

Artifact 7: Patched PE Header
"To hook the functions specified above, the malware allocates a memory buffer for code that will dispatch calls to hooked functions, overwrite some data in MZ header of the image with the code that transfers control to the new functions, and hook the original functions by overwriting its bodies..." [6]
In the previous artifact you might remember us tracking an address at 0x7c900050. If ntdll.dll's base is 0x7c900000, then the address we're tracking is in the PE header. In order to hide code in ntdll.dll's PE header without breaking anything, Stuxnet overwrites some inconsequential fields including most of the "This program cannot be run in DOS mode" message.

To detect the malicious PE header modification, you can use the following yara rule which alerts on any pages of memory that contain a PE header and that do not contain the "This program cannot..." message. The rule was saved to modified_pe_header.yar.
rule modified_pe_header { 
strings:
$msg = "This program cannot"

condition:
uint16(0) == 0x5A4D and
uint32(uint32(0x3C)) == 0x00004550 and
not $msg
}
Now scan for the memory with malfind:
$ ./vol.py malfind -D out -Y modified_pe_header.yar 
Volatile Systems Volatility Framework 2.0
Name Pid Start End Tag Hits Protect
services.exe 668 0x7C900000 0x7C9AEFFF Vad 1 7 (MM_EXECUTE_WRITECOPY)
Dumped to: out/services.exe.2273020.7c900000-7c9aefff.dmp
YARA rule: modified_pe_header

svchost.exe 940 0x7C900000 0x7C9AEFFF Vad 1 7 (MM_EXECUTE_WRITECOPY)
Dumped to: out/svchost.exe.2061da0.7c900000-7c9aefff.dmp
YARA rule: modified_pe_header

lsass.exe 1928 0x7C900000 0x7C9AEFFF Vad 1 7 (MM_EXECUTE_WRITECOPY)
Dumped to: out/lsass.exe.1e47c00.7c900000-7c9aefff.dmp
YARA rule: modified_pe_header
The results tell you Stuxnet has modified ntdll.dll in three processes.

Artifact 8: Mutexes

"Stuxnet communicates between different components via global mutexes." [4]

Based on this statement, you can use the mutantscan and/or handles plugins to list mutexes on the system. However, resources indicate that the mutex name can be random (or at least pseudo-random). Without doing the necessary RE to see if there are any detectable patterns in the mutex name, we don't really know what to look for. So how do we find out which mutexes on the system are artifacts of Stuxnet?

Since Stuxnet injects code into services.exe (based on previous findings), there is a good chance that services.exe has an open handle to the mutex, or mutexes. Let's use the handles plugin to filter by object type (Mutant) and process ID, also ignoring un-named mutexes:
$ ./vol.py handles -t Mutant -p 668
Volatile Systems Volatility Framework 2.0
Offset(V) Pid Type Details
0x81ee3968 668 Mutant 'SHIMLIB_LOG_MUTEX'
0x81db23b0 668 Mutant 'ShimCacheMutex'
0x8205fa78 668 Mutant 'PnP_Init_Mutex'
0x81fc3cc0 668 Mutant '{5EC171BB-F130-4a19-B782-B6E655E091B2}'
0x821b75e0 668 Mutant '{E41362C3-F75C-4ec2-AF49-3CB6BCA591CA}'
0x81f08b98 668 Mutant 'PrefetchFileCacheOwner'
0x81f78e90 668 Mutant 'Spooler_Perf_Library_Lock_PID_01F'
0x82287600 668 Mutant '85991EC7-5621-4A6F-9453-DC19BAE9C542'
0x82287600 668 Mutant '85991EC7-5621-4A6F-9453-DC19BAE9C542'
I've highlighted three of the Stuxnet artifacts in red. But at this point, pretend you still don't know. One advantage of using mutantscan is that it prints the OwnerThread member of each _KMUTANT (if its available). Here is some output to help drive this point home:
$ ./vol.py mutantscan -s 
Volatile Systems Volatility Framework 2.0
Offset Obj Type #Ptr #Hnd Signal Thread CID Name
0x01e4dbe0 0x823c55e0 2 1 1 0x00000000 '_!SHMSFTHISTORY!_'
0x01e8ab88 0x823c55e0 3 2 1 0x00000000 'c:!documents and settings!administrator!cookies!'
0x02108bb0 0x823c55e0 2 1 0 0x81fc0020 668:568 'PrefetchFileCacheOwner'
0x021c3cd8 0x823c55e0 4 3 1 0x00000000 '{5EC171BB-F130-4a19-B782-B6E655E091B2}'
0x023b75f8 0x823c55e0 2 1 0 0x81c6d180 668:476 '{E41362C3-F75C-4ec2-AF49-3CB6BCA591CA}'
0x0240f300 0x823c55e0 2 1 1 0x00000000 'WPA_LT_MUTEX'
0x0240f350 0x823c55e0 2 1 1 0x00000000 'WPA_RT_MUTEX'
0x0241ef40 0x823c55e0 2 1 1 0x00000000 '.NET CLR Data_Perf_Library_Lock_PID_680'
0x0242d248 0x823c55e0 2 1 0 0x8210d200 1712:1716 'SunJavaUpdateSchedulerMutex'
[snip]
Now we know that thread ID 568 owns the PrefetchFileCacheOwner mutex and thread ID 476 owns the one that starts with "{E41362C3". Windows doesn't track creation time for mutex objects, but it does for thread objects. Let's see when the owning threads were created using the thrdscan command:
$ ./vol.py thrdscan
Volatile Systems Volatility Framework 2.0
Offset PID TID Create Time Exit Time StartAddr
---------- ------ ------ ------------------------- ------------------------- ----------
0x021c0020 668 568 2011-06-03 04:26:55 0x7c8106e9
0x01e6d180 668 476 2011-06-03 04:26:55 0x7c8106e9
[snip]
Both threads were created at 04:26:55. The two malicious lsass.exe processes discussed in Artifact 1 were also created at 04:26:55. Now we have a direct temporal relationship between the times when Stuxnet spawned the lsass.exe processes, when it injected code into services.exe (resulting in several new threads), and when the mutexes were created.

A few things should be noted. The PrefetchFileCacheOwner is probably not one of the "global mutexes used for communication" - it is one of the side-effects of some action performed by the thread. Second, to confirm my findings, I infected the system with Stuxnet again, this time with a breakpoint set on CreateMutexW inside the services.exe process. Before long, the breakpoint triggered twice:



That's a wrap for this one!

Artifact 9: File Objects
"The final modifications made by the virus include the creation of four additional files in the C:\Windows\Inf directory: Oem7a.pnf, Mdmeric3.pnf, Mdmcpq3.pnf and Oem6c.pnf." [2]
Based on this statement, you can use the handles command to see if any processes currently have open handles to the specified files, or you can use filescan to see if any FILE_OBJECT structures still reside in memory after the malware created them.
$ ./vol.py handles | grep -i .pnf
Volatile Systems Volatility Framework 2.0
$


$ ./vol.py filescan | grep -i .pnf

Volatile Systems Volatility Framework 2.0
0x01dfa028 0x0x823eb040 1 0 R--r-- - '\\WINDOWS\\inf\\oem7A.PNF'
0x01e0d028 0x0x823eb040 1 0 -WD--- - '\\WINDOWS\\inf\\mdmeric3.PNF'
0x021b53c8 0x0x823eb040 1 0 RW---- - '\\WINDOWS\\inf\\mdmcpq3.PNF'
As you can see, the output of the handles command is empty. Either the process that created the PNF files has terminated, or the process has already called CloseHandle. However, filescan can still locate traces of the activity, which is the whole reason filescan exists in the first place. Kudos to filescan!

Artifact 10: Network Connections
"This function [export #28] is responsible for performing actual data exchange with the C&C server. In the event that there is no iexplore.exe in the system, it calls this function from the address space of the default browser: it starts the default browser as a new process, injects into it the main module, and calls the function performing data exchange.

The malware communicates to the C&C server through http. A list of URLs is included in the Stuxnet configuration data of Stuxnet:
  • www.mypremierfutbol.com
  • www.todaysfutbol.com" [6]
This artifact was not present in the initial memory dump. Export #28 (the function referred to in the quote) wasn't called for one reason or another. To elicit the described behavior, I dumped memory a second time, after manually coercing the execution of Export #28. Then using connscan (one of the Networking commands) you can see the evidence:
$ ./vol.py -f stuxnet2.vmem connscan 
Volatile Systems Volatility Framework 2.0
Offset Local Address Remote Address Pid
---------- ------------------------- ------------------------- ------
0x01da9e68 192.168.16.129:1311 128.61.111.9:51442 1280
0x01e4fe68 192.168.16.129:1233 128.61.111.9:21 1280
0x01eeebf0 172.16.237.145:1170 72.167.202.5:80 528
0x020bf4e0 172.16.237.145:1045 96.17.106.99:80 1648
0x0242ec28 172.16.237.145:1048 96.17.106.99:80 1708
0x025069e8 172.16.237.145:1090 137.254.16.78:80 152

The connection in red is suspicious because, as shown below, it was created by a browser process and the IP maps back to one of the C&C domains.
$ ./vol.py -f stuxnet2.vmem pslist
Volatile Systems Volatility Framework 2.0
Offset(V) Name PID PPID Thds Hnds Time
---------- -------------------- ------ ------ ------ ------ -------------------
[snip]
0x81af13b8 firefox.exe 528 356 4 112 2011-07-20 16:32:09

$ host www.mypremierfutbol.com
www.mypremierfutbol.com is an alias for mypremierfutbol.com.
mypremierfutbol.com has address 72.167.202.5
Perhaps more interesting than the connection itself is the fact that Stuxnet uses a specific message structure for its C&C packets. Each message starts with a 0x1 constant byte and has the OS major version at offset 2, OS minor version at offset 3, OS service pack at offset 4, and so on. That means an XP SP1 system may look like hex "01??050101" and a Windows 7 SP0 may look like "01??060100". A bit more research can help refine these patters even more and before long you can have a Volatility plugin, consisting of a few lines of Python, that finds Stuxnet C&C packets (before encryption) in process memory.

Artifact 11: Registry Keys
"...because we see Lsass.exe drop one of the two Stuxnet drivers, MRxCls.sys, in C:\Windows\System32\Drivers and create its corresponding registry keys" [2]
Based on this statement, we can use printkey to read the cached registry keys in memory. Service registry keys are in the system hive under ControlSet001\Services\SERVICENAME. So based on the article, we know exactly what to look for:
$ ./vol.py printkey -K 'ControlSet001\Services\MrxNet'
Volatile Systems Volatility Framework 2.0
Legend: (S) = Stable (V) = Volatile

----------------------------
Registry: \Device\HarddiskVolume1\WINDOWS\system32\config\system
Key name: MRxNet (S)
Last updated: 2011-06-03 04:26:47

Subkeys:
(V) Enum

Values:
REG_SZ Description : (S) MRXNET
REG_SZ DisplayName : (S) MRXNET
REG_DWORD ErrorControl : (S) 0
REG_SZ Group : (S) Network
REG_SZ ImagePath : (S) \??\C:\WINDOWS\system32\Drivers\mrxnet.sys
REG_DWORD Start : (S) 1
REG_DWORD Type : (S) 1

$ ./vol.py printkey -K 'ControlSet001\Services\MrxCls'
Volatile Systems Volatility Framework 2.0
Legend: (S) = Stable (V) = Volatile

----------------------------
Registry:
\Device\HarddiskVolume1\WINDOWS\system32\config\system
Key name: MRxCls (S)
Last updated: 2011-06-03 04:26:47

Subkeys:
(V) Enum

Values:
REG_SZ Description : (S) MRXCLS
REG_SZ DisplayName : (S) MRXCLS
REG_DWORD ErrorControl : (S) 0
REG_SZ Group : (S) Network
REG_SZ ImagePath : (S) \??\C:\WINDOWS\system32\Drivers\mrxcls.sys
REG_DWORD Start : (S) 1
REG_DWORD Type : (S) 1
REG_BINARY Data : (S)
0000 8F 1F F7 6D 7D B1 C9 09 9D CC 24 7A C6 9F FB 23 ...m}.....$z...#
0010 90 BD 9D BF F1 D4 51 92 2A B4 1F 6A 2E A6 4F B3 ......Q.*..j..O.
0020 CB 69 7C 0B 92 3B 1B C0 D7 75 17 A9 E3 33 48 DC .i|..;...u...3H.
0030 AD F6 DA EA 2F 87 10 C4 21 81 A5 75 68 00 2E B1 ..../...!..uh...
0040 C2 7B EB DD BB 72 47 DC 87 91 14 A5 F3 C4 32 B0 .{...rG.......2.
0050 CC 93 38 36 6B 49 0A F2 6F 1F 1D A1 4A 15 05 80 ..86kI..o...J...
0060 4B 13 A8 AA 82 41 4B 89 DC 89 24 A2 ED 16 37 F3 K....AK...$...7.
0070 42 A9 A0 6A 7F 82 CD 90 E5 3C 49 CC B2 97 CA CB B..j.....< span="">
0080 7B 64 C1 48 B2 4C F5 AE 54 42 74 0F 00 31 FD 80 {d.H.L..TBt..1..
0090 E8 7E 0E 69 12 42 3A EC 0F 6F 03 B8 46 9C 68 97 .~.i.B:..o..F.h.
00A0 AC 62 16 FB 1A 1B D9 33 6C E8 F9 93 C3 56 54 A1 .b.....3l....VT.
00B0 89 7A 7B 77 CE BA 0D 95 A7 0F AB 5E 1C 3C 18 63 .z{w.......^.<.c
00C0 AE 3E 60 A6 81 BC FA 85 FB 37 A0 0A 57 F9 C9 D3 .>`......7..W...
00D0 CF 6B 41 D9 6D CD 39 71 C5 11 83 F1 D9 F3 7D B7 .kA.m.9q......}.
00E0 91 F7 70 46 C2 24 F7 B9 0F 2D B2 60 72 1C 8F F9 ..pF.$...-.`r...
00F0 98 16 34 52 4B 7D 5F 81 5F 35 FD 8B 3E 78 B1 0B ..4RK}_._5..>x..
0100 0A 90 5A D8 30 5A 56 90 9A C0 C1 0F EB 95 D5 2F ..Z.0ZV......../
0110 B7 C5 8D 2B 3F 49 41 8B 86 B4 DB 71 67 69 E6 E8 ...+?IA....qgi..
0120 69 77 29 77 18 82 11 8B D7 5D 26 E4 5A 5C 2C 46 iw)w.....]&.Z.,F
0130 C2 F0 02 28 D8 EA 4B 95 9C 3A 3C 12 DA C4 87 21 ...(..K..:<....!
0140 91 4F D0 6E FA C4 DD B7 C9 AF E2 AE FE 14 0F 53 .O.n...........S
0150 C4 BA DD 31 1A 38 7B 37 C0 9E 83 FF 2C B2 4C 88 ...1.8{7....,.L.
0160 33 C1 89 E5 CA 68 31 2D 20 CE 50 64 7B 39 C7 FB 3....h1- .Pd{9..
0170 B1 9F A9 0D 6C 2A 82 AE 7F 25 43 A7 A2 28 EB 27 ....l*...%C..(.'
0180 73 C9 45 F9 FD 53 A8 F4 A7 FD B4 90 B2 28 D8 0C s.E..S.......(..
0190 5A A8 84 D0 7F ED 99 25 18 FE B8 4C 48 66 8D 59 Z......%...LHf.Y
01A0 40 F6 CC 30 A6 F4 04 E8 76 9C EA 0E F6 A4 4A CE @..0....v.....J.
01B0 D2 .<>
You can see the timestamp from when the registry keys were last modified, the full path on disk to the service binary (the Stuxnet drivers) and the full contents of the encrypted Data value. According to Thabet [5], after decryption "This data contains the name of some system processes and filenames for stuxnet files. This data tells the driver the filename of the stuxnet file and the name of the process that stuxnet needs to inject its file into."

Don't forget you can also use the svcscan command to enumerate services.
$ ./vol.py -f stuxnet.vmem svcscan | grep -i mrx
Volatile Systems Volatility Framework 2.0
0x385d28 0x70 -------- 'MRxDAV' 'WebDav Client Redirector' SERVICE_FILE_SYSTEM_DRIVER SERVICE_RUNNING \FileSystem\MRxDAV
0x385db8 0x71 -------- 'MRxSmb' 'MRxSmb' SERVICE_FILE_SYSTEM_DRIVER SERVICE_RUNNING \FileSystem\MRxSmb
Why do we only see legitimate services MRxDAV and MRxSmb? Well, just because there are entries in the ControlSet001\Services registry key, that doesn't mean the malware used the Service Control Manager (services.exe) to create or start the service. For example, you can skip calling CreateService and StartService by creating the registry keys directly and then calling NtLoadDriver (which is exactly what Stuxnet does).

Artifact 12: Kernel Drivers
"Mrxnet.sys is the driver that the programmer originally sent me and that implements the rootkit that hides files, and Mrxcls.sys is a second Stuxnet driver file that launches the malware when the system boots." [1]
Based on this statement, the two drivers should be visible with the modules or modscan commands.
$ ./vol.py modules
[snip]
0x81f8cb60 \??\C:\WINDOWS\system32\Drivers\mrxcls.sys 0x00f895a000 0x005000 mrxcls.sys
0x81c2a530 \??\C:\WINDOWS\system32\Drivers\mrxnet.sys 0x00b21d8000 0x003000 mrxnet.sys
Feel free to extract the drivers to disk with moddump for inspection with strings, IDA Pro, or for scanning with your favorite antivirus.

Artifact 13: Kernel Callbacks
"That means Mrxcls.sys called PsSetLoadImageNotifyRoutine so that Windows would call it whenever an executable image, such as a DLL or device driver, is mapped into memory." [3]
Based on this statement, you can use the callbacks command.
$ ./vol.py callbacks
Volatile Systems Volatility Framework 2.0
Type Callback Owner
PsSetLoadImageNotifyRoutine 0xb240ce4c PROCMON20.SYS
PsSetLoadImageNotifyRoutine 0x805f81a6 ntoskrnl.exe
PsSetLoadImageNotifyRoutine 0xf895ad06 mrxcls.sys
PsSetCreateThreadNotifyRoutine 0xb240cc9a PROCMON20.SYS
PsSetCreateProcessNotifyRoutine 0xf87ad194 vmci.sys
PsSetCreateProcessNotifyRoutine 0xb240cb94 PROCMON20.SYS
KeBugCheckCallbackListHead 0xf83e65ef NDIS.sys (Ndis miniport)
KeBugCheckCallbackListHead 0x806d77cc hal.dll (ACPI 1.0 - APIC platform UP)
KeRegisterBugCheckReasonCallback 0xf8b7aab8 mssmbios.sys (SMBiosData)
KeRegisterBugCheckReasonCallback 0xf8b7aa70 mssmbios.sys (SMBiosRegistry)
[snip]
The output shows the PsSetLoadImageNotifyRoutine installed by mrxcls.sys. Mark also states "Ironically, Process Monitor also uses this callback functionality to monitor image loads." [3] which is why you see the PROCMON20.SYS entries as well.

Artifact 14: FileSystem Hooks
"The driver also registers to a filesystem registration callback routine in order to hook newly created filesystem objects on the fly." [4]
Based on this statement, if the rootkit used IoRegisterFsRegistrationChange to install the file system registration callback routine, then you can use the callbacks plugin to detect it. However, instead it uses IoRegisterDriverReinitialization, which works a bit differently. In particular, it uses different pool tags, a different structure for storing the callback address, and a different symbol name for the list head (nt!_IopDriverReinitializeQueueHead). Extending the callbacks plugin for this purpose is extremely easy. Just take the information shown in the following screen shot and have a look at the other examples in the malware.py source code.


Artifact 15: Devices & IRPs
"The driver scans for the following filesystem driver objects:

* \FileSystem\ntfs
* \FileSystem\fastfat
* \FileSytstem\cdfs

A new device object is created by Stuxnet and attached to the device chain for each device object managed by these driver objects. [...] By inserting such objects, Stuxnet is able to intercept IRP requests (example: writes, reads, to devices NTFS, FAT, or CD-ROM devices)." [4]
Based on this statement, the Stuxnet driver is exploiting Microsoft's layered driver architecture which you can explore with the devicetree plugin. Let's focus on ntfs to start.
$ ./vol.py devicetree 
Volatile Systems Volatility Framework 2.0
[snip]
DRV 0x0253d180 '\\FileSystem\\Ntfs'
DEV 0x8224e020 (unnamed) FILE_DEVICE_DISK_FILE_SYSTEM
ATT 0x8223e6c0 (unnamed) - '\\FileSystem\\sr' FILE_DEVICE_DISK_FILE_SYSTEM
ATT 0x821d52f0 (unnamed) - '\\Driver\\MRxNet' FILE_DEVICE_DISK_FILE_SYSTEM
DEV 0x8224f790 Ntfs FILE_DEVICE_DISK_FILE_SYSTEM
ATT 0x8223edd0 (unnamed) - '\\FileSystem\\sr' FILE_DEVICE_DISK_FILE_SYSTEM
ATT 0x820d2350 (unnamed) - '\\Driver\\MRxNet' FILE_DEVICE_DISK_FILE_SYSTEM
You can see that the MRxNet driver has created several devices (via IoCreateDevice) and attached them to the device chain of \FileSystem\Ntfs for filtering purposes. The Fastfat file system is not used on the system, so it isn't shown, but Cdfs is:
DRV 0x01f9cf38 '\\FileSystem\\Cdfs'
DEV 0x81d9cc88 Cdfs FILE_DEVICE_CD_ROM_FILE_SYSTEM
ATT 0x81ff3d50 (unnamed) - '\\Driver\\MRxNet' FILE_DEVICE_CD_ROM_FILE_SYSTEM
Stuxnet maintains control over network file systems in addition to local disks and CDROMs. In total, it creates and attaches 11 devices. Other targets include vmhgfs (VMware Host/Guest File System), WebDav, and SMB.
DRV 0x023c6268 '\\FileSystem\\Fs_Rec'
DEV 0x82303030 FatCdRomRecognizer FILE_DEVICE_CD_ROM_FILE_SYSTEM
ATT 0x82127b00 (unnamed) - '\\Driver\\MRxNet' FILE_DEVICE_CD_ROM_FILE_SYSTEM
DEV 0x8233a030 FatDiskRecognizer FILE_DEVICE_DISK_FILE_SYSTEM
ATT 0x8230daf0 (unnamed) - '\\Driver\\MRxNet' FILE_DEVICE_DISK_FILE_SYSTEM
DEV 0x8222d918 UdfsDiskRecognizer FILE_DEVICE_DISK_FILE_SYSTEM
ATT 0x81da8bf8 (unnamed) - '\\Driver\\MRxNet' FILE_DEVICE_DISK_FILE_SYSTEM
DEV 0x82259d38 UdfsCdRomRecognizer FILE_DEVICE_CD_ROM_FILE_SYSTEM
ATT 0x821d7530 (unnamed) - '\\Driver\\MRxNet' FILE_DEVICE_CD_ROM_FILE_SYSTEM
DEV 0x81dcd030 CdfsRecognizer FILE_DEVICE_CD_ROM_FILE_SYSTEM

DRV 0x023c6da0 '\\FileSystem\\vmhgfs'
DEV 0x8230d030 hgfsInternal FILE_DEVICE_UNKNOWN
DEV 0x81eba030 HGFS FILE_DEVICE_NETWORK_FILE_SYSTEM
ATT 0x8222d320 (unnamed) - '\\Driver\\MRxNet' FILE_DEVICE_NETWORK_FILE_SYSTEM

DRV 0x023c7030 '\\FileSystem\\MRxSmb'
DEV 0x81d9ad80 LanmanDatagramReceiver FILE_DEVICE_NETWORK_BROWSER
DEV 0x82266c00 LanmanRedirector FILE_DEVICE_NETWORK_FILE_SYSTEM
ATT 0x81da7f10 (unnamed) - '\\Driver\\MRxNet' FILE_DEVICE_NETWORK_FILE_SYSTEM

DRV 0x021ef6e8 '\\FileSystem\\MRxDAV'
DEV 0x81ee4610 WebDavRedirector FILE_DEVICE_NETWORK_FILE_SYSTEM
ATT 0x81d50190 (unnamed) - '\\Driver\\MRxNet' FILE_DEVICE_NETWORK_FILE_SYSTEM
Also, you can use the driverirp command to analyze which IRPs the Stuxnet drivers handle and which ones they ignore. For example:
$ ./vol.py driverirp -r mrxnet
Volatile Systems Volatility Framework 2.0
DriverStart Name IRP IrpAddr IrpOwner HookAddr HookOwner
0xb21d8000 'MRxNet' IRP_MJ_CREATE 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_CREATE_NAMED_PIPE 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_CLOSE 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_READ 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_WRITE 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_QUERY_INFORMATION 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_SET_INFORMATION 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_QUERY_EA 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_SET_EA 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_FLUSH_BUFFERS 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_QUERY_VOLUME_INFORMATION 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_SET_VOLUME_INFORMATION 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_DIRECTORY_CONTROL 0xb21d84ec mrxnet.sys 0xb21d97c4 mrxnet.sys
0xb21d8000 'MRxNet' IRP_MJ_FILE_SYSTEM_CONTROL 0xb21d8496 mrxnet.sys 0xb21d97c4 mrxnet.sys

0xb21d8000 'MRxNet' IRP_MJ_DEVICE_CONTROL 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_INTERNAL_DEVICE_CONTROL 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_SHUTDOWN 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_LOCK_CONTROL 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_CLEANUP 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_CREATE_MAILSLOT 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_QUERY_SECURITY 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_SET_SECURITY 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_POWER 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_SYSTEM_CONTROL 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_DEVICE_CHANGE 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_QUERY_QUOTA 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_SET_QUOTA 0xb21d8486 mrxnet.sys - -
0xb21d8000 'MRxNet' IRP_MJ_PNP 0xb21d8486 mrxnet.sys - -
The HookAddr and HookOwner column in the output is admittedly a little mis-leading. In this case, the IRPs aren't hooked. The columns are just showing where the next hop of execution leads, based on the instructions at the IRP address. The columns are blank for everything except IRP_MJ_DIRECTORY_CONTROL and IRP_MJ_FILE_SYSTEM_CONTROL. Thus, these two IRPs are handled specially by the rootkit. All other IRPs are probably ignored or passed through.

Artifact 16: PDB Reference
"In the driver file, the project path b:\myrtus\src\objfre_w2k_x86\i386 \guava.pdb was not removed." [4]
Based on this statement, you have a unique string to search for in kernel memory. That can be done with Malfind by passing the -K or --kernel flag.
$ ./vol.py malfind -D out -K -Y "myrtus"
Volatile Systems Volatility Framework 2.0
Name Pid Start End Tag Hits Protect
mrxnet.sys - 0xB21D8000 0xB21DB000 - 1 - (Unknown)
Hit: myrtus
0xb21d9d9b 6d 79 72 74 75 73 5c 73 72 63 5c 6f 62 6a 66 72 myrtus.src.objfr
0xb21d9dab 65 5f 77 32 6b 5f 78 38 36 5c 69 33 38 36 5c 67 e_w2k_x86.i386.g
0xb21d9dbb 75 61 76 61 2e 70 64 62 00 00 00 00 00 00 00 00 uava.pdb........
0xb21d9dcb 00 00 00 00 00 30 18 00 00 1c 1a 00 00 fe ff ff .....0..........
0xb21d9ddb ff 00 00 00 00 d8 ff ff ff 00 00 00 00 fe ff ff ................
0xb21d9deb ff cb 84 1d b2 cf 84 1d b2 00 00 00 00 fe ff ff ................
0xb21d9dfb ff 00 00 00 00 d8 ff ff ff 00 00 00 00 fe ff ff ................
0xb21d9e0b ff 21 85 1d b2 25 85 1d b2 00 00 00 00 fe ff ff .!...%..........
We got a hit in mrxnet.sys at 0xb21d9d9b. Easy enough!

Artifact 17 and 18: Windows & Classes
"It registers a window class with the name "AFX64c313" and creates a window corresponding to the class created. The window procedure of the class monitors WM_DEVICE_CHANGE messages sent when there is a change to the hardware configuration of a device or the computer. The window procedure of the class handles only requests with wParam set to DBT_DEVICEARRIVAL." [6]
This statement is describing two different, but related artifacts. We'll detect them using a few plugins built on information originally disclosed by Moyix in his blog GDI Utilities: Taking Screenshots from Memory Dumps. One is the creation of a window class (see RegisterClassEx) which results in a new Atom. If you're not familiar with atom tables, they can be extremely useful for various tasks, including malware analysis. I'll save the details for a later discussion. For now, let's check for the malicious class atom:
$ ./vol.py atomscan
Volatile Systems Volatility Framework 2.0
Table Atom Refs Flags Name
0xcc05da8 0xc0b1 0x4 0 Performed DropEffect
0xcc05da8 0xc0b7 0x2 0 TargetCLSID
0xcc05da8 0xc0d6 0x1 0 text/richtext
0xcc05da8 0xc0d9 0x1 0 application/base64
0xcc05da8 0xc118 0x2 0 AFX64c313
0xcc05da8 0xc010 0x1 RTL_ATOM_PINNED OleDraw
0xcc05da8 0xc033 0x1 RTL_ATOM_PINNED OTHERWINDOWCREATED
0xcc05da8 0xc069 0x17 0 6.0.2600.5512!SysLink
The next artifact we're looking for is a window (see CreateWindowEx) of the class AFX64c313. Handles to windows (and all other USER objects) are stored in a completely different handle table than mutexes, files, registry keys, etc. We can filter by USER object type using the -t parameter.

Note: I don't want to spoil whatever surprises that Okolica and Peterson plan to share at DFRWS 2011 "Extracting the Windows Clipboard from Memory" but using TYPE_CLIPDATA instead of TYPE_WINDOW to this command is *one* of the ways to dump the contents of the clipboard.
$ ./vol.py userhandles -t TYPE_WINDOW
Volatile Systems Volatility Framework 2.0

[snip]

Process: 668 (services.exe)
Thread: 1420
Type: TYPE_WINDOW (tagWND)
Object: 0xbc951d10
Handle: 0xe00e8
Window text: AFX64c313
Window procedure: 0x13fe695
Class: ['AFX64c313']
Superclass: ['AFX64c313']
Style: WS_MINIMIZEBOX|WS_TABSTOP|WS_DLGFRAME|WS_BORDER|WS_THICKFRAME|WS_CAPTION|WS_SYSMENU|WS_MAXIMIZEBOX|WS_GROUP|WS_OVERLAPPED|WS_CLIPSIBLINGS
ExStyle: WS_EX_LTRREADING|WS_EX_RIGHTSCROLLBAR|WS_EX_WINDOWEDGE|WS_EX_LEFT
Visible: No
Coords: left 88, top 116, right 927, bottom 699
What you see here, in a nutshell, is a window of the class AFX64c313 owned by services.exe. Although the window is not visible, it contains the text "AFX64c313." The window procedure is located at 0x13fe695 in the memory of services.exe. To explore the function, break into a volshell, change context to the right process, and disassemble.
$ ./vol.py volshell
Volatile Systems Volatility Framework 2.0
Current context: process System, pid=4, ppid=0 DTB=0x319000
Welcome to volshell! Current memory image is:
file:///memory/stuxnet.vmem
To get help, type 'hh()'

In [1]: cc(pid=668)
Current context: process services.exe, pid=668, ppid=624 DTB=0xa940080

In [2]: dis(0x13fe695)
0x13fe695 55 PUSH EBP
0x13fe696 8bec MOV EBP, ESP
0x13fe698 817d0c19020000 CMP DWORD [EBP+0xc], 0x219 ; WM_DEVICECHANGE
0x13fe69f 7514 JNZ 0x13fe6b5
0x13fe6a1 ff7514 PUSH DWORD [EBP+0x14]
0x13fe6a4 ff7510 PUSH DWORD [EBP+0x10]
0x13fe6a7 e810000000 CALL 0x13fe6bc
0x13fe6ac 59 POP ECX
0x13fe6ad 33c0 XOR EAX, EAX
0x13fe6af 59 POP ECX
0x13fe6b0 40 INC EAX
0x13fe6b1 5d POP EBP
0x13fe6b2 c21000 RET 0x10
0x13fe6b5 5d POP EBP
0x13fe6b6 ff25c4534401 JMP DWORD [0x14453c4]
0x13fe6bc 55 PUSH EBP
0x13fe6bd 8bec MOV EBP, ESP
0x13fe6bf 83e4f8 AND ESP, -0x8
0x13fe6c2 64a100000000 MOV EAX, [FS:0x0]
0x13fe6c8 6aff PUSH -0x1
0x13fe6ca 68893d4401 PUSH DWORD 0x1443d89
0x13fe6cf 50 PUSH EAX
0x13fe6d0 64892500000000 MOV [FS:0x0], ESP
0x13fe6d7 83ec6c SUB ESP, 0x6c
0x13fe6da 817d0800800000 CMP DWORD [EBP+0x8], 0x8000 ; DBT_DEVICEARRIVAL
0x13fe6e1 53 PUSH EBX
0x13fe6e2 56 PUSH ESI
Artifact 17: check! Artifact 18: check. Capabilities you didn't know existed in any memory analysis framework: check! ;=)

Artifact 19 (and Beyond...)

Okay so we didn't quite make it to 20 artifacts with the time allotted, but with the power of Volatility, we've gotten pretty close. 20 is still a small number in relation to how many artifacts Stuxnet actually leaves on a system. We didn't even touch on the RPC server, jobs/tasks, UPX packing, and fake digital certificates. Also, don't forget there are other memory analysis frameworks with different capabilities.

Conclusion

I hope this has been an informative post for people interested in malware analysis, Stuxnet, Volatility, memory forensics, and related topics. Thank you to the authors of all the quoted articles (the people who did real RE work) and to the Volatility team for completing release 2.0!