跟我们一起
玩转路由器

一次开发遇诡异bug,无奈逆向查旧代码,意外揪出隐藏逻辑漏洞,成了特别的学习之旅。

在从事windows内核开发时,我最近因功能需求需要遍历pspsetcreateprocessnotifyroutine回调函数数组。在获取pspcreateprocessnotifyroutine的过程中,我发现了一些有趣的现象,决定在此与大家分享。由于这是我第一次撰写文章,难免有不足之处,还请各位读者多多包涵。

01

1

首先,让我们讨论一下获取这个数组的思路,以Windows 7 x64为例,其他版本大同小异。PspCreateProcessNotifyRoutine是一个PVOID类型的指针,它存储通过PsSetCreateProcessNotifyRoutine函数设置的各种回调函数。我们可以通过以下步骤获取这个数组:

  1. 使用MmGetSystemRoutineAddress获取PsCreateProcessNotifyRoutine的地址。在这个函数中,可以发现它调用了PspSetCreateProcessNotifyRoutine。

2. 在PspSetCreateProcessNotifyRoutine函数中,偏移0x33处有一次对PspCreateProcessNotifyRoutine的操作。

3. 跟入这个函数地址后,可以看到这是一个函数地址表。然而,通过uf这些地址时,发现这些地址是错误的。这是为什么呢?

4. 实际上,微软对这些函数进行了加密。要获取真实的函数地址,需要先对其进行解密。解密方法非常简单,只需将这些地址取出并进行 &0xfffffffffffffff8 操作即可。

5. 这样就可以获得真实的地址指针,通过访问这个指针,可以跟到真实的挂钩函数地址。按照这个思路编写的遍历代码是可行的,此处就不再展示。

6. 我们可以通过跟入这个函数并对比Pchunter的结果来验证我们的方法是正确的。

7. 在获取这张函数表后,通过对比模块的基地址和模块大小,可以确定函数所属的模块。同样,可以根据模块名在PsSetCreateProcessNotifyRoutine的函数地址传入有目的的地址和True来取消该模块的挂钩。也可以直接对函数头部进行Ret0操作,但由于可能出现问题,不推荐这样做。

02

2

那么我在Win7 x86版本上遇到了什么问题呢?同样使用MmGetSystemRoutineAddress获取PsCreateProcessNotifyRoutine的函数地址时,发现这个地址虽然是一个函数地址,但实际上是错误的。因为在Windbg中uf获取的PsCreateProcessNotifyRoutine与通过MmGetSystemRoutineAddress获取的地址居然不一样!起初我百思不得其解,但冷静下来后,我开始逐步分析这个过程,有趣的旅程就此展开!

  1. 首先,Windbg的u命令一定是准确的。微软自家的调试器在有符号表的情况下,如果连函数位置都不知道,那真是让人无语了!而且我确实进入过u到的函数,确认是正确的。因此,我猜想问题一定出在MmGetSystemRoutineAddress这个关键函数上。

  2. 那么MmGetSystemRoutineAddress是如何工作的呢?经过查阅现有资料,我解开了我的疑惑。MmGetSystemRoutineAddress内部实际上是解析模块文件的EAT(导出表)来获取函数调用者所需的函数。通过解析EAT和模块基地址的运算结合ImageLoad的对齐方式,返回对应的函数位置。因此我们的思路有了,因为是x86操作系统,在没有KPP保护的情况下,我的内核的EAT很可能被一些第三方软件挂钩,导致我获取的函数不正确。于是我在Windbg中使用.reload命令重新加载所有模块信息,然后使用lm命令查看所有模块的地址,通过对比各个模块的基地址和模块大小,大致确定了是哪个模块。PCHunter的内核挂钩也验证了我的猜想。

03

模块分析

既然确认内核是被EAT挂钩的,我们不妨分析一下这个模块。

  1. 通过数字签名,我们不难看出这个模块是一个安全软件的模块,到这里困惑就解开了。我们猜测是因为在x86系统上没有KPP保护,为了保护自身的钩子不被卸载或者为了监控其他软件的钩子,安全软件选择在EAT挂钩以保护自身的钩子不被其他软件取消,这是一个很好的思路。那么到底是不是这样呢,我们接着分析。

2. 既然我们已经知道被替换函数的地址和被EAT挂钩的名称,那么我们接下来从这两点开始进行逆向。首先,我们先寻找字符串信息,根据模块名称。

3. 我们先根据字符串找到对这个字符串引用的地址,很明显只有这一处。我们跟进去,结合上下文看到了一个关键函数ZwQuerySystemInformation。到这里,有过内核开发经验的小伙伴们肯定已经猜到这个函数是在获取模块基地址。看他对v4这个参数的引用,应该就是需要返回的模块地址。我们将这个函数命名为GetKeyModuleAddress,同时参数返回的就是模块大小。

4. 接着我们对其x进行调用分析,可以看到有两处。我们跟入第一处,很幸运地找到了所需的函数。在这个函数中,我们可以看到大量关键函数的字符串,而我们的PsSetCreateProcessNotifyRoutine也在其中。这时我们的第二条主线就排上用场了。我们可以看到下图中使用PsSetCreateProcessNotifyRoutine这个字符串的函数也引用了sub_4A504这个函数,而这个函数正是我们内核被挂钩EAT后跳转过去的函数。因此我们猜测sub_49F84这个函数应该是GetProcAddressAndSetHook这两个功能,于是我们对其命名GetProcAddressAndSetHook,同时对sub_49F84这个函数命名为Hook_PsSetCreateProcessNotifyRoutine。

