更进一步深入的研究ObRegisterCallBack

发布于:2025-06-13 ⋅ 阅读:(25) ⋅ 点赞:(0)

引入

我们如果想hook对象的回调,在上篇文章里我们已经知道了对象回调函数存在一个列表里面,我们通过dt可以看见,这里他是一个LIST_ENTRY结构,但是实际调用的时候,这样是行不通的,说明它结构不对

0: kd> dt _OBJECT_TYPE 86cf5d28
nt!_OBJECT_TYPE
   +0x000 TypeList         : _LIST_ENTRY [ 0x86cf5d28 - 0x86cf5d28 ]
   +0x008 Name             : _UNICODE_STRING "Process"
   +0x010 DefaultObject    : (null) 
   +0x014 Index            : 0x7 ''
   +0x018 TotalNumberOfObjects : 0x25
   +0x01c TotalNumberOfHandles : 0xd0
   +0x020 HighWaterNumberOfObjects : 0x30
   +0x024 HighWaterNumberOfHandles : 0xf9
   +0x028 TypeInfo         : _OBJECT_TYPE_INITIALIZER
   +0x078 TypeLock         : _EX_PUSH_LOCK
   +0x07c Key              : 0x636f7250
   +0x080 CallbackList     : _LIST_ENTRY [ 0x996a88c8 - 0x996a88c8 ]

ObRegisterCallBack 分析

函数上来首先是合法性的检查,先检查版本号,然后从CallbackRegistration 取出OperationRegistrationCount也就是所要注册的回调数

inserted = 0;
  if ( (CallbackRegistration->Version & 0xFF00) != 256 )
    return 0xC000000D;
  OperationRegistrationCount = CallbackRegistration->OperationRegistrationCount;
  if ( !OperationRegistrationCount )
    return 0xC000000D;

在这之后,函数申请了一个空间,这里需要强调,OperationRegistrationCount*36,也就是每个给回调信息结构的空间有36个字节,但是我们回忆一下之前的知识,回调所对应的结构**_OB_OPERATION_REGISTRATION**只有16个字节,也就是我们需要重新分析这个待会在申请空间中与_OB_OPERATION_REGISTRATION类似的结构

 buf_size = 36 * OperationRegistrationCount + CallbackRegistration->Altitude.Length + 16;
  Alloc_buf = (DWORD *)ExAllocatePoolWithTag(PagedPool, buf_size, 0x6C46624Fu);
  Alloc_buf_copy = Alloc_buf;
  if ( !Alloc_buf )
    return 0xC000009A;
  memset(Alloc_buf, 0, buf_size);
typedef struct _OB_OPERATION_REGISTRATION {
  POBJECT_TYPE                *ObjectType;
  OB_OPERATION                Operations;
  POB_PRE_OPERATION_CALLBACK  PreOperation;
  POB_POST_OPERATION_CALLBACK PostOperation;
} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;

之后我分析我一点一点来,所贴出的代码括号不一定是闭合的

在通过if语句验证了CallbackRegistration->OperationRegistrationCount的合法性后除了去初始化这个Num(用来在While循环中遍历),我们可以看见还有一个**v10 = Alloc_buf_copy + 12;**首先,回忆一下,在上面的代码中,我们知道Alloc_buf_copy是所申请的内存的首地址指针,所以这个v10也就是存放了首地址偏移一段所对应的指针。

但是,这里我要特别指出:这个12是根据我们定义的变量类型来改变的,不同的指针类型,IDA分析出来的伪代码都不太一样,所以这种偏移我们最好都回到汇编代码来进行分析,我就一起附在下面了,从下面那个汇编我们就可以看见,这个偏移实际上是0x30也就是48字节。

继续向下看,进入了While循环开始遍历这个回调列表,取出回调结构信息到v11这个变量,复制一份给v25(这里我还没有重命名,变量名字可能不同但是相对位置应该是一样的)

  CallbackRegistrationa = 0;
  if ( CallbackRegistration->OperationRegistrationCount )
  {
    Num = 0;
    v10 = Alloc_buf_copy + 12;
    while ( 1 )
    {
      v11 = &CallbackRegistration->OperationRegistration[Num];
      v25 = v11;
      if ( !v11->Operations || ((*v11->ObjectType)->TypeInfo.ObjectTypeFlags & 0x40) == 0 )
        break;
PAGE:006D62B0 8D 7E 30                      lea     edi, [esi+30h]

上面代码的if ( !v11->Operations || ((*v11->ObjectType)->TypeInfo.ObjectTypeFlags & 0x40) == 0 )检查了两个东西,第一个是我们之前学过的Operations

typedef struct _OB_OPERATION_REGISTRATION {
  POBJECT_TYPE                *ObjectType;
  OB_OPERATION                Operations;
  POB_PRE_OPERATION_CALLBACK  PreOperation;
  POB_POST_OPERATION_CALLBACK PostOperation;
} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;

第二个我们可以用windbg来看看是什么值,这里找到对应的TypeInfo里面的ObjectTypeFlags,源代码里面异或的是0x40,也就是01000000,对应的就是SupportsObjectCallbacks,通过之前的学习我们知道,这里是对象是否支持回调的验证位,也就是现在还是合法性检查.

0: kd> dt _OBJECT_TYPE
nt!_OBJECT_TYPE
  ........................
   +0x028 TypeInfo         : _OBJECT_TYPE_INITIALIZER //这里
   +0x078 TypeLock         : _EX_PUSH_LOCK
 ..........................
0: kd> dt _OBJECT_TYPE_INITIALIZER
nt!_OBJECT_TYPE_INITIALIZER
   +0x000 Length           : Uint2B
   +0x002 ObjectTypeFlags  : UChar
   +0x002 CaseInsensitive  : Pos 0, 1 Bit
   +0x002 UnnamedObjectsOnly : Pos 1, 1 Bit
   +0x002 UseDefaultObject : Pos 2, 1 Bit
   +0x002 SecurityRequired : Pos 3, 1 Bit
   +0x002 MaintainHandleCount : Pos 4, 1 Bit
   +0x002 MaintainTypeList : Pos 5, 1 Bit
   +0x002 SupportsObjectCallbacks : Pos 6, 1 Bit
  .............................................

我们继续回到代码分析上来,可以看见之后是用于检查前后回调的合法性

  if ( v11->PreOperation )
      {
        if ( !MmVerifyCallbackFunction((unsigned int)v11->PreOperation) )
          goto LABEL_21;
        v11 = v25;
      }
      else if ( !v11->PostOperation )
      {
        break;
      }
      if ( v11->PostOperation )
      {
        if ( !MmVerifyCallbackFunction((unsigned int)v11->PostOperation) )
        {
LABEL_21:
          inserted = 0xC0000022;
          goto LABEL_22;

再往下,我们看见了一个类似结构体初始化的操作,这个v10我们回忆一下,就是 **v10 = Alloc_buf_copy + 12;*这里的v10,我们已经知道它偏移了申请空间48字节。这里如果有细心的人来看,就会发现少了一个(v10 - 5)的赋值,这里我们后面再说,但是如果加上这个复制,这里的结构是36字节

      *v10 = 0;
      *(v10 - 7) = (DWORD)(v10 - 8);
      *(v10 - 8) = (DWORD)(v10 - 8);
      *(v10 - 6) = v11->Operations;
      *(v10 - 4) = (DWORD)Alloc_buf_copy;
      *(v10 - 3) = (DWORD)*v11->ObjectType;
      *(v10 - 2) = (DWORD)v11->PreOperation;
      *(v10 - 1) = (DWORD)v11->PostOperation;

如果对最开始我们内存分配方式还有印象的人应该就可以回想起这个36,它正是我们为每个回调结构申请的大小,同时,还可以想起来申请的时候,申请的空间还加了16个字节,在这里也对上了。这里我需要强调的是这个结构的头两个成员,也是指针。

我们在上面强调过,指针相关的偏移是按照我们的定义来算的,所以,这里的减8实际上减的是DWORD*8,也就是32字节。进一步讲,这两个成员装的是我们结构的首地址,其实这里剧透一下,这就是在初始化链表

还需要注意的是第5个成员指向的是我们申请空间的首地址

在这里插入图片描述

有细心的人在读到这里之前可能就有疑问,之前还有一段代码为什么没有介绍,这一段代码经过本人初学的分析,就是在给上图的16个字节赋值,具体的图我放下来了,这里每个框是两个字节

其中,Length指的是海拔字符串的长度,buffer指向的是存放这段字符串的首地址。

*(_WORD *)Alloc_buf_copy = 256;
  Alloc_buf_copy[1] = (DWORD)CallbackRegistration->RegistrationContext;
  Length = CallbackRegistration->Altitude.Length;
  *((_WORD *)Alloc_buf_copy + 5) = Length;
  *((_WORD *)Alloc_buf_copy + 4) = Length;
  v9 = (char *)Alloc_buf_copy + buf_size - Length;//这里可以看出来算出了这个字符串的首地址
  v22 = *((unsigned __int16 *)Alloc_buf_copy + 4);
  Alloc_buf_copy[3] = (DWORD)v9;
  memcpy(v9, CallbackRegistration->Altitude.Buffer, v22);

在这里插入图片描述

我们继续跟着代码往下走,看见调用了一个函数,从语义上来说,这是将回调按照海拔(优先级)插入回调链表的函数,这里我在放一下前面文章提到过的_OBJECT_TYPE结构,也就是插入这个结构0x80的位置

inserted = ObpInsertCallbackByAltitude((int *)v10 - 8, *(v10 - 3));
0: kd> dt _OBJECT_TYPE
nt!_OBJECT_TYPE
   +0x000 TypeList         : _LIST_ENTRY
   +0x008 Name             : _UNICODE_STRING
   +0x010 DefaultObject    : Ptr32 Void
   +0x014 Index            : UChar
   +0x018 TotalNumberOfObjects : Uint4B
   +0x01c TotalNumberOfHandles : Uint4B
   +0x020 HighWaterNumberOfObjects : Uint4B
   +0x024 HighWaterNumberOfHandles : Uint4B
   +0x028 TypeInfo         : _OBJECT_TYPE_INITIALIZER
   +0x078 TypeLock         : _EX_PUSH_LOCK
   +0x07c Key              : Uint4B
   +0x080 CallbackList     : _LIST_ENTRY

这两个参数,向上翻翻就知道一个是链表头,一个是ObjectType,这里不过多分析了。

然后我们进入这个插入函数,来看看是怎么插入的。

我们已经知道了,传入的第一个参数是我们之前分析结构的首地址,也是一个双向链表头

00000000 _OB_LIST_ENTRY struc ; (sizeof=0x24, mappedto_1307)
00000000 callbackList _LIST_ENTRY ?
00000008 Operation dd ?
0000000C UNKNOW dd ?
00000010 Self dd ?
00000014 Object_Type dd ?
00000018 PreOperation dd ?
0000001C PostOperation dd ?
00000020 UNKNOW1 dd ?
00000024 _OB_LIST_ENTRY ends

在知道我们传入的参数a1的结构是分析出来的_OB_LIST_ENTRY参数a2是_OBJECT_TYPE之后,我们的分析就清晰很多了,首先是线程的锁,这个这里不细说

v17 = 0;
  CurrentThread = KeGetCurrentThread();
  --CurrentThread->SpecialApcDisable;
  p_TypeLock = &a2->TypeLock;
  if ( _interlockedbittestandset((volatile signed __int32 *)&a2->TypeLock, 0) )
    ExfAcquirePushLockExclusive();

以后我大概以注释的形式来解释代码,可以看见最后的结果就是将我们选中的链表插入这个callbacklist中,并按照海拔降序排列(其实就是优先级)

p_CallbackList = &a2->CallbackList;//取出链表
  Flink = a2->CallbackList.Flink;
  if ( Flink == p_CallbackList )//如果没有其他链表
    goto LABEL_10;//直接插入
  Altitude2 = (const UNICODE_STRING *)(a1->Self + 8);//取出海拔
  while ( 1 )
  {
    v7 = RtlCompareAltitudes((PCUNICODE_STRING)&Flink[2].Flink[1], Altitude2);//比较海拔,如果当前所要插入链表头的海拔大于所遍历的链表的海拔,则插入。这样就可以保证回调链表的降序,始终是高海拔的huii'd
    v8 = v7 == 0;
    if ( v7 <= 0 )//如果
      break;
    Flink = Flink->Flink;
    if ( Flink == p_CallbackList )
    {
      v8 = v7 == 0;
      break;
    }
  }
  if ( !v8 )
  {
LABEL_10://插入操作
    Blink = Flink->Blink;
    v10 = Blink->Flink;
    a1->callbackList.Flink = Blink->Flink;
    a1->callbackList.Blink = Blink;
    v10->Blink = &a1->callbackList;
    Blink->Flink = &a1->callbackList;
  }
  else
  {
    v17 = 0xC01C0011;//如果海拔相同就报这个错
  }
  Value = p_TypeLock->Value;锁相关这里不细说
  if ( (p_TypeLock->Value & 0xFFFFFFF0) <= 0x10 )
    v12 = 0;
  else
    v12 = Value - 16;
  if ( (Value & 2) != 0 || _InterlockedCompareExchange((volatile signed __int32 *)p_TypeLock, v12, Value) != Value )//
    ExfReleasePushLockShared(p_TypeLock);
  v13 = KeGetCurrentThread();
  if ( !++v13->SpecialApcDisable && ($ECEA6BAF150BF07A2F607C21A5294F19 *)v13->ApcState.ApcListHead[0].Flink != &v13->64 )
    KiCheckForKernelApcDelivery();
  return v17;
}

回到ObRegisterCallBack函数,这里代码大概意思就是移动到下一个结构体,继续重复刚刚的操作

 if ( inserted < 0 )
        goto LABEL_22;
      ++*((_WORD *)Alloc_buf_copy + 1);
      CallbackRegistrationa = (_OB_CALLBACK_REGISTRATION *)((char *)CallbackRegistrationa + 1);
      ++Num;
      v10 += 9;
      if ( (unsigned int)CallbackRegistrationa >= CallbackRegistration->OperationRegistrationCount )
        goto LABEL_29;
    }
    inserted = 0xC000000D;

最后就是我们之前提到在_OB_LIST_ENTRY中的一个位,它当时没有被置位,在最后一段中,每个_OB_LIST_ENTRY的这个位(我们后面称为Flags),都被或了一个1.最后返回了,所申请内存空间的首地址,作为RegistrationHandle,也就是ObRegisterCallBack的第二个参数。

到这里我们就分析完了

LABEL_30:
    v19 = 0;
    if ( *((_WORD *)Alloc_buf_copy + 1) )//OperationRegistrationCount
    {
      v20 = Alloc_buf_copy + 7;//这个就是之前我们标记UNKNOW的位置,我们以后记为FLag位
      do
      {
        *v20 |= 1u;//或了一个1
        ++v19;
        v20 += 9;//这里的九指的是_OB_LIST_ENTRY 里面的9,这样等价于跳到了下个相同结构的Flag,然后重复这个循环
      }
      while ( v19 < *((unsigned __int16 *)Alloc_buf_copy + 1) );
    }
    *RegistrationHandle = Alloc_buf_copy;//最后,这就是我们返回的RegistrationHandle
  }
  return inserted;
}

