首页
社区
课程
招聘
[原创]Windows主机入侵检测与防御内核技术深入解析(7)
发表于: 2024-5-20 08:49 4784

[原创]Windows主机入侵检测与防御内核技术深入解析(7)

2024-5-20 08:49
4784

本系列文章为看雪星星人为看雪安全爱好者创作的原创免费作品。

欢迎评论、交流、转载,转载请保留看雪星星人署名并勿用于商业盈利。

本人水平有限,错漏在所难免,欢迎批评指正。


第4章 防御方案的设计与集成(下)

4.2 可疑库的运用集成

4.2.1 如何在微过滤驱动中获取路径

        在微过滤驱动中实际运用可疑库时,关键问题是如何在微过滤驱动中获得文件的全路径。一般地说,在微过滤驱动的请求过滤回调中,文件的全路径推荐用FltGetFileNameInformation来获取。但仅调用这个函数会有些麻烦:

        1)该函数返回的结果是一个FLT_FILE_NAME_INFORMATION结构的指针。虽然说信息很全面,但用不了那么多,获取全路径还需要再取一次结构成员。

        2FltGetFileNameInformation调用成功之后,返回的结果是要用FltReleaseFileNameInformation释放的。否则不但资源泄漏,有可能本驱动都无法正常卸载。

        3)从FLT_FILE_NAME_INFORMATION中拿到的全路径可能是大小写混杂的,直接拿去计算散列值肯定是错误百出的,所以必须转换成大写。

        因此我们应该对FltGetFileNameInformation进行一个封装,而且也不能直接使用它返回的字符串,而应该将字符串复制到我们自己分配的内存空间中并完成大写化。在实际操作中我是直接将它生成一个如代码4-1中的DUBIOUS_PATH结构的。

        封装了FltGetFileNameInformation的函数DubiousGetFilePathAndUpcase的实现如代码4-11所示。

 

代码4-11 函数DubiousGetFilePathAndUpcase的实现

 

// 一个很有用的节点检查。能帮我们及时发现链表操作的bug
#define DUBIOUS_PATH_ASSERT(a) \
     ASSERT(a == NULL || (MmIsAddressValid(a) && \
         (a->next == NULL || (MmIsAddressValid(a->next) && \
         a->next != a))))
// 此函数为FltGetFileNameInformation的替代品。他可以直接生成一个DUBIOUS_PATH,
// 便于后面插入哈希表中
DUBIOUS_PATH* DubiousGetFilePathAndUpcase(PFLT_CALLBACK_DATA data)  (1)
{
     DUBIOUS_PATH* ret = NULL;
     NTSTATUS status = STATUS_SUCCESS;
     ULONG length = 0;
     PFLT_FILE_NAME_INFORMATION name_info = NULL;
     do {
         BreakIf(KeGetCurrentIrql() > APC_LEVEL);  (2)
         // 获取文件的全路径。这个操作有中断级的要求。
         status = FltGetFileNameInformation(  (3)
              data,
              FLT_FILE_NAME_NORMALIZED,  (4)
              &name_info); (5)
         // 检查返回参数是否合理。路径如果太长就直接返回失败了。注意这如果不加处理,
         // 其实也是一个漏洞。
         BreakIf(status != STATUS_SUCCESS ||
              name_info->Name.Length == 0 ||
              name_info->Name.Length >= DUBIOUS_MAX_PATH);  (6)
         // 分配足够的长度。
         length = sizeof(DUBIOUS_PATH) + name_info->Name.Length;
         ret = (DUBIOUS_PATH*)ExAllocatePoolWithTag(
              NonPagedPool, length, MEM_TAG);(7)
         BreakIf(ret == NULL);
         // 复制字符串,顺便完成大写化。
         memset(ret, 0, length);
          ret->path.Buffer = (PWCHAR)(ret + 1);
         ret->path.MaximumLength = name_info->Name.Length;
         ret->path.Length = 0;
          RtlUpcaseUnicodeString(&ret->path, &name_info->Name, FALSE);  (8)
         DUBIOUS_PATH_ASSERT(ret); 
     } while (0);
     if (name_info != NULL) 
     {
      FltReleaseFileNameInformation(name_info);  (9)
     }
     return ret;
}

        注意上述代码中(1)处,该函数的参数data。这个参数来源于前回调,可参考代码3-2 请求前回调函数WriteIrpProcess中的第一个参数。这是因为(3)处的FltGetFileNameInformation的调用需要这个函数。这也意味着本函数只能在请求前后回调中使用。

        同时因为该函数只能在中断级低于等于APC_LEVEL的时候调用,所以(2)处检查了中断级。如果中断级过高,这个函数会放弃执行,直接返回NULL

        data中已经含有要访问的目标文件的信息,FltGetFileNameInformation无需再额外指定文件对象,就会主动去获取这次请求的目标文件的路径。(4)处的参数FLT_FILE_NAME_NORMALIZED指示系统我们需要获得是“规范化”的路径。这个参数非常有必要,否则可能得到五花八门的路径(想想一下意外拿到一个8~3短文件名然后去计算散列值带来的巨大麻烦)。最后获得的信息被存入(5)处的name_info中。name_info->Name就是文件的全路径。

        (6)处有一系列检查。FltGetFileNameInformation如果失败了,本函数也会返回NULL。接着是name_info->Name.Length不太正常,比如太长。对路径长度之类的可以由外部用户控制的参数如果不加检查,很容易在后面的处理中一不小心就缓冲区溢出,所以这里限制了最大长度。

        同时(6)处的这些限制会导致有时候无法获得文件路径,这也就不存在去检查一个路径是否可疑的可能了。这种情况下,若是要安全优先,则应该所有无法获取的路径一律假定为可疑或禁止执行。若要易用优先,则应该允许。当然,允许毫无疑问会带入安全漏洞。比如攻击者可以构造一个特别长的路径去绕过可疑路径检查。

        接下来的(7)处的代码用ExAllocatePoolWithTag分配了一个我们之前定义过的数据结构DUBIOUS_PATH的空间。其实际空间长度是结构本身的长度加上路径字符串所需要的长度。

        最后在(8)处,函数RtlUpcaseUnicodeStringname_info->Name转成全大写保存到了我们分配的空间中。然后别忘了,name_info是要释放的。在(9)处,FltReleaseFileNameInformation释放了name_info

        请注意在(8)处的下一行有一个DUBIOUS_PATH_ASSERT,这是一个调试时使用的检查宏。它会对DUBIOUS_PATH这个结构的节点进行一系列检查,以确保我在调试的时候及早发现问题。该宏中用到一个关键的内核函数MmIsAddressValid,能检查一个指针指向的空间是否有效。

4.2.2 在微过滤驱动中拦截可执行模块加载

        Windows内核中拦截可执行模块的加载有多种方法。比较常用的是使用PsSetLoadImageNotifyRoutine系列接口来设置回调函数。本例用了微过滤驱动,所以使用微过滤驱动来进行拦截。请回顾第3章中的代码3-1。和当时的情况类似,为了拦截模块加载,我们必须在在过滤操作数组为模块加载指定专门的处理函数,如代码4-12所示。

 

代码4-12 在过滤操作数组为模块加载指定专门的处理函数

     // 文件过滤驱动需要过滤的回调
     CONST FLT_OPERATION_REGISTRATION callbacks[] = {
         …
         {
          IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION,
          FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO,
              AcquireSectionIrpProcess,
              NULL
         },
         …

 

        以上IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION是一类新的文件请求。当可执行模块作为文件加载到内核中时,该请求会被拦截到。如果让该请求返回错误,那么加载无法成功。同时,在处理该请求的过程中,微过滤驱动可以用4.2.1中提到的方法来获取模块全路径。

        同时,相关处理函数AcquireSectionIrpProcess完整的实现如代码4-13所示。

       

代码4-13 AcquireSectionIrpProcess完整的实现

FLT_PREOP_CALLBACK_STATUS
     AcquireSectionIrpProcess(
         PFLT_CALLBACK_DATA data,
         PCFLT_RELATED_OBJECTS flt_obj,
         PVOID* compl_context)
{
     // 监控所有PE文件的加载。且可以在这里拒绝加载。
     FLT_PREOP_CALLBACK_STATUS flt_status =
         FLT_PREOP_SUCCESS_NO_CALLBACK;
     DUBIOUS_PATH* path = NULL;
     do {
         // 只对可执行文件的映射感兴趣
         BreakIf(data->Iopb->Parameters.
          AcquireForSectionSynchronization.SyncType 
              != SyncTypeCreateSection);  (1)
         BreakIf(data->Iopb->Parameters.
          AcquireForSectionSynchronization.PageProtection 
              != PAGE_EXECUTE); (2)
         // 获取路径
         path = DubiousGetFilePathAndUpcase(data);  (3)
         // 这里跳出,实际上导致漏洞产生
         BreakIf(path == NULL);
         // 如果路径本身无可疑,直接放过即可
         BreakIf(!IsDubious(path));  (4)
         // 可疑模块加载。
         LOG(("KRPS: AcquireSectionIrpProcess: dubious path = %wZ is loading\r\n", 
              path));
         // 在实际应用中,这里应该根据path获得文件,然后读取文件计算文件哈希值(比
         // 如md5。如果确认md5为白,则从可以列表中删除该路径。如果确认为黑,则禁止
         // 禁止加载。黑白文件md5可以从服务端下载。对非黑非白的可以提交到服务器要求
         // 判断。这个过程可以用FltSendMessage发送到用户态,在用户态用一个服务程
         // 序来执行。本示例省去了用户态代码,所以这里注释
         // if (FltSendMessage(...) == STATUS_SUCCESS && reply == ALLOWED)
         // { 
         //       DubiousRemove(path))
         //       break;
         // }
         // 如果没有通过验证则阻止
         // 注意,阻止的方式有两种。STATUS_ACCESS_DENIED会弹出提示框并导致进程
         // 退出。而设置STATUS_INSUFFICIENT_RESOURCES阻止非必要的DLL,进程不
         // 会退出,也没有任何提示。这种情况对用户的干扰较小。但必要的DLL不能加载
         // 进程还是会启动失败
         // data->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES;  (5)
         data->IoStatus.Status = STATUS_ACCESS_DENIED;
         LOG(("KRPS: AcquireSectionIrpProcess: Denied.\r\n", path));
         flt_status = FLT_PREOP_COMPLETE;  (6)
     } while (0);
     DoIf(path != NULL, ExFreePool(path));
     return flt_status;
}

 

         实际上,除了可执行文件加载执行之外,其他杂七杂八的文件映射等操作也可能让Windows内核调用到这里。为了筛选出真正的可执行文件加载,上述代码中有(1)(2)两处重要的判断。

        data->Iopb->Parameters. AcquireForSectionSynchronization.SyncTypeSyncTypeCreateSection说明这次请求涉及的时Section的生成。在Windows内核中任何可执行文件加载执行都会先生成对应的Section。此外data->Iopb->Parameters.AcquireForSectionSynchronization.PageProtection同样重要。它为PAGE_EXECUTE则表明生成的Section中的页面是可执行的。

        确认条件之后,在(3)处用4.2.1节中的DubiousGetFilePathAndUpcase函数获得了全大写的路径,然后在(4)处用4.1.2节中说明过的函数IsDubious进行了路径判断。如果路径并非可疑路径,那么直接跳出处理即可。

        假定路径是可疑路径,此时应该根据文件路径获得文件,然后计算文件的散列值,并和服务器下发的白库中的散列值对比。如果确认该文件为服务器认证过的合法文件,则应该将文件路径从可疑库中删除,然后允许请求。

        计算文件散列值和对比白库比较麻烦。此外,对比白库之前,可能还需要和服务器通信询问是否需要更新白库。若白库中不含有该文件散列值,那么还需要提交文件样本到服务器。这些操作不宜放在内核中,应该编写一个用户态服务进程来解决。

        微过滤驱动可以使用FltSendMessage来和用户态进程通信来发出请求和获得用户态处理的结果。本章代码将略去这些,而只做一个简单的处理:如果路径在可疑库中,则阻止这个模块的加载。相关处理在上述代码中的(5)处。

      注意,阻止模块的加载也有两种形式。其中之一是“明确阻止”。明确的阻止会导致用户进程弹框,一般会弹出“无法找到模块xxx.dll”或类似的提示信息。用户点击“确定”之后进程会退出。这样对用户来说比较明确,但缺点是进程将会必定退出。

        只需要将data->IoStatus.Status设置为STATUS_ACCESS_DENIED,即可实现明确的弹框阻止,且进程会结束。

        但加载进进程的模块并不一定是进程必须的。有些情况下(比如进程反注入)我们希望模块加载失败,但进程还可以正常执行。这就必须实现“静默阻止”。静默阻止的情况下程序不会有任何弹框,用户不能感知,但程序还可以正常执行下去。

        data->IoStatus.Status设置为STATUS_INSUFFICIENT_RESOURCES可以取得静默阻止的效果。但是,如果被阻止的模块是进程所必须的,那么还是会出现弹框且进程退出的情况。

        如果该操作已经被阻止,那么必须返回FLT_PREOP_COMPLETE(如(6)处)让内核不再往下转发这个请求,而是立刻结束掉。

4.2.3 最终演示效果

        通过第3章、第4章的编码,我们实际上已经初步完成了执行模块防御的基础功能。集成这些代码在一个完整的内核驱动程序中,编译成sys文件,然后使用工具加载,就可以测试执行的效果。从设计和实现上来说,sys文件加载之后,应该有如下效果:

        1)原始文件均可用:如果没有任何更新,那么机器上原有的软件都可以正常执行。

        2)任何新文件不可用:复制任何一个可执行文件,旧的可执行文件应该可以执行,而复制出来的新的可执行文件不可执行。

        3)任何文件被修改后不可用:用工具打开任何一个可以正常执行的可执行文件,编辑修改任何一个不影响执行的字节,保持,然后执行会失败。

        显而易见这样的好处是所有可执行模块被固化,不存在用户上网或者邮件中偶然点击链接导致可执行模块下载执行,或者是病毒感染现有可执行文件的可能。

        其主要缺点是,软件将会无法更新。用户也无法安装新的软件。但这可以通过将新版本或者是全新的软件提交到后台,由安全部门审核之后统一加入到白库中来解决。

        一个Windows11虚拟机桌面截屏如图4-2所示。在没有启动我们编写的主机防御内核模块的时候,所有软件都可以正常执行。我在桌面上放了一个名为SetDbgPrintFiltering.exe的可执行文件,双击可正确执行它。

 

图4-2 一个Windows11的虚拟机桌面截屏

        Windows自身并不会检查可执行文件是否被修改。现在我尝试验证这一点。我用二进制编辑工具打开编辑SetDbgPrintFiltering.exe,如图4-3示。


图4-3 用二进制编辑工具打开编辑SetDbgPrintFiltering.exe

 

        注意图4-3中箭头所示,原本的字符串中的单词“is”已经被手动修改为“ii”。保存之后的SetDbgPrintFiltering.exe再次测试效果依然如图4-2没有任何变化。这说明,在Windows11的原生环境下,修改可执行文件只要注意不损坏原有的功能代码和必要数据,就不会影响可执行文件的执行。

        下面我用工具将用本书代码编译成的驱动程序kr_hids.sys加载进内核中,如图4-4所示。图中我使用的工具名为OSR Driver Loader,你可以在网上搜索下载,也可以选择其他任何可以加载Windows驱动程序的工具。

        驱动加载之后,因为原本就存在的可执行文件都可以正常执行,因此此时再度执行SetDbgPrintFiltering.exe还是正常的。但是,如果我们尝试将SetDbgPrintFiltering.exe复制一份,那就相当于新建了另一个可执行文件。此时执行复制之后的exe文件,结果如图4-5所示。

        可以看到exe不能正常执行,Windows提示没有适当的权限。实际你可以尝试进行各种操作,比如将原始的SetDbgPrintFiltering.exe删除,然后将新版的SetDbgPrintFiltering.exe重命名和原始的exe一样的文件,结果会发现还是无法执行。原因是我们已经在4.1.4节的代码中很好地处理了可疑文件的重命名。

        如果原始文件没有被删除,那么此时尝试执行原始文件,我们会发现原始文件始终是能够正确运行的。此时,如果用二进制修改工具对原始文件进行如图4-3的少量修改之后保存(记得修改之后一定要点保存),那么再次运行将会失败。修改后的可执行文件再度运行的效果如图4-5所示。

