Skip to content

Commit 5668522

Browse files
committed
Merge remote-tracking branch 'origin/pr/149'
* origin/pr/149: Add event buffering for cloaking user input patterns Replace magic quotes with normal quotes in parse_vm_config Typo fix, change "canot" to "cannot" Pull request description: # Goal Implement the functionality of [kloak](https://github.com/Whonix/kloak) (a tool designed to hide biometric behavior patterns in keystrokes and mouse movements) in qubes-gui-daemon. This PR will implement the functionality requested in QubesOS/qubes-issues#1850 and fleshed out further in QubesOS/qubes-issues#8541. It will also close QubesOS/qubes-issues#8534 as it will no longer be necessary. # TODOs * Test rigorously on Qubes R4.3 (an earlier iteration of the code has been smoke-tested on Qubes R4.2, this hasn't been tested at all on R4.3 yet) # Fixed TODOs: * Figure out why the domU window occasionally freezes until another input event is sent - we aren't buffering info coming from domU to dom0 so why this is happening is a mystery to me, and something for later investigation. (Solved, #149 (comment)) * Potentially change how events are treated (do some events have to operate in pairs for best results?). (Lots of X events are now not buffered in the latest implementation. Only ones that look valuable to buffer are buffered.) * Make the delay duration user-configurable (right now it's hardcoded to 150 milliseconds). (Implemented.) * Allow configuring event delay duration for individual VMs buffering (right now it is applied equally to all VMs). (Implemented.) * Get the configuration code working and test it. (Solved, this ended up requiring a change to [core-admin-client](https://forum.qubes-os.org/t/cannot-change-qubes-gui-daemon-settings-using-qvm-features/29345/3) which I will be submitting as a separate PR.) * Ensure all new code adheres to Qubes OS standards (didn't have time to finish that up) (should be done now) # Rationale Kloak, the inspiration for this PR, is a user input buffering and obfuscation tool. It intercepts keyboard and mouse events at the evdev layer, holds them in a queue for release at a later scheduled time, then releases them to the applications they were intended for periodically. By adding random noise into the user's input patterns, kloak aims to make otherwise recognizable patterns in user behavior (such as keystroke rhythm and mouse movement patterns) too erratic to be used as a method of identifying the user. This is potentially very useful especially for Whonix Workstation domUs, as it denies an adversary access to a remarkably effective biometric fingerprinting mechanism they could otherwise access without specialized tools. Kloak is currently able to operate directly in Qubes domUs if (and only if!) `gui-agent-virtual-input-device` is enabled for the domU in question. Even in these instances, only keyboard events are anonymized, and additionally the domU must have an evdev X driver installed. This is less than ideal from a functionality standpoint, and as @DemiMarie has explained in QubesOS/qubes-issues#8541 it will eventually stop working entirely. There's also the possibility of malware compromise in the domU resulting in the deanonymization of the user. For these reasons, enabling the use of evdev in domUs and running kloak in the domU is not a good solution. The other obvious option is to run kloak directly in dom0. This has several disadvantages: * kloak can now potentially wreak havoc on the user's ability to use their computer. If a bug in kloak locks up the keyboard, or the user does something inadvisable like setting a 20-second event delay, regaining control of the system could be difficult or impossible without doing a hard reset (or worse, booting an external USB in order to chroot into dom0 and disable kloak). * Application of kloak's functionality becomes all-or-nothing - you either anonymize all keyboard and mouse input everywhere, or you anonymize none of it. This could make management of dom0 annoying with larger delay times, and it could prevent the user from making use of applications or websites that require input pattern telemetry to function (such as some bank websites). * kloak's configuration options similarly apply globally. One might want a comfortable delay of only 25ms in a domU they expect to be safe, but wish to use an extremely long one like 1000ms in a domU they believe is compromised and actively exfiltrating data. With kloak running directly in dom0, this is impossible. This PR implements a third option - *inserting the functionality of kloak directly into qubes-gui-daemon.* kloak upstream never needs to be involved, only the functionality of it must be. This functionality I have termed "event buffering", and as this implementation works with X server events I have called it "event buffering", or "ebuf" for short (which is the term used for it in the code). Previously I had called this "X event buffering" and used "xbuf" for short, but as @3hhh pointed out that name would become inaccurate when this is ported to Wayland, so I changed it to "ebuf" so as to make the name be display server agnostic. By working inside the GUI daemon, the following advantages are gained: * No evdev support needed at all, we can work with X events instead. * The amount of additional code needed is smaller. * Per-VM application and configuration of event buffering is now possible - some VMs can use a small, comfortable delay, others can use a very long one, and others can skip delays entirely. * Even if something goes very wrong and buffering prevents the user from inputting anything into any Qube, the user retains control of dom0 and can recover their system from there. * A compromised domU with event buffering enabled will be less likely to leak valuable biometric info to the malware within the VM. # How it works Most of the code should be fairly self-explanatory. In a nutshell, we use a tail queue to store a list of delayed, scheduled X events. As events come from dom0 to a domU, they are captured, scheduled for release at a later time, and thrown into the queue. Events in the queue are regularly checked to see if their scheduled release time has arrived, and those events are released when appropriate. The scheduler inserts some random noise into the delays, making it difficult to uniquely identify the user's typing and mouse movement/usage patterns. By default, event buffering is disabled and all events are passed through without buffering. To enable it, one must use `qvm-features` to set `gui-ebuf-max-delay` to a value greater than 0. It is worth noting that 0 is interpreted not as a "don't add any delay when buffering events", but rather it is interpreted as "don't buffer events at all". This configuration feature **does not work** without the `ebuf_max_events` setting being added to the list of GUI daemon configuration settings in qubes-core-admin-client. The pull request for that is at QubesOS/qubes-core-admin-client#309. This PR needs more testing (especially on Qubes R4.3), but it is solid enough that I feel comfortable asking for a review on it. Thanks for your help!
2 parents 7f440ea + 0d83229 commit 5668522

File tree

5 files changed

+166
-19
lines changed

5 files changed

+166
-19
lines changed

gui-common/txrx-vchan.c

+3-12
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ int vchan_is_closed = 0;
3838
*/
3939
int double_buffered = 1;
4040

41-
static int wait_for_vchan_or_argfd_once(libvchan_t *vchan, int fd);
42-
4341
void vchan_register_at_eof(void (*new_vchan_at_eof)(void)) {
4442
vchan_at_eof = new_vchan_at_eof;
4543
}
@@ -99,7 +97,7 @@ int read_data(libvchan_t *vchan, char *buf, int size)
9997
int ret;
10098
while (written < size) {
10199
while (!libvchan_data_ready(vchan))
102-
wait_for_vchan_or_argfd_once(vchan, -1);
100+
wait_for_vchan_or_argfd_once(vchan, -1, 1000);
103101
ret = libvchan_read(vchan, buf + written, size - written);
104102
if (ret <= 0)
105103
handle_vchan_error(vchan, "read data");
@@ -109,15 +107,15 @@ int read_data(libvchan_t *vchan, char *buf, int size)
109107
return size;
110108
}
111109

112-
static int wait_for_vchan_or_argfd_once(libvchan_t *vchan, int fd)
110+
int wait_for_vchan_or_argfd_once(libvchan_t *vchan, int fd, int timeout)
113111
{
114112
int ret;
115113
write_data(vchan, NULL, 0); // trigger write of queued data, if any present
116114
struct pollfd fds[] = {
117115
{ .fd = libvchan_fd_for_select(vchan), .events = POLLIN, .revents = 0 },
118116
{ .fd = fd, .events = POLLIN, .revents = 0 },
119117
};
120-
ret = poll(fds, fd == -1 ? 1 : 2, 1000);
118+
ret = poll(fds, fd == -1 ? 1 : 2, timeout);
121119
if (ret < 0) {
122120
if (errno == EINTR)
123121
return -1;
@@ -140,10 +138,3 @@ static int wait_for_vchan_or_argfd_once(libvchan_t *vchan, int fd)
140138
}
141139
return ret;
142140
}
143-
144-
int wait_for_vchan_or_argfd(libvchan_t *vchan, int fd)
145-
{
146-
int ret;
147-
while ((ret=wait_for_vchan_or_argfd_once(vchan, fd)) == 0);
148-
return ret;
149-
}