实践一下,验证逆向结果

在之前我们对象回调的代码基础上,打印出RegistrationHandle的位置

0: kd> g
[+] The state is 0
[+] RegistrationHandle is in 996a88b8

在这里插入图片描述

dd来看一下,为了方便我把图又放了一次

0: kd> dd 996a88b8
ReadVirtual: 996a88b8 not properly sign extended
996a88b8  00010100 00000000 000c000c 996a88ec
996a88c8  86cf5da8 86cf5da8 00000003 00000001
996a88d8  996a88b8 86cf5d28 98c08150 98c08140//这里的996a88b8指向了自己的首地址,
996a88e8  00000000 00320031 00340033 00360035
996a88f8  06040209 74416553 00000000 996a8904
996a8908  996a8904 00000000 996a8910 996a8910
996a8918  06080204 61564d43 002c0000 80c4d874
996a8928  00176b76 80000004 ffffffff 00000004

首先便是一个00010100,0001指的是我们回调数量,为1;0100是256,符合我们的预期

然后就是RegistrationContext这是我们函数参数,我没传所以是00000000,然后是海拔的bufffer 996a88ec,db看一下这个值,可以看见123456,符合预期。这16字节正确之后,继续向下走。

0: kd> db 996a88ec
ReadVirtual: 996a8938 not properly sign extended
996a88ec  31 00 32 00 33 00 34 00-35 00 36 00 09 02 04 06  1.2.3.4.5.6.....
996a88fc  53 65 41 74 00 00 00 00-04 89 6a 99 04 89 6a 99  SeAt......j...j.
996a890c  00 00 00 00 10 89 6a 99-10 89 6a 99 04 02 08 06  ......j...j.....
996a891c  43 4d 56 61 00 00 2c 00-74 d8 c4 80 76 6b 17 00  CMVa..,.t...vk..
996a892c  04 00 00 80 ff ff ff ff-04 00 00 00 01 00 35 00  ..............5.
996a893c  4d 69 6e 50 6f 73 31 32-37 34 78 31 31 39 39 78  MinPos1274x1199x
996a894c  39 36 28 31 29 2e 79 00-00 00 00 00 08 02 04 06  96(1).y.........
996a895c  53 65 41 74 00 00 00 00-64 89 6a 99 64 89 6a 99  SeAt....d.j.d.j.

