Search This Blog

Sunday, July 17, 2005

The Art of Breaking and Entering - Windows Hooks

Okay, so now we know how to inject code and data into a foreign process on both Windows NT and 9x. But suppose we actually want to, say, RUN the code we injected; what then? Well, there are ways of accomplishing that, too. Three ways, in fact; at least, three ways that are suitable for general use.

The first method we'll examine is "the easy way": windows hooks. Note the absence of capitalization: we're talking about windows on screen, not Windows the OS. A window hook is a function that gets called in some circumstance involving a window. A hook function could be called when the program gets a message from its message queue, when the window's message handler gets called, when a key on the keyboard gets pressed, or a variety of other times, depending on the type of hook you set. In any case, you set the hook using SetWindowsHookEx, and remove the hook using UnhookWindowsHookEx.

So, how does this help us? Well, as it turns out, windows hooks are executed in the process that owns the hooked window. Due to the protected address spaces for each process, that requires that your code gets loaded into the process that owns the window. Windows takes responsibility for injecting the code into the process; in other words, Windows does all the work for you. For this to work, the hook function must be in a DLL, as it is the DLL that Windows loads into the process.

However, this ease of use comes with some significant drawbacks. Most significantly, in order to set a windows hook, there must be a window to hook. This implies two things: before you can inject your DLL, the program must be running, and the program must create a window. If the program doesn't create a window, you're out of luck; similarly, if you need to execute your code before the program starts up, you're also out of luck.

