4.2 Windows驱动开发:内核中进程线程与模块

news2025/1/17 19:36:31

内核进程线程和模块是操作系统内核中非常重要的概念。它们是操作系统的核心部分,用于管理系统资源和处理系统请求。在驱动安全开发中,理解内核进程线程和模块的概念对于编写安全的内核驱动程序至关重要。

内核进程是在操作系统内核中运行的程序。每个进程都有一个唯一的进程标识符(PID),它用于在系统中唯一地标识该进程。在内核中,进程被表示为一个进程控制块(PCB),它包含有关进程的信息,如进程状态、优先级、内存使用情况等。枚举进程可以让我们获取当前系统中所有正在运行的进程的PID和其他有用的信息,以便我们可以监视和管理系统中的进程。

线程是在进程内部执行的轻量级执行单元。与进程不同,线程不拥有自己的地址空间和系统资源,它们共享它们所属进程的资源。在内核中,线程被表示为线程控制块(TCB),它包含有关线程的信息,如线程状态、调度信息、执行上下文等。枚举线程可以让我们获取当前系统中所有正在运行的线程的PID、线程ID和其他有用的信息,以便我们可以监视和管理系统中的线程。

内核模块是一种可加载的内核组件,它可以动态地添加到内核中。内核模块通常用于向内核添加新的设备驱动程序或系统功能。在驱动安全开发中,理解内核模块的概念对于编写安全的内核驱动程序非常重要。枚举内核模块可以让我们获取当前系统中加载的所有内核模块的名称、版本号和其他有用的信息,以便我们可以分析和调试内核模块。

在总体上,内核进程线程和模块是操作系统内核中非常重要的概念。通过了解这些概念,我们可以更好地理解操作系统内部的工作原理,从而编写更安全的内核驱动程序。

4.2.1 内核中实现枚举进程

进程就是活动起来的程序,每一个进程在内核里,都有一个名为 EPROCESS 的结构记录它的详细信息,其中就包括进程名,PID,PPID,进程路径等,通常在应用层枚举进程只列出所有进程的编号即可,不过在内核层需要把它的 EPROCESS 地址给列举出来。

在内核中枚举进程我们可通过循环语句遍历进程句柄0-100000以内的值,每次通过PsLookupProcessByProcessId打开一个进程并得到进程EPROCESS结构,当获取到该结构体时只需要调用不同的三个内核函数即可获取到当前句柄所对应的进程相关信息。

当我们需要通过EPROCESS得到进程名时可使用PsGetProcessImageFileName()这个内核函数,该函数的具体定义规范如下所示;

PCHAR PsGetProcessImageFileName(
  PEPROCESS Process
);

其中,参数Process是一个PEPROCESS类型的指针,表示要获取映像文件名的进程的EPROCESS结构体指针;返回值是一个PCHAR类型的指针,指向包含指定进程映像文件名的空字符结尾字符串。

与之功能类似,当我们需要通过EPROCESS获取进程PID时,则可以调用PsGetProcessId()来获取到,该函数的具体定义规范如下所示;

HANDLE PsGetProcessId(
  PEPROCESS Process
);

其中,参数Process是一个PEPROCESS类型的指针,表示要获取进程ID的进程的EPROCESS结构体指针;返回值是一个HANDLE类型的进程ID值。

而如果当我们想要获取到进程的父进程时,则同样可使用PsGetProcessInheritedFromUniqueProcessId()来获取,该函数的具体定义规范如下所示;

HANDLE PsGetProcessInheritedFromUniqueProcessId(
  PEPROCESS Process
);

其中,参数Process是一个PEPROCESS类型的指针,表示要获取父进程ID的进程的EPROCESS结构体指针;返回值是一个HANDLE类型的父进程ID值。

有了这三个函数的支持,我们就可以实现遍历当前所有运行的进程啦,具体实现代码如下所示;

#include <ntifs.h>

// 未公开的进行导出即可
NTKERNELAPI UCHAR* PsGetProcessImageFileName(IN PEPROCESS Process);

// 未公开进行导出
NTKERNELAPI HANDLE PsGetProcessInheritedFromUniqueProcessId(IN PEPROCESS Process);

// 根据进程ID返回进程EPROCESS结构体,失败返回NULL
PEPROCESS LookupProcess(HANDLE Pid)
{
    PEPROCESS eprocess = NULL;
    NTSTATUS Status = STATUS_UNSUCCESSFUL;
    Status = PsLookupProcessByProcessId(Pid, &eprocess);
    if (NT_SUCCESS(Status))
        return eprocess;
    return NULL;
}