再接下来的两个86cf5da8其实是链表头链表尾,但是我们只有一个函数所以一致

这个结构应该就是我们分析的_OB_LIST_ENTRY,首先是链表头链表尾996a88c8 996a88c8 ,对照上面,这里确实是我们这个_OB_LIST_ENTRY的地址

0: kd> dd 86cf5da8
ReadVirtual: 86cf5da8 not properly sign extended
86cf5da8  996a88c8 996a88c8 04190019 d46a624f
86cf5db8  8dc05880 00080006 8dc011f8 00000000
86cf5dc8  86cf5d00 86cf5f08 00000000 00000000
86cf5dd8  00000002 00000000 00000000 13030002
86cf5de8  00000000 00000000 86cf5df0 86cf5df0
86cf5df8  00080006 8dc011f8 00000000 00000006
86cf5e08  00000002 00000002 00000004 00000004
86cf5e18  00080050 00000000 00000000 00020004

接下来是86cf5d28,这个应该是我们的OBJECT_TYPE,可以看见是进程对象,完美,并且最下面0x80的链表也对上了.

0: kd> dt _OBJECT_TYPE 86cf5d28
nt!_OBJECT_TYPE
   +0x000 TypeList         : _LIST_ENTRY [ 0x86cf5d28 - 0x86cf5d28 ]
   +0x008 Name             : _UNICODE_STRING "Process"
   +0x010 DefaultObject    : (null) 
   +0x014 Index            : 0x7 ''
   +0x018 TotalNumberOfObjects : 0x25
   +0x01c TotalNumberOfHandles : 0xd0
   +0x020 HighWaterNumberOfObjects : 0x30
   +0x024 HighWaterNumberOfHandles : 0xf9
   +0x028 TypeInfo         : _OBJECT_TYPE_INITIALIZER
   +0x078 TypeLock         : _EX_PUSH_LOCK
   +0x07c Key              : 0x636f7250
   +0x080 CallbackList     : _LIST_ENTRY [ 0x996a88c8 - 0x996a88c8 ]

