Icesword rootkit 检测器,它是如何工作的?

信息安全 反恶意软件 Rootkit
2021-08-12 20:10:49

我很好奇如何了解更多关于icesword rootkit 检测器的信息。

我只用过它,从来没有自己研究过。我一直很好奇它是如何工作的。据我了解,它是通过直接查看内存中不同的 Windows 数据结构并将其发现与内核调用返回的结果进行比较来工作的?这有多真实?(我的猜测是我不正确......或部分正确)。

有人可以向我解释它是如何/为什么起作用的吗?

1个回答

tl;dr - 比较两个做同样事情的函数的结果,并寻找差异。

与其专注于单个 rootkit 扫描程序,我将讨论 rootkit 使用的通用技术以及我们如何找到它们。这应该让您更好地了解所涉及的挑战。

Rootkit 通过拦截某些系统调用并修改它们的参数或结果来工作。如果不解释钩子是如何工作的,就很难解释 rootkit 查找器是如何工作的。

例如,在 Windows 上,调用CreateToolhelp32Snapshot创建当前正在运行的进程的快照,并将其存储在全局堆中。然后,Process32FirstandProcess32Next函数允许应用程序遍历进程列表。

这是一个简单的例子:

PROCESSENTRY32 proc;
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);
    printf("-----\n");
}
while (Process32Next(procList, &proc));

现在让我们假设我们想从列表中隐藏一个特定的进程。有两种主要方法可以做到这一点:在用户模式和内核模式。第一个(用户模式)是挂钩我们想要欺骗的进程的导入地址表(IAT),即我们不希望我们的隐藏进程被看到的进程。还有其他方法,但这是最简单的解释。

当您编译一个程序时,可执行文件包含一个导入列表,其中包括您要从中导入的 DLL 的名称、该 DLL 中的 API 名称及其相对虚拟地址 (RVA)。这全部存储在导入目录中,您可以使用PEInfo等工具查看该目录。

当程序执行时,可执行文件被加载到内存中。内核查看导入列表并确定哪些 DLL 需要加载到内存中,哪些已经在共享内存中。完成后,它会在内存中找到导入函数的地址(通过 RVA 或名称)并将这些地址写入内存中的 RVA 值。结果是IAT对所有导入的API就像一个大跳转表,所以程序可以像call [addrOfIAT + n*4]调用n表中的第一个API一样运行指令。

如果我们替换内存中IAT中的地址,我们可以让程序调用我们自己的代码而不是API。在这种情况下,隐藏进程的最简单方法是挂钩Process32FirstProcess32Next如果我们解析可执行文件的标头,我们可以找到 IAT 将被加载的地址,以及 IAT 中 API 的偏移量。一旦我们知道了这一点,我们就可以挂钩 IAT。有很多方法可以做到这一点——直接覆盖 IAT 内存,注入代码来完成它,或者注入一个 DLL。你怎么做并不重要,它超出了这个答案的范围。目标是从 IAT 复制地址,然后用您自己的代码覆盖地址。在这种情况下,让我们想象下面的伪代码:

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;
}

那么我们在这里做什么呢?

  1. Process32First为和Process32Next跳过我们现在尝试的过程实现包装函数。
  2. 从 IAT 获取原始函数的地址。
  3. 用我们的钩子覆盖 IAT 中的地址。

这意味着当程序试图调用一个钩子 API 时,它真的调用了我们的钩子。然后我们的钩子调用原始函数并处理结果。

那么,rootkit 检测器如何找到这些钩子呢?有几种方法:

  • 根据可执行文件中的导入表,遍历内存中每个进程的 IAT,并将地址与应该存在的地址进行比较。不利的一面是,如果 rootkit 检测器本身已在内存中修补了其 IAT,则 rootkit 可以简单地操纵内存和文件读取函数的结果。
  • 使用非标准 API 来迭代进程、读取内存等(例如ntdll函数)。只要用户模式 ​​rootkit 不修补这些,这将起作用。
  • 实现内核模式驱动程序以遍历进程并执行其他检查,然后将结果与用户模式扫描的结果进行比较。如果缺少某些东西,则某些东西正在操纵事物的用户模式方面。这将成功检测几乎所有用户模式的 rootkit。

现在是第二种rootkit:内核模式。在这种情况下,rootkit 做了几乎完全相同的事情,除了它挂钩系统服务调度表 (SSDT) 而不是 IAT,后者为用户模式到内核模式调用提供服务。SSDT 与 IAT 基本相同,只是它包含每个内核模式 API 的地址。rootkit 只是简单地挂钩负责为CreateToolhelp32Snapshot调用提供服务的内核 API,过滤掉它想要隐藏的进程。这会阻止正常的差异扫描工作,因为扫描仪看到的结果也被钩住了。

那么,我们如何扫描内核模式的 rootkit?答案是:很难。如果 rootkit 正在挂钩 SSDT,我们就不能依赖它。因此,我们必须求助于实现我们自己的内核 API 版本来读取和操作内核对象。这称为直接内核对象修改 (DKOM)。这很棘手,因为这些对象通常未记录或仅部分记录,并且可能在 Windows 版本之间发生变化。Windows 内核中的进程由EPROCESS双向链表中的结构表示。

// 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;

while(1)
{
    // 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!
        KdPrint("END\n");
        break;
    }

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

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

然后,我们可以将此进程 ID 列表与正常内核模式和用户模式 ​​API 生成的列表进行比较,以找出隐藏的进程以及挂钩的位置。

不幸的是,恶意软件也有同样的能力做到这一点。他们可以从列表中删除其隐藏进程的 EPROCESS:

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;
}

这有效地从列表中删除了进程,使我们以前的 DKOM 方法无法扫描。

从这里开始,我们唯一能做的就是进行工件扫描的军备竞赛这涉及识别对隐藏对象具有潜在引用的内核对象,以便识别它们。或者,我们可以在内存中扫描看起来像EPROCESS条目或其他内核对象的对象,以识别异常值。SSDT 钩子可以通过这种方式识别,方法是在内存中查找真实的 API(通过签名扫描)并将它们的真实地址与存储在 SSDT 中的地址进行比较。

希望这能让您更全面地了解 rootkit 的工作原理以及我们如何查找它们。

进一步阅读: