22、进程保护与隐藏:DKOM技术、进程隐藏、进程保护、反反调试

说实话,进程保护与隐藏这个话题,在Windows内核开发里算是个“老牌劲旅”了。我早年做安全产品的时候,几乎天天跟这些东西打交道。你想想看,一个恶意软件想藏起来不被发现,一个反病毒软件又想把它揪出来——这本质上就是一场猫鼠游戏。

今天这一章,咱们就聊聊DKOM(Direct Kernel Object Manipulation)技术。说白了,就是直接操作内核对象。这招很底层,也很暴力。我个人的习惯是,先理解它为什么能工作,再动手写代码。

DKOM的基本原理

Windows内核里管理进程,靠的是一个叫EPROCESS的结构体。每个进程都有一个。这些结构体通过一个双向链表串在一起。你打开任务管理器,看到的进程列表,其实就是遍历这个链表的结果。

那么问题来了:如果我把某个进程的EPROCESS从链表里摘掉,任务管理器是不是就看不到它了?

没错,这就是DKOM隐藏进程的核心思路。

核心思想: 直接修改内核中的EPROCESS链表,将目标进程的节点从链表中移除,从而达到“隐藏”的效果。

但这里有个坑。我当年第一次做这个实验时,直接把节点摘掉,结果系统蓝屏了。为什么?因为链表操作必须保证前后指针的正确性。你摘掉一个节点,必须把它的前一个节点和后一个节点连起来。

// 伪代码:从双向链表中移除一个节点
// 假设 pEProcess 指向目标进程的 EPROCESS 结构
PLIST_ENTRY pListEntry = &pEProcess->ActiveProcessLinks;
PLIST_ENTRY pPrev = pListEntry->Blink;
PLIST_ENTRY pNext = pListEntry->Flink;

pPrev->Flink = pNext;
pNext->Blink = pPrev;

// 注意:这里只是从链表移除,并没有释放内存
// 进程本身还在运行,只是“看不见”了

警告: 直接操作内核链表必须关中断,并且要保证操作是原子的。否则在操作过程中被线程切换打断,链表就乱套了。我曾经因为这个原因,调试了整整一个下午。

进程隐藏的完整实现

隐藏进程,不仅仅是摘链表那么简单。你想想看,任务管理器看不到它了,但ZwQuerySystemInformation这个API呢?很多安全软件是通过这个API枚举进程的。

所以,真正的隐藏需要做两件事:

  1. 摘链表:让EPROCESS不出现在内核的进程链表中。
  2. Hook API:拦截ZwQuerySystemInformation,在返回结果前过滤掉目标进程。

我个人更推荐第二种方式,因为它更安全。直接摘链表容易导致某些依赖链表遍历的内核功能出问题。

// Hook ZwQuerySystemInformation 的简化逻辑
NTSTATUS HookedZwQuerySystemInformation(
    SYSTEM_INFORMATION_CLASS SystemInformationClass,
    PVOID SystemInformation,
    ULONG SystemInformationLength,
    PULONG ReturnLength)
{
    NTSTATUS status;
    
    // 先调用原始函数
    status = OriginalZwQuerySystemInformation(
        SystemInformationClass,
        SystemInformation,
        SystemInformationLength,
        ReturnLength);
    
    if (NT_SUCCESS(status) && 
        SystemInformationClass == SystemProcessInformation)
    {
        // 遍历返回的进程信息,移除目标进程
        RemoveTargetProcessFromList(SystemInformation);
    }
    
    return status;
}

提示: 我在项目中遇到过一个问题:Hook了ZwQuerySystemInformation之后,某些杀毒软件会检测到我们的Hook。后来我改用Inline Hook + 硬件断点的组合方式,才绕过了检测。嗯,这里面的门道很深。

进程保护:让进程“杀不死”

隐藏是第一步,保护是第二步。什么叫保护?就是别人想结束你的进程,做不到。

常见的结束进程方式有两种:

  • 用户态:调用TerminateProcess API。
  • 内核态:直接调用ZwTerminateProcess

保护的核心思路,就是拦截这些调用。我常用的方法是:

  1. Hook ObReferenceObjectByHandle:当有人想打开你的进程句柄时,返回拒绝。
  2. Hook NtTerminateProcess:直接拦截结束进程的请求。