VOID EnumProcess()
{
    PEPROCESS eproc = NULL;
    for (int temp = 0; temp < 100000; temp += 4)
    {
        eproc = LookupProcess((HANDLE)temp);
        if (eproc != NULL)
        {
            DbgPrint("进程名: %s --> 进程PID = %d --> 父进程PPID = %d\r\n",PsGetProcessImageFileName(eproc),PsGetProcessId(eproc),
                PsGetProcessInheritedFromUniqueProcessId(eproc));
            ObDereferenceObject(eproc);
        }
    }
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint(("Uninstall Driver Is OK \n"));
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    EnumProcess();
    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

输出效果图如下所示:

4.2.2 内核中实现枚举线程

内核线程的枚举与枚举进程十分相似,内核线程中也存在一个ETHREAD结构,但在枚举线程之前需要先来枚举到指定进程的eprocess结构,然后再根据eprocess结构对指定线程进行枚举。

在内核中实现枚举线程需要遵循以下步骤:

  • 枚举指定进程的eprocess结构:在内核中,每个进程都有一个唯一的eprocess结构表示,该结构包含了该进程的各种信息,包括其线程列表。首先,需要枚举到指定进程的eprocess结构。可以通过访问系统的进程链表,找到该进程的eprocess结构。
  • 遍历线程列表:一旦枚举到了指定进程的eprocess结构,就可以通过该结构中的线程列表来枚举该进程的所有线程。线程列表中包含每个线程的ETHREAD结构。
  • 枚举每个线程的ETHREAD结构:遍历线程列表,对于每个线程,可以通过其ETHREAD结构访问该线程的各种信息,包括其状态、优先级、CPU时间等等。
  • 处理枚举结果:枚举过程中可以将每个线程的ETHREAD结构存储到一个缓冲区中,以便后续处理。

需要注意的是,在枚举线程的过程中,需要保证访问的安全性和正确性。例如,需要确保在访问每个线程的ETHREAD结构时,该线程不会被销毁或修改。同时,还需要考虑内核与用户空间的交互,以及多处理器系统中的并发访问等问题。

为了能写出完整的代码,这里我们还需要介绍三个未导出函数,PsGetProcessImageFileNamePsLookupThreadByThreadIdIoThreadToProcess这三个函数是实现枚举线程的关键,它们提供了枚举线程相关的关键功能;

PsGetProcessImageFileName 函数的作用是获取指定进程的可执行文件名。在枚举线程时,可以使用该函数获取线程所属进程的可执行文件名,从而可以更方便地识别线程。

NTKERNELAPI PCHAR PsGetProcessImageFileName(IN PEPROCESS Process)

其中,PEPROCESS是一个指向进程对象的指针,该函数将返回一个指向进程可执行文件名的指针。

PsLookupThreadByThreadId 函数的作用是根据线程ID查找线程对象。在枚举线程时,可以使用该函数根据线程ID获取线程对象的指针,进而获取线程的相关信息。

NTKERNELAPI PETHREAD PsLookupThreadByThreadId(IN HANDLE ThreadId)

其中,HANDLE是一个线程ID,该函数将返回一个指向线程对象的指针。

IoThreadToProcess 函数的作用是获取线程所属进程的指针。在枚举线程时,可以使用该函数获取线程所属进程的指针,进而获取进程的相关信息。

NTKERNELAPI PEPROCESS IoThreadToProcess(IN PETHREAD Thread)

其中,PETHREAD是一个指向线程对象的指针,该函数将返回一个指向线程所属进程的指针。

有了上述三个函数的支持,那么实现枚举线程就变得非常简单了,EnumThread则是用于实现线程枚举的核心代码;

  • 首先,定义了一个用于循环遍历线程ID的变量i,并且初始化为4,因为Windows系统的线程ID从4开始。
  • 定义了两个指针类型的变量ethrd和eproc,用于保存获取到的线程对象和线程所属进程对象的指针。
  • 循环遍历线程ID,每次增加4,直到262144为止。这个范围应该是保守估计,实际上可能更小,因为一般来说系统中并不会存在那么多的线程。
  • 调用LookupThread函数,根据线程ID查找线程对象。如果找到了线程对象,则获取线程所属进程对象的指针,并且判断该进程对象是否与指定的进程对象相同。
  • 如果是指定的进程对象,则打印出线程对象和线程ID。最后释放线程对象的引用计数。

其完整实现代码如下所示;

#include <ntddk.h>
#include <windef.h>

// 声明API
NTKERNELAPI UCHAR* PsGetProcessImageFileName(IN PEPROCESS Process);
NTKERNELAPI NTSTATUS PsLookupProcessByProcessId(HANDLE Id, PEPROCESS *Process);
NTKERNELAPI NTSTATUS PsLookupThreadByThreadId(HANDLE Id, PETHREAD *Thread);
NTKERNELAPI PEPROCESS IoThreadToProcess(PETHREAD Thread);

// 根据进程ID返回进程EPROCESS,失败返回NULL
PEPROCESS LookupProcess(HANDLE Pid)
{
    PEPROCESS eprocess = NULL;
    if (NT_SUCCESS(PsLookupProcessByProcessId(Pid, &eprocess)))
        return eprocess;
    else
        return NULL;
}

// 根据线程ID返回线程ETHREAD,失败返回NULL
PETHREAD LookupThread(HANDLE Tid)
{
    PETHREAD ethread;
    if (NT_SUCCESS(PsLookupThreadByThreadId(Tid, &ethread)))
        return ethread;
    else
        return NULL;
}

// 枚举指定进程中的线程
VOID EnumThread(PEPROCESS Process)
{
    ULONG i = 0, c = 0;
    PETHREAD ethrd = NULL;
    PEPROCESS eproc = NULL;

  // 一般来说没有超过100000的PID和TID
    for (i = 4; i<262144; i = i + 4)
    {
        ethrd = LookupThread((HANDLE)i);
        if (ethrd != NULL)
        {
            // 获得线程所属进程
            eproc = IoThreadToProcess(ethrd);
            if (eproc == Process)
            {
                // 打印出ETHREAD和TID
                DbgPrint("线程: ETHREAD=%p TID=%ld\n",ethrd,(ULONG)PsGetThreadId(ethrd));
            }
            ObDereferenceObject(ethrd);
        }
    }
}

// 通过枚举的方式定位到指定的进程,这里传递一个进程名称
VOID MyEnumThread(char *ProcessName)
{
    ULONG i = 0;
    PEPROCESS eproc = NULL;
    for (i = 4; i<100000000; i = i + 4)
    {
        eproc = LookupProcess((HANDLE)i);
        if (eproc != NULL)
        {
            ObDereferenceObject(eproc);
            if (strstr(PsGetProcessImageFileName(eproc), ProcessName) != NULL)
            {
        // 相等则说明是我们想要的进程,直接枚举其中的线程
                EnumThread(eproc);
            }
        }
    }
}

VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{

}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
    MyEnumThread("lyshark.exe");
    DriverObject->DriverUnload = DriverUnload;
    return STATUS_SUCCESS;
}

输出效果图如下所示:

4.2.3 内核中实现枚举进程模块

枚举进程中的所有模块信息,DLL模块信息被记录在PEBLDR链表里,LDR是一个双向链表枚举链表即可。

在操作系统内核中实现枚举进程模块的过程中,需要首先访问进程的PEB(进程环境块)数据结构。PEB是一个系统数据结构,记录了进程的各种信息,包括进程的内存布局、环境变量、进程的模块列表等。

进程的模块信息被记录在PEB的LDR(Loader)链表中。这个链表是一个双向链表,记录了进程的所有模块,包括已加载和未加载的模块。

要枚举进程中的所有模块信息,需要遍历LDR链表。在遍历LDR链表时,可以通过遍历双向链表中的节点来获取每个模块的详细信息,如模块的基址、模块的大小、模块的名称等。

遍历LDR链表的过程中,可以使用双向链表的常见操作,如while循环遍历,或使用指针的操作来访问下一个或上一个节点。在访问每个节点时,可以通过节点的指针访问节点中记录的模块信息,例如通过节点的指针访问模块的基址、大小、名称等信息。

通过枚举LDR链表,可以获取进程中的所有模块信息,并且可以在内核中对这些模块进行操作,如卸载模块、加载模块等。

在开始实现枚举进程模块之前,我们需要手动寻找peb.ldr以及peb.ldr.InLoadOrderModuleList的实际偏移地址,该偏移地址在不同的系统内是不同的,通过WinDBG调试Windows系统,并输入如下命令,即可找到我们所需的内核偏移值;

1: kd> dt _PEB
ntdll!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x003 BitField         : UChar
   +0x003 ImageUsesLargePages : Pos 0, 1 Bit
   +0x003 IsProtectedProcess : Pos 1, 1 Bit
   +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit
   +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit
   +0x003 IsPackagedProcess : Pos 4, 1 Bit
   +0x003 IsAppContainer   : Pos 5, 1 Bit
   +0x003 IsProtectedProcessLight : Pos 6, 1 Bit
   +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit
   +0x004 Padding0         : [4] UChar
   +0x008 Mutant           : Ptr64 Void
   +0x010 ImageBaseAddress : Ptr64 Void
   +0x018 Ldr              : Ptr64 _PEB_LDR_DATA                   // LDR结构
   +0x020 ProcessParameters : Ptr64 _RTL_USER_PROCESS_PARAMETERS

1: kd> dt _PEB_LDR_DATA
ntdll!_PEB_LDR_DATA
   +0x000 Length           : Uint4B
   +0x004 Initialized      : UChar
   +0x008 SsHandle         : Ptr64 Void
   +0x010 InLoadOrderModuleList : _LIST_ENTRY                     // 链表结构
   +0x020 InMemoryOrderModuleList : _LIST_ENTRY
   +0x030 InInitializationOrderModuleList : _LIST_ENTRY
   +0x040 EntryInProgress  : Ptr64 Void
   +0x048 ShutdownInProgress : UChar
   +0x050 ShutdownThreadId : Ptr64 Void

获取到这两个关键偏移值以后,接下来就是封装EnumModule实现函数了,如下方核心代码的核心是在内核模式下枚举指定进程的模块列表,并打印每个模块的基址、大小和路径。它首先获取指定进程的 PEB,然后通过访问进程的 Ldr 数据结构获取模块列表信息,并使用 ProbeForRead 函数测试访问内存的可读性。

通过循环将所有的Module格式化为PLDR_DATA_TABLE_ENTRY结构并打印每个模块的信息,输出结束后取消对进程的依附,以此来实现枚举进程内所有的加载模块信息;

#include <ntddk.h>
#include <windef.h>

// 声明结构体
typedef struct _KAPC_STATE
{
    LIST_ENTRY ApcListHead[2];
    PKPROCESS Process;
    UCHAR KernelApcInProgress;
    UCHAR KernelApcPending;
    UCHAR UserApcPending;
} KAPC_STATE, *PKAPC_STATE;

