I'm curious about how I can learn more about icesword rootkit detector.

I've only used it and never studied it myself. Ive always been curious how it works. As far as I understand it works by looking directly at different windows data structures in memory and compares its findings to what the kernel calls returns? How true is this? (my guess is I'm incorrect.....or partially correct).

Would someone be able to explain to me how/why it works?

1 Answer 1


tl;dr - compare the results of two functions that do the same thing, and look for differences.

Instead of focusing on that single rootkit scanner, I'm going to talk about generic techniques that rootkits use and how we can find them. This should give you a better overview of the challenges involved.

Rootkits work by intercepting certain system calls and modifying their parameters or results. It's difficult to explain how a rootkit finder works without explaining how hooks work.

For example, on Windows, a call to CreateToolhelp32Snapshot creates a snapshot of the current running processes, and stores it on the global heap. The Process32First and Process32Next functions then allow the application to iterate through the list of processes.

Here's a quick example:

procList = CreateToolhelp32Snapshot(flags, 0);

Process32First(procList, &proc); // seek to first item in list and store it in proc

do {
    printf("Process ID: %d\n", proc.th32ProcessID);
    printf("Thread count: %d\n", proc.th32Threads);
    printf("Path: %s\n", proc.szExeFile);
while (Process32Next(procList, &proc));

Now let's imagine we want to hide a particular process from the list. There are two main ways to do this: in usermode and in kernelmode. The first (usermode) is to hook the Import Address Table (IAT) of the process we want to spoof, i.e. the process we don't want our hidden process to be seen by. There are other ways, but this is the simplest to explain.

When you compile a program, the executable contains a list of imports, which consist of the name of the DLL you want to import from, the name of the API within that DLL, and its Relative Virtual Address (RVA). This is all stored in the import directory, which you can view with a tool such as PEInfo.

When the program is executed, the executable file is loaded into memory. The kernel looks through the import list and identifies which DLLs need to be loaded into memory, and which are already in shared memory. Once that's done, it finds the addresses of the imported functions in memory (either by RVA or name) and writes those addresses over the RVA values in memory. The result is that the IAT acts like a big jump table to all the imported APIs, so the program can run instructions like call [addrOfIAT + n*4] to call the nth API in the table.

If we replace the addresses in the IAT in memory, we can make the program call our own code instead of the API. In this case, the easiest way to hide a process is to hook Process32First and Process32Next. If we parse the executable's headers, we can find the address where the IAT will be loaded, and the offset of the APIs in the IAT. Once we know that, we can hook the IAT. There's a bunch of ways to do it - directly writing over the IAT memory, inject code to do it, or inject a DLL. Doesn't really matter how you do it, and it's outside the scope of this answer. The goal is to copy the address from the IAT, then overwrite the address with that of your own code. In this case, let's imagine the following pseudo-code:

void* Process32FirstOriginal;
void* Process32NextOriginal;

const int HiddenProcessId = 1234; // ID of the process we want to hide

void InstallHook()
    int offsetP32First = 0x40; // offsets in the IAT
    int offsetP32Next = 0x44;

    // make a backup of the actual API addresses
    // this isn't actually how we'd access the IAT, I'm just being simplistic
    Process32FirstOriginal = IAT[offsetP32First];
    Process32NextOriginal = IAT[offsetP32Next];

    // patch the IAT with the address of our hooks
    IAT[offsetP32First] = &Process32FirstHooked;
    IAT[offsetP32Next] = &Process32NextHooked;

int Process32FirstHooked(void* snapshot, PROCESSENTRY32* proc)
    // call the original function
    int result = Process32FirstOriginal(snapshot, proc);
    // did we just fetch the process we're trying to hide?
    if (proc.th32ProcessId == HiddenProcessId)
        // skip the process we're trying to hide and get the next one
        result = Process32NextHooked(snapshot, proc);
    return result;

int Process32NextHooked(void* snapshot, PROCESSENTRY32* proc)
    int result = Process32NextOriginal(snapshot, proc);
    if (proc.th32ProcessId == HiddenProcessId)
        result = Process32NextHooked(snapshot, proc);
    return result;

So what are we doing here?

  1. Implement wrapper functions for Process32First and Process32Next that skip over the process we're trying to now.
  2. Get the addresses of the original functions from the IAT.
  3. Overwrite the addresses in the IAT with those of our hooks.

What this means is that when the program tries to call a hooked API, it really calls our hook. Our hook then calls the original function and manipulates the results.

So, how does a rootkit detector find these hooks? There are a few ways:

  • Iterate through each process's IAT in memory and compare the addresses to those that should be there, according to the import table in the executable file. Downside of this is that, if the rootkit detector itself has had its IAT patched in memory, the rootkit could simply manipulate the results of the memory and file reads functions.
  • Use non-standard APIs to iterate through processes, read memory, etc (e.g. ntdll functions). This will work as long as a usermode rootkit doesn't patch these.
  • Implement a kernel-mode driver to iterate through processes and perform other checks, then compare the results to that of a user-mode scan. If stuff is missing, something is manipulating the user-mode side of things. This will successfully detect almost any user-mode rootkit.

Now comes the second type of rootkit: kernel mode. In this case the rootkit does almost exactly the same thing, except it hooks the System Service Dispatch Table (SSDT) instead of the IAT, which services user-mode to kernel-mode calls. The SSDT is basically the same as an IAT, except it contains the address of every kernel-mode API. The rootkit simply hooks the kernel APIs responsible for servicing the CreateToolhelp32Snapshot call, filtering out the process that it wants to hide. This stops normal discrepancy scanning from working, because the results the scanner sees are hooked too.

So, how do we scan for kernel-mode rootkits? The answer is: it's difficult. If the rootkit is hooking the SSDT, we can't rely on it. So, we have to resort to implementing our own versions of the kernel API to read and manipulate kernel objects. This is called Direct Kernel Object Modification (DKOM). This is tricky, since these objects are usually undocumented or only partially documented, and may change between versions of Windows. Processes in the Windows kernel are represented by EPROCESS structures, in a doubly linked list.

// get the EPROCESS struct for the current executing process
EPROCESS* eproc = PsGetCurrentProcess();
// get the LIST_ENTRY item for the EPROCESS, so we can iterate the linked list
LIST_ENTRY currentEntry = eproc->ActiveProcessLinks;
// store the first pID, so we know when we've looped the list
DWORD startPID = (DWORD) eproc->UniqueProcessId;
int count = 0;

    // find the EPROCESS structure from the LIST_ENTRY object
    eproc = (EPROCESS*)((DWORD)currentEntry - OFFSET_LIST_FLINK);

    // are we at the end of the list?
    if (count > 0 && eproc->UniqueProcessId == startPID)
        // we've gone through the whole list!

    // print the process ID to the debugger
    KdPrint("Process ID: %d\n", eproc->UniqueProcessId);

    // go to the next entry
    currentEntry = *currentEntry.FLink;

We can then compare this list of process IDs to a list produced by normal kernel-mode and user-mode APIs, to find out which processes are hidden and where the hooking has been done.

Unfortunately, malware has the same ability to do this. They can remove the EPROCESS of their hidden process from the list:

void HideProcess(EPROCESS* proc)
    LIST_ENTRY hideEntry = eproc->ActiveProcessLinks;

    // get the previous and next list entries
    LIST_ENTRY prevEntry = *hideEntry.BLink;
    LIST_ENTRY nextEntry = *hideEntry.Flink;
    // set their forward and backward links to skip over the hidden entry
    prevEntry.FLink = &nextEntry;
    nextEntry.BLink = &prevEntry;
    // set the hidden entry's forward and backward links to itself
    hideEntry.FLink = &hideEntry;
    hideEntry.BLink = &hideEntry;

This effectively removes the process from the list, making our previous DKOM approach to scanning impossible.

From here, the only approach we can do is an arms race of artifact scanning. This involves identifying kernel objects that have latent references to hidden objects, in order to identify them. Alternatively, we can scan through memory for objects that look like EPROCESS entries or other kernel objects, in order to identify unusual values. SSDT hooks can be identified in this way, by looking for the real APIs in memory (scan by signature) and comparing their real addresses to the addresses stored in the SSDT.

Hopefully this gives you a more comprehensive understanding of how rootkits work, and how we can look for them.

Further reading:

  • By the way, these principles apply to Linux and other operating systems too. The structures and APIs are different, but the ideas remain the same. I decided to focus on Windows because I'm familiar with the Windows NT6 kernel.
    – Polynomial
    Aug 25, 2012 at 11:28
  • 1
    @Rell3oT Glad it helped. I really suggest checking out the book by Bill Blunden. It doesn't cost a lot for the amount of knowledge you gain from it, and it's crammed full of sourcecode and real life examples. I've learnt huge amounts about the Windows kernel and rootkits from it.
    – Polynomial
    Aug 25, 2012 at 13:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy