查看原文
其他

摘除MiniFilter回调的正确姿势

adc又死了 看雪学苑 2024-04-20




引言


前段时间因有需要我必须摘除一个MiniFilter的回调,了解的人都知道FltUnregisterFilter是移除MiniFilter的API,但是MSDN强调过这个API只能模块自身使用,于是当我尝试拿他去对我的目标驱动手时,代码进入FltUnregisterFilter后就一去不返了。




一篇启发性文章


在看雪上拜读到这篇文章(https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458474374&idx=2&sn=f9d7aa29896869bc9a748d7bdfd61426&chksm=b18e670c86f9ee1a5228821364bcc870767ca4bad19dceee879794e5b82d4e70562e644047c7&scene=27),文中定位了阻塞的原因:进入FltUnregisterFilter后,ExWaitForRundownProtectionRelease会检查filter对象的引用计数,按照MSDN的解释,如果不为0那么就会阻塞。作者的实验场景中此时引用计数为2,那么一定也就阻塞了。

作者再观察使用
PCHunter来摘除MiniFilter的过程,发现在进入ExWaitForRundownProtectionRelease之前这个filter对象的引用计数已经变为了0,于是他猜测PCHunter使用APIExReleaseRundownProtection来减少了引用计数。因此他也在自己驱动中在调用FltUnregisterFilter之前添加执行了ExReleaseRundownProtection,然后成功了。




有没有问题


我使用作者这个方法在一个环境下测试成功了,但是换了一个环境后并没成功,原因是要摘除的filter的引用计数在ExWaitForRundownProtectionRelease之前并不能到达0:
执行ExReleaseRundownProtection后进入FltUnregisterFilter之时引用计数值为0x7623:



执行到ExWaitForRundownProtectionRelease之时这个引用计数值还剩0x6:



接着进入ExWaitForRundownProtectionRelease就再次一去不返了。




寻找一个真正的解决方法


所以我需要调多次ExReleaseRundownProtection吗?如果是多次那么到底应该调几次才是恰当的?我尝试寻找一个真正的具有通用性的解决办法:

我尝试连续调用
ExReleaseRundownProtection,发现每次这个引用计数都会减少2:



所以如果我循环“引用计数/2”次是不是就能清零了?如下:

UINT64 count = *((UINT64*)pFilter + 1);
for (size_t i = 0; i < count /2; i++)
{
ExReleaseRundownProtection(RunRefs);
}
FltUnregisterFilter(pFilter);

这样在执行FltUnregisterFilter之前,引用计数清零了:



但是继续执行后,依然阻塞了。

暂停下来看传入对象的引用计数:



可见这个计数变得很大,高位为ffff,可以猜测是不是它在为0后经过了多次递减最终导致这么大的数字,现在就需要看看FltUnregisterFilter中发生了什么事了,关键反编译代码如下:

__int64 __fastcall FltUnregisterFilter(__int64 a1)
{
_QWORD **v6;
_RBX = a1;
v6 = (_QWORD **)(_RBX + 208);
for (_QWORD * i = *v6; i != v6; i = (_QWORD *)*i )
{
if ( !(*(_DWORD *)(i - 14) & 1) && !(*(_DWORD *)(i - 5) & 4) )
{
v4 = FltObjectReference((__int64)(i - 14));
if ( v4 >= 0 )
{
ExReleaseResourceLite((PERESOURCE)(_RBX + 104));
KeLeaveCriticalRegion();
// Inside here the references reduced
FltpFreeInstance((__int64)(i - 14), 2 * (*(_DWORD *)(_RBX + 88) & 1) + 2, v8); // a.
goto LABEL_7;
}
}
}
......

FltObjectDereference(_RBX); // b.
FltpWaitForRundownProtectionReleaseInternal(_RBX + 8, 0); // c. Here the block resides
FltpTerminateActiveConnections(_RBX);

......
}

经过简单调试就可以发现,从刚进入FltUnregisterFilter到断点FltpWaitForRundownProtectionReleaseInternal,filter对象的引用计数发生较大的减小,a处FltpFreeInstance函数和b处FltObjectDereference执行了修改操作。

看看具体引用计数是怎么被修改的,先看
FltObjectDereference

__int64 __fastcall FltObjectDereference(__int64 a1)
{
return ExReleaseRundownProtection(a1 + 8); //located in ntoskrnl.exe
}

int __fastcall ExfReleaseRundownProtection(volatile signed __int64 *_RCX)
{
……
v3 = _InterlockedCompareExchange(_RCX, v1 - 2, v1);
……

return v3;
}

可见InterlockedCompareExchange会将计数减2。

再看看
FltpFreeInstance,由于里面调用栈较深,我这里通过访问断点来演示,在filter+8处下写断点很快就命中了,调用栈如下:

1: kd> ba w4 ffff9e07`a8346a68
1: kd> g
Breakpoint 1 hit
nt!ExfReleaseRundownProtection+0x1c:
fffff801`1be219dc 4c8bc0 mov r8,rax

# Child-SP RetAddr Call Site
00 ffff8506`709795c0 fffff801`1ba58d29 nt!ExfReleaseRundownProtection+0x1c
01 ffff8506`709795f0 fffff801`1ba8a6b1 FLTMGR!DoReleaseContext+0xf9
02 ffff8506`70979630 fffff801`1ba9de47 FLTMGR!FltpDeleteContextList+0xc1
03 ffff8506`70979660 fffff801`1ba8e627 FLTMGR!FltpCleanupStreamListCtrlForInstanceRemoval+0xf1b7
04 ffff8506`709796b0 fffff801`1baa85aa FLTMGR!FltpFreeInstance+0x1db
05 ffff8506`70979780 fffff801`451816a6 FLTMGR!FltUnregisterFilter+0x11a
06 ffff8506`70979840 fffff801`451813d6 MyDriver0!RemoveCsMinifilters+0x156
07 ffff8506`709798b0 fffff801`1c36dd1c MyDriver0!DriverEntry+0x3d6

这里也是每次减计数2:



从上面给的FltUnregisterFilter精简代码可以看到,a会被循环调用,再观察下这个for循环的结构很容易猜测到这是一个_LIST_ENTRY链接的链表,偏移位于208处。

现在大概知道
FltUnregisterFilter怎么处理filter对象的引用计数了:首先它会遍历对象偏移0xD0位置的链表,对每个节点使用FltpFreeInstance最终使用nt! ExfReleaseRundownProtection来释放这个节点和它对filter对象的引用计数,这一步完成后,此时要求filter的引用计数剩且仅剩2次,然后下面再调用FltObjectDereference减去最后两次引用,这样在进入FltpWaitForRundownProtectionReleaseInternal前引用计数就清零了。

所以该怎么来恰如其分地释放filter对象?现在的问题在于,不知道在前面a处循环后到达b时还剩多少次引用计数,如本节开头我演示的实验,剩的引用计数为6,那么可以在
FltUnregisterFilter前使用三次ExReleaseRundownProtection即可。

但是要是再换一个环境或者当这个环境发生变化时这个计数又会变为多少?那我能不能计算在节点循环时引用计数会减少多少,然后再加上后面FltpWaitForRundownProtectionReleaseInternal之前减少的2,两者之和与总引用计数值的差值就是我需要手动调用ExReleaseRundownProtection来减少的计数? 

或许可以,这意味着我需要逆向分析FltpFreeInstance内部实现,然后编写代码模拟其判断逻辑,才能精确计算出到底他使用了多少次引用计数,我想我没有兴趣做这件繁琐的事。

这里有另一个办法:在执行FltpWaitForRundownProtectionReleaseInternal前进行hook,调用ExReleaseRundownProtection减掉多余的引用计数值




实现


Hook怎么来编写?先看下fltmgr!FltUnregisterFilter中调用FltpWaitForRundownProtectionReleaseInternal处的代码长什么样:



这是在Windows 10 22H2的fltmgr.sys中的样子,可以拿上图 "1C0058627"到"1C005862D+1"部分作为特征定位位置, 然后从"1C005861F"写入hook代码,到"1C005862D"正好有14个字节的空间。由于x64下内联hook进行跳转至少需要两条指令,因此在没有特殊条件下,至少14个字节完成如下两条指令:

mov rax, shellcode_addr
jump rax

示例代码如下:

NTSTATUS HookFltUnregisterFilter()
{
DbgPrint("Enter HookFltUnregisterFilter\n");
NTSTATUS status = STATUS_UNSUCCESSFUL;
BYTE* phookaddr = 0;

BYTE* pfun = (BYTE*)FltUnregisterFilter;
pfun = pfun + 6 + *(LONG*)(pfun + 2);
pfun = *(BYTE**)pfun;

//searching for the codes behind "call FltObjectDereference"
//33 D2 48 8D 4B 08
BYTE searchBytes[6] = { 0x33,0xD2, 0x48, 0x8D, 0x4B, 0x08 };
for(int i = 0;i<0x400;i++)
{
if (memcmp(pfun + i, searchBytes, 6) == 0)
{
phookaddr = pfun + i-8;
break;
}
}

if (phookaddr == 0)
{
DbgPrint("HookFltUnregisterFilter cant find hoodaddr\n");
return status;
}


/*
mov rcx, rbx //48 8b cb
mov rax, 0x0000000000000000 //48 b8 00 00 00 00 00 00 00 00
call rax //ff d0

mov edx, 1 //BA 01 00 00 00
lea rcx, [rbx+8] //48 8b 4b 08

lock cmpxchg [rcx], rdx
cmp rax, 2 //F0 48 0F B1 11 48 83 F8 02
jnb short loc_1402F84CC // 73 offset

xor edx, edx //33 d2 must set rdx 0 due to the origin codes
*/

BYTE hookcode[] = {
0x48,0x8b,0xcb,
0x48,0xb8,0x00,0x0,0x00,0x00,0x00,0x00,0x00,0x00,
0xff,0xd0,
0xba,0x01,0x00,0x00,0x00,
0x48,0x8d,0x4b,0x08,
0xf0,0x48,0x0f,0xb1,0x11,0x48,0x83,0xf8,0x02,
0x73,0x00,
0x33,0xd2
};

//mov rax, next_addr ; jmp rax
BYTE jumpcode[12] = { 0x48,0xB8,0xFF,0xFF,0xFF,0xFF,0xAA,0xAA,0xAA,0xAA,0xFF,0xE0 };
int jmpcodelen = 12;
int jmpcode_uselen = 14;
int hookcodelen = sizeof(hookcode);

//1 backup
g_hookpoint1.g_HookBak = (BYTE*)ExAllocatePool(NonPagedPool, 0x100);
memcpy(g_hookpoint1.g_HookBak, phookaddr, jmpcode_uselen);
g_hookpoint1.g_HookBakSize = jmpcode_uselen;

//2 prepare hook buffer
g_hookpoint1.g_HookAddr = phookaddr;
g_hookpoint1.g_HookBuffer = (BYTE*)ExAllocatePool(NonPagedPool, 0x100);
///here i must calculate the abs addr of FltObjectDereference
LONG offset = *(LONG*)((UINT64)phookaddr + 3 + 1);
UINT64 nFltObjectDereferenceAddr = (UINT64)phookaddr + 3 + 5 + offset;
*(UINT64*)(hookcode + 5) = nFltObjectDereferenceAddr;
DbgPrint("nFltObjectDereferenceAddr:<0x%p>\n", nFltObjectDereferenceAddr);
///calculate the offset for loop
BYTE bytejmp = 0 - hookcodelen + 2;
hookcode[hookcodelen - 3] = bytejmp;
memcpy(g_hookpoint1.g_HookBuffer, hookcode, hookcodelen);
///jump back
*(UINT64*)(jumpcode + 2) = (UINT64)phookaddr + jmpcode_uselen;
memcpy(g_hookpoint1.g_HookBuffer+ hookcodelen, jumpcode, jmpcodelen);
g_hookpoint1.g_HookBufferSize = 0x100;

//3 now set the hook
*(UINT64*)(jumpcode + 2) = (UINT64)g_hookpoint1.g_HookBuffer;
KIRQL tmpirql = WriteProtectOff(); //turn off write protect
memcpy(phookaddr, jumpcode, jmpcodelen);
WriteProtectOn(tmpirql); //recover

DbgPrint("Leave HookFltUnregisterFilter\n");
return status;
}

搜索hook点和编写hook代码部分在部分平台会有差异,自行微调。当设置这个hook后,就只需要在简单对filter对象执行一次FltUnregisterFilter即可稳定地摘除它的回调。




有没有问题


这个方法在hook中减掉了多余的引用计数,但是这会不会有个问题,内核对象的引用都是有意义的,如果一个对象的引用不是由使用者在使用完毕后自行关闭,而是被其他模块在不告知的情况下关闭,那么使用者再次使用这个对象时就可能出现未知问题。所以如果注册MiniFilter的模块再次使用这个filter对象时是有可能出现BSOD的,因为我在摘除后一直没有遇到问题所以暂时还没研究这个问题。




这是不是最正确的姿势


这是一种劫持执行流的解决方案,而且如0x5点所述我还不确定绝不会出现问题,不过这里似乎还有另一个方法,就是直接编写代码模仿FltUnregisterFilter的行为,FltUnregisterFilter反编译后可以看到其实并不是很复杂,如果手动循环调用FltpFreeInstance后再在FltpWaitForRundownProtectionReleaseInternal之前根据引用计数决定释放次数,然后结合调试执行其余关键的操作。我想理论上是可行的,这种办法还能避开不同系统版本的适配性问题,但这也无法解决0x5点中提到的问题,所以或许也不是拆除MiniFilter的最正确姿势。




看雪ID:adc又死了

https://bbs.kanxue.com/user-home-969962.htm

*本文为看雪论坛优秀文章,由 adc又死了 原创,转载请注明来自看雪社区



# 往期推荐

1、ELF文件脱壳纪事

2、Glibc-2.35下对tls_dtor_list的利用详解

3、对旅行APP的检测以及参数计算分析【Simplesign篇】

4、2023强网杯warmup题解

5、Directory Opus 13.2 逆向分析

6、Pwn-oneday题目解析



球分享

球点赞

球在看



点击阅读原文查看更多

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存