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 _ETIMERKTIMER.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.
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
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 ScavengerTimerSo 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.
.data:0001F990 _ScavengerTimer
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 128On Windows XP SP0-SP3 x86 and Windows 2003 SP0, Microsoft expanded the array to 258 LIST_ENTRYs.
#define LIST_ENTRY KiTimerTableListHead[TIMER_TABLE_SIZE];
#define TIMER_TABLE_SIZE 256On 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 LIST_ENTRY KiTimerTableListHead[TIMER_TABLE_SIZE];
#define TIMER_TABLE_SIZE 512Starting 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.
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];
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!
0 comments:
Post a Comment