我很好奇如何了解更多关于icesword rootkit 检测器的信息。
我只用过它,从来没有自己研究过。我一直很好奇它是如何工作的。据我了解,它是通过直接查看内存中不同的 Windows 数据结构并将其发现与内核调用返回的结果进行比较来工作的?这有多真实?(我的猜测是我不正确......或部分正确)。
有人可以向我解释它是如何/为什么起作用的吗?
我很好奇如何了解更多关于icesword rootkit 检测器的信息。
我只用过它,从来没有自己研究过。我一直很好奇它是如何工作的。据我了解,它是通过直接查看内存中不同的 Windows 数据结构并将其发现与内核调用返回的结果进行比较来工作的?这有多真实?(我的猜测是我不正确......或部分正确)。
有人可以向我解释它是如何/为什么起作用的吗?
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。在这种情况下,隐藏进程的最简单方法是挂钩Process32First和Process32Next。如果我们解析可执行文件的标头,我们可以找到 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;
}
那么我们在这里做什么呢?
Process32First为和Process32Next跳过我们现在尝试的过程实现包装函数。这意味着当程序试图调用一个钩子 API 时,它真的调用了我们的钩子。然后我们的钩子调用原始函数并处理结果。
那么,rootkit 检测器如何找到这些钩子呢?有几种方法:
ntdll函数)。只要用户模式 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 的工作原理以及我们如何查找它们。
进一步阅读: