Search code examples
c++cwinapimouseeventnonblocking

Keylogger and mousetracker: should I use non-blocking I/O?


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;
}

Solution

  • 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.