gui-daemon/guid.conf

+6
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,10 @@ global: {
8585
# 256 to 256000 characters. Default is 64000 characters.
8686
#
8787
# max_clipboard_size = 64000
88+
89+
# Maximum delay in milliseconds for event buffering (used for obfuscating
90+
# biometric data that could be obtained by observing keyboard and mouse
91+
# patterns). Set to 0 to disable event buffering entirely.
92+
#
93+
# events_max_delay = 0;
8894
}

gui-daemon/xside.c

+137-6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
#include <sys/wait.h>
3838
#include <sys/resource.h>
3939
#include <sys/uio.h>
40+
#include <sys/queue.h>
41+
#include <sys/random.h>
4042
#include <signal.h>
4143
#include <poll.h>
4244
#include <errno.h>
@@ -2592,11 +2594,85 @@ static void process_xevent_xembed(Ghandles * g, const XClientMessageEvent * ev)
25922594

25932595
}
25942596

2597+
/* get current time */
2598+
static int64_t ebuf_current_time_ms()
2599+
{
2600+
int64_t timeval;
2601+
struct timespec spec;
2602+
clock_gettime(CLOCK_MONOTONIC, &spec);
2603+
timeval = (((int64_t)spec.tv_sec) * 1000LL) + (((int64_t)spec.tv_nsec) / 1000000LL);
2604+
return timeval;
2605+
}
2606+
2607+
/* get random delay value */
2608+
static uint32_t ebuf_random_delay(uint32_t upper_bound, uint32_t lower_bound)
2609+
{
2610+
uint32_t maxval;
2611+
uint32_t randval;
2612+
size_t randsize;
2613+
union ebuf_rand ebuf_rand_data;
2614+
2615+
if (lower_bound >= upper_bound) {
2616+
if (lower_bound > upper_bound) {
2617+
fprintf(stderr,
2618+
"Bug detected - lower_bound > upper_bound, events may get briefly stuck");
2619+
}
2620+
return upper_bound;
2621+
}
2622+
2623+
maxval = upper_bound - lower_bound + 1;
2624+
2625+
do {
2626+
randsize = getrandom(ebuf_rand_data.raw, sizeof(uint32_t), 0);
2627+
if (randsize != sizeof(uint32_t))
2628+
continue;
2629+
} while (ebuf_rand_data.val > UINT32_MAX - ((uint32_t)((((uint64_t)UINT32_MAX) + 1) % maxval)));
2630+
2631+
randval = ebuf_rand_data.val % maxval;
2632+
return lower_bound + randval;
2633+
}
2634+
2635+
/* queue input event */
2636+
static void ebuf_queue_xevent(Ghandles * g, XEvent xev)
2637+
{
2638+
int64_t current_time;
2639+
uint32_t random_delay;
2640+
struct ebuf_entry *new_ebuf_entry;
2641+
uint32_t lower_bound;
2642+
2643+
current_time = ebuf_current_time_ms();
2644+
2645+
/*
2646+
* Each event is scheduled by taking the current time and adding a delay.
2647+
* We do not want events later in the queue having a release timestamp
2648+
* that is *less* than an event earlier in the queue. This means that
2649+
* whatever delay we add *must* be at least enough to give a release
2650+
* timestamp larger than the one generated last time. To facilitate this,
2651+
* we set lower_bound to the scheduled release time of the last event
2652+
* minus the current time. Some sanity checks are included to make sure
2653+
* lower_bound is never less than 0 or greater than ebuf_max_delay.
2654+
*/
2655+
lower_bound = min(max(g->ebuf_prev_release_time - current_time, 0), g->ebuf_max_delay);
2656+
2657+
random_delay = ebuf_random_delay(g->ebuf_max_delay, lower_bound);
2658+
new_ebuf_entry = malloc(sizeof(struct ebuf_entry));
2659+
if (new_ebuf_entry == NULL) {
2660+
perror("Could not allocate ebuf_entry:");
2661+
exit(1);
2662+
}
2663+
if (current_time > 0 && random_delay > (INT64_MAX - current_time)) {
2664+
fprintf(stderr, "Event scheduler overflow detected, cannot continue");
2665+
exit(1);
2666+
}
2667+
new_ebuf_entry->time = current_time + random_delay;
2668+
new_ebuf_entry->xev = xev;
2669+
TAILQ_INSERT_TAIL(&(g->ebuf_head), new_ebuf_entry, entries);
2670+
g->ebuf_prev_release_time = new_ebuf_entry->time;
2671+
}
2672+
25952673
/* dispatch local Xserver event */
2596-
static void process_xevent(Ghandles * g)
2674+
static void process_xevent_core(Ghandles * g, XEvent event_buffer)
25972675
{
2598-
XEvent event_buffer;
2599-
XNextEvent(g->display, &event_buffer);
26002676
switch (event_buffer.type) {
26012677
case KeyPress:
26022678
case KeyRelease:
@@ -2655,6 +2731,40 @@ static void process_xevent(Ghandles * g)
26552731
}
26562732
}
26572733