typedef struct _LDR_DATA_TABLE_ENTRY
{
    LIST_ENTRY64    InLoadOrderLinks;
    LIST_ENTRY64    InMemoryOrderLinks;
    LIST_ENTRY64    InInitializationOrderLinks;
    PVOID           DllBase;
    PVOID           EntryPoint;
    ULONG           SizeOfImage;
    UNICODE_STRING  FullDllName;
    UNICODE_STRING  BaseDllName;
    ULONG           Flags;
    USHORT          LoadCount;
    USHORT          TlsIndex;
    PVOID           SectionPointer;
    ULONG           CheckSum;
    PVOID           LoadedImports;
    PVOID           EntryPointActivationContext;
    PVOID           PatchInformation;
    LIST_ENTRY64    ForwarderLinks;
    LIST_ENTRY64    ServiceTagLinks;
    LIST_ENTRY64    StaticLinks;
    PVOID           ContextInformation;
    ULONG64         OriginalBase;
    LARGE_INTEGER   LoadTime;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

// peb.ldr
ULONG64 LdrInPebOffset = 0x018;

// peb.ldr.InLoadOrderModuleList
ULONG64 ModListInPebOffset = 0x010;

// 声明API
NTKERNELAPI UCHAR* PsGetProcessImageFileName(IN PEPROCESS Process);
NTKERNELAPI PPEB PsGetProcessPeb(PEPROCESS Process);
NTKERNELAPI HANDLE PsGetProcessInheritedFromUniqueProcessId(IN PEPROCESS Process);

// 根据进程ID返回进程EPROCESS失败返回NULL
PEPROCESS LookupProcess(HANDLE Pid)
{
    PEPROCESS eprocess = NULL;
    if (NT_SUCCESS(PsLookupProcessByProcessId(Pid, &eprocess)))
        return eprocess;
    else
        return NULL;
}

// 枚举指定进程的模块
VOID EnumModule(PEPROCESS Process)
{
    SIZE_T Peb = 0;
    SIZE_T Ldr = 0;
    PLIST_ENTRY ModListHead = 0;
    PLIST_ENTRY Module = 0;
    ANSI_STRING AnsiString;
    KAPC_STATE ks;
    
  // EPROCESS地址无效则退出
    if (!MmIsAddressValid(Process))
        return;
    
  // 获取PEB地址
    Peb = (SIZE_T)PsGetProcessPeb(Process);
    
  // PEB地址无效则退出
    if (!Peb)
        return;
    
  // 依附进程
    KeStackAttachProcess(Process, &ks);
    __try
    {
        // 获得LDR地址
        Ldr = Peb + (SIZE_T)LdrInPebOffset;
        
    // 测试是否可读,不可读则抛出异常退出
        ProbeForRead((CONST PVOID)Ldr, 8, 8);
        
    // 获得链表头
        ModListHead = (PLIST_ENTRY)(*(PULONG64)Ldr + ModListInPebOffset);
        
    // 再次测试可读性
        ProbeForRead((CONST PVOID)ModListHead, 8, 8);
        
    // 获得第一个模块的信息
        Module = ModListHead->Flink;
        while (ModListHead != Module)
        {
            // 打印信息:基址、大小、DLL路径
            DbgPrint("模块基址=%p 大小=%ld 路径=%wZ\n",(PVOID)(((PLDR_DATA_TABLE_ENTRY)Module)->DllBase),
                (ULONG)(((PLDR_DATA_TABLE_ENTRY)Module)->SizeOfImage),&(((PLDR_DATA_TABLE_ENTRY)Module)->FullDllName));
            
      Module = Module->Flink;
            
      // 测试下一个模块信息的可读性
            ProbeForRead((CONST PVOID)Module, 80, 8);
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER){;}
    
  // 取消依附进程
    KeUnstackDetachProcess(&ks);
}

// 通过枚举的方式定位到指定的进程,这里传递一个进程名称
VOID MyEnumModule(char *ProcessName)
{
    ULONG i = 0;
    PEPROCESS eproc = NULL;
    for (i = 4; i<100000000; i = i + 4)
    {
        eproc = LookupProcess((HANDLE)i);
        if (eproc != NULL)
        {
            ObDereferenceObject(eproc);
            if (strstr(PsGetProcessImageFileName(eproc), ProcessName) != NULL)
            {
        // 相等则说明是我们想要的进程,直接枚举其中的线程
                EnumModule(eproc);
            }
        }
    }
}

VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{

}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
    MyEnumModule("calc.exe");
    DriverObject->DriverUnload = DriverUnload;
    return STATUS_SUCCESS;
}

输出效果图如下所示:

4.2.4 内核中实现枚举加载的驱动

内核中的SYS文件也是通过双向链表的方式相连接的,我们可以通过遍历驱动自身LDR_DATA_TABLE_ENTRY结构(遍历自身DriverSection成员),就能够得到全部的模块信息。

在操作系统内核中,SYS文件通常作为设备驱动程序的一部分加载到内存中。为了管理这些模块,Windows使用了一个双向链表来维护已加载模块的信息。链表中的每个节点是一个LDR_DATA_TABLE_ENTRY结构,它包含了模块的各种信息,如模块名、模块基地址、模块大小、模块导入表等等。

当一个SYS文件被加载到内存中时,系统会创建一个LDR_DATA_TABLE_ENTRY结构并将其插入到内核模块列表的末尾。在插入时,系统会将新节点的前一个节点的ForwardLink指向新节点,将新节点的BackLink指向前一个节点,并将新节点的ForwardLink指向链表尾部的哨兵节点。

遍历内核模块列表时,可以通过遍历LDR_DATA_TABLE_ENTRY结构中的DriverSection成员,找到所有已加载的SYS文件,并获得它们的基本信息。从链表头部开始遍历链表,可以使用ForwardLink指针来访问下一个节点,直到访问到链表尾部的哨兵节点为止。

如下代码中,在DriverEntry()开始处,定义了一些变量,包括pLdr、pListEntry、pModulepCurrentListEntry,它们分别代表当前驱动程序的LDR_DATA_TABLE_ENTRY结构、模块列表中的链表头、当前模块的LDR_DATA_TABLE_ENTRY结构和当前遍历到的链表节点。

接着,使用DriverObject->DriverSection获取当前驱动程序的LDR_DATA_TABLE_ENTRY结构,并通过pLdr->InLoadOrderLinks.Flink获取模块列表中的链表头。使用pListEntry->Flink获取链表中的第一个节点,并将其赋值给pCurrentListEntry

之后,通过一个循环遍历整个模块列表。在每次循环中,使用CONTAINING_RECORD宏获取当前节点对应的LDR_DATA_TABLE_ENTRY结构,并检查该模块的基本信息是否为空。如果不为空,将该模块的基址、结束地址、大小和模块名打印到调试窗口中。

最后,在函数结尾处设置了驱动程序的卸载例程DriverUnload,并返回STATUS_SUCCESS表示函数执行成功,至此枚举内核模块就完成了,其完整代码如下;

#include <ntddk.h>
#include <wdm.h>

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;
    ULONG SizeOfImages;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    ULONG Flags;
    USHORT LoadCount;
    USHORT TlsIndex;
    union {
        LIST_ENTRY HashLinks;
        struct {
            PVOID SectionPointer;
            ULONG CheckSum;
        };
    };
    union {
        struct {
            ULONG TimeDateStamp;
        };
        struct {
            PVOID LoadedImports;
        };
    };
}LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{

}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    ULONG count = 0;
    NTSTATUS Status;
    DriverObject->DriverUnload = DriverUnload;
    
    PLDR_DATA_TABLE_ENTRY pLdr = NULL;
    PLIST_ENTRY pListEntry = NULL;
    PLDR_DATA_TABLE_ENTRY pModule = NULL;
    PLIST_ENTRY pCurrentListEntry = NULL;

    pLdr = (PLDR_DATA_TABLE_ENTRY)DriverObject->DriverSection;
    pListEntry = pLdr->InLoadOrderLinks.Flink;
    pCurrentListEntry = pListEntry->Flink;

    while (pCurrentListEntry != pListEntry)
    {
        pModule = CONTAINING_RECORD(pCurrentListEntry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
        if (pModule->BaseDllName.Buffer != 0)
        {
            DbgPrint("基址:%p ---> 偏移:%p ---> 结束地址:%p---> 模块名:%wZ \r\n", pModule->DllBase, pModule->SizeOfImages - (LONGLONG)pModule->DllBase, 
                (LONGLONG)pModule->DllBase + pModule->SizeOfImages,pModule->BaseDllName);
        }
        pCurrentListEntry = pCurrentListEntry->Flink;
    }

    DriverObject->DriverUnload = DriverUnload;
    return STATUS_SUCCESS;
}

输出效果图如下所示:

4.2.5 内核中实现获取特定进程PID

用户传入指定进程名称,调用GetPidByProcessName()可得到该进程名称所对应的进程PID号。这段代码其大多数功能实现已经在前面的章节中实现了,需要注意的是GetProcessID()函数内部,通过strstr(PsGetProcessImageFileName(eproc), ProcessName) != NULL对比如果是我们所需要提取的进程结构,则直接PsGetProcessId(eproc)返回该进程的PID号。

#include <ntifs.h>
#include <windef.h>

// 声明API
NTKERNELAPI UCHAR* PsGetProcessImageFileName(IN PEPROCESS Process);
NTKERNELAPI PPEB PsGetProcessPeb(PEPROCESS Process);
NTKERNELAPI HANDLE PsGetProcessInheritedFromUniqueProcessId(IN PEPROCESS Process);

// 根据进程ID返回进程EPROCESS,失败返回NULL
PEPROCESS LookupProcess(HANDLE Pid)
{
  PEPROCESS eprocess = NULL;
  if (NT_SUCCESS(PsLookupProcessByProcessId(Pid, &eprocess)))
    return eprocess;
  else
    return NULL;
}

// 根据用户传入进程名得到该进程PID
HANDLE GetProcessID(char *ProcessName)
{
  ULONG i = 0;
  PEPROCESS eproc = NULL;
  for (i = 4; i<100000000; i = i + 4)
  {
    eproc = LookupProcess((HANDLE)i);
    if (eproc != NULL)
    {
      ObDereferenceObject(eproc);
      if (strstr(PsGetProcessImageFileName(eproc), ProcessName) != NULL)
      {
        return PsGetProcessId(eproc);
      }
    }
  }
  return NULL;
}

VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{

}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
  HANDLE ref = GetProcessID("KmdManager.exe");
  DbgPrint("[LyShark.com] 进程PID = %d \n", ref);

  DriverObject->DriverUnload = DriverUnload;
  return STATUS_SUCCESS;
}

输出效果图如下所示:

4.2.6 内核中实现判断进程状态

内核中实现判断进程状态的方法,通过传入一个 EProcess 结构体来判断指定进程的状态,包括进程是否存在、是否为僵尸进程等。这些功能通常被用于反内核工具的开发。

接下来,将逐个介绍并实现几个相关的功能,包括 IsProcessDie 函数用于验证进程空间是否有效,IsRealProcess 函数用于验证进程是否是真实进程,以及 GetProcessCreateTime 函数用于获取进程创建时间戳等功能。

IsProcessDie 函数用于验证特定进程空间是否有效,函数接受一个 PEPROCESS 类型的参数 EProcess,表示待验证的进程。函数会检查传入的 EProcess 参数是否为有效地址,并且会检查进程对象表的地址是否为有效地址。如果传入的参数或进程对象表地址无效,函数将返回 TRUE,表示进程空间已经无效或不存在。反之,如果地址有效,函数将返回 FALSE,表示进程空间有效。

函数的执行步骤如下:

  • 首先判断 MmIsAddressValid 函数是否存在且有效,如果无效则直接返回 TRUE,表示进程空间无效。
  • 检查传入的 EProcess 参数是否为有效地址,如果地址无效则直接返回 TRUE,表示进程空间无效。
  • 通过计算 EProcess 结构体中进程对象表的偏移量,并检查该地址是否为有效地址。如果进程对象表地址无效,表示进程空间已经无效或不存在,直接返回 TRUE。
  • 如果传入的参数和进程对象表地址均为有效地址,则获取进程对象表指针并进行进一步检查。
  • 如果进程对象表指针为 NULL 或者其地址无效,则表示进程空间已经无效或不存在,返回 TRUE,否则返回 FALSE,表示进程空间有效。
// 验证进程空间是否有效
BOOLEAN IsProcessDie(PEPROCESS EProcess)
{
  BOOLEAN bDie = FALSE;

  if (MmIsAddressValid &&
    EProcess &&
    MmIsAddressValid(EProcess) &&
    MmIsAddressValid((PVOID)((ULONG_PTR)EProcess + ObjectTableOffsetOf_EPROCESS)))
  {
    PVOID ObjectTable = *(PVOID*)((ULONG_PTR)EProcess + ObjectTableOffsetOf_EPROCESS);

    if (!ObjectTable || !MmIsAddressValid(ObjectTable))
    {
      bDie = TRUE;
    }
  }
  else
  {
    bDie = TRUE;
  }
  return bDie;
}

