2

I've wrote a kernel driver recently and it does multiple things, but I wanted to add a IAT hook for a certain driver.

I want to hook the IAT of another driver from my driver if that makes sense.

So I understand your typical IAT hook is done via DLL injection so you're within the same address space as said module.

The main question I'm having is can someone give an example of IAT hooking a kernel driver via a kernel driver? I can't figure it out.

I've been using multiple examples of regular IAT hooking via dll injection but applying it to the Kernel driver instead.

Expected outcome: Driver1.sys (does the IAT hook) parses the import table of Driver2.sys and replaces the function "DbgPrintEx" import with a function located in Driver1.sys aka accomplishing an IAT hook.

Here is what I wrote / referenced:

NTSTATUS _HOOKS_::HookFn(PVOID ModuleBase, const char* FunctionName, uintptr_t HookFunc, uintptr_t* OrigFunc) {

IMAGE_DOS_HEADER DosHeader{ 0 };
memcpy(&DosHeader, ModuleBase, sizeof(IMAGE_DOS_HEADER));

IMAGE_NT_HEADERS NtHeaders{ 0 };
memcpy(&NtHeaders, (PVOID)((uintptr_t)ModuleBase + DosHeader.e_lfanew), sizeof(IMAGE_NT_HEADERS));

IMAGE_IMPORT_DESCRIPTOR ImportDescriptor{ 0 };
IMAGE_DATA_DIRECTORY ImportsDirectory = NtHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];

memcpy(&ImportDescriptor, (PVOID)((uintptr_t)ModuleBase + ImportsDirectory.VirtualAddress), sizeof(ImportDescriptor));

LPCSTR LibraryName = NULL;
PVOID Library = NULL;
IMAGE_IMPORT_BY_NAME FunctionNamee{ 0 };

while (ImportDescriptor.Name != NULL) {
    memcpy(&LibraryName, &ImportDescriptor.Name + (uintptr_t)ModuleBase, sizeof(LibraryName));

    IMAGE_THUNK_DATA OriginalFirstThunk{ 0 }, firstThunk{ 0 };
    memcpy(&OriginalFirstThunk, (PVOID)(ImportDescriptor.OriginalFirstThunk + (uintptr_t)ModuleBase), sizeof(IMAGE_THUNK_DATA));
    memcpy(&firstThunk, (PVOID)(ImportDescriptor.FirstThunk + (uintptr_t)ModuleBase), sizeof(IMAGE_THUNK_DATA));

    while (OriginalFirstThunk.u1.AddressOfData != NULL) {

        memcpy(&FunctionNamee, (PVOID)(OriginalFirstThunk.u1.AddressOfData + (uintptr_t)ModuleBase), sizeof(IMAGE_IMPORT_BY_NAME));

        Log::Debug("First function name -> %s\n", FunctionNamee);

        if (!strcmp(FunctionNamee.Name, FunctionName)) {
            Log::Debug("FOUND FUNCTION!!! Address -> %p\n", firstThunk.u1.Function);
        }
    }
}


return STATUS_SUCCESS;

This code BSOD's on: memcpy(&ImportDescriptor, (PVOID)((uintptr_t)ModuleBase + ImportsDirectory.VirtualAddress), sizeof(ImportDescriptor));

Correct me if I'm wrong but since kernel drivers share the same address space I can just simply use memcpy or RtlCopyMemory to obtain the structs needed for this hook, does anyone have any references or some sort of documentation I can read?

Thanks to all in advance!

extra info:

  • Windows 10
  • Using VMWare for testing
  • I'm testing the hook on my own driver

EDIT: Posted the faulting source line that causes BSOD.

Upon debugging via WinDbg(x64), the driver obtains all the addresses correctly comparing them to memory view. Except ImportDescriptor gets an RVA of 0x6000, when inspecting ModuleBase + 0x6000 for the RVA it shows that this memory region is invalid, so I believe that the 0x6000 RVA is incorrect and I'm not sure why

BUG CHECK CODES:

  • BSOD Error Code: PAGE_FAULT_IN_NONPAGED_AREA
  • BUG Check Code: 50
2
  • do the addresses passed to memcpy look correct? You should be able to check it in the debugger before stepping inside the call.
    – Igor Skochinsky
    Nov 24 at 22:57
  • The addresses being passed to memcpy look correct but upon looking at what some of the data types are receiving look incorrect, for example ImportsDirectory IMAGE_DATA_DIRECTORY ImportsDirectory = NtHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; seems to be returning an invalid virtual address, it returns 0x6000 so ImportDescriptor should be DriverBase + 0x6000, and viewing this memory region in WINDBG shows that this memory region is invalid. Do you know why I'm getting these logical errors? I believe I'm getting everything correctly?
    – Ballers
    Nov 24 at 23:56