2734+
/* dispatch queued events */
2735+
static void ebuf_release_xevents(Ghandles * g)
2736+
{
2737+
int64_t current_time;
2738+
struct ebuf_entry *current_ebuf_entry;
2739+
2740+
current_time = ebuf_current_time_ms();
2741+
while ((current_ebuf_entry = TAILQ_FIRST(&(g->ebuf_head)))
2742+
&& (current_time >= current_ebuf_entry->time)) {
2743+
XEvent event_buffer = current_ebuf_entry->xev;
2744+
process_xevent_core(g, event_buffer);
2745+
TAILQ_REMOVE(&(g->ebuf_head), current_ebuf_entry, entries);
2746+
free(current_ebuf_entry);
2747+
}
2748+
current_ebuf_entry = TAILQ_FIRST(&(g->ebuf_head));
2749+
if (current_ebuf_entry == NULL) {
2750+
g->ebuf_next_timeout = VCHAN_DEFAULT_POLL_DURATION;
2751+
} else {
2752+
g->ebuf_next_timeout = (int)(current_ebuf_entry->time - current_time);
2753+
}
2754+
}
2755+
2756+
/* handle or queue local Xserver event */
2757+
static void process_xevent(Ghandles * g)
2758+
{
2759+
XEvent event_buffer;
2760+
XNextEvent(g->display, &event_buffer);
2761+
if (g->ebuf_max_delay > 0) {
2762+
ebuf_queue_xevent(g, event_buffer);
2763+
} else {
2764+
process_xevent_core(g, event_buffer);
2765+
}
2766+
}
2767+
26582768

26592769
/* handle VM message: MSG_SHMIMAGE
26602770
* pass message data to do_shm_update - there input validation will be done */
@@ -4477,11 +4587,23 @@ static void parse_vm_config(Ghandles * g, config_setting_t * group)
44774587
g->disable_override_redirect = 0;
44784588
else {
44794589
fprintf(stderr,
4480-
"unsupported value ‘%s’ for override_redirect (must be disabled or allow)\n",
4590+
"unsupported value '%s' for override_redirect (must be 'disabled' or 'allow')\n",
44814591
value);
44824592
exit(1);
44834593
}
44844594
}
4595+
4596+
if ((setting =
4597+
config_setting_get_member(group, "events_max_delay"))) {
4598+
int delay_val = config_setting_get_int(setting);
4599+
if (delay_val < 0 || delay_val > 5000) {
4600+
fprintf(stderr,
4601+
"unsupported value '%d' for events_max_delay (must be >= 0 and <= 5000)",
4602+
delay_val);
4603+
exit(1);
4604+
}
4605+
g->ebuf_max_delay = delay_val;
4606+
}
44854607
}
44864608