// 保护进程:拦截 TerminateProcess
NTSTATUS HookedNtTerminateProcess(HANDLE ProcessHandle, NTSTATUS ExitStatus)
{
    PEPROCESS pEProcess = NULL;
    
    // 获取目标进程的 EPROCESS
    if (NT_SUCCESS(PsLookupProcessByProcessId(
        GetCurrentProcessId(), &pEProcess)))
    {
        // 检查是否是受保护的进程
        if (IsProtectedProcess(pEProcess))
        {
            ObDereferenceObject(pEProcess);
            return STATUS_ACCESS_DENIED;
        }
        ObDereferenceObject(pEProcess);
    }
    
    // 不是受保护进程,放行
    return OriginalNtTerminateProcess(ProcessHandle, ExitStatus);
}

关键点: 保护进程时,一定要考虑“递归调用”的问题。比如你的Hook函数里又调用了PsLookupProcessByProcessId,而这个函数内部可能又调用了你Hook的函数……那就死循环了。我建议在Hook函数入口处加一个“重入标志”。

反反调试:与调试器的博弈

反调试和反反调试,说白了就是“你藏我找”的游戏。调试器想附加到你的进程,你不想让它附加。怎么办?

常见的反调试手段有:

  • 检测调试器:调用NtQueryInformationProcess检查ProcessDebugPort
  • 时间差检测:执行一段代码前后计时,如果时间差过大,说明被调试了。
  • 异常处理:故意触发异常,如果被调试器捕获,说明有调试器存在。

但反反调试也有对策。我记得有一次,一个恶意软件用NtSetInformationProcess把自己从调试器里“踢”出来。我当时是怎么应对的?

嗯,我在内核里Hook了NtSetInformationProcess,当检测到有人想修改ProcessDebugPort时,直接返回成功,但实际上什么也不做。这样调试器就“赖”在进程里不走了。

// 反反调试:阻止调试器被移除
NTSTATUS HookedNtSetInformationProcess(
    HANDLE ProcessHandle,
    PROCESS_INFORMATION_CLASS ProcessInformationClass,
    PVOID ProcessInformation,
    ULONG ProcessInformationLength)
{
    // 如果试图修改调试端口,直接返回成功
    if (ProcessInformationClass == ProcessDebugPort)
    {
        return STATUS_SUCCESS;
    }
    
    // 其他信息正常处理
    return OriginalNtSetInformationProcess(
        ProcessHandle,
        ProcessInformationClass,
        ProcessInformation,
        ProcessInformationLength);
}

注意: 反调试和反反调试是一个动态博弈的过程。你今天写的代码,明天可能就被新的技术绕过了。我个人的建议是:不要追求“绝对安全”,而是要做到“足够难破解”。

知识体系总览

下面这张图,是我对本章知识点的总结。你可以把它当作一个“思维导图”来看。

DKOM 技术 进程隐藏 进程保护 反反调试 摘除 EPROCESS 链表节点 Hook ZwQuerySystemInformation 过滤进程信息列表 Hook NtTerminateProcess Hook ObReferenceObjectByHandle 重入保护机制 检测调试器存在 时间差检测 Hook NtSetInformationProcess 核心原则:操作前备份,操作后恢复 避免死锁和递归调用

避坑指南

最后,分享几个我踩过的坑:

  • 链表操作一定要关中断:否则在操作过程中被中断,链表指针可能被其他线程修改,导致蓝屏。
  • Hook函数要处理重入:你的Hook函数里如果调用了被Hook的原始函数,一定要有重入保护。否则就是死循环。
  • 不要忘记恢复:卸载驱动时,一定要把被修改的内核对象恢复原状。否则系统会变得不稳定。
  • 测试环境要用虚拟机:我当年直接在物理机上测试,结果蓝屏了,数据全丢了。从那以后,我再也不敢在物理机上做内核实验了。

个人经验: 做内核开发,一定要有“敬畏之心”。你写的每一行代码,都可能让整个系统崩溃。我建议你在写代码之前,先在纸上画清楚逻辑,想清楚所有边界情况。嗯,这听起来很老套,但真的有用。


公众号:蓝海资料掘金营,微信deep3321