2

The problem seems to be in the source address calculation:

&ImportDescriptor.Name + (uintptr_t)ModuleBase

This adds the address of a local variable (which is on the stack) to the image base, so the result is probably some non-existent address.

You should add its value, which is an RVA, to the image base. I.e. something like this:

memcpy(&LibraryName, (PVOID)((ULONG_PTR)ModuleBase+ImportDescriptor.Name) , sizeof(LibraryName))
1
  • My apologizes I posted the wrong line, but that is indeed an error I did not spot. The function never reaches that line I posted above. It actually crashes on getting the ImportDescriptor data. I edited the main post correcting my error.
    – Ballers
    Nov 24 at 22:25
1

So, it turns out that if you're testing an IAT hook on a driver, the driver must be compiled in the release mode rather than in the debug one.

If the driver is compiled in the debug mode, the RVA for the Import Descriptor seems to be wrong. But the issue is resolved with compiling in the release rather than in the debug mode.

Thanks to all for the help.

6
  • This, again, sort of suggests that you were running at the wrong IRQL or similar (on account of. Unfortunately given the insufficient information in both the question and the answer, hardly anyone will get out any insights from this Q&A now. We still don't know the exact bug-check code and so even someone knowing the ropes in KM will still at most be able to guess what exactly it is you encountered here. And your "conclusion" will lead to magical thinking by other laypeople that release mode is somehow "safer" and to eventual cargo cult. -1
    – 0xC0000022L
    Nov 26 at 8:18
  • neither release nor debug should alter logic if it alters logic you should actually ascertain if it is a HEISENBUG
    – blabb
    Nov 26 at 10:57
  • @blabb this is simply untrue. I think with more than sixteen years of Windows KM development and some formal training in the subject matter, I know a thing or two. And even if you don't dabble in KM your are certainly aware that assertions don't fire in "release" (free) builds, but do in "debug" (checked) ones. Heck that's the whole reason for the name checked builds in the first place. Similarly macros expand differently. Loads of them actually. Now one could argue that this is "technically" not altering the logic. But in fact it is, because differently expanding macros mean different code...
    – 0xC0000022L
    Nov 27 at 22:06
  • ... being executed. Not that this is a clear-cut case here, since the question didn't even originally state the bug-check code.
    – 0xC0000022L
    Nov 27 at 22:09
  • 1
    @0xC0000022L yes infact there are documented apis that behave differently in release versus build ](docs.microsoft.com/en-us/windows/win32/api/realtimeapiset/…) i was commenting about the calculation about a dereferenced address and / or its page being present
    – blabb
    Nov 29 at 11:23
1

This is unfortunately not going to be an answer, because you don't give enough information. If I end up writing an answer, it will be a different one. So long I'll make this non-answer a community wiki, attempting to give clues, ask for details and point to resources.

Correct me if I'm wrong but since kernel drivers share the same address space I can just simply use memcpy or RtlCopyMemory [...]

Uhm this statement is "sort of" true, but then it also isn't. It depends on so many aspects in (Windows) kernel mode. While the address space (i.e. min through max address) is technically the same, it depends a whole lot in which context your code executes (e.g. the IRQL) and of the attributes for the memory you attempt to access (e.g. NonPagedPool vs. PagedPool).

However, this is introductory material for KM driver development, so I recommend you pick up a book such as "Windows Kernel Programming" from Pavel Yosifovich or even the somewhat older work "Programming the Microsoft Windows Driver Model" from Walter Oney. The courses offered by OSR and Yosifovich would also be a great way to learn this stuff.

Besides you forgot to mention which bug-check code you're getting. This is very much relevant to help you. The parameters included in the bug-check can also give an idea in absence of a full memory dump (or in your case a live debug session). The parameters are those four items included alongside the bug-check code in a blue screen.

  • At what IRQL is your code executing during the bug-check? You can't touch
  • How is the code marked in your driver? (e.g. PAGED_CODE() or similar?)
    • Also, in case you attempt this in DriverEntry that one always runs at PASSIVE_LEVEL either way ...
  • Who is the caller of the function?
  • Assuming you're not doing this inside an I/O request code path, you're still fairly wasteful with stack. KM code has quite the limits when it comes to available stack.
  • All the above questions, comments, remarks also should be checked for code you call (such as Log::Debug()).

That said, your above statement should be true for all intents and purposes at PASSIVE_LEVEL and to some extent at APC_LEVEL. But even then it pays of to make use of SEH for some code and make use of MmIsAddressValid and ProbeForRead and similar routines as appropriate.

Btw: perhaps you'd like instead to tell us what it is you're trying? Although admittedly none of this is very closely related to RCE.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.