44874609
static void parse_config(Ghandles * g)
@@ -4604,11 +4726,13 @@ int main(int argc, char **argv)
46044726
/* parse cmdline, possibly overriding values from config */
46054727
parse_cmdline(&ghandles, argc, argv);
46064728
get_boot_lock(ghandles.domid);
4729+
/* init event queue */
4730+
TAILQ_INIT(&(ghandles.ebuf_head));
46074731

46084732
if (!ghandles.nofork) {
46094733
// daemonize...
46104734
if (pipe(pipe_notify) < 0) {
4611-
perror("canot create pipe:");
4735+
perror("cannot create pipe:");
46124736
exit(1);
46134737
}
46144738

@@ -4799,8 +4923,15 @@ int main(int argc, char **argv)
47994923
handle_message(&ghandles);
48004924
busy = 1;
48014925
}
4926+
if (ghandles.ebuf_max_delay > 0) {
4927+
ebuf_release_xevents(&ghandles);
4928+
}
48024929
} while (busy);
4803-
wait_for_vchan_or_argfd(ghandles.vchan, xfd);
4930+
if (ghandles.ebuf_max_delay > 0) {
4931+
wait_for_vchan_or_argfd_once(ghandles.vchan, xfd, ghandles.ebuf_next_timeout);
4932+
} else {
4933+
wait_for_vchan_or_argfd_once(ghandles.vchan, xfd, VCHAN_DEFAULT_POLL_DURATION);
4934+
}
48044935
}
48054936
return 0;
48064937
}

gui-daemon/xside.h

+19
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272

7373
#define MAX_SCREENSAVER_NAMES 10
7474

75+
#define VCHAN_DEFAULT_POLL_DURATION 1000
76+
7577
#ifdef __GNUC__
7678
# define UNUSED(x) UNUSED_ ## x __attribute__((__unused__))
7779
#else
@@ -82,6 +84,7 @@
8284
#include <stdbool.h>
8385
#include <assert.h>
8486
#include <unistd.h>
87+
#include <sys/queue.h>
8588
#include <libvchan.h>
8689
#include <X11/Xlib.h>
8790
#include <xcb/xcb.h>
@@ -139,6 +142,17 @@ struct extra_prop {
139142
int nelements; /* data size, in "format" units */
140143
};
141144

145+
struct ebuf_entry {
146+
XEvent xev;
147+
int64_t time;
148+
TAILQ_ENTRY(ebuf_entry) entries;
149+
};
150+
151+
union ebuf_rand {
152+
uint32_t val;
153+
char raw[sizeof(uint32_t)];
154+
};
155+
142156
/* global variables
143157
* keep them in this struct for readability
144158
*/
@@ -240,6 +254,11 @@ struct _global_handles {
240254
int xen_fd; /* O_PATH file descriptor to /dev/xen/gntdev */
241255
int xen_dir_fd; /* file descriptor to /dev/xen */
242256
bool permit_subwindows : 1; /* Permit subwindows */
257+
uint32_t ebuf_max_delay;
258+
/* ebuf state */
259+
TAILQ_HEAD(tailhead, ebuf_entry) ebuf_head;
260+
int64_t ebuf_prev_release_time;
261+
int ebuf_next_timeout;
243262
};
244263

245264
typedef struct _global_handles Ghandles;

include/txrx.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ int read_data(libvchan_t *vchan, char *buf, int size);
3333
x.untrusted_len = sizeof(y); \
3434
real_write_message(vchan, (char*)&x, sizeof(x), (char*)&y, sizeof(y)); \
3535
} while(0)
36-
int wait_for_vchan_or_argfd(libvchan_t *vchan, int fd);
36+
int wait_for_vchan_or_argfd_once(libvchan_t *vchan, int fd, int timeout);
3737
void vchan_register_at_eof(void (*new_vchan_at_eof)(void));
3838

3939
#endif /* _QUBES_TXRX_H */

0 commit comments

Comments
 (0)