I'm writing a simple keylogger/mouselogger in C/C++ for Windows. To do that, I use the Win32 functions LowLevelMouseProc
and LowLevelKeyboardProc
.
If relevant, here is a GitHub gist with my code, which is ultra-elementary: define the event callback and register it along with a callback for SIGINT. I'll add a summarized version at the end of the question.
My question is the following: in order to minimize overhead, how should I save these events to disk?
Answers in both C or C++ are welcome.
Is it a good practice to simply write to a buffered file each time I get a new event and let the file handle flushing when the buffer is full? I heard about non-blocking I/O but microsoft's doc says that there is an additional overhead. And finally, I'm not sure wether I should create a second thread for this.
I'd like to use some sort of buffering to avoid many little disk I/O. Ideally I would write to disk once before my process is killed. But I have no idea how to achieve this.
CODE:
#include "pch.h"
#include <stdio.h>
#include <Windows.h>
HHOOK handle;
LRESULT CALLBACK lowLevelMouseProc(
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
MSLLHOOKSTRUCT* lp = (MSLLHOOKSTRUCT*)lParam;
if (wParam == WM_MOUSEMOVE) {
// Best way to save pt.x and pt.y to disk?
printf("%d %d \n", lp->pt.x, lp->pt.y);
}
return CallNextHookEx(0, nCode, wParam, lParam);
}
int main()
{
handle = SetWindowsHookExA(WH_MOUSE_LL, &lowLevelMouseProc, NULL, 0);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0));
UnhookWindowsHookEx(handle)
return 0;
}
Use 2 buffers. One for writing, one for reading (flushing to disk). Once some condition is met (buffer full, program shutdown, ...), swap the buffers and start flushing to disk in a seperate thread.
This might look something like:
#include <Windows.h>
#include <vector>
#include <thread>
#include <fstream>
#include <atomic>
struct Point
{
long x, y;
};
class Buffer
{
public:
Buffer(std::string _file = "log.txt", const size_t _buffer_size = 100000) : buffer_size(_buffer_size), file(_file)
{
points1.reserve(_buffer_size);
points2.reserve(_buffer_size);
}
void write(Point p)
{
buf->push_back(p);
if (buf->size() >= buffer_size && !thread_running.load())
to_disk();
}
private:
const size_t buffer_size;
const std::string file;
std::atomic<bool> thread_running{ false };
std::vector<Point> points1, points2;
std::vector<Point> *buf = &points1, *other = &points2;
void swap_buffer()
{
std::swap(buf, other);
}
void to_disk()
{
swap_buffer();
auto tmp_buf = other;
auto tmp_file = file;
auto tmp_flag = &thread_running;
auto fn = [tmp_buf, tmp_file, tmp_flag]() {
tmp_flag->store(true);
std::fstream f(tmp_file, std::ios::app);
for (auto &v : *tmp_buf)
f << v.x << ' ' << v.y << '\n';
tmp_buf->clear();
tmp_flag->store(false);
};
std::thread t(fn);
t.detach();
}
};
Buffer buffer("log.txt");
HHOOK handle;
LRESULT CALLBACK lowLevelMouseProc(
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
MSLLHOOKSTRUCT* lp = (MSLLHOOKSTRUCT*)lParam;
if (wParam == WM_MOUSEMOVE) {
buffer.write({ lp->pt.x, lp->pt.y });
}
return CallNextHookEx(0, nCode, wParam, lParam);
}
int main()
{
handle = SetWindowsHookExA(WH_MOUSE_LL, &lowLevelMouseProc, NULL, 0);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0));
UnhookWindowsHookEx(handle);
return 0;
}
In this case, the buffer gets written to disk when a certain size limit is reached. This could be further optimized, by not checking the size on every write for example.
Note: In this example, error handling is omitted and the lifetime of the internal buffers should be managed accordingly.