Export drivers on the NT platform

Export drivers are basically nothing more than just DLLs with a .sys file extension. Although they have a DriverEntry, this is most likely never being called if the driver gets loaded as the dependency of another driver dynamically linked to it (i.e. importing the export driver’s functions or variables). Not in any case that I am aware of, at least.

Why is that? Normally, if you load a driver, the I/O manager will take care of calling the DriverEntry of your driver. However, the I/O manager is not involved if the driver gets loaded as a dependency – I call that loaded implicitly. In this case the I/O manager has of course called the DriverEntry of the depending driver, but the PE-loader is responsible to satisfy other dependencies and does not know of such internals as the DriverEntry of an export driver. But that is not the only thing that distinguishes an export driver from other “normal” drivers. If an export driver gets loaded implicitly, it will never ever get unloaded again. This should be logical for several reasons. If the DriverEntry of the export driver is not being called, how would the I/O manager keep track of the driver object which is associated with “normal” drivers? Right, it cannot. And if there is no driver object associated with the export driver, how would the DRIVER_OBJECT.DriverUnload routine get called? There is simply none registered because the image does not have an associated driver object, device object or dispatch function table.

So what is the advantage of using export drivers then, you ask? Well, the disadvantage – that it never gets unloaded – is its biggest advantage too. Let us consider kernel patching, the thing about which Agnitum panicked recently and let us consider 32bit systems which still allow for patching. In an article about the Sony rootkit on the Lavasoft blog I have already explained it in great detail, but let me reiterate some things and quote some of my own words:

Why the “Sony Rootkit” is badly implemented …

The rootkit has several flaws that make it problematic from the security view. The most important is, that it hides objects regardless of whether or not these belong to the actual product, thus allowing malware to slip through scanners undetected. Another one is the method how the function is diverted: this is done in a way that alone is unsafe, because it is not multi-processor-safe and because it removes the manipulation regardless of whether someone came after it. That is important. Consider the following example:
ZwCreateFile is to be replaced, but we still want to allow “our” processes to call the original function, so we save the value of ZwCreateFile somewhere and use it for our purposes whenever we like. The original address is being replaced by the address to “our” function AriesReplaceZwCreateFile (inside aries.sys). Now someone else comes along and also wants to divert the ZwCreateFile function, but what he finds is the pointer to AriesReplaceZwCreateFile, so he saves it for later use and replaces it with its own pointer to MyZwCreateFile (inside mydriver.sys). Now someone asks the “Sony Rootkit” driver to unload, which it does – it even restores the old addresses inside the SSDT effectively cutting off mydriver.sys! But what happens if the function MyZwCreateFile was already called by the system call dispatcher and then interrupted, while the aries.sys module performed its unloading? Well, since mydriver.sys still has the address to AriesReplaceZwCreateFile in the no more existing module aries.sys, it will just call it and *BANG* you got a blue-screen.

Since also some antivirus vendors resort to diverting of functions, you may encounter the blue-screen more often than it actually seems at first glance. Let alone the myriads of other products that use these techniques.

But besides this really stupid unload routine (which should have been left out at all), there is another thing that makes it unsafe. At the point where the replacement function addresses are being set, the process may be interrupted at any point (professionals call it “pre-emption”). This is even dangerous on single-processor systems and can be easily avoided – but who am I to advise the rootkit writers how to write their rootkits?!

Let me continue from the last paragraph this time. What could we do to prevent the described race condition? Well, first it would come to mind that we allocate some nonpaged pool and use it as a jump table to the diverted or original function respectively (depending on whether the hook is established or not). This would certainly work, but it has a drawback – it uses nonpaged pool which will just dangle around after the driver which allocated it got unloaded. A memory leak at its best. So this is no option, not in kernel mode, not for a precious resource such as nonpaged pool. Also imagine what happens if you unload the hooking driver and then load a newer version (or even the old one again). The new version will never ever find the “old” jumptable and therefore is not able to reuse it – something we would really appreciate, though.

If all fails, the export driver comes in handy 😉

Remember one of those seemingly bad habits of export drivers? They stay resident, even though the driver having loaded the export driver implicitly has been unloaded already. How can we use this to our advantage? Don’t you have the solution already?

Instead of using a jumptable in nonpaged pool, use a jumptable in the export driver. Export a variable from the driver and dynamically link to it from the hooking driver. Wow, that was easy, wasn’t it? Well, no! If you try the suggested solution you will find another hurdle. The variable which you reference has not been “initialized”. In fact you will just fail to access it at all. How can we overcome this? Nothing easier than that: write a function which gives back a pointer to the static variable.

Yes, this works fine. But if you are as lazy as I am, you always strive for maintainable software. Software that solves problems that a future programmer might not be aware of. In our case imagine the hooking driver to be one part of a bigger product. Now imagine the hooking driver needs to be updated or upgraded for some reason?! Since the export driver remains loaded in memory and a new version of the hooking driver could reference the same functions and variables of the export driver without a problem, it would be a waste of resources to rename the export driver and have it load again. Instead we could just reuse the jumptable. And since we have to implement some function in the export driver already, why not implement the whole interface for SSDT hooking in the export driver, let the jump table live in the export driver and have very maintainable code? This is left as an exercise for the valued reader 😉 …

Since we Germans are not as brutal as the Brits who use to kill two birds with one stone – we always kill two flies with one fly-flap – I would say putting the implementation of your hooking mechanism and the jump table in one export driver kills two flies with one fly-flap :mrgreen: 😉

// Oliver

PS: Oh, you ask how to actually write the export driver? Well, you know how to write a conventional Windows DLL, don’t you? Start over with your knowledge. In the SOURCES file change the target type line to read

TARGETTYPE=EXPORT_DRIVER

and create a module definition file with the name $(TARGETNAME).def (if your target name if foo, the file ought to be named foo.def) containing the names and/or ordinals of your exported functions (except for DriverEntry) and let compiler and linker do the rest for you. Note: if you use NAME something.sys in your module definition file, this might actually override the target name given in the SOURCES file. So don’t wonder if the output is named something.sys instead of foo.sys for the above example. And yes, the calling convention is __stdcall by definition.

This entry was posted in Programming. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.