Windows对象初始化器实验:绕过对象回调实现进程监控与反调试

本文探讨了在Windows内核中通过修改对象类型初始化器中的函数指针,实现进程创建监控和系统级反调试的技术方法,无需传统对象回调注册即可达到类似安全监控效果。

实验Windows对象初始化器 - 参见PG合规性免责声明* - 逆向工程

概述

在本文中,我想介绍一种有趣的方法,通过替代手段(实验性地)执行类似于Windows对象回调启用的功能。众所周知,Windows系统上的反恶意软件、反作弊和通用监控工具经常使用这些回调。然而,它们的可用性仅限于具有签名模块的各方,并且这些回调带有一些风险,主要是如果未充分验证,这些回调很容易被篡改。我将展示一个利用这种未记录方法的简单示例。我们将探讨所提出的方法如何在时间使用约束下或PG禁用时实现可比较的结果。我不会花太多时间讨论Windows对象的高级细节 - 我强烈推荐《Windows内部原理》或《Windows内核编程》以获取更多详细信息。我们将按任意顺序涵盖对象构造、各种类型、通知例程和用例,特别是在反恶意软件和反作弊软件中,然后检查一些问题,并详细说明替代进程通知和反调试方法的实现。

免责声明

此实现在Windows 11 23H2(OS构建22631.3085)上进行了测试。如果它们利用相同的机制,这些方法可能适用于早期版本的Windows,除了那些有PatchGuard哈希处理的版本,如本文所述。未来部署的Windows 11可能会更改这些机制及其组织或保护。 后来的Windows版本显示,PatchGuard在5分钟到6小时内的任何地方都会抛出错误。PsProcessType和IoDriverObjectType两者都明显放置在PG上下文中,以及ObpTypeObjectType。ObpObjectTypes列表也使用SHA256进行哈希处理并放置在PG上下文中。在处理任何对象类型时,请注意潜在的崩溃。所有结构都由PG保护。但是,_OBJECT_TYPE.CallbackList条目不受保护,可以在运行时取消链接/重组以插入或删除回调。修改各种对象类型(如PsProcessType)的回调列表可以实现类似的效果。 ‡ 109是Windows的CRITICAL_STRUCTURE_CORRUPTION错误检查(BSOD)代码的简写引用。

构建块

Windows内核中的对象是操作系统操作和记账的基础。我假设对Windows对象有轻度熟悉,但如果您需要复习,一些示例包括进程、线程、文件、互斥体、信号量、IoRing等。它们都由各自的组件在操作系统初始化期间构造,并由对象管理器管理(例程以Ob为前缀在ntoskrnl中)。我们将在以下小节中坚持使用一个熟悉的对象:进程。

进程创建和通知

Windows中的进程通知回调是系统监控和安全的基石。这些回调主要由反恶意软件和反作弊系统使用,提供关于进程创建和终止事件的实时通知。它们将初始化适当的结构,然后调用PsSetCreateProcessNotifyRoutine来注册回调。对于不熟悉的人来说,安全产品为什么利用这种机制可能很明显,但它支持广泛的操作,从一般日志记录到基于回调中提供的信息的首机会验证或进程终止。 当软件注册此通知例程时,它将被附加到内核中标记为PspCreateProcessNotifyRoutine的回调列表中。每当通过API(如NtCreateUserProcess或NtCreateProcess)创建进程时,结果将始终包括枚举此列表并随后执行任何添加的回调。从调用到通知的一般流程如下:

1
2
3
4
5
6
|- ntdll.dll!NtCreateUserProcess
|    |- ntoskrnl.exe!NtCreateUserProcess
|    |    |- ntoskrnl.exe!PspInsertThread
|    |    |    |- ntoskrnl.exe!PspCallProcessNotifyRoutines
|    |    |    |    |- <N_module>!NmHandleProcessNotification
|    |    |    |    |    |- etc...

如果我们查看PspCallProcessNotifyRoutines的内部,我们会看到枚举和执行每个添加的回调。

