逆向ObRegisterCallbacks 分析回调表结构
题外话
1. 因为本人水平有限,某些地方写的可能是错的,也请各位大佬指正,不胜感激。
2. 为什么要发这个帖子?
① 因为后期如果我们想对进程线程搞一些事情的话,需要了解 ObRegisterCallbacks表的结构。
② 当我去查ObRegisterCallbacks表结构资料的时候,全网搜了半小时,也没找到啥能用的,当然这可能是我搜索的姿势不对。
正文
用到的文件:win7 sp1 x64 内核文件ntoskrnl.exe
用到的工具:IDA 7.0
阅读提醒:
① 以下所有被命名为p_oft_xx都表示在缓冲区 offset 为xx位置的变量。
② 为了更清晰的表示结构体在结构体成员前面也加了oft_xx 表示偏移。
ObRegisterCallbacks函数故名思意,是一个注册对象钩子的函数。
可以让用户定义多个自定义的回调函数,在某些事件即将发生,或者已经发生的时候被触发。
比如进程线程句柄的创建,句柄的拷贝。在时下,经常被用作进程监测,进程防杀。
函数定义
NTSTATUS ObRegisterCallbacks(
POB_CALLBACK_REGISTRATION CallbackRegistration,
PVOID *RegistrationHandle
);
参数结构体相关
typedef struct _OB_CALLBACK_REGISTRATION {
USHORT Version;
USHORT OperationRegistrationCount;
UNICODE_STRING Altitude;
PVOID RegistrationContext;
OB_OPERATION_REGISTRATION *OperationRegistration;
} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;
<br>
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;
在_OB_CALLBACK_REGISTRATION结构体中,有一个需要注意的成员 OperationRegistrationCount。
这个成员表示注册回调函数的个数。
学过内核的大佬们知道,内核有一些表是全局的数组,因为是全局的,所以对表项数目会有要求,比如最多只能注册8项之类的。
那么,看到微软官方的解释,并没有限制注册的数量,而且,注册条目这里类型是USHORT。
也就是说,理论上我们可以注册 65536个回调函数。如果用全局数组,并不现实,推测使用了链表之类的数据结构。
所以,逆向的时候,我从这个角度入手,重点找内存申请的相关操作,确定表结构。
定位到关键位置,开始逆向。
首先看一下整个函数的规模,也不是特别大,挺好挺好。
F5一下,看看反汇编的结果。
看到这个就有点辣眼睛了,为了让IDA 能够完全发挥功能,定义一波用到的结构体先。
00000000 _OB_CALLBACK_REGISTRATION struc ; (sizeof=0x14, mappedto_1310)
00000000 Version dw ?
00000002 OperationRegistrationCount dw ?
00000004 Altitude UNICODE_STRING ?
0000000C p_RegistrationContext dd ?
00000010 p_OperationRegistration dd ?
00000014 _OB_CALLBACK_REGISTRATION ends
00000014
00000000 _OB_OPERATION_REGISTRATION struc ; (sizeof=0x10, mappedto_1309)
00000000 p_ObjectType dd ?
00000004 Operations dd ?
00000008 PreOperation dd ?
0000000C PostOperation dd ?
00000010 _OB_OPERATION_REGISTRATION ends
继续分析函数功能:
和大部分函数一样,上来就是一波参数检查。
检测Version成员,OperationRegistrationCount成员的合法性。
接下来我们看到了一波申请内存的操作。
n_alloc_size = 36 * u_reg_cnt + CallbackRegistration->Altitude.Length + 16;
结合上面的观点,整个表很可能是一个链表之类的数据结构,那么这块内存很可能就是数据成员的大小了。
变量解释:
n_alloc_size:申请内存的大小
u_reg_cnt:要注册的回调函数条目数量
CallbackRegistration->Altitude.Length:参数结构体中unicode字符串的长度
所以上面这个式子用文字来表达就是:
申请内存大小=36 * 要注册的回调函数条目数量+参数结构体中unicode字符串的长度+16
这个很可能是数据结构中,单个数据成员占用内存空间,我们先继续向后看。
可以看到上面来了一波赋值而且都是针对刚申请的缓冲区的赋值。
为了方便阅读,我把上图的代码整理了下,如下:
*(_WORD *)p_alloc_buf1 = 256; //OB_FLT_REGISTRATION_VERSION 这里是version成员
*((_DWORD *)p_alloc_buf1 + 1) = CallbackRegistration->p_RegistrationContext;
//这里是一个 UNICODE_STRING 结构体
n_str_len = CallbackRegistration->Altitude.Length;
*((_WORD *)p_alloc_buf1 + 4) = n_str_len; // UNICODE_STRING Length 成员
*((_WORD *)p_alloc_buf1 + 5) = n_str_len; // UNICODE_STRING MaximumLength成员
p_u_str = (char *)p_alloc_buf1 + n_alloc_size - n_str_len; // 在缓冲区的最后部分存字符串
*((_DWORD *)p_alloc_buf1 + 3) = p_u_str; // UNICODE_STRING Buffer 成员
n_str_len1 = *((unsigned __int16 *)p_alloc_buf1 + 4);
memcpy(p_u_str, CallbackRegistration->Altitude.Buffer, n_str_len1); // 拷贝字符串到内存
变量解释:
p_alloc_buf1:刚刚申请的内存首地址
n_str_len:参数中unicode字符串长度
p_u_str:unicode字符串缓冲区的地址
那么,如果这个缓冲区存储的是结构体数据的话,部分成员已经被分析出来了,我把这个这个结构体命名为 St_Call_Back_Table,如下:
struct St_Call_Back_Table
{
oft_0: USHORT Version; // _OB_CALLBACK_REGISTRATION结构体Version成员
oft_2: unknow //成员未知
oft_4: PVOID RegistrationContext; //_OB_CALLBACK_REGISTRATION结构体 RegistrationContext
//MaximumLength 和 Length 数值一样 都是字符串长度
oft_8: UNICODE_STRING Altitude //_OB_CALLBACK_REGISTRATION结构体 Altitude 成员
oft_16 --- oft_last unknow //成员未知
//在缓冲区最后 sizeof(p_alloc_buf1) - altitude.length的位置
oft_last: wchar_t[xx]; //unicode字符串 当前结构体中 Altitude.buffer 成员指向的位置
}
上面一波赋值完之后,有一个if语句块,针对 oft_2 oft_28 两个成员进行了操作,暂时先不理,看下面 else块的代码。
下面是上面图片的完整代码:
else
{
v23 = 0;
p_oft_48 = (char *)p_alloc_buf1 + 48;
while ( 1 )
{
p_oper_reg = (_OB_OPERATION_REGISTRATION *)(v23 + p_call_back_reg->p_OperationRegistration);
p_oper_reg1 = (_OB_OPERATION_REGISTRATION *)(v23 + p_call_back_reg->p_OperationRegistration);//
// 检测数据成员是否合法
if ( !p_oper_reg->Operations || !(*(_BYTE *)(*(_DWORD *)p_oper_reg->p_ObjectType + 42) & 0x40) )
break;
// 检测回调函数 调用前 指针是否合法
if ( p_oper_reg->PreOperation )
{
if ( !MmVerifyCallbackFunction(p_oper_reg->PreOperation) )
goto LABEL_21;
p_oper_reg = p_oper_reg1;
}
// 检测调用后指针是否合法
else if ( !p_oper_reg->PostOperation )
{
break;
}
if ( p_oper_reg->PostOperation )
{
if ( !MmVerifyCallbackFunction(p_oper_reg->PostOperation) )
{
LABEL_21:
u_ret = -1073741790;
goto LABEL_22;
}
p_oper_reg = p_oper_reg1;
}
.......
.......
}
else 块中进行了一波检测,检测了成员的合法性,只要有一个条件不满足就跳出。
首先我把上面的图一步步进行拆解分析。
首先我们看到了一波赋值,这里赋值了36个字节的数据:
36
36
36
这个数字是不是很熟悉?
申请内存大小 = 36 * 要注册的回调函数条目数量 + 参数结构体中unicode字符串的长度 + 16;
结合上下文的汇编,得出这是个单独的结构体,具体分析代码稍后写出来。当前结构体在缓冲区 oft_16 的位置处,p_oft_48 - sizeof(DWORD) * 8 = p_oft_48 - 32 = p_oft_16
//这里是对缓冲区的又一波赋值 p_oft_48 表示在缓冲区首地址 + 48 的位置
// 这里的 p_oft_48定义为一个 DWORD* 指针 所以 - 1 就是 - sizeof(DWORD) 请知悉
//p_oft_48 - 8 是表示 p_oft_48 - sizeof(DWORD) * 8
*p_oft_48 = 0; //固定为0
*(p_oft_48 - 7) = p_oft_48 - 8; //指向 本结构首地址
*(p_oft_48 - 8) = p_oft_48 - 8; //指向 本结构首地址
*(p_oft_48 - 6) = p_oper_reg->Operations; // _OB_OPERATION_REGISTRATION Operations成员
*(p_oft_48 - 4) = p_alloc_buf1; //缓冲区首地址
*(p_oft_48 - 3) = *(_DWORD *)p_oper_reg->p_ObjectType; // _OB_OPERATION_REGISTRATION ObjectType 成员
*(p_oft_48 - 2) = p_oper_reg->PreOperation; //_OB_OPERATION_REGISTRATION PreOperation 成员
*(p_oft_48 - 1) = p_oper_reg->PostOperation; // _OB_OPERATION_REGISTRATION PostOperation成员
仔细观察这里有一个成员没有被赋值*(p_oft_48 - 5)也就是缓冲区 oft_28在这里没有出现,前面if 块中有对这个成员的操作*(p_oft_48 - 5) 就是 if 块里面的变量,p_oft_28稍后分析。
我把这个结构命名为 St_Reg_Info。
//结构体大小36字节 9个成员 有些成员的功能会在下面进一步分析
struct St_Reg_Info
{
oft_0: PUNCHAR p_self0; //指向本结构首地址 怀疑跟LIST_ENTRY有关
oft_4: PUNCHAR p_self1; //指向本结构首地址
oft_8: OB_OPERATION Operations; //_OB_OPERATION_REGISTRATION Operations成员
oft_12: DWORD dw_flag; //待分析
oft_16: PUCHAR p_call_back_self; //指向缓冲区 St_Call_Back_Table 首地址
oft_20: POBJECT_TYPE ObjectType; //_OB_OPERATION_REGISTRATION ObjectType成员
oft_24: POB_PRE_OPERATION_CALLBACK PreOperation; //_OB_OPERATION_REGISTRATION PreOperation成员
oft_28: POB_POST_OPERATION_CALLBACK PostOperation; //_OB_OPERATION_REGISTRATION PostOperation成员
oft_32: DWORD zero_flag; //直接赋值为0 含义暂时不知道
}
赋值完一波结构体后,调用了一个参数ObpInsertCallbackByAltitude,顾名思义是一个插入链表的函数,
arg1 St_Reg_Info::p_self0
arg2 *(St_Reg_Info:: p_ObjectType)
//插入链表
u_ret = ObpInsertCallbackByAltitude(p_oft_48 - 8, *(p_oft_48 - 3));
if ( u_ret < 0 )
{
goto LABEL_22;
}
进去函数看看
看到了一个教科书般的链表插入动作。
由此可以判定St_Reg_Info 结构体中p_self实际上是个LIST_ENTRY结构体。
更新一下分析结果:
//结构体大小36字节 9个成员 有些成员的功能会在下面进一步分析
struct St_Reg_Info
{
oft_0: LIST_ENTRY list_entry; //回调函数链表项
oft_8: OB_OPERATION Operations; //_OB_OPERATION_REGISTRATION Operations成员
oft_12: DWORD dw_flag; //待分析
oft_16: PUCHAR p_call_back_self; //指向缓冲区 St_Call_Back_Table 首地址
oft_20: POBJECT_TYPE ObjectType; //_OB_OPERATION_REGISTRATION ObjectType成员
oft_24: POB_PRE_OPERATION_CALLBACK PreOperation; //_OB_OPERATION_REGISTRATION PreOperation成员
oft_28: POB_POST_OPERATION_CALLBACK PostOperation; //_OB_OPERATION_REGISTRATION PostOperation成员
oft_32: DWORD zero_flag; //直接赋值为0 含义暂时不知道
}
链表成功插入后oft_2 的位置进行了一个递增操作,猜测是表示成功,因为缓冲区在开始的时候被赋值为0,所以这个值是从0开始累积的。
++*((_WORD *)p_alloc_buf1 + 1); // 缓冲区偏移为2的数递增
接下来指针向后轮询,开始新一轮的插入操作。
此刻在结构体中还有一个成员未分析,St_Reg_Info 偏移为12位置的 dw_flag;
根据 if 块的代码分析,可以知道dw_flag成员在一个循环里,只要注册成功,这个值都会最低位置1 。所以猜测是一个标志位,表示是否有效。
更新一波分析到的结构体:
struct St_Call_Back_Table
{
oft_0: USHORT Version; // _OB_CALLBACK_REGISTRATION结构体Version成员
oft_2: USHORT u_valid_cnt; //有效注册条目 注册失败的条目不算
oft_4: PVOID RegistrationContext; //_OB_CALLBACK_REGISTRATION结构体 RegistrationContext
//MaximumLength 和 Length 数值一样 都被赋值为字符串长度
oft_8: UNICODE_STRING Altitude //_OB_CALLBACK_REGISTRATION结构体 Altitude 成员
//以下结构主要看 注册条目有几个 结构大小为36个字节
//n_reg_cnt 表示注册条目 注意这里不是 n_valid_cnt 有效条目
oft_16: St_Reg_Info st_reg_info[u_reg_cnt];
//在缓冲区最后 sizeof(p_alloc_buf1) - altitude.length的位置
oft_last: wchar_t[xx]; //unicode字符串 当前结构体中 Altitude.buffer 成员指向的位置
}
struct St_Reg_Info
{
oft_0: LIST_ENTRY list_entry; //回调函数链表项
oft_8: OB_OPERATION Operations; //_OB_OPERATION_REGISTRATION Operations成员
oft_12: DWORD dw_flag; //标志位 最低位为1 表示有效
oft_16: PUCHAR p_call_back_self; //指向缓冲区 St_Call_Back_Table 首地址
oft_20: POBJECT_TYPE ObjectType; //_OB_OPERATION_REGISTRATION ObjectType成员
oft_24: POB_PRE_OPERATION_CALLBACK PreOperation; //_OB_OPERATION_REGISTRATION PreOperation成员
oft_28: POB_POST_OPERATION_CALLBACK PostOperation; //_OB_OPERATION_REGISTRATION PostOperation成员
oft_32: DWORD dw_zero_flag; //直接赋值为0 含义暂时不知道
}
最后做一波验证
申请内存大小=36*要注册的回调函数条目数量 +参数结构体中unicode字符串的长度+16;
这里36 是结构体St_Reg_Info的大小,16是结构体St_Call_Back_Table中前4个成员占用的总字节数。
Bingo!!!!验证成功!
虽然下面还有一些针对成员的操作,但是时间原因不多分析了(主要原因是懒)。
本来是想上传IDB文件的,但是压缩了后还是有14M多,附件最多8M,所以表示遗憾了。
如果帖子有什么错误也请各位大佬指出明示,毕竟我自己水平有限。
希望本贴可以抛砖引玉,能有更多的人加入到对这个表的分析也是好的。
2019.5.9 于武汉
- End -
看雪ID:上海刘一刀
https://bbs.pediy.com/user-807993.htm
本文由看雪论坛 上海刘一刀 原创
转载请注明来自看雪社区
戳
热门文章阅读
公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com
↙点击下方“阅读原文”,查看更多干货