├── .github └── workflows │ └── ci.yml ├── .gitignore ├── history.markdown ├── makefile ├── readme.markdown └── xkbcat.c /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # Trigger on push and pull-request events, on any branch 5 | push: 6 | pull_request: 7 | 8 | # Can be run manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Automatically rerun at 13:00 on the 10th day of each month 12 | schedule: 13 | - cron: '00 13 10 * *' 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | compiler: [gcc, clang] 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: install dependencies 24 | run: sudo apt-get install libx11-dev libxi-dev 25 | - name: make 26 | run: CC=${{ matrix.compiler }} make 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | xkbcat 2 | -------------------------------------------------------------------------------- /history.markdown: -------------------------------------------------------------------------------- 1 | This program is based Jon A. Maxwell "JAM" `jmaxwell@acm.vt.edu` 's discovery 2 | and proof-of-concept that X11 keyboard state can be logged without superuser 3 | permissions. (I'm not aware of others prior.) 4 | 5 | His program `xspy` is an X11 keylogger which output is squarely aimed at 6 | human-readability, which makes it great for (as the name suggests) spying on 7 | someone and quickly making out what they're doing. Close variations of it have 8 | hence featured in security-focused Linux distributions ([in Kali][1] and [in 9 | BlackArch][2], [among others][3]). 10 | 11 | This is a complete rework of his idea, using a modern C version and with the 12 | aim of producing machine-readable output better suitable for physical key press 13 | statistics, deprioritising human parseability of the output. 14 | 15 | 16 | [1]: http://www6.frugalware.org/mirrors/linux/kali/kali/pool/main/x/xspy/ 17 | [2]: https://github.com/BlackArch/blackarch/blob/master/packages/xspy/PKGBUILD 18 | [3]: http://www.freshports.org/security/xspy/ 19 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | xkbcat: xkbcat.c 2 | $(CC) -O3 --std=c99 -pedantic -Wall xkbcat.c -o xkbcat -lX11 -lXi 3 | clean: 4 | rm --force xkbcat 5 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # xkbcat [![](https://img.shields.io/github/actions/workflow/status/anko/xkbcat/ci.yml?branch=master&style=flat-square)](https://github.com/anko/xkbcat/actions) ![](https://img.shields.io/github/languages/code-size/anko/xkbcat?style=flat-square) 2 | 3 | Simple X11 keylogger. 4 | 5 | - Simple output format: One line on `stdout` per key event. 6 | - Simple to audit: One short file of modern C. 7 | - Simple to run: Does not need `sudo`. 8 | 9 | ## Examples 10 | 11 | ### Keypresses only 12 | 13 | Given no options, `xkbcat` prints only keypresses, one per line. Here's the 14 | output when I type "Hi": 15 | 16 | Shift_L 17 | h 18 | i 19 | 20 | ### Keypresses and key-ups 21 | 22 | With key-ups enabled (`xkbcat -up`), the format changes to show them: 23 | 24 | +Shift_L 25 | +h 26 | -h 27 | -Shift_L 28 | +i 29 | -i 30 | 31 | Lines starting `+` are key-downs; `-` are key-ups. 32 | 33 | ## Compilation 34 | 35 | Just `make`. 36 | 37 | Don't have `X11/extensions/XInput2.h`? Install your distro's `libxi-devel` 38 | package. 39 | 40 | ## Usage 41 | 42 | Options you can pass (all optional): 43 | 44 | - `-display `: set target X display (default `:0`) 45 | - `-up`: also prepend key-ups (default: don't) 46 | - `-help`: print usage hints and exit 47 | 48 | Then just use your computer as usual. Interrupt signal (`C-c`) to quit. 49 | 50 | ## Related programs 51 | 52 | ### Other keyloggers 53 | 54 | - If you need to log keys across a whole Linux system (also in the 55 | framebuffer—not just in X11), try [keysniffer][1]. It works via a kernel 56 | module, and needs `sudo`. 57 | - If you want to see what characters the user actually typed (with modifier 58 | keys, backspace, etc resolved into text), [`xspy`][2] or [`logkeys`][3] 59 | might be better for you. 60 | 61 | ### Programs that work well together with `xkbcat` 62 | 63 | - If you want to add timestamps to each line for logging purposes, I recommend 64 | piping to the [moreutils package][4]'s `ts`. [These answers][5] feature 65 | various other tools good for the purpose. 66 | - If you only want to see key names when you press keys in the same terminal 67 | where `xkbcat` is running, you can temporarily disable terminal echo with 68 | `stty -echo && xkbcat`. (`stty` is in coreutils.) 69 | 70 | ### Programs for logging other X11 events 71 | 72 | - [xinput][6] invoked as `xinput --test-xi2 --root` logs everything 73 | input-related; even mouse movements and clicks, and touchpad stuff. Its 74 | output is very comprehensive, but harder to parse. 75 | 76 | - If you need to log X11 events more generally, various protocol monitoring 77 | programs are listed in the [X11 debugging guide][7]. 78 | 79 | ## Versioning 80 | 81 | The git-tagged version numbers follow [semver][8]. 82 | 83 | Error outputs (on stderr) are intended to be read by people. Changes to their 84 | wording are not considered breaking changes. Don't parse them 85 | programmatically. 86 | 87 | ## License 88 | 89 | [ISC][9]. 90 | 91 | 92 | [1]: https://github.com/jarun/keysniffer 93 | [2]: http://www.freshports.org/security/xspy/ 94 | [3]: http://code.google.com/p/logkeys/ 95 | [4]: http://joeyh.name/code/moreutils/ 96 | [5]: http://stackoverflow.com/questions/21564/is-there-a-unix-utility-to-prepend-timestamps-to-lines-of-text 97 | [6]: https://www.x.org/archive/current/doc/man/man1/xinput.1.xhtml 98 | [7]: https://www.x.org/wiki/guide/debugging/ 99 | [8]: http://semver.org/ 100 | [9]: http://opensource.org/licenses/ISC 101 | -------------------------------------------------------------------------------- /xkbcat.c: -------------------------------------------------------------------------------- 1 | // xkbcat: Logs X11 keypresses, globally. 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | const char * DEFAULT_DISPLAY = ":0"; 12 | const bool DEFAULT_PRINT_UP = false; 13 | 14 | int printUsage() { 15 | printf("\ 16 | USAGE: xkbcat [-display ] [-up]\n\ 17 | display target X display (default %s)\n\ 18 | up also print key-ups (default %s)\n", 19 | DEFAULT_DISPLAY, (DEFAULT_PRINT_UP ? "yes" : "no") ); 20 | exit(0); 21 | } 22 | 23 | int main(int argc, char * argv[]) { 24 | 25 | const char * xDisplayName = DEFAULT_DISPLAY; 26 | bool printKeyUps = DEFAULT_PRINT_UP; 27 | 28 | // Get arguments 29 | for (int i = 1; i < argc; i++) { 30 | if (!strcmp(argv[i], "-help")) printUsage(); 31 | else if (!strcmp(argv[i], "-up")) printKeyUps = true; 32 | else if (!strcmp(argv[i], "-display")) { 33 | // Read next entry to find value 34 | ++i; 35 | if (i >= argc) { 36 | fprintf(stderr, "No value given to option `-display`\n"); 37 | printUsage(); 38 | exit(5); 39 | } 40 | xDisplayName = argv[i]; 41 | } 42 | else { printf("Unexpected argument `%s`\n", argv[i]); printUsage(); } 43 | } 44 | 45 | // Connect to X display 46 | Display * disp = XOpenDisplay(xDisplayName); 47 | if (NULL == disp) { 48 | fprintf(stderr, "Cannot open X display '%s'\n", xDisplayName); 49 | exit(1); 50 | } 51 | 52 | int xiOpcode; 53 | { // Test for XInput 2 extension 54 | int queryEvent, queryError; 55 | if (! XQueryExtension(disp, "XInputExtension", &xiOpcode, 56 | &queryEvent, &queryError)) { 57 | fprintf(stderr, "X Input extension not available\n"); 58 | exit(2); 59 | } 60 | } 61 | { // Request XInput 2.0, to guard against changes in future versions 62 | int major = 2, minor = 0; 63 | int queryResult = XIQueryVersion(disp, &major, &minor); 64 | if (queryResult == BadRequest) { 65 | fprintf(stderr, "Need XI 2.0 support (got %d.%d)\n", major, minor); 66 | exit(3); 67 | } else if (queryResult != Success) { 68 | fprintf(stderr, "XIQueryVersion failed!\n"); 69 | exit(4); 70 | } 71 | } 72 | { // Register to receive XInput events 73 | Window root = DefaultRootWindow(disp); 74 | XIEventMask m; 75 | m.deviceid = XIAllMasterDevices; 76 | m.mask_len = XIMaskLen(XI_LASTEVENT); 77 | m.mask = calloc(m.mask_len, sizeof(char)); 78 | // Raw key presses correspond to physical key-presses, without 79 | // processing steps such as auto-repeat. 80 | XISetMask(m.mask, XI_RawKeyPress); 81 | if (printKeyUps) XISetMask(m.mask, XI_RawKeyRelease); 82 | XISelectEvents(disp, root, &m, 1 /*number of masks*/); 83 | XSync(disp, false); 84 | free(m.mask); 85 | } 86 | 87 | int xkbOpcode, xkbEventCode; 88 | { // Test for Xkb extension 89 | int queryError, majorVersion, minorVersion; 90 | if (! XkbQueryExtension(disp, &xkbOpcode, &xkbEventCode, &queryError, 91 | &majorVersion, &minorVersion)) { 92 | fprintf(stderr, "Xkb extension not available\n"); 93 | exit(2); 94 | } 95 | } 96 | // Register to receive events when the keyboard's keysym group changes. 97 | // Keysym groups are normally used to switch keyboard layouts. The 98 | // keyboard continues to send the same keycodes (numeric identifiers of 99 | // keys) either way, but the active keysym group determines how those map 100 | // to keysyms (textual names of keys). 101 | XkbSelectEventDetails(disp, XkbUseCoreKbd, XkbStateNotify, 102 | XkbGroupStateMask, XkbGroupStateMask); 103 | int group; 104 | { // Determine initial keysym group 105 | XkbStateRec state; 106 | XkbGetState(disp, XkbUseCoreKbd, &state); 107 | group = state.group; 108 | } 109 | 110 | while ("forever") { 111 | XEvent event; 112 | XGenericEventCookie *cookie = (XGenericEventCookie*)&event.xcookie; 113 | XNextEvent(disp, &event); 114 | 115 | if (XGetEventData(disp, cookie)) { 116 | // Handle key press and release events 117 | if (cookie->type == GenericEvent 118 | && cookie->extension == xiOpcode) { 119 | if (cookie->evtype == XI_RawKeyRelease 120 | || cookie->evtype == XI_RawKeyPress) { 121 | XIRawEvent *ev = cookie->data; 122 | 123 | // Ask X what it calls that key; skip if unknown. 124 | // Ignore shift-level argument, to show the "basic" key 125 | // regardless of what modifiers are held down. 126 | KeySym s = XkbKeycodeToKeysym( 127 | disp, ev->detail, group, 0 /*shift level*/); 128 | 129 | // Non-zero keysym groups are "overlays" on the base (`0`) 130 | // group. If the current group has no keysym for this 131 | // keycode, defer to the base group instead. (This usually 132 | // happens with common shared keys like Return, Backspace, 133 | // or numeric keypad keys.) 134 | if (NoSymbol == s) { 135 | if (group == 0) continue; 136 | else { 137 | s = XkbKeycodeToKeysym(disp, ev->detail, 138 | 0 /* base group */, 0 /*shift level*/); 139 | if (NoSymbol == s) continue; 140 | } 141 | } 142 | char *str = XKeysymToString(s); 143 | if (NULL == str) continue; 144 | 145 | // Output line 146 | if (printKeyUps) printf("%s", 147 | cookie->evtype == XI_RawKeyPress ? "+" : "-"); 148 | printf("%s\n", str); 149 | fflush(stdout); 150 | } 151 | } 152 | // Release memory associated with event data 153 | XFreeEventData(disp, cookie); 154 | } else { // No extra data to release; `event` contains everything. 155 | // Handle keysym group change events 156 | if (event.type == xkbEventCode) { 157 | XkbEvent *xkbEvent = (XkbEvent*)&event; 158 | if (xkbEvent->any.xkb_type == XkbStateNotify) { 159 | group = xkbEvent->state.group; 160 | } 161 | } 162 | } 163 | } 164 | } 165 | --------------------------------------------------------------------------------