windows驱动开发-进程结构体初探

目录

内核结构 进程结构体 字段 以及简单的应用

一丶进程结构体

1.1 简介

​ 我们有进程的概念,也有线程的概念。 而这些其实在操作系统内核中是由记录的。

这里我简单的说下重要字段。 我们可以使用 windbg 加载好符号之后

使用 dt _EPROCESS 来查看这个结构体

这里我之所以讲下字段是因为在每个系统中进程结构体中偏移中记录的成员变量可能会有差异。

首先 EPROCESS的第一个成员是 KPROCESS 所以我们先说下 KPROCESS代表的是啥

1.2 KPROCESS 结构体

请注意这里只说下重要结构,且操作系统内核中的结构是随着系统改变的。所以简单了解下。

我会删除一些不需要的偏移。

成员如下:

nt!_KPROCESS
   +0x000 Header           : _DISPATCHER_HEADER      //此字段如果有值代表我们可以被 waitforsingobject来进行等待(ring3的线程同步函数)
   +0x028 DirectoryTableBase : Uint8B                //很重要的字段,代表当前进程的页目录表 (也就是CR3,熟悉此字段可以进行内存读写操作)
   +0x030 ThreadListHead   : _LIST_ENTRY
   +0x050 Affinity         : _KAFFINITY_EX			//此字段代表允许当前进程在那个CPU上跑。按位来判断的。 如: 0000 0001 代表在0核上跑 0000 0010 代表在1核上跑。
   +0x1c0 BasePriority     : Char                    //基础的优先级,记录了当前进程的所有线程的基础优先级
   +0x1c3 Flags            : _KEXECUTE_OPTIONS       //标志,一般都是很重要的。
   +0x274 KernelTime       : Uint4B                 //统计信息,代表当前进程启动时间
   +0x278 UserTime         : Uint4B                 //统计用户的时间
   +0x27c ReadyTime        : Uint4B                 //时间信息

其中上面最重要的成员就是页表。 熟悉了此结构可以实现 自定义的 内存读写函数

自己实现 ReadProcessMemory 以及 WriteProcessMemory

第二个就是Affinity 可以使用内核函数设置当前线程在那个CPU上跑。

KeSetSystemAffinityThread 应该是这个函数。 设置之后我们则可以获取每个核心的GDT表并且显示出来。

1.3 EPROCESS

EPROCESS 记录的信息很多。记录的东西比KPROCESS 还多。

看下结构 在我调试的Windows10 1909上面结构体成员偏移已经突破了 0x800了 所以暂时没在我认知能力中的成员以及不重要的成员都会删除

nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS                  //记录了KPROCESS 参考上面
   +0x2e8 UniqueProcessId  : Ptr64 Void                 //记录了当前进程的PID
   +0x2f0 ActiveProcessLinks : _LIST_ENTRY			   //重要成员,记录了当前进程的活动链表。
   +0x30c Flags            : Uint4B				       //清零可以使我们的进程不被打开。 Ring3层的 OpenProcess就会打开我们失败
   +0x310 CreateTime       : _LARGE_INTEGER             //进程的创建时间
   +0x318 ProcessQuotaUsage : [2] Uint8B                //物理页的使用信息
   +0x328 ProcessQuotaPeak : [2] Uint8B
   +0x338 PeakVirtualSize  : Uint8B
   +0x340 VirtualSize      : Uint8B                         
   
   +0x348 SessionProcessLinks : _LIST_ENTRY			   //Session链表
   +0x358 ExceptionPortData : Ptr64 Void                //异常以及调试信息相关成员
   +0x358 ExceptionPortValue : Uint8B
   +0x358 ExceptionPortState : Pos 0, 3 Bits
   
   +0x360 Token            : _EX_FAST_REF               //当前进程的Tokenx信息
   													//我认为的SESSION比较重要的字段
   +0x3c0 SectionObject    : Ptr64 Void
   +0x3c8 SectionBaseAddress : Ptr64 Void
   +0x3d0 Cookie           : Uint4B

   +0x3f8 Peb              : Ptr64 _PEB                     //ring0层也记录的PEB 信息。 PEB信息掌握好了可以进行模块隐藏
   +0x400 Session          : Ptr64 _MM_SESSION_SPACE	    // 当前进程Session信息

   +0x410 QuotaBlock       : Ptr64 _EPROCESS_QUOTA_BLOCK
   +0x418 ObjectTable      : Ptr64 _HANDLE_TABLE           //句柄表,记录了当前进程使用的其它的内核对象的句柄
   +0x420 DebugPort        : Ptr64 Void                    //调试端口。 如果清0 可以让我们不被调试(注意是线程一直清零来达到反调试的目的)
 
   +0x448 ImageFilePointer : Ptr64 _FILE_OBJECT            //记录的当前进程的 文件对象 可以通过它查询File的信息
   +0x450 ImageFileName    : [15] UChar                    //记录了当前进程的名字,但是只有15个可用字节,所以我们可以使用FILE_OBJECT得到名字或者路径
 
   +0x488 ThreadListHead   : _LIST_ENTRY                 //记录了当前进程的所有线程
   +0x498 ActiveThreads    : Uint4B                      //记录了当前进程的 活动线程的数量
   
   +0x4e8 CommitChargeLimit : Uint8B                     //虚拟内存统计信息
   +0x4f0 CommitCharge     : Uint8B
   +0x4f8 CommitChargePeak : Uint8B

   +0x658 VadRoot          : _RTL_AVL_TREE              //VAD树,记录了ring3层未使用的内存,在ring3申请内存的时候都会查询
   +0x660 VadHint          : Ptr64 Void
   +0x668 VadCount         : Uint8B
   +0x670 VadPhysicalPages : Uint8B
   +0x678 VadPhysicalPagesLimit : Uint8B                //Vad物理内存界限
   +0x6c0 ExitTime         : _LARGE_INTEGER             //进程的退出时间
   

比较重要的成员我会列出来 可以实现什么共呢个也进行说明

  • KPROCESS-DirectoryTableBase 成员

    记录了当先进程的页目录表。也就是CR3控制寄存器的值。 可以通过它来进行实现自己的Read/WriteProcessMemory 读写内存函数

  • KPROCESS-Affinity 成员

    可以设置当前进程的线程在那个CPU上执行。 可以获取获取每个核心的GDT表。

  • EPROCESS-UniqueProcessId

    记录了当前进程的PID

  • EPROCESS-ActiveProcessLinks

    当前系统中所有的进程都会当作链表进行串联起来。 而串联的链表头则是PsActiveProcessHead 它是操作系统导出的全局变量。

    是一个双向链表。 指向了下一个EPROCESS.ActiveProcessLinks 地址。

    所以我们在查看PsActiveProcessHead 的时候(dq PsActiveProcessHead ) 注意他的链表指向的下一个节点是EPROCESS的成员的偏移位置。

    所以我们可以使用 CONTAINING_RECORD 宏计算出EPROCESS结构体的真实地址。

    实际操作:

    dq PsActiveProcessHead
    fffff804`4c6d7920  ffffa883`f96e0370 ffffa884`030dd7b0
    fffff804`4c6d7930  00000000`00000332 00000000`00000000
    fffff804`4c6d7940  00000000`00000000 ffffa883`f9688638
    fffff804`4c6d7950  fffff804`4cb6ae50 00000000`00000000
    fffff804`4c6d7960  00000000`00000000 00000000`00000000
    fffff804`4c6d7970  fffff804`4cb6ada0 00000000`00000000
    fffff804`4c6d7980  ffffa883`fb95cea0 fffff804`4c6e32c0
    fffff804`4c6d7990  00000000`00000000 00000000`00000000
    
    ffffa883`f96e0370 = 第一个节点,指向了下一个EPROCESS.ActiveProcessLinks 的地址。 而ActiveProcessLinks成员距离结构体首地址是0x2F0的偏移
    所以
    NextEPROCESSaddr = ffffa883`f96e0370 - 0x2F0; 
    dt _EPROCESS NextEPROCESSaddr 即可看到解析出来的地址
    

    下面是断链操作,实现进程的隐藏。(不过PG)

    当然遍历这个链表也可以拿到进程名字,相当于遍历进程了。

    NTKERNELAPI UCHAR* PsGetProcessImageFileName(IN PEPROCESS Process); //未公开的进行导出即可
    UNICODE_STRING GetUstrProcessByProcess(PEPROCESS PProcess)   //注意,此处不应该使用PsGetProcessImageFileName 
    	{
    		ANSI_STRING aStr;
    		auto uname = PsGetProcessImageFileName(PProcess);
    		RtlInitString(&aStr, (PCSZ)uname);
    		//to unicode
    		UNICODE_STRING uStr = { 0 };
    		RtlAnsiStringToUnicodeString(&uStr, &aStr, TRUE);
    		return uStr;
    	}
    	NTSTATUS MyIterprocessByActiveProcessLinks()
    	{
    		PEPROCESS pcurprocess = PsGetCurrentProcess();
    		//先输出下自己
    		UNICODE_STRING uCurProcessName = GetUstrProcessByProcess(pcurprocess);
    		DbgPrint("活动进程名 = %wZ \n", uCurProcessName);
    		RtlFreeUnicodeString(&uCurProcessName);
    
    		PLIST_ENTRY pCurActiveProcessLink =
    			reinterpret_cast<PLIST_ENTRY>(
    				reinterpret_cast<char*>(pcurprocess) + 0x2F0);
    		PLIST_ENTRY pNext = pCurActiveProcessLink->Flink;
    		//遍历链表
    		while (pNext != pCurActiveProcessLink)
    		{
    			//自己实现CONTAINING_RECORD 宏
    			PEPROCESS ptemp = reinterpret_cast<PEPROCESS>(
    				reinterpret_cast<char*>(pNext) - 0x2F0
    				);
    			//获取名字
    
    			UNICODE_STRING uProcessName = GetUstrProcessByProcess(ptemp);
    			DbgPrint("活动进程名 = %wZ \n", uProcessName);
    			//注意释放内存
    			RtlFreeUnicodeString(&uProcessName);
    			pNext = pNext->Flink;
    		}
    
    		return STATUS_SUCCESS;
    	}
    
    NTSTATUS MyHideProcessByProcessName(UNICODE_STRING hideProcessName)
    	{
    		UNREFERENCED_PARAMETER(hideProcessName);
    
    		/*
    		思路:
    		1.活动进程是一个链表。 通过查看可以看到+0x2F0是偏移
    		2.获取本进程的EPROCESS 他会加入到活动链表中
    		3.定义一个指针指向当前的活动链表
    		4.注意要自己实现CONTAINING_RECORD宏实现的操作
    		*/
    
    		PEPROCESS pcurprocess = PsGetCurrentProcess();
    		//先输出下自己
    		UNICODE_STRING uCurProcessName = GetUstrProcessByProcess(pcurprocess);
    		//if (RtlCompareUnicodeString(&hideProcessName, &uCurProcessName, false) == 0)
    		DbgPrint("当前想要隐藏的进程是 = %wZ \n", uCurProcessName);
    		//是我们要隐藏的进程,那么直接拖链进行隐藏即可。
    		PLIST_ENTRY pCurActiveLinks = reinterpret_cast<PLIST_ENTRY>(
    			reinterpret_cast<char*>(pcurprocess) + 0x2f0
    			);
    
    		pCurActiveLinks->Flink->Blink = pCurActiveLinks->Blink;
    		pCurActiveLinks->Blink->Flink = pCurActiveLinks->Flink;
    		pCurActiveLinks->Flink = pCurActiveLinks;
    		pCurActiveLinks->Blink = pCurActiveLinks;
    		//注意释放内存
    
    		RtlFreeUnicodeString(&uCurProcessName);
    		return STATUS_SUCCESS;
    }
    

    隐藏之后PChunter还是可以看得到 并且显示为红色。

  • EPROCESS-Flags

    修改为0可以防止当前进程被openprocess打开(为尝试,根据资料看到的)

  • EPROCESS-Peb

    记录的当前进程的PEB结构,ring3也有一个。 PEB中有模块加载顺序表。以及当前进程是否被调试,前者通过锻链可以直接隐藏模块。 后者可以进行反调试

  • EPROCESS-ObjectTable

    句柄表很重要。以后结构中会说明。

  • ERPCESS-DebugPort

    调试器调试进程的一个桥梁。清0会导致调试器无法找到我们的进程,进而实现反调试

  • EPROCESS-ImageFileName-ImageFilePointer

    ImageFileName 可以使用PsGetProcessImageFileName 来获取当前进程名字,但是他只有15个字节,所以最好不要用他。

    而是使用 ImageFilePointer 这个FILE_OBJECT对象来进行详细的查找。

  • EPROCESS-ActiveThreads

    表示当前进程中所有活动的线程

上一篇:C语言操作符详解(和bug郭一起学C系列)


下一篇:SSE图像算法优化系列三十一:Base64编码和解码算法的指令集优化(C#自带函数的3到4倍速度)。