IsRealProcess 函数的功能是验证进程是否是僵尸进程。该函数接受一个 PEPROCESS 类型的参数 EProcess,表示待验证的进程。函数内部会先通过 KeGetObjectType 函数获取传入的进程对象的类型,然后将其与进程类型进行比较,如果相同且进程空间有效,则说明该进程不是僵尸进程,返回 TRUE,否则返回 FALSE。

KeGetObjectType 函数中,先判断输入参数是否为有效地址,如果无效则返回 NULL,表示取对象类型失败。如果地址有效,则通过 GetFunctionAddressByName 函数获取 ObGetObjectType 函数的地址,然后调用 ObGetObjectType 函数获取对象类型。最后将对象类型作为返回值返回。

IsRealProcess主函数中,首先获取进程类型,然后检查传入的进程对象是否为有效地址。如果进程类型和获取的对象类型相同,且进程空间有效,则说明该进程不是僵尸进程,返回 TRUE。反之,如果进程对象无效或进程类型不匹配,则说明该进程是僵尸进程,返回 FALSE。

// 取出对象类型
ULONG_PTR KeGetObjectType(PVOID Object)
{
  ULONG_PTR ObjectType = NULL;
  pfnObGetObjectType ObGetObjectType = NULL;

  if (!MmIsAddressValid || !Object || !MmIsAddressValid(Object))
  {
    return NULL;
  }
  ObGetObjectType = (pfnObGetObjectType)GetFunctionAddressByName(L"ObGetObjectType");
  if (ObGetObjectType)
  {
    ObjectType = ObGetObjectType(Object);
  }
  return ObjectType;
}

// 验证进程是否是僵尸进程
BOOLEAN IsRealProcess(PEPROCESS EProcess)
{
  ULONG_PTR ObjectType;
  ULONG_PTR    ObjectTypeAddress;
  BOOLEAN bRet = FALSE;

  ULONG_PTR ProcessType = ((ULONG_PTR)*PsProcessType);

  if (ProcessType && MmIsAddressValid && EProcess && MmIsAddressValid((PVOID)(EProcess)))
  {
    ObjectType = KeGetObjectType((PVOID)EProcess);
    if (ObjectType &&
      ProcessType == ObjectType &&
      !IsProcessDie(EProcess))
    {
      bRet = TRUE;
    }
  }

  return bRet;
}

GetProcessCreateTime 函数用于获取指定进程的创建时间戳。通过调用PsLookupProcessByProcessId函数获取到进程对象,然后调用PsGetProcessCreateTimeQuadPart函数获取进程的创建时间戳。在获取时间戳之前,需要将当前线程的Previous Mode设置为内核模式,以便访问EPROCESS结构体中的成员。在获取时间戳之后,需要将Previous Mode恢复到之前的值,并释放进程对象。

函数的执行步骤如下:

  • 通过调用PsLookupProcessByProcessId函数获取指定进程的进程对象。
  • 调用PsGetCurrentThread函数获取当前线程的ETHREAD对象,调用ChangePreMode函数将当前线程的Previous Mode设置为内核模式,并保存之前的Previous Mode的值。
  • 调用PsGetProcessCreateTimeQuadPart函数获取指定进程的创建时间戳,并将时间戳保存到OutputBuffer指向的缓冲区中。
  • 最后调用RecoverPreMode函数将当前线程的Previous Mode恢复到之前的值,并释放进程对象。
// 获取进程时间戳
BOOLEAN GetProcessCreateTime(ULONG_PTR ProcessID, LONGLONG* OutputBuffer)
{
  NTSTATUS  Status;
  PEPROCESS EProcess = NULL;
  PETHREAD  EThread = NULL;
  CHAR      PreMode = 0;

  Status = PsLookupProcessByProcessId((HANDLE)ProcessID, &EProcess);
  if (!NT_SUCCESS(Status))
  {
    return FALSE;
  }

  EThread = PsGetCurrentThread();
  PreMode = ChangePreMode(EThread);
  *OutputBuffer = PsGetProcessCreateTimeQuadPart(EProcess);
  RecoverPreMode(EThread, PreMode);
  ObfDereferenceObject(EProcess);
  return TRUE;
}

我们将上述三个功能进行整合,并最终得到一段完整的代码,如下所示;

#include <ntifs.h>

ULONG_PTR ObjectTableOffsetOf_EPROCESS = 0;   // 句柄表偏移
ULONG_PTR PreviousModeOffsetOf_KTHREAD = 0;   // 权限相关的偏移

typedef ULONG_PTR(*pfnObGetObjectType)(PVOID pObject);

// 验证进程空间是否有效
BOOLEAN IsProcessDie(PEPROCESS EProcess)
{
    BOOLEAN bDie = FALSE;

    if (MmIsAddressValid &&
        EProcess &&
        MmIsAddressValid(EProcess) &&
        MmIsAddressValid((PVOID)((ULONG_PTR)EProcess + ObjectTableOffsetOf_EPROCESS)))
    {
        PVOID ObjectTable = *(PVOID*)((ULONG_PTR)EProcess + ObjectTableOffsetOf_EPROCESS);

        if (!ObjectTable || !MmIsAddressValid(ObjectTable))
        {
            bDie = TRUE;
        }
    }
    else
    {
        bDie = TRUE;
    }
    return bDie;
}

//通过 函数名称 得到函数地址
PVOID GetFunctionAddressByName(WCHAR *szFunction)
{
    UNICODE_STRING uniFunction;
    PVOID AddrBase = NULL;

    if (szFunction && wcslen(szFunction) > 0)
    {
        RtlInitUnicodeString(&uniFunction, szFunction);
        AddrBase = MmGetSystemRoutineAddress(&uniFunction);
    }
    return AddrBase;
}