5. 接着我们对GetProcAddressAndSetHook(sub_4A504)的流程进行分析。首先进入该函数后,根据传入参数我们不难发现有一个函数名称和一个hook的函数地址。根据使用这两项的函数,我们猜测sub_49DDE这个函数的功能是获取函数地址。在他调用的函数sub_49D60中可以明显看到存在PE特征0x5A 0x4D以及0x45 0x50这几个字节码,所以这个函数肯定是根据函数名称获取函数地址无疑,我们将其命名为GetFunAddress(sub_49D60),其首参数为返回需求函数的地址。

6. 接着跟入sub_49B6E这个函数进行分析,我们可以看到明显的内存页操作,调用了IoAllocateMdl,MmProbeAndLockPages,MmMapLockedPagesSpecifyCache这几个关键函数。这很明显是申请MDL对内存页进行锁定,防止换页造成缺页异常等问题,这一般是hook的必要操作,所以我们对其命名为LockPage。

7. 在完成页锁定之后,必然就是进行hook操作。这里采用了一个很好的方法,并没有使用memcpy或者其他内存填充写入的方法,而是采用了原子操作。这种一箭双雕的方法,首先使用_InterlockedExchange这个原子操作交换函数可以很方便地解决了同步问题,其次在_InterlockedExchange调用的时候返回值是上一次的状态,也很方便地保存了上一次的地址,以便于恢复。所以说是一种一箭双雕的方法,InterlockedExchange的第二个参数也使用到了我们的传入地址,以及刚才的LockPage。

8. 在完成原子交换之后,GetProcAddressAndSetHook的第五个参数被使用。这里可以看到使用结束之后,之前的地址被保存下来,所以可以论证这里是用于恢复使用的。而且结合外面的函数传入值来看,这里是一个全局对象,而且这里这个值在hook的函数之中仍然需要去调用,所以也论证了这一点。

  1. 在之后紧接着调用了sub_4B340这个函数,在这个函数中就是一些基本的解除页面锁定的函数,我们将其命名为UnlockPage。

至此,Hook的全套流程就已经分析完毕了。接下来我们来看一看hook掉的代理函数做了一些什么。

04

代理函数分析

1. 首先,第一个函数sub_4A3F2的操作非常奇怪,该函数作为替换函数应该是一个两参函数,但很不幸IDA分析失败了。最开始因为经验欠缺我没有明白这个函数的意义,但随着之后的分析我茅塞顿开。这个函数是通过栈寄存器来获取调用地址的,因为在栈上有函数的调用地址,所以在之后的LogAboutInformation中会有使用。

2. 根据Hook_PsSetCreateProcessNotifyRoutine的第二个参数True或False来确定具体流程,无论是在取消设置还是在设置函数中都会调用sub_49CE0这个函数,这个函数的唯一作用就是调用之前保存下来给全局变量的原始的PsSetCreateProcessNotifyRoutine,我们将其命名为CallRightOldPsSetCreaetProcess。

3. 接着会根据对于PsSetCreateProcessNotifyRoutine调用和失败会进入到LogAboutInformation(sub_4A2C8)这个函数中,跟入该函数结合传入参数分析该函数的唯一意义就是获取设置的函数地址模块名称以及调用者的模块名称,对其进行格式化之后结合特定标志位上传到r3上。

4. 有意思的是在设置回调的代理函数Hook_PsSetCreateProcessNotifyRoutine中在设置行为下是存在拦截操作的,拦截操作的行为依据来源于LogAboutInformation的返回值并且返回0xC0000022,但是在LogAboutInformation的第三个参数为0的情况下LogAboutInformation直接返回0,所以也就是说在该版本下拦截其实并不生效。

05

恢复与绕过思路

  1. 通用思路,首先同样获取系统内核模块的相关函数地址,模拟MmGetSystemRoutineAddtess的流程,但是我们这里需要解析文件并且解些EAT载入内存,如果采用读文件的方式的话需要注意内存对齐问题。

  2. 拿到真正的函数地址时,如果需要恢复的话可以直接学习刚才那一套mdl锁+原子交换的理念,这样就可以解决了。

  3. 但这里其实并不提倡这种方法,因为在一些软件中会对于代码有crc校验等功能,如果强行解除hook的话很有可能导致crc校验失败导致不可预料的结果,所以直接可以将获取到的函数进行指针强转直接调用即可。

赞(0)
版权声明:本文采用知识共享 署名4.0国际许可协议 [BY-NC-SA] 进行授权
文章名称:《一次开发遇诡异bug,无奈逆向查旧代码,意外揪出隐藏逻辑漏洞,成了特别的学习之旅。》
文章链接:https://www.lu-you.com/wangluo/xt/25347.html
本站资源来源于互联网整理,若有图片影像侵权,联系邮箱429682998@qq.com删除,谢谢。

评论 抢沙发

登录

找回密码

注册