/* [ʞ] nkvd.c - XDG directory enforcer * ~ lexi hale * © AGPLv3 * $ cc -fPIC -pie -shared nkvd.c -Wl,-E -onkvd -ldl * [-D_NO_GNU [-D_LIBC=…]] [-D_CONF_HOME=…] * $ export LD_PRELOAD=nkvd.so * * ! NOTE: for unknown reasons, nkvd currently only works * when built without optimizations. it's probably that * wrecker Trotsky's fault. i'm working on it. * - partial solution on GCC: using #pragma to disable * optimization around the sensitive code. unclear if * this bug affects other compilers. * - bizarre complication: the bug can only be reproduced * if the -O flag itself is present; turning on the * same individual optimization flags that -O is * supposed to represent does not trigger the problem. * - conclusion: compiler devils * * ! WARNING: test this program thoroughly before you start * exporting it from your shellrcs; if things get fucked, * you may not be able to execute any binaries with the * variable set, and will need to log in as root or boot * off a rescue disk to fix the problem. LD_PRELOAD is * not a toy. you have been warned. * * ! WARNING: while the code *should* theoretically work * with non-GNU libcs, i don't have access to any * computers running under such a configuration, so i was * only able to test that the compilation process doesn't * explode. there are likely to be bugs. reports & merge * requests especially are, as always, welcome. * * ? tired of programs disrespecting your XDG configuration * and shitting all over home sweet ~ ? fear no more! * nkvd is your very own digital thug to whip those * dissidents into line like the counterrevolutionary * scum they are. simply command LD to preload nkvd.so, * and it will wiretap startup, then intercept any calls * to open(2) (not just fopen) to make sure they * point where they're supposed to. nkvd's behavior is * controlled by a number of environment variables: * * - XDG_CONFIG_HOME specifies the redirection target, * to, but can be overridden with nkvd_gulag. if * neither are set, $HOME/.config will be used * instead. * * - nkvd_hq specifies which directory to protect from * cyberkulaks. it defaults to your home directory. * attempts to access dotfiles or dotfolders with * appropriate names in this directory will be * redirected. * * - nkvd_subversives tells nkvd which programs to * interdict. it is a colon-separated list of names. * to determine if a program is on the List, nkvd * will check the value of basename(argv[0]) against * its members. if nkvd_subversives is unset, nkvd * will interdict all programs except for system * utilities like rm and ls. the first character of * nkvd_subversives must be either "+", in which case * it functions as a blacklist, or a "-", in which * case it functions as a whitelist. * * - nkvd_interdict_all tells nkvd which dotfile * accesses to redirect. by default, it will * redirect access to files or folders whose paths * begin with the string $HOME/.(basename(argv[0])), * instead returning fds to $XDG_CONFIG_HOME/$1. if * nkvd_interdict_all is set, it will redirect ALL * accesses to files whose paths begin with * $HOME/. this is a particularly extreme mode * to operate nkvd in and it is liable to cause * serious problems, in particular for any programs * attempting to access files in ~/.config * or ~/.local, another XDG directory. it is * recommended that nkvd_interdict_all should only be * used with a carefully selected blacklist! * * NOTE: if you are compiling nkvd.c for a libc other than * glibc, be sure to pass the flag -D_NO_GNU to the * compiler to ensure appropriate behavior at runtime. * * TODO: extend nkvd to enforce other XDG directories? * * TODO: build labor power, arm the workers, and smash the * bourgeoisie to impose a dictatorship of the * proletariat. * * TODO: figure out a way to deal with {open,fstat,unlink}at(2) * * TODO: do a better job of canonicalizing paths, in case * anyone tries to be tricky. (alas, realpath() et al * are not viable for these purposes as they only work * for files already in existence.) * * TODO: -pie allows the program to be run as a standalone * binary. exploit this; allow it to launch other * binaries with itself as LD_PRELOAD? detecting this * state of affairs will likely only be possible * through checking if LD_PRELOAD is empty. due to the * difficulty of determining a binary's path on linux, * this may not be feasible. switch back to -shared * and delete -Wl,-E if so. * * TODO: support a mappings file/variable for those programs * too special to simply name their config file * "~/.(argv[0])". * * TODO: exempt xdg dirs beginning with ~/. from proscription * in nkvd_interdict_all=1 mode * * TODO: instead of function passthrough, alter environment * to delete LD_PRELOAD and re-exec whitelisted apps * without nkvd loading at all */ #include #include #include #include #include #include #include #ifdef _NO_GNU /* the POSIX version of basename is a goddamn mess and * it's better to use the glibc version where possible. */ # include # define SYMSRC hnd #else /* glibc's basename() is imported from via * the following directive. i have no idea how this * magic works unless they're abusing asm declarations */ # define _GNU_SOURCE /* for basename() */ # define __USE_GNU /* for RTLD_NEXT */ # ifdef _USE_RTLD_NEXT # define SYMSRC RTLD_NEXT # else # define SYMSRC hnd # endif #endif #include #include #ifndef _LIBC # define _LIBC "libc.so.6" #endif #ifndef _CONF_HOME # define _CONF_HOME ".config" #endif #define pstr(x) x,sizeof x #define hl(txt) "\x1b[1;31m" txt "\x1b[m" #define bold(txt) "\x1b[1m" txt "\x1b[21m" #define e_fatal hl("(nkvd fatal)") " " #define fail(code, err) { write(2,pstr(e_fatal " " err)); _exit(code); } #define dbg(x) write(2,pstr(bold("(nkvd)") " " x)) typedef _Bool bool; enum { false = 0, true = 1 }; static bool intercept; static int wiretap_argc = 0; static const char** wiretap_argv = NULL; static const char* redir_prefix; static const char* redir_to; static size_t redir_len; static const char* hq; static size_t hq_len; static void configdirs() { const char* nkvd = getenv("nkvd_gulag"); if (nkvd != NULL) redir_to = nkvd, redir_len = strlen(nkvd); const char* xdg = getenv("XDG_CONFIG_HOME"); if (xdg != NULL) redir_to = xdg, redir_len = strlen(xdg); const char* home = getenv("HOME"); if (home == NULL) { const char* user = getenv("USER"); struct passwd* p; if (user == NULL) { uid_t u = geteuid(); p = getpwuid(u); } else p = getpwnam(user); if(p == NULL) fail(-5, "XDG_CONFIG_HOME, HOME, and USER " "environment variables are all unset, and no home " "directory can be identified for your user ID. " "something is seriously wrong with your system, " "comrade. bailing."); home = p -> pw_dir; } size_t homelen = strlen(home); const char* h = getenv("nkvd_hq"); if (h != NULL) hq = h, hq_len = strlen(h); else hq = strdup(home), hq_len = homelen; # define CONFDIR "/" _CONF_HOME "/" char* buf = malloc(homelen + sizeof CONFDIR); /* since this will be used throughout the lifetime of the * program, we don't need to free it, and it would actually * just slow down the exit process to do so. */ strcpy(buf, home); strcpy(buf + homelen, CONFDIR); redir_len = homelen + sizeof CONFDIR; # undef CONFDIR redir_to = buf; } static bool checkpath(const char* path, size_t len, char* gulag) { char c[hq_len + len + 4]; strncpy(c, hq, hq_len); c[hq_len] = '/'; c[hq_len+1] = '.'; c[hq_len+2] = 0; const char* all = getenv("nkvd_interdict_all"); size_t clen; if (all == NULL || !(all[0] == '1' && all[1] == 0)) { strcpy(c + hq_len + 2, wiretap_argv[0]); clen = strlen(c); c[clen] = '/'; c[clen+1] = 0; if (len > clen) ++clen; } else { clen = strlen(c); } if (strncmp(path, c, clen) == 0) { /* guilty as sin, your honor */ strncpy(gulag,redir_to,redir_len); strncpy(gulag + redir_len - 1, path + hq_len + 2, len - (hq_len + 1)); return true; } else { /* no further questions, citizen */ return false; } } static bool interrogate(const char* path, size_t plen, char* gulag) { /* papers, please */ if (path[0] != '/') for (size_t i = 64; i (b) ? (a) : (b)) int rename(const char* volatile from, const char* volatile to) { if (intercept) { const size_t fromlen = strlen(from), tolen = strlen(from); char fromgulag[redir_len + tolen ], togulag[redir_len + fromlen]; const char* fromfinal, * tofinal; if (interrogate(from, fromlen, fromgulag)) fromfinal = fromgulag; else fromfinal = from; if (interrogate(to, tolen, togulag)) tofinal = togulag; else tofinal = to; return (*libc_rename)(fromfinal,tofinal); } return (*libc_rename)(from,to); }; int open(const char* volatile path, int flags, ...) { /* fuck you for using varargs, asshole */ va_list v; va_start(v,flags); mode_t m; if(flags & O_CREAT) m = va_arg(v, mode_t); va_end(v); if (intercept) { size_t plen = strlen(path); char gulag[redir_len + plen]; if (interrogate(path, plen, gulag)) { return (*libc_open)(gulag, flags, m); } } return (*libc_open)(path, flags, m); } #include int nkvd_init(int argc, const char** argv) { wiretap_argc = argc; wiretap_argv = argv; # ifndef _USE_RTLD_NEXT /* RTLD_NEXT is buggy as hell so we avoid it by default */ void* SYMSRC = dlopen(_LIBC,RTLD_LAZY); if (SYMSRC == NULL) fail(-2,"compiled in non-RTLD_NEXT mode and cannot load " "libc from " bold(_LIBC) "; please recompile " " and specify your libc with -D_LIBC= on the " "compile command line."); # endif # define load_fn(fn,pfx) libc_##fn = dlsym(SYMSRC, #pfx #fn) load_fn(open,); load_fn(unlink,); load_fn(access,); load_fn(eaccess,); load_fn(rename,); load_fn(stat,); load_fn(lstat,); load_fn(stat64,); load_fn(lstat64,); # ifndef _NO_GNU load_fn(xstat,__); load_fn(lxstat,__); load_fn(xstat64,__); load_fn(lxstat64,__); # endif # undef load_fn # define dump(v) printf("-- " #v ": %p", libc_##v) dump(lstat); dump(lstat64); dump(lxstat); dump(lxstat64); /* have enough stat fns been found for us to proceed? */ bool statfns = ( (libc_stat && libc_lstat) || (libc_stat64 && libc_lstat64) #ifndef _NO_GNU || (libc_xstat && libc_lxstat) || (libc_xstat64 && libc_lxstat64) #endif ); if (! (libc_open && statfns && libc_unlink && libc_access && libc_eaccess && libc_rename)) fail(-3, "your libc is defective. bailing."); intercept = shitlist(argv[0]); if (intercept) { configdirs(); } return 0; /* ?? why is this int, not void */ } __attribute__((section(".init_array"))) static void* nkvd_constructor = &nkvd_init; int main() { return 0; } /* stub for -pie */