Monday, February 19, 2007

The Zen of R3 Hooking

In my daytime job, from time to time we end up needing a way to patch user mode code (Ring 3, or R3 for short) to perform a targeted alteration of the behavior of a target application to improve the security or to make it behave better under a more locked down configuration. This modification typically this happens at the boundary between the application (which we treat as an undocumented black box) and the windows API. Thus, what we do in a hook is to 'take over' a particular API and execute some code of our choice before and then after the original API call.

If you google for patching or hooking or interception you will find plenty of code and articles out there. For example, last time I checked just codeproject.com has no fewer than 4 full blown articles in the subject. Today I will show you a neat trick that so far I have not seen disclosed widely.

To patch you need to solve two problems:

1) Injecting in the virtual address space of the target process the code that is going to be executed before and after the original API call and 2) Re-wire the target process so our code is called instead of the windows API.

I won't touch on the first item since I have nothing to add to it today. For the second item your options are:

a- Modify the IAT (in the PE-in-memory) to point to your code
b- Modify the EAT (in the PE-in-memory) to point to your code
c- Overwrite the start of the API call in memory with a 'jmp' stub

But I won't talk about them. What I want to talk about is a different way to do this in the context of a special (but fairly common) case. It being if you need to hook an API that you know jumps to kernel using the standard system call dispatch table.

This trick works only in 32 bits Windows XP SP2 and Win2K3 SP1. I also assume you need to patch R3 code from R3. If this is your case, read on:

You should know that when an application calls (for example) CreateMutex(..) it does so via a the appropriate EAT entry in kernel32.dll where after doing some simple transformations of the input parameters it calls ZwCreateMutant(..) which lives in ntdll.dll. Here what used to happen circa year 2000 under the covers:


ntdll!ZwCreateMutant:
77f853b8 b825000000 mov eax,0x25
77f853bd 8d542404 lea edx,[esp+0x4]
77f853c1 cd2e int 2e <-- jump to kernel


Where 2e is the standard system call interrupt gateway to ring zero (R0). Well, times have changed and most (if not all) processors now support a faster way to transition to R0 using the new instruction 'sysenter'. Here is what happens in XP with no service packs:


ntdll!ZwCreateMutant:
77f7e663 b82b000000 mov eax,0x2b
77f7e668 ba0003fe7f mov edx,0x7ffe0300
77f7e66d ffd2 call edx


where the routine at address 0x7ffe0300 is formally know as the SystemCallStub:


7ffe0300 8bd4 mov edx,esp
7ffe0302 0f34 sysenter <-- jump to kernel
7ffe0304 c3 ret


What they have done is to add one level of indirection, so in older processors where the sysenter instruction is not supported, then the SystemCall stub will contain the usual int 2e code. The memory page at 0x7ffe0000 is a fixed block known as the SharedUserData that is mapped at the same address in all processes. You could patch it, with a jmp instruction to your injected code but the good people at Microsoft changed things once again when XP SP2 was released. For example:


ntdll!ZwAllocateVirtualMemory:
7c90d4de b811000000 mov eax,0x11
7c90d4e3 ba0003fe7f mov edx,0x7ffe0300
7c90d4e8 ff12 call dword ptr [edx]


If you compare the last function with the previous ZwCreateMutant you'll see that the only difference is that the call using EDX is now an indirect call. Therefore the memory at 0x7ffe0300 does not contain code anymore but a pointer to the actual SystemCallStub. Cool.

So that is the punch line: if you overwrite the pointer at address 0x7ffe0300 you have patched every system call that the process can make. Make sure you modify the page protection to make it copy_on_write so you don't affect other processes; remember that your injected code lives only one the target process.

Of course, since in your hook routine you are getting every system call, you need to look at the EAX register for the API id that you care about.

It does not get more Zen than that.

1 comment:

Iron Guts Morla said...

One thing that I forgot to mention was that such patch cannot be done from within the process because the 0x7ffe0000 page is write protected in a way that cannot be unprotected from inside.