![]() |
Platforms, Frameworks & Libraries »
Vista Security »
General
Advanced
License: The MIT License
Bypassing PatchGuard 3By Christoph HusseThis article shows how to bypass PatchGuard 3 on the latest windows versions. |
C++, C++/CLI, C, Windows (WinXP, Win2003, Vista), Win32, Win64, Architect, Dev
|
||||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
For better maintainence, you will find updates at http://www.codeplex.com/easyhook. This article belongs to a project that also supports kernel mode hooking.
Please note that this article is designed to be read parallel with the source code. Also I highly recommend to read the first uninformed article (link follows), otherwise you won't understand much of the work covered here. This is no tutorial and beginners should not read it. There are so much things to know about PatchGuard that it is impossible to cover them all in one single article. Also I don't want to repeat other guys work!
You will find a PDF version of this documentation here.
• PatchGuard 1: http://www.uninformed.org/?v=3&a=3&t=pdf (recommended)
• PatchGuard 2: http://www.uninformed.org/?v=6&a=1&t=pdf
• PatchGuard 3: http://www.uninformed.org/?v=8&a=5&t=pdf
Bcdedit /debug ON
Bcdedit /dbgsettings SERIAL DEBUGPORT:1 BAUDRATE:115200 /start AUTOENABLE /noumex • Modifying system service tables, for example, by hookingPG uses a system check routine that validates all the above structures and data. Basically PatchGuard is about how to protect this system check routine from being cracked. Cracking means to violate at least one of the above statements without knowledge of PatchGuard. This article describes how to violate ALL of them by completely disabling PatchGuard!KeServiceDescriptorTable
• Modifying the interrupt descriptor table (IDT)
• Modifying the global descriptor table (GDT)
• Using kernel stacks that are not allocated by the kernel
• Patching any part of the kernel (detected only on AMD64-based systems)
• Find out how the system check routine is invokedTo accomplish this task, PatchGuard uses various tricks and uncommon processor behavior to obfuscate as much as possible. The system check routine itself will be executed every few minutes.
• Interfere with this invocation (by preventing/redirecting it)
• Disassemble the system check routine or find out its entry point address

