突破 Windows NT 内核进程监视设置限制

监视进程创建和销毁,最常用的手段就是用 PsSetCreateProcessNotifyRoutine()
设置一个CALLBACK函数来完成。该函数的原形如下:

VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE) (
    IN HANDLE  ParentId,
    IN HANDLE  ProcessId,
    IN BOOLEAN  Create
    );   
NTSTATUS
  PsSetCreateProcessNotifyRoutine(
  IN PCREATE_PROCESS_NOTIFY_ROUTINE  NotifyRoutine,
  IN BOOLEAN  Remove
  );

安全类软件,诸如防火墙,AV,包括系统自身也在用这种方法来监视进程。不要问我为啥,有
文档的东西,大家都喜欢用。问题出来了,你有没有想过自己的监控回调函数就一定能设置成
功吗?很不幸的告诉你,不一定。特别是在非正规软件充斥internet的今天。原因很简单:
PsSetCreateProcessNotifyRoutine 最多只能设置8个回调函数,这点很多玩内核的人都知道。
然而狼多肉少,大家都想通过进程创建监控对付自己脑海中的"敌人".于是乎,自古以来抢地盘
的战争又在这里爆发了。

    最残忍的抢地盘手段:为了自己的回调函数能够正常设置成功,干脆把之前其它软件设置
的回调一并干掉,也不管是正规或者非正规软件,反正挡路者死!这下好,用户倒霉了,很多
软件包括防火墙咋不好使了呢?

    作为正规软件或者有道德的非正规软件作者就开始郁闷了,既要让自己的软件生效,又要
不影响用户的正常使用。难题既然出来了,就得想出来解决办法。于是

NTSTATUS
PsSetCreateProcessNotifyRoutineMustSuccess(
    IN PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
    IN BOOLEAN Remove
);

这个函数就诞生,该函数被笔者封装到了一个lib里面供兄弟们写驱动的时候使用,用以代替原始
的 PsSetCreateProcessNotifyRoutine 函数,使用方法和它一模一样。该函数的特点就是可以使
通过它设置的进程监控回调函数,无论在8个蹲位是否已经被占满的情况下都可以设置成功,并且
可以和之前由其它软件设置的进程监控回调函数和平共存。


工作原理:

    PsSetCreateProcessNotifyRoutineMustSuccess(NotifyRoutine)
    ||
    ||获得PspCreateProcessNotifyRoutine地址
    \/
        GetPspCreateProcessNotifyRoutine()
||
||调用原始函数
        \/
  PsSetCreateProcessNotifyRoutine(NotifyRoutine)                   调用GetFastRefObject()
  ||                             /---------->pCallBackRoutineBlock
     设置成功 || 设置失败则直接操作        保存  |        ||
  //------------\/----------->PspCreateProcessNotifyRoutine[1]-----> 或                ||成员Function
  ||                 ||                   |                 \/
  || ||替换为             \---------->pOldNotifyRoutine
  \/ || ^
over 调用 \/            再调用 |
[新进程创建]----或---------------->AgentProcessMonitor--------------------------/
|         ||
|         ||先调用
| 调用         \/       
\-----------------> NotifyRoutine


    也不知道画的流程清晰不,核心的工作原理就是真对PspCreateProcessNotifyRoutine
