同步问题在安全领域用的最多的地方无疑是在做Inline-HOOK时。
一直以来,包括现在,很多程序都在使用:
KeInitializeSpinLock
KeAcquireSpinLock
KeReleaseSpinLock
来进行同步处理,调用这个自旋锁在会提升IRQL到DPC级别来屏蔽线程切换,在单核下这是有效的,这点大家是有共识的。
但在多核下,DPC的提升只对当前处理器有效,所以仅IRQL已无法屏蔽另一处理器上的线程调度,也就是说在你的HOOK进行一半时很可能被另一个线程打断而出错。
比如,你在安装HOOK时,需要写一句代码:JMP XXXXXXX 当刚写入JMP还没来得及写入XXXXXXX时,另一个处理器中的另一个线程已经开始执行这一句代码,那结果可想而知。
那么
KeInitializeSpinLock
KeAcquireSpinLock
KeReleaseSpinLock
在这种多核情况下可以起到作用么?看看这个都做了哪些工作就了解了。
nt!KeInitializeSpinLock: ;仅仅是把锁结构清零
80541970 8b442404 mov eax,dword ptr [esp+4]
80541974 c70000000000 mov dword ptr [eax],0
8054197a c20400 ret 4
而KeAcquireSpinLock最终会调用hal!KfAcquireSpinLock,反汇编代码如下:
hal!KfAcquireSpinLock: ;先提升IRQL级别
806e7830 8b158000feff mov edx,dword ptr ds:[0FFFE0080h]
806e7836 c7058000feff41000000 mov dword ptr ds:[0FFFE0080h],41h
806e7840 c1ea04 shr edx,4
806e7843 0fb68298226f80 movzx eax,byte ptr hal!HalpVectorToIRQL (806f2298)[edx]
806e784a f00fba2900 lock bts dword ptr [ecx],0 ;ECX中是锁结构,以原子操作指令判断是不是零,如果是零则置位成1(即上锁)如果是1则执行Pause后再JMP回去循环测试。
806e784f 7203 jb hal!KfAcquireSpinLock+0x24 (806e7854)
806e7851 c3 ret
806e7854 f70101000000 test dword ptr [ecx],1
806e785a 74ee je hal!KfAcquireSpinLock+0x1a (806e784a)
806e785c f390 pause
806e785e ebf4 jmp hal!KfAcquireSpinLock+0x24 (806e7854)
由以上代码来看:
KeInitializeSpinLock
KeAcquireSpinLock
KeReleaseSpinLock
对其它处理器上的其它线程是无效的,除非那个线程用的是跟你同一个自旋锁,也就是KeInitializeSpinLock的是同一个地址。
结论有了,但方法呢?究竟怎么才能实现多核同步呢?
现在流行一种方法,那就是多CPU同时提升IRQL到DPC,以实现多核下的同步,这也有人在用了,自己的HOOK线程工作在一个CPU,让其它CPU在DPC级别空转,直到HOOK操作完成。
理论上这种方法是可行的,实际中也是可行的,但稳定性有待考验。
而我却一直有个疑问,Windows难道就没有提供现成的多核同步机制吗?难道Windows本身就不需要多核同步吗?显然不是的,Windows也需要同步。
翻了一些资料,逆了一些Windows代码后,找到了一种我认为是系统自用的多核同步方式,经初步试验是有一定效果的,至于是否能完全实现同步,还需进一步验证,欢迎大家测试。
在说之前,先看一段源码:
VOID
NTAPI
KiInitSpinLocks(IN PKPRCB Prcb,
IN CCHAR Number)
{
ULONG i;
/* Initialize Dispatcher Fields */
Prcb->QueueIndex = 1;
Prcb->ReadySummary = 0;
Prcb->DeferredReadyListHead.Next = NULL;
for (i = 0; i < MAXIMUM_PRIORITY; i++)
{
/* Initialize the ready list */
InitializeListHead(&Prcb->DispatcherReadyListHead[i]);
}
/* Initialize DPC Fields */
InitializeListHead(&Prcb->DpcData[DPC_NORMAL].DpcListHead);
KeInitializeSpinLock(&Prcb->DpcData[DPC_NORMAL].DpcLock);
Prcb->DpcData[DPC_NORMAL].DpcQueueDepth = 0;
Prcb->DpcData[DPC_NORMAL].DpcCount = 0;
Prcb->DpcRoutineActive = FALSE;
Prcb->MaximumDpcQueueDepth = KiMaximumDpcQueueDepth;
Prcb->MinimumDpcRate = KiMinimumDpcRate;
Prcb->AdjustDpcThreshold = KiAdjustDpcThreshold;
KeInitializeDpc(&Prcb->CallDpc, NULL, NULL);
KeSetTargetProcessorDpc(&Prcb->CallDpc, Number);
KeSetImportanceDpc(&Prcb->CallDpc, HighImportance);
/* Initialize the Wait List Head */
InitializeListHead(&Prcb->WaitListHead);
/* Initialize Queued Spinlocks */
Prcb->LockQueue[LockQueueDispatcherLock].Next = NULL;
Prcb->LockQueue[LockQueueDispatcherLock].Lock = &KiDispatcherLock;
Prcb->LockQueue[LockQueueExpansionLock].Next = NULL;
Prcb->LockQueue[LockQueueExpansionLock].Lock = NULL;
Prcb->LockQueue[LockQueuePfnLock].Next = NULL;
Prcb->LockQueue[LockQueuePfnLock].Lock = &MmPfnLock;
Prcb->LockQueue[LockQueueSystemSpaceLock].Next = NULL;
Prcb->LockQueue[LockQueueSystemSpaceLock].Lock = &MmSystemSpaceLock;
Prcb->LockQueue[LockQueueBcbLock].Next = NULL;
Prcb->LockQueue[LockQueueBcbLock].Lock = &CcBcbSpinLock;
Prcb->LockQueue[LockQueueMasterLock].Next = NULL;
Prcb->LockQueue[LockQueueMasterLock].Lock = &CcMasterSpinLock;
Prcb->LockQueue[LockQueueVacbLock].Next = NULL;
Prcb->LockQueue[LockQueueVacbLock].Lock = &CcVacbSpinLock;
Prcb->LockQueue[LockQueueWorkQueueLock].Next = NULL;
Prcb->LockQueue[LockQueueWorkQueueLock].Lock = &CcWorkQueueSpinLock;
Prcb->LockQueue[LockQueueNonPagedPoolLock].Next = NULL;
Prcb->LockQueue[LockQueueNonPagedPoolLock].Lock = &NonPagedPoolLock;
Prcb->LockQueue[LockQueueMmNonPagedPoolLock].Next = NULL;
Prcb->LockQueue[LockQueueMmNonPagedPoolLock].Lock = &MmNonPagedPoolLock;
Prcb->LockQueue[LockQueueIoCancelLock].Next = NULL;
Prcb->LockQueue[LockQueueIoCancelLock].Lock = &IopCancelSpinLock;
Prcb->LockQueue[LockQueueIoVpbLock].Next = NULL;
Prcb->LockQueue[LockQueueIoVpbLock].Lock = &IopVpbSpinLock;
Prcb->LockQueue[LockQueueIoDatabaseLock].Next = NULL;
Prcb->LockQueue[LockQueueIoDatabaseLock].Lock = &IopDatabaseLock;
Prcb->LockQueue[LockQueueIoCompletionLock].Next = NULL;
Prcb->LockQueue[LockQueueIoCompletionLock].Lock = &IopCompletionLock;
Prcb->LockQueue[LockQueueNtfsStructLock].Next = NULL;
Prcb->LockQueue[LockQueueNtfsStructLock].Lock = &NtfsStructLock;
Prcb->LockQueue[LockQueueAfdWorkQueueLock].Next = NULL;
Prcb->LockQueue[LockQueueAfdWorkQueueLock].Lock = &AfdWorkQueueSpinLock;
Prcb->LockQueue[LockQueueUnusedSpare16].Next = NULL;
Prcb->LockQueue[LockQueueUnusedSpare16].Lock = NULL;
/* Loop timer locks */
for (i = 0; i < LOCK_QUEUE_TIMER_TABLE_LOCKS; i++)
{
/* Initialize the lock and setup the Queued Spinlock */
KeInitializeSpinLock(&KiTimerTableLock[i]);
Prcb->LockQueue[LockQueueTimerTableLock + i].Next = NULL;
Prcb->LockQueue[LockQueueTimerTableLock + i].Lock =
&KiTimerTableLock[i];
}
/* Initialize the PRCB lock */
KeInitializeSpinLock(&Prcb->PrcbLock);
/* Check if this is the boot CPU */
if (!Number)
{
/* Initialize the lock themselves */
KeInitializeSpinLock(&KiDispatcherLock);
KeInitializeSpinLock(&KiReverseStallIpiLock);
KeInitializeSpinLock(&MmPfnLock);
KeInitializeSpinLock(&MmSystemSpaceLock);
KeInitializeSpinLock(&CcBcbSpinLock);
KeInitializeSpinLock(&CcMasterSpinLock);
KeInitializeSpinLock(&CcVacbSpinLock);
KeInitializeSpinLock(&CcWorkQueueSpinLock);
KeInitializeSpinLock(&IopCancelSpinLock);
KeInitializeSpinLock(&IopCompletionLock);
KeInitializeSpinLock(&IopDatabaseLock);
KeInitializeSpinLock(&IopVpbSpinLock);
KeInitializeSpinLock(&NonPagedPoolLock);
KeInitializeSpinLock(&MmNonPagedPoolLock);
KeInitializeSpinLock(&NtfsStructLock);
KeInitializeSpinLock(&AfdWorkQueueSpinLock);
}
}
这是系统启动阶段初始化锁时的一段代码,在这里系统初始化了一些自己需要的锁,而我们需要关心的是这个锁:
KiDispatcherLock
这个“分发器自旋锁”,当系统进行线程分发时会偿试获取这个锁。
而锁住这个也就可以屏蔽系统的线程分发,理论上好像也就不会有线程去打断你的执行了。(个人理解,不一定对,没有文档这么说过)
系统在进行线程级操作时,的确也用到了这个锁,我们看看暂停线程时用锁的情况:
nt!KeSuspendThread:
804fe4e8 8bff mov edi,edi
804fe4ea 55 push ebp
804fe4eb 8bec mov ebp,esp
804fe4ed 83ec0c sub esp,0Ch
804fe4f0 53 push ebx
804fe4f1 56 push esi
804fe4f2 8b7508 mov esi,dword ptr [ebp+8]
804fe4f5 57 push edi
804fe4f6 8d8ee8000000 lea ecx,[esi+0E8h]
804fe4fc 8d55f4 lea edx,[ebp-0Ch]
804fe4ff ff152c914d80 call dword ptr [nt!_imp_KeAcquireInStackQueuedSpinLockRaiseToSynch (804d912c)]
804fe505 64a120000000 mov eax,dword ptr fs:[00000020h]
804fe50b 8d8818040000 lea ecx,[eax+418h]
804fe511 e86a350400 call nt!KeAcquireQueuedSpinLockAtDpcLevel (80541a80)
804fe516 0fbebeb9010000 movsx edi,byte ptr [esi+1B9h]
804fe51d 83ff7f cmp edi,7Fh
804fe520 8b1d14914d80 mov ebx,dword ptr [nt!_imp_KeReleaseInStackQueuedSpinLock (804d9114)]
804fe526 7520 jne nt!KeSuspendThread+0x60 (804fe548)
804fe528 64a120000000 mov eax,dword ptr fs:[00000020h]
804fe52e 8d8818040000 lea ecx,[eax+418h]
804fe534 e873350400 call nt!KeReleaseQueuedSpinLockFromDpcLevel (80541aac)
暂停线程时,系统使用了这个锁,先调用nt!KeAcquireQueuedSpinLockAtDpcLevel上锁,后调用nt!KeReleaseQueuedSpinLockFromDpcLevel解锁。
看代码:
nt!KeAcquireQueuedSpinLockAtDpcLevel:
80541a80 8b5104 mov edx,dword ptr [ecx+4]
1: kd> r
eax=f9c2c120 ebx=8187fe18 ecx=f9c2c538 edx=00000000 esi=8187fda8 edi=00000000
eip=80541a80 esp=f795c988 ebp=f795c9bc iopl=0 nv up ei ng nz ac po cy
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000293
nt!KeAcquireQueuedSpinLockAtDpcLevel:
80541a80 8b5104 mov edx,dword ptr [ecx+4] ds:0023:f9c2c53c={nt!KiDispatcherLock (80554880)}
注意:ECX+4里面就是KiDispatcherLock线程分发自旋锁了。
这个ECX是哪里来的呢?找不到KiDispatcherLock的指针,说什么都没用,因为我们的理念是与系统共用一个锁来禁止在我们操作期间产生线程切换,所以锁的指针是必须的。
其实获取也很简单:
804fe528 64a120000000 mov eax,dword ptr fs:[00000020h]
804fe52e 8d8818040000 lea ecx,[eax+418h]
这里就是了!
但需要说明的是,这个锁的使用,并非简单的置位1或0,这里我模拟Windows写了两段上锁解锁代码,供大家使用,简单的模拟实现,没做什么优化,大家凑合着用吧:
;=====================================================
;多核同步锁
;=====================================================
_LockCPU proc
assume fs:nothing
mov eax,dword ptr fs:[00000020h]
add eax,420h
mov ecx,eax
mov edx,dword ptr [ecx+4]
mov eax,ecx
xchg eax,dword ptr [edx]
cmp eax,0
jne StartTest
or edx,2
mov dword ptr [ecx+4],edx
ExitLock:
ret
StartTest:
or edx,1
mov dword ptr [ecx+4],edx
mov dword ptr [eax],ecx
LoopTest:
test dword ptr [ecx+4],1
je ExitLock
db 0f3h,90h
jmp LoopTest
_LockCPU endp
;;------------------------------
_UnLockCPU proc
assume fs:nothing
mov eax,dword ptr fs:[00000020h]
add eax,420h
mov ecx,eax
mov edx,dword ptr [ecx]
mov ecx,dword ptr [ecx+4]
test edx,edx
btr ecx,1
mov dword ptr [eax+4],ecx
jne UnExit
xor edx,edx
push eax
lock cmpxchg dword ptr [ecx],edx
pop eax
jne UnLoopTest
ret
UnExit:
xor dword ptr [edx+4],3
mov dword ptr [eax],0
ret
UnLoopTest:
mov edx,dword ptr [eax]
test edx,edx
jne UnExit
db 0f3h,90h
jmp UnLoopTest
_UnLockCPU endp
我做了一个测试,InlineHOOK了调用相对频繁的KeInsertQueueApc,将HOOK与UnHOOK代码放到了一个定时器中,每秒种HOOK与UnHOOK一次,仅仅用上面两段代码加锁,没加任何其它措施,比如提升IRQL等,在多核处理器中跑了很长时间后,没发现问题。
当然了,这并不能说明就没有问题,谁用谁测试吧,有什么问题欢迎反馈。
http://musehero.blog.tianya.cn。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课