[原创]64位CreateProcess逆向:(六)新进程、新线程的管理
点击下面进入总目录:
64位Windows创建64位进程逆向分析(总目录)
在这个系列的上两篇文章中,我们已经谈及了进程、线程的创建。按照教科书式的说法,Windows操作系统中,各个进程与进程之间的内存空间是独立的,而同一个进程的多个线程是共享同一个内存空间的。但是,在windows编程中又一定会涉及到“进程间通信”,这说明进程与进程之间是不可能完全独立的。直接点说,站在Windows操作系统(0环)的角度看,Windows是保存有所有进程的信息、并能够管理他们的关系,以此完成进程间通信、线程间切换的一系列问题。
在这篇文章中,我们通过分析0环的PspInsertProcess函数与PspInsertThread函数,可以看到windows其实就是以链表这种数据结构管理进程、线程。
新建进程的管理PspInsertProcess
在0环中,Windows通过调用PspInsertProcess来管理新进程。他的函数原型如下:
NTSTATUS PspInsertProcess( IN PEPROCESS pCurentEprocess, IN PEPROCESS pParentEprocess, IN ACCESS_MASK DesiredAccess, IN ULONG nCreateFlags, IN ULONG JobMemberLevel, IN HANDLE DebugObjectHandle, IN ULONG unKnownFlag, IN ULONG enumType, OUT ACCESS_STATE_EXPAND* ptrAccessState );
简单而言,它只有一个核心功能:将代表新进程的_EPROCESS结构体指针,插入到PsActiveProcess链表中。
以下是PspInsertProcess的工作流程图:

为了集中内容,我们仅关注与进程信息有关的结构体更新的各步骤,其它部分(如APC、进程权限等)我们留在ida文件中大家自行参阅(与前面提供的IDB文件一致):
将当前PID写入当前进程句柄表

更新全局pActiveProcessLinks,头插法插入当前EPROCESS到链表

拷贝调试端口到新的进程

创建一个访问状态结构体

将EPROCESS插入到当前进程的句柄表中和对象目录中

如果插入失败,则释放之前创建的访问结构体

以上涉及到了PID与句柄,我们也可以顺便提一下windows编程中常见新手问题“进程ID与进程句柄有什么区别?”其实,从以上的代码中就可以看出,PID是与进程对应且独一无二的(_EPROCESS.UniqueProcessId),即使PID写入到当前进程的句柄表中,仍然是放在一个特殊且独一无二的成员中(_HANDLETALBE_UniqueProcessId)。那么,我们在SDK编程中调用的OpenProcess返回的是PID这个“句柄”吗?答案是否定的。这是因为在由A进程打开B进程,其实涉及到两个进程的多对多关系,操作系统需要记录好A、B进程两者的PID(以及其它权限信息等),而为了更快的索引这些信息,会给这些结构体使用统一的“下标”,那就是句柄,也正是OpenProcess的返回值。
以上提到的pActiveProcessLinks中记录有当前运行的进程信息,因此,通过遍历pActiveProcessLinks就可以列举出所有当前进程,这是一种方法,使得我们不调用进程快照函数就可以遍历当前进程。此外,通过修改pActiveProcessLinks的链表结构(断链),我们还可以隐藏进程。
新线程的管理PspInsertThread
PspInsertThread函数的原型如下:
NTSTATUS PspInsertThread(PETHREAD ptrEthread, PEPROCESS ptrEprocess, PINITIAL_TEB initTeb, ULONG ptrByteProcessFlags, ACCESS_MASK DesiredAccess, LONG nFlag, CreateProcessContext createProcessContext, ACCESS_STATE_EXPAND *AccessState, GROUP_AFFINITY* pGroupAffinity, HANDLE* pProcessHandle, PCLIENT_ID pClientID)
它初始化EHTREAD结构,调用注册的线程回调函数,将线程插入到进程中的链表中。这个函数中的ptrEthread与ptrEprocess,在函数内部体现了进程与线程的一对多关系。
函数的流程图如下:

函数内部详细步骤如下:
保存当前PEB,并且判断参数CreateProcessContext是否为空

判断参数pGroupAffinity是否为null, 如果不为空,再次判断作业对象是否存在


对EPROCESS中的ProcessInserted, ProcessDelete, ProcessSelfDelete标志位进行检测

如果满足以上条件,跳转到下面,做释放工作,最终返回STATUS_UNSUCCESSFUL 或者
STATUS_PROCESS_IS_TERMINATING(当EProcess.Flags & STATUS_SERIAL_MORE_WRITES 不为0)


调用KeStartThread来填充当前的ETHREAD对象,插入到PKProcess->ThreadListHead中

更新ETHREAD中的CrossThreadFlags标志位,并且更新ActiveThreads的数值

将当前的ETHREAD插入到EPROCESS的LISTENTRY中,同样是头插法

如果是suspend调用,则挂起线程

如果CrossThreadFlags 第0位置1,则调用KeForceResumeThread唤醒线程


创建访问状态对象,并写入函数参数

插入对象到句柄表和对象目录

判断线程插入的是否是当前函数调用进程,不是的话,再判断参数Flags的标志位,如果不满足,调用PspAdjustThreadSecurityDescriptor调整线程安全描述符



填充输出参数clientID

如果开启了线程回调,循环遍历PspCreateThreadNotifyRoutine,调用ExGetCallBackBlockRoutine 获得回调函数地址,并调用


创建线程对象句柄

调用KeReadyThread , 将线程置于运行状态

在PspInsertThread中,我们会发现其对EPROCESS.ThreadListHead的操作,将新线程更新到LISTENTRY中去,这也等于告诉我们,可以通过遍历该链表(而不是调用API),来获取指定进程的所有线程。
DEMO: ProcessPeek
正如我们以上的分析,通过遍历0环中的进程链表、线程链表,我们就可以获取到当前所有进程、线程的信息。更进一步,如果我们将代表某进程的节点,从进程链表中删除掉,就可以达到隐藏特定进程的目的。事实上,不少病毒就是用这个技术隐藏自己的进程的。
附件中的demo,就是通过对进程、线程链表操作来显示所有进程或隐藏指定进程的:

使用前请看解压包中的“说明.txt”
不过,在实际对抗中,进程的信息不仅存放在pPorcessLink中,而是以冗余的形式存放在多处,真要完全隐藏又要稳定,需要对各细节进行大量测试。
另外一方面,从对抗的角度,若已经知道某些程序会对进程链表或线程链表“断链”来隐藏自身,我们怎样遍历才能让它隐藏失败呢?我们想到的,可以通过句柄表(系统未导出,要定位可在论坛里找相关资料)来找到被隐藏的进程。不过这个思路一是需要大量实验,二与我们这篇文章话题较远。就不公布在这里了,当作抛砖引玉,供大家讨论。