[PSP_MAX_CREATE_PROCESS_NOTIFY] 这个有8个蹲位的
(#define PSP_MAX_CREATE_PROCESS_NOTIFY 8 ) 数组进行操作而已。实质上
PsSetCreateProcessNotifyRoutine 这个函数也就是对 PspCreateProcessNotifyRoutine
数组进行操作,把用户设置的NotifyRoutine给存到这个数组里面,当有新进程创建的时候,
系统就会从这个数组中取得回调函数NotifyRoutine,并且调用之。这也就是为什么我们最
多只能设置8个回调函数的原因了。

    既然用 PsSetCreateProcessNotifyRoutine 设置失败,那么我们只好自己操作
PspCreateProcessNotifyRoutine 这个数组了,为我们的CALLBACK求得一个容身之处。
之前我们调用了 GetPspCreateProcessNotifyRoutine,该函数的目的就是要找到
PspCreateProcessNotifyRoutine 数组的地址,方便我们之后的DIY,嘿嘿。
   
    找到PspCreateProcessNotifyRoutine了,就可以开始DIY了。我们的LIB中用了第二
个坑,即 PspCreateProcessNotifyRoutine[1] 兄弟们如果要自己实现的话,当然可以随
便选择位置。这里需要注意的是2000和xp以后系统的区别,在2000的系统上,
PsSetCreateProcessNotifyRoutine 直接将 NotifyRoutine 回调函数的地址存到了
PspCreateProcessNotifyRoutine 数组的坑里,因此我们可以直接保存和替换先前别的软件
设置的回调函数。而xp之后进行了一些改进,出于安全性的考虑,
PspCreateProcessNotifyRoutine 不再直接存放回调函数的地址,而是存放了一个叫
FastRef 的结构,其大小也是一个DWORD,内容是一个 PEX_CALLBACK_ROUTINE_BLOCK 类型
的指针+该指针的引用记数,而真正的 NotifyRoutine 回调函数的地址被存放在了
PEX_CALLBACK_ROUTINE_BLOCK->Function 成员里。函数 GetFastRefObject 就是为了从
FastRef 结构里得到真正的 PEX_CALLBACK_ROUTINE_BLOCK 类型的指针 pCallBackRoutineBlock,
然后通过 pCallBackRoutineBlock->Function 就可以像 2000 上一样方便的保存和替换先前别
的软件设置的回调函数了。

    现在我们可以方便的替换已有的回调函数了,为了兼容被替换掉的回调函数,与其和平
共处,我们需要在自己的回调函数中调用调用一下之前被我们替换掉的回调。我们要提供的
是一个新的库函数,要做到对调用者的透明,当然不能要求函数的使用者在自己的回调函数
中调用之前被替换掉的回调函数,所以我们在库的内部提供这么一个代理回调
AgentProcessMonitor,真正替换原始回调的是这个代理函数而不是用户设置的 NotifyRoutine。
接下来我们在 AgentProcessMonitor 中先调用新设置的 NotifyRoutine,再调用原始的被
替换掉的那个函数,就能实现和平共处了。

VOID
AgentProcessMonitor(
    IN HANDLE ParentId,
    IN HANDLE ProcessId,
    IN BOOLEAN Create
)
{
if( pNewNotifyRoutine)
{
pNewNotifyRoutine(ParentId,ProcessId,Create);
}
if(MmIsAddressValid((PVOID)pOldNotifyRoutine))
{
if(memcmp((PVOID)pOldNotifyRoutine,pSigCode,0x40)==0)
{
pOldNotifyRoutine(ParentId,ProcessId,Create);
}
}
}  
    
    兄弟们可能奇怪了,MmIsAddressValid((PVOID)pOldNotifyRoutine) 和
memcmp((PVOID)pOldNotifyRoutine,pSigCode,0x40)==0 是干嘛的。因为我们用
AgentProcessMonitor 替换掉了原始的回调函数 pOldNotifyRoutine,这样会导致设置
pOldNotifyRoutine 该回调函数的驱动卸载这个回调的时候失败,但这时驱动已经被卸
载掉了,如果我们还继续调用 pOldNotifyRoutine,后果可想而之,所以我们再调用前
要判断一下。接下来如果又有新驱动被加载起来,并且占用了 pOldNotifyRoutine 指
向的地址,那么继续调用 pOldNotifyRoutine 也会死,所以我们还得判断一下这个地
址入口的若干字节是否为先前那个驱动的代码,然后才能调它。


    该函数在xpsp2和2000sp4下测试通过。

    感谢大牛dingkai与我讨论,给予我一些提醒。
 

你可能感兴趣的:(windows,进程,内核,设置,休闲)