图4-4 将驱动程序kr_hids.sys加载进内核中

 

图4-4 执行复制之后的exe文件

       

        本节的演示似乎意味着我们开发的模块执行防御初步达到了我们的设计目的。但遗憾的是,简单的测试无法替代真正的漏洞分析。很多时候手动测试显示的结果似乎是完美无瑕的,但渗透测试人员很容易找到漏洞。实际上安全如同用层层的纱布去阻挡水流,漏洞是永远都找不完的。但如果没有进行正确的漏洞分析仅凭简单测试就结束的项目,往往是真正的千疮百孔,无法达到安全的目的。在第5章中,我们将尝试对已经开发的部分进行漏洞分析,并提出弥补漏洞的方案。

 


图4-5 修改后的可执行文件再度运行的效果



[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2024-5-20 08:53 被星星人编辑 ,原因:
收藏
免费 4
支持
分享
最新回复 (2)
雪    币: 250
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
大佬,请教个问题呢,如果我想阻止特定的.py脚本文件执行,但又不想阻止python进程,应该如何操作呢?希望大佬能给点思路,感谢
2天前
0
雪    币: 1701
活跃值: (1638)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
3
桥式整流电路 大佬,请教个问题呢,如果我想阻止特定的.py脚本文件执行,但又不想阻止python进程,应该如何操作呢?希望大佬能给点思路,感谢
监控进程Python.exe读的文件,对.py文件做个过滤就可以了
1天前
0
游客
登录 | 注册 方可回帖
返回