// 取出对象类型
ULONG_PTR KeGetObjectType(PVOID Object)
{
    ULONG_PTR ObjectType = NULL;
    pfnObGetObjectType ObGetObjectType = NULL;

    if (!MmIsAddressValid || !Object || !MmIsAddressValid(Object))
    {
        return NULL;
    }
    ObGetObjectType = (pfnObGetObjectType)GetFunctionAddressByName(L"ObGetObjectType");
    if (ObGetObjectType)
    {
        ObjectType = ObGetObjectType(Object);
    }
    return ObjectType;
}

// 验证进程是否是僵尸进程
BOOLEAN IsRealProcess(PEPROCESS EProcess)
{
    ULONG_PTR ObjectType;
    ULONG_PTR    ObjectTypeAddress;
    BOOLEAN bRet = FALSE;

    ULONG_PTR ProcessType = ((ULONG_PTR)*PsProcessType);

    if (ProcessType && MmIsAddressValid && EProcess && MmIsAddressValid((PVOID)(EProcess)))
    {
        ObjectType = KeGetObjectType((PVOID)EProcess);
        if (ObjectType &&
            ProcessType == ObjectType &&
            !IsProcessDie(EProcess))
        {
            bRet = TRUE;
        }
    }

    return bRet;
}

CHAR ChangePreMode(PETHREAD EThread)
{
    CHAR PreMode = *(PCHAR)((ULONG_PTR)EThread + PreviousModeOffsetOf_KTHREAD);
    *(PCHAR)((ULONG_PTR)EThread + PreviousModeOffsetOf_KTHREAD) = KernelMode;
    return PreMode;
}

VOID RecoverPreMode(PETHREAD EThread, CHAR PreMode)
{
    *(PCHAR)((ULONG_PTR)EThread + PreviousModeOffsetOf_KTHREAD) = PreMode;
}

