From 3361b4030b85d1f024d3e096a34a39f5e5ebeab2 Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Wed, 15 Feb 2017 16:25:49 -0800 Subject: [PATCH] Integrate and test linux privilege drop code (from contributor PR). It works now if a "zerotier-one" user is present on a Linux system. Does everything automagically. --- make-linux.mk | 4 +- one.cpp | 154 +++++++++++++++++++++++++++++-- osdep/LinuxDropPrivileges.cpp | 164 ---------------------------------- osdep/LinuxDropPrivileges.hpp | 9 -- 4 files changed, 148 insertions(+), 183 deletions(-) delete mode 100644 osdep/LinuxDropPrivileges.cpp delete mode 100644 osdep/LinuxDropPrivileges.hpp diff --git a/make-linux.mk b/make-linux.mk index 8ee0f88c2..29d19830f 100644 --- a/make-linux.mk +++ b/make-linux.mk @@ -107,8 +107,8 @@ endif all: one -one: $(OBJS) service/OneService.o one.o osdep/LinuxEthernetTap.o osdep/LinuxDropPrivileges.o - $(CXX) $(CXXFLAGS) $(LDFLAGS) -o zerotier-one $(OBJS) service/OneService.o one.o osdep/LinuxEthernetTap.o osdep/LinuxDropPrivileges.o $(LDLIBS) +one: $(OBJS) service/OneService.o one.o osdep/LinuxEthernetTap.o + $(CXX) $(CXXFLAGS) $(LDFLAGS) -o zerotier-one $(OBJS) service/OneService.o one.o osdep/LinuxEthernetTap.o $(LDLIBS) $(STRIP) zerotier-one ln -sf zerotier-one zerotier-idtool ln -sf zerotier-one zerotier-cli diff --git a/one.cpp b/one.cpp index 377b85d32..3b1d6b4ac 100644 --- a/one.cpp +++ b/one.cpp @@ -43,10 +43,15 @@ #include #include #include +#include +#include #include - -#ifdef __linux__ -#include "osdep/LinuxDropPrivileges.hpp" +#ifdef __LINUX__ +#include +#include +#include +#include +#include #endif #endif @@ -875,6 +880,142 @@ static void _sighandlerQuit(int sig) } #endif +// Drop privileges on Linux, if supported by libc etc. and "zerotier-one" user exists on system +#ifdef __LINUX__ +#ifdef PR_CAP_AMBIENT +#define ZT_LINUX_USER "zerotier-one" +#define ZT_HAVE_DROP_PRIVILEGES 1 +namespace { + +// libc doesn't export capset, it is instead located in libcap +// We ignore libcap and call it manually. +struct cap_header_struct { + __u32 version; + int pid; +}; +struct cap_data_struct { + __u32 effective; + __u32 permitted; + __u32 inheritable; +}; +static inline int _zt_capset(cap_header_struct* hdrp, cap_data_struct* datap) { return syscall(SYS_capset, hdrp, datap); } + +static void _notDropping(const char *procName,const std::string &homeDir) +{ + struct stat buf; + if (lstat(homeDir.c_str(),&buf) < 0) { + if (buf.st_uid != 0 || buf.st_gid != 0) { + fprintf(stderr, "%s: FATAL: failed to drop privileges and can't run as root since privileges were previously dropped (home directory not owned by root)" ZT_EOL_S,procName); + exit(1); + } + } + fprintf(stderr, "%s: WARNING: failed to drop privileges (kernel may not support required prctl features), running as root" ZT_EOL_S,procName); +} + +static int _setCapabilities(int flags) +{ + cap_header_struct capheader = {_LINUX_CAPABILITY_VERSION_1, 0}; + cap_data_struct capdata; + capdata.inheritable = capdata.permitted = capdata.effective = flags; + return _zt_capset(&capheader, &capdata); +} + +static void _recursiveChown(const char *path,uid_t uid,gid_t gid) +{ + struct dirent de; + struct dirent *dptr; + lchown(path,uid,gid); + DIR *d = opendir(path); + if (!d) + return; + dptr = (struct dirent *)0; + for(;;) { + if (readdir_r(d,&de,&dptr) != 0) + break; + if (!dptr) + break; + if ((strcmp(dptr->d_name,".") != 0)&&(strcmp(dptr->d_name,"..") != 0)&&(strlen(dptr->d_name) > 0)) { + std::string p(path); + p.push_back(ZT_PATH_SEPARATOR); + p.append(dptr->d_name); + _recursiveChown(p.c_str(),uid,gid); // will just fail and return on regular files + } + } + closedir(d); +} + +static void dropPrivileges(const char *procName,const std::string &homeDir) +{ + if (getuid() != 0) + return; + + // dropPrivileges switches to zerotier-one user while retaining CAP_NET_ADMIN + // and CAP_NET_RAW capabilities. + struct passwd *targetUser = getpwnam(ZT_LINUX_USER); + if (!targetUser) + return; + + if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_IS_SET, CAP_NET_RAW, 0, 0) < 0) { + // Kernel has no support for ambient capabilities. + _notDropping(procName,homeDir); + return; + } + if (prctl(PR_SET_SECUREBITS, SECBIT_KEEP_CAPS | SECBIT_NOROOT) < 0) { + _notDropping(procName,homeDir); + return; + } + + // Change ownership of our home directory if everything looks good (does nothing if already chown'd) + _recursiveChown(homeDir.c_str(),targetUser->pw_uid,targetUser->pw_gid); + + if (_setCapabilities((1 << CAP_NET_ADMIN) | (1 << CAP_NET_RAW) | (1 << CAP_SETUID) | (1 << CAP_SETGID)) < 0) { + _notDropping(procName,homeDir); + return; + } + + int oldDumpable = prctl(PR_GET_DUMPABLE); + if (prctl(PR_SET_DUMPABLE, 0) < 0) { + // Disable ptracing. Otherwise there is a small window when previous + // compromised ZeroTier process could ptrace us, when we still have CAP_SETUID. + // (this is mitigated anyway on most distros by ptrace_scope=1) + fprintf(stderr,"%s: FATAL: prctl(PR_SET_DUMPABLE) failed while attempting to relinquish root permissions" ZT_EOL_S,procName); + exit(1); + } + + // Relinquish root + if (setgid(targetUser->pw_gid) < 0) { + perror("setgid"); + exit(1); + } + if (setuid(targetUser->pw_uid) < 0) { + perror("setuid"); + exit(1); + } + + if (_setCapabilities((1 << CAP_NET_ADMIN) | (1 << CAP_NET_RAW)) < 0) { + fprintf(stderr,"%s: FATAL: unable to drop capabilities after relinquishing root" ZT_EOL_S,procName); + exit(1); + } + + if (prctl(PR_SET_DUMPABLE, oldDumpable) < 0) { + fprintf(stderr,"%s: FATAL: prctl(PR_SET_DUMPABLE) failed while attempting to relinquish root permissions" ZT_EOL_S,procName); + exit(1); + } + + if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_NET_ADMIN, 0, 0) < 0) { + fprintf(stderr,"%s: FATAL: prctl(PR_CAP_AMBIENT,PR_CAP_AMBIENT_RAISE,CAP_NET_ADMIN) failed while attempting to relinquish root permissions" ZT_EOL_S,procName); + exit(1); + } + if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_NET_RAW, 0, 0) < 0) { + fprintf(stderr,"%s: FATAL: prctl(PR_CAP_AMBIENT,PR_CAP_AMBIENT_RAISE,CAP_NET_RAW) failed while attempting to relinquish root permissions" ZT_EOL_S,procName); + exit(1); + } +} + +} // anonymous namespace +#endif // PR_CAP_AMBIENT +#endif // __LINUX__ + /****************************************************************************/ /* Windows helper functions and signal handlers */ /****************************************************************************/ @@ -1283,11 +1424,8 @@ int main(int argc,char **argv) #ifdef __UNIX_LIKE__ -#ifndef ZT_ONE_RUN_AS_ROOT -#ifdef __linux__ - if (!skipRootCheck) - dropPrivileges(homeDir); -#endif +#ifdef ZT_HAVE_DROP_PRIVILEGES + dropPrivileges(argv[0],homeDir); #endif std::string pidPath(homeDir + ZT_PATH_SEPARATOR_S + ZT_PID_PATH); diff --git a/osdep/LinuxDropPrivileges.cpp b/osdep/LinuxDropPrivileges.cpp deleted file mode 100644 index e2688e65a..000000000 --- a/osdep/LinuxDropPrivileges.cpp +++ /dev/null @@ -1,164 +0,0 @@ -#include "LinuxDropPrivileges.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace ZeroTier { - -#ifndef PR_CAP_AMBIENT -// if we are on old libc, dropPrivileges is nop -void dropPrivileges(std::string homeDir) {} - -#else - -const char* TARGET_USER_NAME = "zerotier-one"; - -struct cap_header_struct { - __u32 version; - int pid; -}; - -struct cap_data_struct { - __u32 effective; - __u32 permitted; - __u32 inheritable; -}; - -// libc doesn't export capset, it is instead located in libcap -// We ignore libcap and call it manually. - -int capset(cap_header_struct* hdrp, cap_data_struct* datap) { - return syscall(SYS_capset, hdrp, datap); -} - -void notDropping(std::string homeDir) { - struct stat buf; - if (lstat(homeDir.c_str(), &buf) < 0) { - if (buf.st_uid != 0 || buf.st_gid != 0) { - fprintf(stderr, "ERROR: failed to drop privileges. Refusing to run as root, because %s was already used in nonprivileged mode.\n", homeDir.c_str()); - exit(1); - } - } - fprintf(stderr, "WARNING: failed to drop privileges, running as root\n"); -} - -int setCapabilities(int flags) { - cap_header_struct capheader = {_LINUX_CAPABILITY_VERSION_1, 0}; - cap_data_struct capdata; - capdata.inheritable = capdata.permitted = capdata.effective = flags; - return capset(&capheader, &capdata); -} - -void createOwnedHomedir(std::string homeDir, struct passwd* targetUser) { - struct stat buf; - if (lstat(homeDir.c_str(), &buf) < 0) { - if (errno == ENOENT) { - mkdir(homeDir.c_str(), 0755); - } else { - perror("cannot access home directory"); - exit(1); - } - } - - if (buf.st_uid != 0 || buf.st_gid != 0) { - // should be already owned by zerotier-one - if (targetUser->pw_uid != buf.st_uid) { - fprintf(stderr, "ERROR: %s not owned by zerotier-one or root\n", homeDir.c_str()); - exit(1); - } - return; - } - - // Change homedir owner to zerotier-one user. This is safe, because this directory is writable only by root, so no one could have created malicious hardlink. - long p = (long)fork(); - int exitcode = -1; - if (p > 0) { - waitpid(p, &exitcode, 0); - } else if (p == 0) { - std::string ownerString = std::to_string(targetUser->pw_uid) + ":" + std::to_string(targetUser->pw_gid); - execlp("chown", "chown", "-R", ownerString.c_str(), "--", homeDir.c_str(), NULL); - _exit(-1); - } - - if (exitcode != 0) { - fprintf(stderr, "failed to change owner of %s to %s\n", homeDir.c_str(), targetUser->pw_name); - exit(1); - } -} - -void dropPrivileges(std::string homeDir) { - // dropPrivileges switches to zerotier-one user while retaining CAP_NET_ADMIN - // and CAP_NET_RAW capabilities. - struct passwd* targetUser = getpwnam(TARGET_USER_NAME); - if (targetUser == NULL) { - // zerotier-one user not configured by package - return; - } - - createOwnedHomedir(homeDir, targetUser); - - if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_IS_SET, CAP_NET_RAW, 0, 0) < 0) { - // Kernel has no support for ambient capabilities. - notDropping(homeDir); - return; - } - - if (prctl(PR_SET_SECUREBITS, SECBIT_KEEP_CAPS | SECBIT_NOROOT) < 0) { - notDropping(homeDir); - return; - } - - if (setCapabilities((1 << CAP_NET_ADMIN) | (1 << CAP_NET_RAW) | (1 << CAP_SETUID) | (1 << CAP_SETGID)) < 0) { - fprintf(stderr, "ERROR: failed to set capabilities (not running as real root?)\n"); - exit(1); - } - - int oldDumpable = prctl(PR_GET_DUMPABLE); - - if (prctl(PR_SET_DUMPABLE, 0) < 0) { - // Disable ptracing. Otherwise there is a small window when previous - // compromised ZeroTier process could ptrace us, when we still have CAP_SETUID. - // (this is mitigated anyway on most distros by ptrace_scope=1) - perror("prctl(PR_SET_DUMPABLE)"); - exit(1); - } - - if (setgid(targetUser->pw_gid) < 0) { - perror("setgid"); - exit(1); - } - if (setuid(targetUser->pw_uid) < 0) { - perror("setuid"); - exit(1); - } - - if (setCapabilities((1 << CAP_NET_ADMIN) | (1 << CAP_NET_RAW)) < 0) { - perror("could not drop capabilities after setuid"); - exit(1); - } - - if (prctl(PR_SET_DUMPABLE, oldDumpable) < 0) { - perror("could not restore dumpable flag"); - exit(1); - } - - if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_NET_ADMIN, 0, 0) < 0) { - perror("could not raise ambient CAP_NET_ADMIN"); - exit(1); - } - - if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_NET_RAW, 0, 0) < 0) { - perror("could not raise ambient CAP_NET_RAW"); - exit(1); - } -} - -#endif -} diff --git a/osdep/LinuxDropPrivileges.hpp b/osdep/LinuxDropPrivileges.hpp deleted file mode 100644 index 111f682e7..000000000 --- a/osdep/LinuxDropPrivileges.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef ZT_LINUXDROPPRIVILEGES_HPP -#define ZT_LINUXDROPPRIVILEGES_HPP -#include - -namespace ZeroTier { - void dropPrivileges(std::string homeDir); -} - -#endif