CmpEnableLazyFlushDpcRoutine
CmpLazyFlushDpcRoutine
ExpTimeRefreshDpcRoutine
ExpTimeZoneDpcRoutine
ExpCenturyDpcRoutine
ExpTimerDpcRoutine
IopTimerDispatch
IopIrpStackProfilerTimer
KiScanReadyQueues
PopThermalZoneDpcDeferredContext. One may say that this is
buggy, because dereferencing an invalid pointer at dispatch level
throws a non-catchable trap resulting in a bug check. But PatchGuard is
using a so called non-canonical pointer. Such a pointer does not follow
the x64 processor specification that requires a pointer to have to
upper 16 bits either set to one or zero (that is 0x0000 or 0xFFFF). A
non-canonical pointer starts with 0x6238, 0xF10A and so on.
Dereferencing them will instead cause a general protection fault, which
is catchable and in case of PatchGuard, executes the system check
routine. DeferredContext and probably also on the DPC itself. So we just don’t know!
The driver filters all DPCs with PatchGuard specific parameters as DeferredContext.
Further it catches the non-SEH code path of PatchGuard which
previously has been overwritten with unhandled breakpoints, finding
their way back to the exception handler in my driver! Of course there
are still some DPCs left and this is why we need to add further
blocking.By overwriting the dynamic stubs and parts of the PatchGuard DPCs with breakpoints, execution continues in the DPC interceptor’s exception handler instead of the system check routine.
“ExpWorkerThread” to “Dynamic stub” and “Unknown”
Another hook is applied to the worker thread queue. This is filtering all dynamic worker routines and wrapping all others in a try-except statement. Some special kinds of system check invocations which seem to be incompatible with the next blocking mechanism are always originated from the worker queue. But as I said, we filter them, so their incompatibility won’t cause any harm...
“System check” to “KeBugCheckEx”
Only PatchGuard methods raised overExQueueWorkItemget here. It is a burden to reproduce this case because you have to restart 10 – 30 times…
This block just suspends allCRITICAL_STRUCTURE_CORRUPTIONs. So they never cause the system to BSOD.
The problem was that the driver caught the bug check attempt but failed to suspend the calling worker thread. Then I also hookedExpWorkerThreadand since that step, only suspend able calls toKeBugCheckExare made.
nt!KeBugCheckEx
nt! ?? ::FNODOBFM::`string'+0x12767
nt!KiExceptionDispatch+0xae
nt!KiBreakpointTrap+0xb7
nt!ExpTimeRefreshDpcRoutine+0x1e6
nt!_C_specific_handler+0x8c
nt!RtlpExecuteHandlerForException+0xd
nt!RtlDispatchException+0x228
nt!KiDispatchException+0xc2
nt!KiExceptionDispatch+0xae
nt!KiGeneralProtectionFault+0xcd
nt!ExpTimeRefreshDpcRoutine+0xf1
nt!_C_specific_handler+0x140
nt!RtlpExecuteHandlerForUnwind+0xd
nt!RtlUnwindEx+0x233
nt!_C_specific_handler+0xcc
nt!RtlpExecuteHandlerForException+0xd
nt!RtlDispatchException+0x228
nt!KiDispatchException+0xc2
nt!KiExceptionDispatch+0xae
nt!KiGeneralProtectionFault+0xcd
nt!KiCustomRecurseRoutine0+0xd
nt!KiCustomRecurseRoutine9+0xd
nt!KiCustomRecurseRoutine8+0xd
nt!KiCustomRecurseRoutine7+0xd
nt!KiCustomAccessRoutine7+0x22
nt!ExpTimeRefreshDpcRoutine+0x54 <== PatchGuard DPC
nt!KiRetireDpcList+0x155
nt!KiIdleLoop+0x5f
nt!KiSystemStartup+0x1d4
nt!KeBugCheckEx
nt! ?? ::FNODOBFM::`string'+0x12767
nt!KiExceptionDispatch+0xae
nt!KiBreakpointTrap+0xb7
0xfffffa80010b3cc9 <== PatchGuard’s dynamic method
0xfffffa80010bbc00 <== PatchGuard’s optional intro
PG3Disable!VistaAll_DpcInterceptor+0x34
nt!KiRetireDpcList+0x117
nt!KiIdleLoop+0x62
…
KiBreakPointTrap should
be self explaining. I patched the code paths with unhandled
breakpoints. So instead of invoking the system check routine, KeBugCheckEx will create a memory dump and we can work with it in a post-mortem session using WinDbg.DeferredContext values
and cancel related timers. Since PatchGuard 3, this alone is not
sufficient enough. But it is still a good starting point to filter out
most of PatchGuard’s DPCs before they actually raise the system check
routine.BOOLEAN CheckSubValue(ULONGLONG InValue)
{
ULONG i;
ULONG Result;
UCHAR* Chars = (UCHAR*)&InValue;
// random values will have a result around 120...
Result = 0;
for(i = 0; i < 8; i++)
{
Result += ((Chars[i] & 0xF0) >> 4) + (Chars[i] & 0x0F);
}
// the maximum value is 240, so this should be safe...
if(Result < 70)
return TRUE;
return FALSE;
}
BOOLEAN PgIsPatchGuardContext(void* Ptr)
{
ULONGLONG Value = (ULONGLONG)Ptr;
UCHAR* Chars = (UCHAR*)&Value;
LONG i;
// those are sufficient proves for canonical pointers...
if((Value & 0xFFFF000000000000) == 0xFFFF000000000000)
return FALSE;
if((Value & 0xFFFF000000000000) == 0)
return FALSE;
// sieve out other common values...
if(CheckSubValue(Value) || CheckSubValue(~Value))
return FALSE;
if(Ptr == NULL)
return FALSE;
//This must be the last check and filters latin-char UTF16 strings...
for(i = 7; i >= 0; i -= 2)
{
if(Chars[i] != 0)
return TRUE;
}
// this should only return true if the pointer is a unicode string!!!
return FALSE;
} DeferredContext value such as zero, some
table indices like “1, 2, 3, 4, 5, …”, Unicode sequences, etc. All in
all the above code detects pseudo random values.KeBugCheckEx and suspend all threads raising a CRITICAL_STRUCTURE_CORRUPTION. Please note that the latter is a very rare case and won’t impact system performance. But this is still not enough. Some of the KeBugCheckEx calls are not suspend able. I solved this by also hooking ExpWorkerThread, filtering out dynamic stubs and wrapping all others in a try-except statement.KiRetireDpcList and KiTimerExpiration are the only points in the kernel which are responsible for dispatching queued DPCs. If you look at the disassembly for KiTimerExpiration and KiRetireDpcList, which is quite too long for showing, you will find the following four indirect call code blocks:nt!KiTimerExpiration+0x888:
488b5308 mov rdx, qword ptr [rbx+8]
488b4bf8 mov rcx, qword ptr [rbx-8]
4d8bcc mov r9, r12
4c8bc7 mov r8, rdi
ff13 call qword ptr [rbx]
4084f6 test sil, sil
742c je nt!KiTimerExpiration+0x8d3
nt!KiTimerExpiration+0x679:
488b5308 mov rdx,qword ptr [rbx+8]
488b4bf8 mov rcx,qword ptr [rbx-8]
4d8bcc mov r9,r12
4d8bc5 mov r8,r13
ff13 call qword ptr [rbx]
4084ed test bpl, bpl
742c je nt!KiTimerExpiration+0x7e5
nt!KiTimerExpiration+0x799:
488b5308 mov rdx,qword ptr [rbx+8]
488b4bf8 mov rcx,qword ptr [rbx-8]
4d8bcc mov r9,r12
4d8bc5 mov r8,r13
ff13 call qword ptr [rbx]
4084ed test bpl, bpl
742c je nt!KiTimerExpiration+0x7e5
nt!KiRetireDpcList+0x145:
4d8bcc mov r9, r12
4c8bc5 mov r8, rbp
488bd6 mov rdx, rsi
488bcf mov rcx, rdi
ff542470 call qword ptr [rsp+70h]
4584ff test r15b, r15b
742b je nt!KiRetireDpcList+0x185
Nt!KiTimerListExpire+0x31a:
458b4e04 mov r9d,dword ptr [r14+4]
458b06 mov r8d,dword ptr [r14]
4189ac24a0370000 mov dword ptr [r12+37A0h], ebp
488b5308 mov rdx, qword ptr [rbx+8]
488b4bf8 mov rcx, qword ptr [rbx-8]
ff13 call qword ptr [rbx]
4084ff test dil, dil
0f856c8ffdff jne nt! ?? ::FNODOBFM::`string'+0x39742
nt!KiRetireDpcList+0x107:
4d8bce mov r9,r14
4d8bc5 mov r8,r13
498bd4 mov rdx,r12
488bcb mov rcx, rbx
ff542470 call qword ptr [rsp+70h]
4584ff test r15b,r15b
0f856e7ffdff jne nt! ?? ::FNODOBFM::`string'+0x39888 KiTimerExpiration and KiRetireDpcList if
we nowhere can insert a JMP instruction without messing up the whole
code logic? We will overwrite the last MOV instruction too and emulate
it in a proper jumper table before we continue execution in our
interception method:Nt!KiTimerListExpire+0x31a:
458b4e04 mov r9d,dword ptr [r14+4]
458b06 mov r8d,dword ptr [r14]
4189ac24a0370000 mov dword ptr [r12+37A0h], ebp
488b5308 mov rdx, qword ptr [rbx+8]
90 nop
E8XXXXXXXX call TIMER_FIX
4084ff test dil, dil
0f856c8ffdff jne nt! ?? ::FNODOBFM::`string'+0x39742
nt!KiRetireDpcList+0x107:
4d8bce mov r9,r14
4d8bc5 mov r8,r13
498bd4 mov rdx,r12
90 nop
90 nop
E8XXXXXXXX call DPC_FIX
4584ff test r15b,r15b
0f856e7ffdff jne nt! ?? ::FNODOBFM::`string'+0x39888
HIGH_LEVEL
and putting an absolute call instruction in the two code blocks above.
This is the only way one could replace more than eight bytes as an
atomic operation. The other way is to hijack one of those ten very same
KiCustomAccessRoutines, PatchGuard is using to obfuscate
the call stack. So what we do here is placing a jumper as first
instruction of such a routine and redirecting it to another one. Now we
can work with the other bytes, and there are quite enough, to build our
jump table. Of course I know that even the atomic MOV instruction is
not 100% safe. But the chance that a thread will be between the MOV and
CALL, which our patch overwrites, and all this within such a short
timeframe, is quite small. The NOPs have to be placed in front of the
CALL because if the changes are rolled back when the driver is
unloading, any code that currently is within the CALL instruction (this
chance is in contrast pretty high) will still return to valid code, the
TEST instruction, because the return address stays the same.nt!KiCustomAccessRoutine4:
E9XXXXXXXX jmp KiCustomAccessRoutine0
TIMER_FIX:
488b4bf8 mov rcx,qword ptr [rbx-8]
eb03 jmp INTERCEPTOR
DPC_FIX:
488bcb mov rcx,rbx
INTERCEPTOR:
48b8XXXXXXXXXXXXXXXX mov rax, VistaAll_DpcInterceptor
ffe0 jmp rax
void VistaAll_DpcInterceptor(
PKDPC InDpc,
PVOID InDeferredContext,
PVOID InSystemArgument1,
PVOID InSystemArgument2)
{
ULONGLONG Routine = (ULONGLONG)InDpc->DeferredRoutine;
__try
{
if((Routine >= 0xFFFFFA8000000000) &&
(Routine <= 0xFFFFFAA000000000))
{
}
else if(KeContainsSymbol((void*)Routine))
{
if(!PgIsPatchGuardContext(InDeferredContext))
InDpc->DeferredRoutine(
InDpc,
InDeferredContext,
InSystemArgument1,
InSystemArgument2);
}
else
InDpc->DeferredRoutine(
InDpc,
InDeferredContext,
InSystemArgument1,
InSystemArgument2);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
}
}
DeferredContext. Also I don’t know any sane driver that needs dynamic DPCs. After this check we can just skip all obvious PatchGuard DPCs and DeferredRoutines residing in dynamic memory.DeferredContexts. This way it would pass our filter and get to KeBugCheckEx.
The problem now is that PatchGuard may decide to directly invoke it
(not using a worker thread), thus our hook would run at DPC level and
cause a bug check (because a wait attempt at DPC level is not
permitted). I solved this by overwriting some fingerprints with
breakpoints, which actually converts such non-SEH code paths into
SEH ones, because unhandled breakpoints throw a catchable
exception! The following is the prototype of all memory resident
dynamic methods:nt!KiTimerDispatch:
6690 xchg ax,ax
9c pushfq
4883ec20 sub rsp,20h
8b442420 mov eax,dword ptr [rsp+20h]
4533c9 xor r9d,r9d
4533c0 xor r8d,r8d
4889442430 mov qword ptr [rsp+30h],rax
488b4140 mov rax,qword ptr [rcx+40h]
48b90000000000f8ffff mov mov rcx,0FFFFF80000000000h
4833c2 xor rax,rdx
480bc1 or rax,rcx
48b9f048311148315108 mov rcx,8513148113148F0h <<<<<<
488b10 mov rdx,qword ptr [rax]
c700f0483111 mov dword ptr [rax],113148F0h
4833d1 xor rdx,rcx
488bc8 mov rcx,rax
ffd0 call rax
4883c420 add rsp,20h
59 pop rcx
c3 ret
DeferredContext values and that’s why we can filter it entirely.
Patching the DPC code alone is not enough. PatchGuard 3 also uses worker items to accomplish the same task. Our worker thread interceptor looks something similar, but is not the same:
VOID VistaAll_ExpWorkerThreadInterceptor(
PWORKER_THREAD_ROUTINE InRoutine,
VOID* InContext,
VOID* InRSP)
{
ULONGLONG Val = (ULONGLONG)InRoutine;
if((Val >= 0xfffffa8000000000) && (Val <= 0xfffffaa000000000))
return;
__try
{
InRoutine(InContext);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
}
}
What we do here is to filter out all routines residing in dynamic memory, just like we did it in the DPC hook. Additionally we wrap all other routines in a try-except statement. The context of a PatchGuard worker thread is canonical, so we have no chance to filter it out explicitly.
What I experienced so far is that PatchGuard is using dynamic methods also for the worker queue. After blocking them like above, there were still some bug checks left. They were originated from ExpWorkerThread, but never gone through our handler! This is something strange because the disassembly shows that we only have on single point where the worker items are actually called. I only can imagine that PatchGuard is again using exceptions to redirect execution to a special exception handler, just like it did for the DPC handler. And this is why I also wrap the calls in a try-except statement!
The procedure of patching the worker queue is very similar to the DPC queue. We have the following bytes to patch:
nt!ExpWorkerThread+0x11a:
5c pop rsp
2470 and al,70h
4c8b6318 mov r12,qword ptr [rbx+18h]
488b7b10 mov rdi,qword ptr [rbx+10h]
>498bcc mov rcx,r12
>ffd7 call rdi
4c8d9ee0020000 lea r11,[rsi+2E0h]
4d391b cmp qword ptr [r11],r11
And redirect them to the very same jump table. The difference is that we don’t restore the registers in the jump table but in a prepared jump target in our driver:
VistaSp0_ExpWorkerThread_Fix PROC
mov rcx, rdi
mov rdx, r12
mov r8, rsp
jmp VistaAll_ExpWorkerThreadInterceptor
VistaSp0_ExpWorkerThread_Fix ENDP
This will call the above interceptor and we are done!
KeBugCheckEx to
solve this issue. Well it is not that easy, because PatchGuard actually
overwrites the method with a fresh copy, before invoking it. So we need
to hook a subroutine. If we look at the first instructionsnt!KeBugCheckEx:
48894c2408 mov qword ptr [rsp+8],rcx
4889542410 mov qword ptr [rsp+10h],rdx
4c89442418 mov qword ptr [rsp+18h],r8
4c894c2420 mov qword ptr [rsp+20h],r9
9c pushfq
4883ec30 sub rsp,30h
fa cli
65488b0c2520000000 mov rcx,qword ptr gs:[20h]
4881c120010000 add rcx,120h
e8c1050000 call nt!RtlCaptureContext
, we see that RtlCaptureContext seems to be perfect for our task. This is because KeBugCheckEx is calling it very early and thus the system is still in a sane state. In order to hook RtlCaptureContext, we need to place a jumper as usual:Nt!RtlCaptureContext:
50 push rax
48b9Xxx mov rax, PG3Disable! RtlCaptureContext_Hook
ffe0 jmp rax RtlCaptureContext:RtlCaptureContext_Hook PROC
; call high level handler without messing up the context structure...
pushfq
push rcx
push rdx
push r8
push r9
push r10
push r11
mov rcx, qword ptr[rsp + 136]
mov rdx, qword ptr[rsp + 8 * 8]
sub rsp, 32
call KeBugCheck_Hook
mov qword ptr [rsp], rax
add rsp, 32
pop r11
pop r10
pop r9
pop r8
pop rdx
pop rcx
popfq
pop rax
; recover destroyed bytes of RtlCaptureContext
pushfq
mov word ptr [rcx+38h],cs
mov word ptr [rcx+3Ah],ds
mov word ptr [rcx+3Ch],es
mov word ptr [rcx+42h],ss
; jump behind destroyed bytes... (return value of KeBugCheck_Hook)
jmp qword ptr[rsp - 32 - 8 * 7 + 8]
RtlCaptureContext_Hook ENDP
KeBugCheck_Hook method with the bug check code as first parameter and the caller of RtlCaptureContext as second one. Secondly, it recovers the overwritten instructions of RtlCaptureContext and finally continues execution behind the jumper.KeBugCheck_HookULONGLONG KeBugCheck_Hook(ULONGLONG InBugCode, ULONGLONG InCaller)
{
FAST_MUTEX WaitAlways;
if((InCaller >= KeBugCheckEx_Sym) &&
(InCaller <= KeBugCheckEx_Sym + 100))
{
if(InBugCode == CRITICAL_STRUCTURE_CORRUPTION)
{
EnableInterrupts();
ExInitializeFastMutex(&WaitAlways);
ExAcquireFastMutex(&WaitAlways);
ExAcquireFastMutex(&WaitAlways);
}
}
return RtlCaptureContext_Sym + 14;
} , you may observe that it just checks whether the caller is KeBugCheckEx and the bug check code is CRITICAL_STRUCTURE_CORRUPTION.
If that is the case, it reenables interrupts, and block the thread
forever. We are only able to do this, because we eliminated the chance,
that this bug check was raised though the DPC dispatcher and it
definitely would be, if we skip our DPC filter! If the caller was any
other symbol, we just execute RtlCaptureContext(). I
assume that every thread switch will call this method, so this is the
next critical execution path of Windows which we are hooking…ExQueueWorkItem and if we block all other code paths, there is a point when no PatchGuard contexts are left). “C:\WinDDK” for example. You have to register and sign in to Microsoft Passport I believe but after all it is still free.“WDKROOT” with the value of your installation path.“C:\PGDisable”; so that the project solution is located in “C:\PGDisable\PGDisable.sln”. > Bcdedit –set TESTSIGNING ON> cd c:\winddk\bin\SelfSign
> MakeCert -r -pe -ss PGDisableCertStore -n "CN=PGDisableCert" “c:\PGDisable\PGDisableCert.cer” “Install certificate”
– “Next” – “place all certificates in the following store” – “Trusted
Root Certification Authorities” – “OK” – “Next” – “Finish” and do the same procedure to add it to the “Trusted Publishers” store.“PGDisableCert” is in the “PGDisableCertStore”. This will automatically sign the drivers after each build and you have to care about nothing.“sc delete PG3Disable.sys”
into a root shell. If the driver is currently running, this command
will still succeed, but the driver actually stays installed. To prevent
this you also have to stop it by using “sc stop PG3Disable.sys”.
But keep in mind that due to the missing rollback of patches you won’t
be able to load the driver again in the current system session (if you
disabled PatchGuard)! Writes a list of fingerprints to “C:\patchguard.log”. If you execute this command after disabling PatchGuard, the file should only contain eight custom access routines.This silently disables PatchGuard on success.
GetLastError:Operation has been completed successfully.
This is returned either in case your system is not supported or PatchGuard was already patched by the driver. For stability reasons, the driver may decide to refuse patching sometimes; ten just try it 5-10 minutes later.
ERROR_SUCCESS as result. Secondly, you may load/unload the driver as often as you like. Furthermore it exports an additional command:Installs a test hook for KeCancelTimer. Please note that your system will BSOD if PatchGuard is not already disabled! Please note that PG2Disable won’t work on Windows Vista SP1.“PATCHGUARD_INVOKATION”,
for example, and use it all over the windows source code, but only for
unexported APIs of higher order (not called inside any public API).
Then a pre-build event could automatically replace such a macro with
randomly generated invocation stubs, or even do nothing for most
occurrences. One could base the randomness on a constant value, so that
major updates will use different constant values resulting in totally
different PatchGuard invocation stubs, while minor updates won’t cause
any changes to PatchGuard related sections in the binaries. KiRetireDpcList, for example).KeSetTimerEx, KeCancelTimer, etc. KeCancelTimer seems to be the best starting point, because it is so small:// push rbx
// sub rsp,20h
KIRQL OldIrql = 0;
BOOLEAN Existed = FALSE;
PKSPIN_LOCK_QUEUE LockArray = NULL;
ULONG LockIndex = 0;
KTIMER_TABLE_ENTRY* TimerEntry = NULL;
// mov r9,rcx
// call nt!KiAcquireDispatcherLockRaiseToSynch
OldIrql = KiAcquireDispatcherLockRaiseToSynch();
// mov bl,byte ptr [r9+3]
// test bl,bl
// mov r10b,al
Existed = InTimer->Header.Inserted;
// je nt!KeCancelTimer+0x76
if(Existed)
{
// nt!KeCancelTimer+0x19:
// mov rcx,qword ptr gs:[28h]
LockArray = KeGetPcr()->LockArray;
// movzx r8d, byte ptr [r9+2]
// mov eax,r8d
// shr eax,4
// and eax,0Fh
// add eax,11h
LockIndex = ((InTimer->Header.Hand / sizeof(KSPIN_LOCK_QUEUE)) & 0x0F) + LockQueueTimerTableLock;
// shl rax,4
// add rcx,rax
// call nt!KeAcquireQueuedSpinLockAtDpcLevel
KeAcquireQueuedSpinLockAtDpcLevel(&LockArray[LockIndex]);
// mov byte ptr [r9+3],0
InTimer->Header.Inserted = FALSE;
// mov rax,qword ptr [r9+28h]
// mov rdx,qword ptr [r9+20h]
// cmp rdx,rax
// mov qword ptr [rax],rdx
// mov qword ptr [rdx+8],rax
//jne nt!KeCancelTimer+0x71
if(RemoveEntryList(&InTimer->TimerListEntry))
{
//nt!KeCancelTimer+0x58:
// lea rdx,[r8+r8*2]
// lea rax,[nt!KiTimerTableListHead]
// lea r8,[rax+rdx*8]
TimerEntry = &KiTimerTableListHead[InTimer->Header.Hand];
// cmp r8,qword ptr [r8]
//jne nt!KeCancelTimer+0x71
if(TimerEntry == (KTIMER_TABLE_ENTRY*)TimerEntry->Entry.Flink)
{
//nt!KeCancelTimer+0x6c:
// or dword ptr [r8+14h],0FFFFFFFFh
TimerEntry->Time.HighPart = 0xFFFFFFFF;
}
}
//nt!KeCancelTimer+0x71:
//call nt!KeReleaseQueuedSpinLockFromDpcLevel
KeReleaseQueuedSpinLockFromDpcLevel(&LockArray[LockIndex]);
}
//nt!KeCancelTimer+0x76:
//call nt!KiReleaseDispatcherLockFromSynchLevel
KiReleaseDispatcherLockFromSynchLevel();
// mov cl,r10b
// call nt!KiExitDispatcher
KiExitDispatcher(OldIrql);
// mov al,bl
return Existed;
// add rsp,20h
// pop rbx
// ret Probably locks the timer and/or DPC database. Raises the IRQL to DISPATCH_LEVEL and returns the previous state. void KeAcquireQueuedSpinLockAtDpcLevel(PKSPIN_LOCK_QUEUE)Is similar to the publicly availableKeAcquireInStackQueuedSpinLock. You may use this method to acquire any of the locks inKPCR::LockArray. Please note that this method shall be called atDISPATCH_LEVELonly. The following constants may be helpful to index the right one:
LockQueueDispatcherLock 0
LockQueueExpansionLock 1
LockQueuePfnLock 2
LockQueueSystemSpaceLock 3
LockQueueVacbLock 4
LockQueueMasterLock 5
LockQueueNonPagedPoolLock 6
LockQueueIoCancelLock 7
LockQueueWorkQueueLock 8
LockQueueIoVpbLock 9
LockQueueIoDatabaseLock 10
LockQueueIoCompletionLock 11
LockQueueNtfsStructLock 12
LockQueueAfdWorkQueueLock 13
LockQueueBcbLock 14
LockQueueMmNonPagedPoolLock 15
LockQueueTimerTableLock 17 Is similar to the publicly availableKeReleaseInStackQueuedSpinLock. You have to call it to release any of the previously acquired locks inKPCR::LockArray. Please note that this method shall be called atDISPATCH_LEVELonly.
Probably releases the timer/DPC lock at DISPATCH_LEVEL and does NOT change the IRQL.Shall be called atDISPATCH_LEVELand will lower the IRQL toInOldIrql. I think this method was not combined with the previous method, to allow performing more operations atDISPATCH_LEVEL(without holding the lock) before actually lowering the IRQL. I recently read in some Microsoft paper, that it also may schedule a new thread, now after the DPC level operation is done…
// a little helper…
PKSPIN_LOCK_QUEUE KeTimerIndexToLockQueue(UCHAR InTimerIndex)
{
return &(KeGetPcr()->LockArray[((InTimerIndex / sizeof(KSPIN_LOCK_QUEUE)) & 0x0F) + LockQueueTimerTableLock]);
}
// this is where the enumeration starts
OldIrql = KiAcquireDispatcherLockRaiseToSynch();
for(Index = 0; Index < TIMER_TABLE_SIZE; Index++)
{
LockQueue = KeTimerIndexToLockQueue((UCHAR)(Index & 0xFF));
KeAcquireQueuedSpinLockAtDpcLevel(LockQueue);
// now we can work with the timer list...
TimerListHead = &KiTimerTableListHead[Index];
TimerList = TimerListHead->Entry.Flink;
while(TimerList != (PLIST_ENTRY)TimerListHead)
{
Timer = CONTAINING_RECORD(TimerList, KTIMER, TimerListEntry); TimerList = TimerList->Flink;
// TODO: work with the timer…
}
KeReleaseQueuedSpinLockFromDpcLevel(LockQueue);
}
KiReleaseDispatcherLockFromSynchLevel();
KiExitDispatcher(OldIrql); // TODO: work with the timer…
Timer->Header.Inserted = FALSE;
if(RemoveEntryList(&Timer->TimerListEntry))
TimerListHead->Time.HighPart = 0xFFFFFFFF;
KiWaitNever and KiWaitAlways. With those symbols you may decrypt the KDPC pointer with the following code:ULONGLONG RDX = (ULONGLONG)Timer->Dpc;
RDX ^= InKiWaitNever;
RDX = _rotl64(RDX, *KiWaitNever & 0xFF);
RDX ^= (ULONGLONG)Timer;
RDX = _byteswap_uint64(RDX);
RDX ^= *KiWaitAlways;
return (KDPC*)RDX; KeCancelTimer during
the enumeration as this would cause a deadlock! The PG2Disable-Driver
may write all the timer information to a log file, even if we are
running at DPC level during enumeration.KeCancelTimer we had:TimerEntry = &KiTimerTableListHead[InTimer->Header.Hand];TimerListHead = &KiTimerTableListHead[Index];Index in our enumeration actually has a range from zero to 511. This is because the public WDK constant TIMER_TABLE_SIZE has a value of 512. Now you might see the problem: InTimer->Header.Hand is only one byte wide according to the publicly available DISPATCH_HEADER structure. This causes Hand to overflow if the timer is placed in a linked list with an index greater than 255. This also explains the strange switch we extracted from KeCancelTimer, which again checks whether the linked list is empty, even if the use of RemoveEntryList already
proved it. Redmon probably realized that there is something wrong and
applied this workaround to make sure that only the timestamp of empty
timer lists is reset.