// 获取进程时间戳
BOOLEAN GetProcessCreateTime(ULONG_PTR ProcessID, LONGLONG* OutputBuffer)
{
    NTSTATUS  Status;
    PEPROCESS EProcess = NULL;
    PETHREAD  EThread = NULL;
    CHAR PreMode = 0;

    Status = PsLookupProcessByProcessId((HANDLE)ProcessID, &EProcess);
    if (!NT_SUCCESS(Status))
    {
        return FALSE;
    }

    EThread = PsGetCurrentThread();
    PreMode = ChangePreMode(EThread);
    *OutputBuffer = PsGetProcessCreateTimeQuadPart(EProcess);
    RecoverPreMode(EThread, PreMode);
    ObfDereferenceObject(EProcess);
    return TRUE;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint("驱动卸载成功 \n");
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    NTSTATUS Status = STATUS_UNSUCCESSFUL;
    PEPROCESS EProcess = NULL;
    HANDLE pid = (HANDLE)6932;

    // 根据PID获取进程EProcess结构
    Status = PsLookupProcessByProcessId(pid, &EProcess);

    // 判断进程是否有效
    if (NT_SUCCESS(Status) && IsProcessDie(EProcess))
    {
        DbgPrint("[LyShark.com] 进程有效 \n");
    }

    // 判断是否为僵尸进程
    if (NT_SUCCESS(Status) && IsRealProcess(EProcess))
    {
        DbgPrint("[LyShark.com] 僵尸进程 \n");
    }

    // 验证进程时间戳
    LONGLONG time;
    BOOLEAN ref = GetProcessCreateTime(pid, &time);
    if (NT_SUCCESS(Status) && ref)
    {
        DbgPrint("[LyShark.com] 该进程时间戳: %x \n", time);
    }

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

输出效果图如下所示:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1225037.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

zyj-ha 安装过程及使用部署

一&#xff0e;安装过程排坑 1. 硬件环境准备 排坑 1 首先&#xff0c;服务器至少需要 2 台&#xff0c;每台服务器至少需要 2 块网卡&#xff0c;并且必须有预留 心跳线网口&#xff0c;不能被其他业务占用&#xff0c;否则容易出现脑裂。 2. 通过配置管理工具导入安装包 …

【南京】最新ChatGPT/GPT4科研技术应用与AI绘图及论文高效写作

2023年我们进入了AI2.0时代。微软创始人比尔盖茨称ChatGPT的出现有着重大历史意义&#xff0c;不亚于互联网和个人电脑的问世。360创始人周鸿祎认为未来各行各业如果不能搭上这班车&#xff0c;就有可能被淘汰在这个数字化时代&#xff0c;如何能高效地处理文本、文献查阅、PPT…

大师学SwiftUI第18章Part1 - 图片选择器和相机

如今&#xff0c;个人设备主要用于处理图片、视频和声音&#xff0c;苹果的设备也不例外。SwiftUI可以通过​​Image​​视图显示图片&#xff0c;但需要其它框架的支持来处理图片、在屏幕上展示视频或是播放声音。本章中我们将展示Apple所提供的这类工具。 图片选择器 Swift…

OpenCV入门6——图像基本变换

文章目录 图像的放大与缩小缩放算法放大 图像的翻转图像的旋转仿射变换之图像平移仿射变换之获取变换矩阵仿射变换之变换矩阵之二OpenCV透视变换 图像的放大与缩小 缩放算法 # -*- coding: utf-8 -*- import cv2 import numpy as npimg cv2.imread(E://pic//4.jpg) # (600, 48…

搭建 AI 图像生成器 (SAAS) php laravel

今天来搭一套&#xff0c;AI 图像生成器 是基于 Openai DALLE 2 和 Openai DALLE 3 以及 Stability AI 和稳定扩散 API 构建的脚本&#xff0c;为用户提供了使用简单的提示和大小生成独特自定义图像的可能性。在这个平台上&#xff0c;创意得以快速、高效地实现&#xff0c;借助…

继承语法详解

继承语法详解 一:继承1&#xff1a;什么是继承 二&#xff1a;访问成员变量三&#xff1a;访问成员方法四&#xff1a;访问父类的成员变量和成员方法super关键字super和this关键字的区别 五&#xff1a;子类的构造方法六&#xff1a;代码块七&#xff1a;final关键字八&#xf…

驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接

参考&#xff1a;https://www.cnblogs.com/sam-snow-v/p/15917898.html eclipse链接SQL Server出现问题 笔者使用Open JDK 17&#xff0c;SQL Server 2016&#xff0c;项目中使用JPA操作数据库。测试环境没问题&#xff0c;生产环境出现如题所示“驱动程序无法通过使用安全套接…

[Docker]六.Docker自动部署nodejs以及golang项目

一.自动部署nodejs 1.创建node项目相关文件 app.js代码如下: var express require(express);var appexpress();app.get(/,function(req,res){res.send(首页update); }) app.get(/news,function(req,res){res.send(首页); })//docker做端口映射的时候不要指定ip app.listen(30…

浏览器页面被恶意控制时的解决方法

解决360流氓软件控制浏览器页面 提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、接受360安全卫士的好意&#xff08;尽量不要选&#xff09;二、拒绝360安全卫士的好意&#xff08;强烈推荐&#xff09;第…

unity教程

前言 伴随游戏行业的兴起&#xff0c;unity引擎的使用越来越普遍&#xff0c;本文章主要记录博主本人入门unity的相关记录大部分依赖siki学院进行整理。12 一、认识unity引擎&#xff1f; 1、Unity相关信息&#xff1a; Unity的诞生&#xff1a;https://www.jianshu.com/p/550…

webpack 中,filename 和 chunkFilename 的区别

filename filename 是一个很常见的配置&#xff0c;就是对应于 entry 里面的输入文件&#xff0c;经过webpack打包后输出文件的文件名。比如说经过下面的配置&#xff0c;生成出来的文件名为 index.min.js。 chunkFilename chunkFilename 指未被列在 entry 中&#xff0c;却…

LangChain 2模块化prompt template并用streamlit生成网站 实现给动物取名字

上一节实现了 LangChain 实现给动物取名字&#xff0c; 实际上每次给不同的动物取名字&#xff0c;还得修改源代码&#xff0c;这周就用模块化template来实现。 1. 添加promptTemplate from langchain.llms import OpenAI # 导入Langchain库中的OpenAI模块 from langchain.p…

springboot323基于Java的美妆购物网站的设计与实现

交流学习&#xff1a; 更多项目&#xff1a; 全网最全的Java成品项目列表 https://docs.qq.com/doc/DUXdsVlhIdVlsemdX 演示 项目功能演示&#xff1a; ————————————————

资产设备管理系统

dtAsset 是一个固定资产设备管理系统&#xff0c;它专为中小企业的需求而设计。该软件提供了对常用资产设备进行信息化管理的功能&#xff0c;并支持自定义设备类型、导入导出数据、维护工作统计、采购管理、文档管理、运维监控 (使用 Zabbix)、知识库等功能。 主要模块 1.系统…

【Rust】快速教程——闭包与生命周期

前言 你怎么向天生的瞎子说清颜色&#xff1f;怎么用手势向天生的聋子描述声音&#xff1f; 鲜花就在眼前&#xff0c;雷鸣就在头顶&#xff0c;对他们来说却都毫无意义 眼睛看不到&#xff0c;鼻子可以嗅闻花香&#xff0c;耳朵听不见&#xff0c;手指可以触碰窗纸的震动。 犯…

QtCreator13源码windows编译

1.下载QtCreator13源码: https://download.qt.io/snapshots/qtcreator/13.0/13.0.0-beta1/installer_source/latest/qt-creator-opensource-src-13.0.0-beta1.zip 2.下载并安装llvm Release LLVM 17.0.5 llvm/llvm-project GitHub 3.系统 要求&#xff1a; Windows 10 (64…

2023OceanBase年度发布会后,有感

很荣幸收到了OceanBase邀请&#xff0c;于本周四&#xff08;11月16日&#xff09;参加了OceanBase年度发布会并参加了DBA老友会&#xff0c;按照理论应该我昨天&#xff08;星期五&#xff09;就回到成都了&#xff0c;最迟今天白天就该把文章写出来了&#xff0c;奈何媳妇儿买…

vue2【axios请求】

1&#xff1a;axios作用 axios&#xff08;发音&#xff1a;艾克c奥斯&#xff09;是前端圈最火的&#xff0c;专注于数据请求的库。 Axios 是一个基于 promise 的 HTTP 库&#xff0c;可以用在浏览器和 node.js 中axios的github:https://github.com/axios/axios 中文官网地址…

ESP32网络开发实例-非接触式水位监测

非接触式水位监测 文章目录 非接触式水位监测1、HC-SR04介绍2、软件准备3、硬件准备4、代码实现在本文中,我们将使用 HC-SR04 超声波传感器和 ESP32 创建一个水位监测网络服务器。 这将是一个非接触式水位测量系统。 首先,我们将介绍HC-SR04 与 ESP32 连接。 使用ESP32对超声…

如何去掉图片上的水印?这三种去水印的方法帮你解决!

当我们从网上看到喜欢的图片&#xff0c;想要保存下来作为头像或者插入到工作汇报中时&#xff0c;却发现下载的图片带有水印。这不仅影响了图片的美观&#xff0c;还可能对图片的可用性造成影响。那么&#xff0c;如何去掉图片上的水印呢? 实际上&#xff0c;现在市面上的很多…