在接着就是98c08150 98c08140这两个我们注册的前后回调,这里没必要演示了,最后就是我们之前分析的v10 = 0,整个结构体结束.

到这里我们就验证了我们结构体的正确性

完结,撒花😀

最后的结果

00000000 _OB_LIST_ENTRY struc ; (sizeof=0x24, mappedto_1307)
00000000                                         ; XREF: _OB_HANDLE/r
00000000 callbackList _LIST_ENTRY ?
00000008 Operation dd ?
0000000C Flag dd ?
00000010 Self dd ?
00000014 Object_Type dd ?
00000018 PreOperation dd ?
0000001C PostOperation dd ?
00000020 UNKNOW1 dd ?
00000024 _OB_LIST_ENTRY ends
00000024
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 _OB_HANDLE struc ; (sizeof=0x34, mappedto_1308)
00000000 Version dw ?
00000002 OperationRegistrationCount dw ?
00000004 RegistrationContext dd ?
00000008 Length1 dw ?
0000000A Length2 dw ?
0000000C buffer dd ?
00000010 CallBackList _OB_LIST_ENTRY ?
00000034 _OB_HANDLE ends
00000034

RegistrationHandle实际的结构就是上面写的 _OB_HANDLE,然后回调链表的实际结构就是 _OB_LIST_ENTRY


网站公告

今日签到

点亮在社区的每一天
去签到