攻击者防止这种首机会访问进程创建的几种方法已被记录。此博客上的一篇较早文章解决了一种潜在方法,从看到上述内容的下一步逻辑是定位感兴趣的回调条目并将其从PspCreateProcessNotifyRoutine列表中移除。有一篇文章详细介绍了这种方法。要点是,反恶意软件/反作弊/通用安全产品通常依赖这些回调,并可能假设它们未被篡改;然而,如前所述 - 通过滥用硬件和/或安全供应商推出的无数WHQL签名驱动程序,攻击这些机制的可靠性和可用性有些琐碎。 现在,让我们考虑不太合法的观点。在几年前,您可以使用未签名的驱动程序注册对象回调和进程通知回调(即,使用那些允许无限制访问系统资源的WHQL签名驱动程序之一来映射您自己的驱动程序)。一种方法是对DriverObject->DriverSection执行一些技巧,如这里所述。然而,如今,当Windows未处于测试签名模式或没有签名模块时,尝试注册对象通知时,您将遇到STATUS_ACCESS_DENIED结果。此方法绕过了修改驱动程序节属性、签名驱动程序或在测试签名模式下运行以获得与传统对象回调相同功能的需要。

函数指针重绑定

好了,不再有令人瞌睡的解释。让我们直接深入了解如何通过完全避免对象回调列表来实现进程通知回调。我将呈现一张图片;我相信您会立即看到这是如何工作的。如果没有,别担心……当第一个概念验证呈现时,它会变得清晰。准备好了吗?

啊……不错。 在PspInitPhase0函数中应用适当的类型到变量后,指向几种方法的指针脱颖而出。很好,那么如何找到这些的调用?我很高兴您没有问,让我展示给您。我凑合了一个IDA Python脚本来查找从起点N深度处函数的引用。对于在目标模块中 pinpointing opportunities 非常棒(是的,我本可以在PspProcessOpen上设置断点,但我对调用图中的所有间接调用感到好奇)。 让我们看看从数千个转储结果中的一些结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[1] ----------------------------------------------------------------------------------
|    |- ntoskrnl.exe!NtCreateUserProcess
|    |    |- ntoskrnl.exe!PspInsertProcess
|    |    |    |- ntoskrnl.exe!ObInsertObjectEx
|    |    |    |    |- ntoskrnl.exe!ObpCreateHandle
|    |    |    |    |    |- ntoskrnl.exe!ObpIncrementHandleCountEx
|    |    |    |    |    |    |- ntoskrnl.exe!PspChargeQuota
|    |    |    |    |    |    |    |- ntoskrnl.exe!PspExpandQuota @ 0x14048494E
[2] ----------------------------------------------------------------------------------
|    |- ntoskrnl.exe!NtCreateUserProcess
|    |    |- ntoskrnl.exe!PspInsertProcess
|    |    |    |- ntoskrnl.exe!ObInsertObjectEx
|    |    |    |    |- ntoskrnl.exe!ObpCreateHandle
|    |    |    |    |    |- ntoskrnl.exe!ObpIncrementHandleCountEx @ 0x14064B733
...

[2]和[9]项立即引起了兴趣,因为我不熟悉这些例程中执行的间接调用。在进一步检查地址0x14064B733后……

让我们稍微符号化一下。

谁需要打开WinDbg当你有DFS?我们确实需要……如果我们要彻底并验证这被命中。如果我们回顾初始图像,我们会看到PsProcessType的ObTypeInit.OpenProcedure指向PspProcessOpen。我将在WinDbg中设置断点以确认我的假设:bp nt!PspProcessOpen “kb;g”。结果很多,但一个确认了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
00 fffff806`62b432c3     : 00000000`00000001 ffffc309`606ff040 ffffc309`606b3e60 ffffc309`00000000 : nt!PspProcessOpen
01 fffff806`62b404ba     : 00000000`00000200 00000000`00000401 ffffe480`633b0da0 00000000`00000000 : nt!ObpIncrementHandleCountEx+0x4d3
02 fffff806`62afef42     : 00000000`00000000 00000000`00000200 ffffc309`67538080 ffffc309`606b3e60 : nt!ObpCreateHandle+0x21a
03 fffff806`675d9eb8     : ffffaf06`01719570 ffffbf85`2ea9ea68 ffffbf85`2ea9ea20 ffffaf06`01719570 : nt!ObOpenObjectByPointer+0x152
04 fffff806`675ee472     : 00000000`00001558 ffffc309`67538080 00000000`00000424 ffffbf85`2ea9ea20 : WdFilter!MpCreateProcessContext+0x208
05 fffff806`675ee04a     : ffffbf85`2ea9ebc0 ffffbf85`2ea9eb00 ffffbf85`2ea9f538 ffffbf85`2ea9ebc0 : WdFilter!MpHandleProcessNotification+0xe6
06 fffff806`62b24ab8     : ffffbf85`2ea9ebc0 ffffbf85`2ea9ebc0 00000000`00000000 ffffbf85`2ea9f538 : WdFilter!MpCreateProcessNotifyRoutineEx+0xaa
07 fffff806`62b235db     : 00000000`00000000 00000000`00000000 00000000`00000001 ffffc309`713050c0 : nt!PspCallProcessNotifyRoutines+0x204
08 fffff806`62b742ce     : ffffc309`707d9080 ffffc309`67538080 ffffbf85`2ea9f400 ffffbf85`2ea9f2b8 : nt!PspInsertThread+0x72f
09 fffff806`6282bbe5     : 00000000`00000000 00000000`00000000 ffffc309`702ca080 fffff806`62aee7f6 : nt!NtCreateUserProcess+0xa2e
0a 00007fff`3b130d44     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x25

这是进程创建的一个命中,是我证明浪费时间搞这个所需要的。好了,那么我们现在如何利用这个?嗯,让我们列出一些我们知道的事情。

  • 对象类型在内核初始化时创建。
  • 每个对象类型都有一个与之关联的名称。
  • 对象类型对象存储在其各自索引的ObTypeIndexTable中。
  • 初始图像中的过程存储在_OBJECT_TYPE结构的TypeInfo字段中,该结构是ObTypeIndexTable中每个条目的类型。
  • PG检查结构,但要么您假设PG被禁用,要么这将只在非常短的时间内保留。
  • ObGetObjectType可以通过MmGetSystemRoutineAddress获取。
  • Zydis存在。
  • lock xchg go brrr。
  • ???
  • 利润。

知道以上内容,我们可以检测这些函数以实现我们的目标。首先,这里是一些您如果想要复制的话会想要的结构定义:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
typedef struct __declspec( align( 8 ) ) _object_dump_control {
    void* Stream;
    unsigned int Detail;
} object_dump_control;

enum e_ob_open_reason : int {
    ob_create_handle = 0x0,
    ob_open_handle = 0x1,
    ob_duplicate_handle = 0x2,
    ob_inherit_handle = 0x3,
    ob_max_reason = 0x4,
};

typedef struct _ob_extended_parse_paramters {
    unsigned short length;
    unsigned int restricted_access_mask;
    _EJOB* silo;
} ob_extended_parse_parameters, * pob_extended_parse_parameters;

typedef struct _object_name_information {
    UNICODE_STRING Name;
} object_name_information, * pobject_name_information;

using dump_procedure_ty = void( __fastcall* )( void*, object_dump_control* );
using open_procedure_ty = int( __fastcall* )( e_ob_open_reason, char, PEPROCESS, void*, unsigned int*, unsigned int );
using close_procedure_ty = void( __fastcall* )( PEPROCESS, void*, unsigned long long, unsigned long long );
using delete_procedure_ty = void( __fastcall* )( void* );
using parse_procedure_ty = int( __fastcall* )( void*, void*, ACCESS_STATE*, char, unsigned int, UNICODE_STRING*, UNICODE_STRING*, void*, SECURITY_QUALITY_OF_SERVICE*, void** );
using parse_procedure_ex_ty = int( __fastcall* )( void*, void*, ACCESS_STATE*, char, unsigned int, UNICODE_STRING*, UNICODE_STRING*, void*, SECURITY_QUALITY_OF_SERVICE*, ob_extended_parse_parameters*, void** );
using security_procedure_ty = int( __fastcall* )( void*, SECURITY_OPERATION_CODE, unsigned int*, void*, unsigned int*, void**, POOL_TYPE, GENERIC_MAPPING*, char );
using query_name_procedure_ty = int( __fastcall* )( void*, unsigned char, object_name_information*, unsigned int, unsigned int*, char );
using okay_to_close_procedure_ty = unsigned char( __fastcall* )( PEPROCESS, void*, void*, char );

union parse_procedure_detail_ty {
    parse_procedure_ty parse_procedure;
    parse_procedure_ex_ty parse_procedure_ex;
};

struct object_type_initializer {
    unsigned short length;
    union {
        unsigned short flags;
        unsigned char case_insensitive : 1;
        unsigned char unnamed_objects_only : 1;
        unsigned char use_default_object : 1;
        unsigned char security_required : 1;
        unsigned char maintain_handle_count : 1;
        unsigned char maintain_type_list : 1;
        unsigned char supports_object_callbacks : 1;
        unsigned char cache_aligned : 1;
        unsigned char use_extended_parameters : 1;
        unsigned char reserved : 7;
    } object_type_flags;
    unsigned int object_type_code;
    unsigned int invalid_attributes;
    GENERIC_MAPPING generic_mapping;
    unsigned int valid_access_mask;
    unsigned int retain_access;
    POOL_TYPE pool_type;
    unsigned int default_paged_pool_charge;
    unsigned int default_non_paged_pool_charge;
    void( __fastcall* dump_procedure )( void*, object_dump_control* );
    int( __fastcall* open_procedure )( e_ob_open_reason, char, PEPROCESS, void*, unsigned int*, unsigned int );
    void( __fastcall* close_procedure )( PEPROCESS, void*, unsigned long long, unsigned long long );
    void( __fastcall* delete_procedure )( void* );
    union {
        int( __fastcall* parse_procedure )( void*, void*, ACCESS_STATE*, char, unsigned int, UNICODE_STRING*, UNICODE_STRING*, void*, SECURITY_QUALITY_OF_SERVICE*, void** );
        int( __fastcall* parse_procedure_ex )( void*, void*, ACCESS_STATE*, char, unsigned int, UNICODE_STRING*, UNICODE_STRING*, void*, SECURITY_QUALITY_OF_SERVICE*, ob_extended_parse_parameters*, void** );
    } parse_procedure_detail;
    int( __fastcall* security_procedure )( void*, SECURITY_OPERATION_CODE, unsigned int*, void*, unsigned int*, void**, POOL_TYPE, GENERIC_MAPPING*, char );
    int( __fastcall* query_name_procedure )( void*, unsigned char, object_name_information*, unsigned int, unsigned int*, char );
    unsigned char( __fastcall* okay_to_close_procedure )( PEPROCESS, void*, void*, char );
    unsigned int wait_object_flag_mask;
    unsigned short wait_object_flag_offset;
    unsigned short wait_object_pointer_offset;
};

typedef struct _ex_push_lock_flags {
    unsigned long long Locked : 1;
    unsigned long long Waiting : 1;
    unsigned long long Waking : 1;
    unsigned long long MultipleShared : 1;
    unsigned long long Shared : 60;
} ex_push_lock_flags;

typedef struct _ex_push_lock {
    union {
        ex_push_lock_flags flags;
        unsigned long long value;
        void* ptr;
    } u;
} ex_push_lock, * pex_push_lock;

typedef struct object_type {
    LIST_ENTRY type_list;
    UNICODE_STRING name;
    void* default_object;
    unsigned char index;
    unsigned int total_number_of_objects;
    unsigned int total_number_of_
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计