Another noteworthy point, although it can be overcome, is how your code gets executed. When the DLL is first loaded, it receives a DLL_PROCESS_ATTACH notification in DllMain. At this point, you can't do much, as many Windows API functions are not safe to call at this point, due to how Windows calls DllMain. Not only that, but you don't know if your DLL is being loaded in the source process (your program) or the target (the program you're invading), as DllMain will get called for both, just the same.

These problems can be circumvented by delaying initialization until the first time the hook function gets called. But at the same time, this adds a new complication: the hook function must get called after you set the hook, before your code can actually get executed.

This can similarly be worked around by setting a hook that you can ensure will be called, such as a get message hook. This hook will be called every time the program snags a message for that window using GetMessage, which you can ensure will get called by sending a message to the window with PostMessage (SendMessage won't work in this case, because GetMessage does not return messages sent with SendMessage). The usual message to use to accomplish this (although this won't work for all hook types) is WM_NULL, which does nothing, but calls the hook function just the same.

Okay, so that's the basic procedure; however, there are still a couple of details that need to be addressed. To start with, your hook must, after performing whatever it needs to do, call the next hook with CallNextHookEx. This is something that must be done manually (at least for certain types of hooks; ours is one such type). This requires that the target process know the HHOOK that your process got from SetWindowsHookEx; the HHOOK value must be communicated between processes. Fortunately, we already have a way of accomplishing this: named memory mapped files.

The next detail is that your window hook will ONLY get called if the event that is hooked occurs while the hook is in place. In our case, that means that GetMessage must return while the hook is set; if the hook is removed before the window thread gets a chance to run, your hook will never get called (wouldn't it be nice if SendMessage worked for get message hooks?). There are a couple solutions, depending on how lazy you are. The lazy solution would be to simply leave the hook installed indefinitely. However, this would also require that we leave the memory mapped file open indefinitely as well, as we wouldn't know when the target process is done with it.

A better way is to wait until the target process is done with the file mapping. This is, in fact, trivial, given that we already have a file mapping to pass data between the two processes. The classy way would be to create an event to wait on, duplicate the event HANDLE into the target process, and wait for the event to get signalled. I'm too lazy for this, given that there are easier ways of getting the same result (although not as elegant). I use a spin loop; but no just any spin loop - a Sleep-spin loop! In other words, we have a loop that checks for a return value and Sleeps if it's not there. This isn't as efficient as waiting on an event, but it works just as well. Lazy as I am, I decided to use the HHOOK value itself (the copy in the file mapping) as the return value. The target process will read the HHOOK, then set it to NULL before closing the file mapping. Thus, your process needs only to wait for it to be set to NULL, then it can know that the hook function has been called, and the hook can be removed.

The full code (note that I have all of this in the "ThingyDLL" DLL - InjectIntoWindowProcess is a function that is exported for your program to call):
HINSTANCE g_hDLL = NULL;

bool g_bInitialized = false;
HHOOK g_hHook = NULL;

BOOL APIENTRY DllMain(HINSTANCE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
 if (ul_reason_for_call == DLL_PROCESS_ATTACH)
   g_hDLL = (HINSTANCE)hModule;  // Save the HINSTANCE

   return TRUE;
}

// Gets the HHOOK we have on the main window from the file mapping
HHOOK GetHHOOK()
{
 // Open the file mapping - these should not fail for any reasonable reason
 HANDLE hMapping = OpenProcessSection("ThingyDLL");
 assert(hMapping != NULL);

 volatile HHOOK *lphHook = (volatile HHOOK *)MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, 0);
 assert(lphHook);

 // There's a very small, but real, chance that the patching thread will get interrupted after setting the hook, before it can write the HHOOK to the file mapping. Sleep-spin wait for the HHOOK to be set.
 while (*lphHook == NULL)
   Sleep(10);

 // Get the HHOOK
 HHOOK hHook = *lphHook;

 // Signal the injector that we've received the HHOOK
 *lphHook = NULL;

 // Close the mapping
 UnmapViewOfFile((void *)lphHook);
 CloseHandle(hMapping);

 return hHook;
}

LRESULT CALLBACK GetMessageHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
 // Are we getting called for the first time?
 if (!g_bInitialized)
 {
   // We need to set this RIGHT now, because we're going to indirectly generate messages in our MessageBox call, which would put us in an infinite loop.
   g_bInitialized = true;

   // Get the HHOOK for this hook
   g_hHook = GetHHOOK();

   // Do our stuff. For demonstration, display a message box for the hooked window, to show we're inside the process.

   // lParam in this case is a MSG * for the message being retrieved
   MSG *lpMsg = (MSG *)lParam;

   MessageBox(lpMsg->hwnd, "Hello from the inside!", "GetMessageHookProc", MB_OK | MB_ICONEXCLAMATION);
 }

 // Call the next hook
 return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

_declspec(dllexport) bool __stdcall InjectIntoWindowProcess(HWND hWnd, DWORD nTimeoutMS)
{
 // Lookup the thread and process ID for the window
 DWORD nProcessID, nThreadID = GetWindowThreadProcessId(hWnd, &nProcessID);
 if (!nThreadID)
   return false;

 // Create the file mapping to share the HHOOK with the patched process
 HANDLE hMapping = CreateProcessSection(sizeof(HHOOK), "ThingyDLL", nProcessID);
 assert(hMapping);

 // Open the file mapping
 volatile HHOOK *lphHook = (volatile HHOOK *)MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, 0);
assert(lphHook);

 *lphHook = NULL;

 bool bSuccess = false;  // Failed until proven otherwise

 // Set the window hook
 HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMessageHookProc, g_hDLL, nThreadID);

 if (hHook)
 {
   // Tell patched process the hook
   *lphHook = hHook;

   // Queue a message that will activate the hook function
   if (PostThreadMessage(nThreadID, WM_NULL, 0, 0))
   {
     // Wait for the hook function to reply
     for (int ms = 0; ms < nTimeoutMS; ms += 10)
     {
       if (*lphHook == NULL)
       {
         bSuccess = true;
         break;
       }

       Sleep(10);
     }
   }

   // Release the hook now that the DLL has been injected and been initialized
   UnhookWindowsHookEx(hHook);
 }

 // Close the file mapping
 UnmapViewOfFile((void *)lphHook);
 CloseHandle(hMapping);

 return bSuccess;
}

No comments: