/* * SSLsplit - transparent SSL/TLS interception * Copyright (c) 2009-2016, Daniel Roethlisberger * All rights reserved. * http://www.roe.ch/SSLsplit * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions, and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "sys.h" #include "log.h" #include "defaults.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef _SC_NPROCESSORS_ONLN #include #endif /* !_SC_NPROCESSORS_ONLN */ #if HAVE_DARWIN_LIBPROC #include #endif #include /* * Permanently drop from root privileges to an unprivileged user account. * Sets the real, effective and stored user and group ID and the list of * ancillary groups. This is only safe if the effective user ID is 0. * If username is unset and the effective uid != uid, drop privs to uid. * This is to support setuid bit configurations. * If groupname is set, it will be used instead of the user's default primary * group. * If jaildir is set, also chroot to jaildir after reading system files * but before dropping privileges. * Returns 0 on success, -1 on failure. */ int sys_privdrop(const char *username, const char *groupname, const char *jaildir) { struct passwd *pw = NULL; struct group *gr = NULL; int ret = -1; if (groupname) { errno = 0; if (!(gr = getgrnam(groupname))) { log_err_printf("CRITICAL: Failed to getgrnam group '%s': %s\n", groupname, strerror(errno)); goto error; } } if (username) { errno = 0; if (!(pw = getpwnam(username))) { log_err_printf("CRITICAL: Failed to getpwnam user '%s': %s\n", username, strerror(errno)); goto error; } if (gr != NULL) { pw->pw_gid = gr->gr_gid; } if (initgroups(username, pw->pw_gid) == -1) { log_err_printf("CRITICAL: Failed to initgroups user '%s': %s\n", username, strerror(errno)); goto error; } } if (jaildir) { if (chroot(jaildir) == -1) { log_err_printf("CRITICAL: Failed to chroot to '%s': %s\n", jaildir, strerror(errno)); goto error; } if (chdir("/") == -1) { log_err_printf("CRITICAL: Failed to chdir to '/': %s\n", strerror(errno)); goto error; } } if (username) { if (setgid(pw->pw_gid) == -1) { log_err_printf("CRITICAL: Failed to setgid to %i: %s\n", pw->pw_gid, strerror(errno)); goto error; } if (setuid(pw->pw_uid) == -1) { log_err_printf("CRITICAL: Failed to setuid to %i: %s\n", pw->pw_uid, strerror(errno)); goto error; } } else if (getuid() != geteuid()) { if (setuid(getuid()) == -1) { log_err_printf("CRITICAL: Failed to setuid(getuid()): %s\n", strerror(errno)); goto error; } } ret = 0; error: if (pw) { endpwent(); } return ret; } /* * Returns 1 if username can be loaded from user database, 0 otherwise. */ int sys_isuser(const char *username) { errno = 0; if (!getpwnam(username)) { if (errno != 0 && errno != ENOENT) { log_err_printf("CRITICAL: Failed to load user '%s': %s (%i)\n", username, strerror(errno), errno); } return 0; } endpwent(); return 1; } /* * Returns 1 if groupname can be loaded from group database, 0 otherwise. */ int sys_isgroup(const char *groupname) { errno = 0; if (!getgrnam(groupname)) { if (errno != 0 && errno != ENOENT) { log_err_printf("CRITICAL: Failed to load group '%s': %s (%i)\n", groupname, strerror(errno), errno); } return 0; } return 1; } /* * Open and lock process ID file fn. * Returns open file descriptor on success or -1 on errors. */ int sys_pidf_open(const char *fn) { int fd; if ((fd = open(fn, O_RDWR|O_CREAT, DFLT_PIDFMODE)) == -1) { log_err_printf("CRITICAL: Failed to open '%s': %s\n", fn, strerror(errno)); return -1; } if (flock(fd, LOCK_EX|LOCK_NB) == -1) { log_err_printf("CRITICAL: Failed to lock '%s': %s\n", fn, strerror(errno)); close(fd); return -1; } return fd; } /* * Write process ID to open process ID file descriptor fd. * Returns 0 on success, -1 on errors. */ int sys_pidf_write(int fd) { char pidbuf[4*sizeof(pid_t)]; int rv; rv = snprintf(pidbuf, sizeof(pidbuf), "%d\n", getpid()); if (rv == -1 || rv >= (int)sizeof(pidbuf)) return -1; write(fd, pidbuf, strlen(pidbuf)); fsync(fd); fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC); return 0; } /* * Close and remove open process ID file before quitting. */ void sys_pidf_close(int fd, const char *fn) { unlink(fn); close(fd); } /* * Converts a local uid into a printable string representation. * Returns an allocated buffer which must be freed by caller, or NULL on error. */ char * sys_user_str(uid_t uid) { static int bufsize = 0; if (!bufsize) { /* on some platforms this compiles, but does not succeed */ if ((bufsize = sysconf(_SC_GETPW_R_SIZE_MAX)) == -1) { bufsize = 64; } } char *buf, *newbuf; struct passwd pwd, *result = NULL; int rv; char *name; if (!(buf = malloc(bufsize))) return NULL; do { rv = getpwuid_r(uid, &pwd, buf, bufsize, &result); if (rv == 0) { if (result) { name = strdup(pwd.pw_name); free(buf); return name; } free(buf); /* no entry found; return the integer representation */ if (asprintf(&name, "%llu", (long long) uid) < 0) { return NULL; } return name; } bufsize *= 2; if (!(newbuf = realloc(buf, bufsize))) { free(buf); return NULL; } buf = newbuf; } while (rv == ERANGE); free(buf); log_err_printf("CRITICAL: Failed to lookup uid: %s (%i)\n", strerror(rv), rv); return NULL; } /* * Converts a local gid into a printable string representation. * Returns an allocated buffer which must be freed by caller, or NULL on error. */ char * sys_group_str(gid_t gid) { static int bufsize = 0; if (!bufsize) { /* on some platforms this compiles, but does not succeed */ if ((bufsize = sysconf(_SC_GETGR_R_SIZE_MAX)) == -1) { bufsize = 64; } } char *buf, *newbuf; struct group grp, *result = NULL; int rv; char *name; if (!(buf = malloc(bufsize))) return NULL; do { rv = getgrgid_r(gid, &grp, buf, bufsize, &result); if (rv == 0) { if (result) { name = strdup(grp.gr_name); free(buf); return name; } free(buf); /* no entry found; return the integer representation */ if (asprintf(&name, "%llu", (long long) gid) < 0) { return NULL; } return name; } bufsize *= 2; if (!(newbuf = realloc(buf, bufsize))) { free(buf); return NULL; } buf = newbuf; } while (rv == ERANGE); free(buf); log_err_printf("CRITICAL: Failed to lookup gid: %s (%i)\n", strerror(rv), rv); return NULL; } /* * Parse an ascii host/IP and port tuple into a sockaddr_storage. * On success, returns address family and fills in addr, addrlen. * Returns -1 on error. */ int sys_sockaddr_parse(struct sockaddr_storage *addr, socklen_t *addrlen, char *naddr, char *nport, int af, int flags) { struct evutil_addrinfo hints; struct evutil_addrinfo *ai; int rv; memset(&hints, 0, sizeof(hints)); hints.ai_family = af; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; hints.ai_flags = EVUTIL_AI_ADDRCONFIG | flags; rv = evutil_getaddrinfo(naddr, nport, &hints, &ai); if (rv != 0) { log_err_printf("CRITICAL: Cannot resolve address '%s' port '%s': %s\n", naddr, nport, gai_strerror(rv)); return -1; } memcpy(addr, ai->ai_addr, ai->ai_addrlen); *addrlen = ai->ai_addrlen; af = ai->ai_family; freeaddrinfo(ai); return af; } /* * Converts an IPv4/IPv6 sockaddr into printable string representations of the * host and the service (port) part. Writes allocated buffers to *host and * *serv which must both be freed by the caller. Neither *host nor *port are * freed by this function before newly allocating. * Returns 0 on success, -1 otherwise. When -1 is returned, pointers in *host * and *serv are invalid and must not be used nor freed by the caller. */ int sys_sockaddr_str(struct sockaddr *addr, socklen_t addrlen, char **host, char **serv) { char tmphost[INET6_ADDRSTRLEN]; int rv; size_t hostsz; *serv = malloc(6); /* max decimal digits of short plus terminator */ if (!*serv) { log_err_printf("CRITICAL: Cannot allocate memory\n"); return -1; } rv = getnameinfo(addr, addrlen, tmphost, sizeof(tmphost), *serv, 6, NI_NUMERICHOST | NI_NUMERICSERV); if (rv != 0) { log_err_printf("CRITICAL: Cannot get nameinfo for socket address: %s\n", gai_strerror(rv)); free(*serv); return -1; } hostsz = strlen(tmphost) + 1; /* including terminator */ *host = malloc(hostsz); if (!*host) { log_err_printf("CRITICAL: Cannot allocate memory\n"); free(*serv); return -1; } memcpy(*host, tmphost, hostsz); return 0; } /* * Sanitizes a valid IPv4 or IPv6 address for use in a filename, i.e. removes * characters that are invalid on NTFS and replaces them with more innocent * characters. The function assumes that the input is a valid IPv4 or IPv6 * address; it is not a generic filename sanitizer. * * Returns a copy of string s that must be freed by the caller. * * Invalid NTFS characters are < > : " / \ | ? * according to * https://msdn.microsoft.com/en-gb/library/windows/desktop/aa365247.aspx */ char * sys_ip46str_sanitize(const char *s) { char *copy, *p; copy = strdup(s); if (!copy) return NULL; p = copy; while (*p) { switch (*p) { case ':': case '%': *p = '_'; break; } p++; } return copy; } /* * Returns 1 if path points to an existing directory node in the filesystem. * Returns 0 if path is NULL, does not exist, or points to a file of some kind. */ int sys_isdir(const char *path) { struct stat s; if (stat(path, &s) == -1) { if (errno != ENOENT) { log_err_printf("CRITICAL: Error stating file: %s (%i)\n", strerror(errno), errno); } return 0; } if (s.st_mode & S_IFDIR) return 1; return 0; } /* * Create directory including parent directories with mode_t. * Mode of existing parent directories is not changed. * Returns 0 on success, -1 and sets errno on error. */ int sys_mkpath(const char *path, mode_t mode) { char parent[strlen(path)+1]; char *p; memcpy(parent, path, sizeof(parent)); p = parent; do { /* skip leading '/' characters */ while (*p == '/') p++; p = strchr(p, '/'); if (p) { /* overwrite '/' to terminate the string at the next * parent directory */ *p = '\0'; } struct stat sbuf; if (stat(parent, &sbuf) == -1) { if (errno == ENOENT) { if (mkdir(parent, mode) != 0) return -1; } else { return -1; } } else if (!S_ISDIR(sbuf.st_mode)) { errno = ENOTDIR; return -1; } if (p) { /* replace the overwritten slash */ *p = '/'; p++; } } while (p); return 0; } /* * Iterate over all files in a directory hierarchy, calling the callback * cb for each file, passing the filename and arg as arguments. Files and * directories beginning with a dot are skipped, symlinks are followed. */ int sys_dir_eachfile(const char *dirname, sys_dir_eachfile_cb_t cb, void *arg) { FTS *tree; FTSENT *node; char * paths[2]; int rv = 0; paths[1] = NULL; paths[0] = strdup(dirname); if (!paths[0]) return -1; tree = fts_open(paths, FTS_NOCHDIR | FTS_LOGICAL, NULL); if (!tree) { log_err_printf("CRITICAL: Cannot open directory '%s': %s\n", dirname, strerror(errno)); rv = -1; goto out1; } while ((node = fts_read(tree))) { if (node->fts_level > 0 && node->fts_name[0] == '.') fts_set(tree, node, FTS_SKIP); else if (node->fts_info & FTS_F) { rv = cb(node->fts_path, arg); if (rv == -1) goto out2; } } if (errno) { log_err_printf("CRITICAL: Error reading directory entry: %s\n", strerror(errno)); rv = -1; goto out2; } out2: fts_close(tree); out1: free(paths[0]); return rv; } /* * Portably get the number of CPU cores online in the system. */ uint32_t sys_get_cpu_cores(void) { #ifdef _SC_NPROCESSORS_ONLN return sysconf(_SC_NPROCESSORS_ONLN); #else /* !_SC_NPROCESSORS_ONLN */ int mib[2]; uint32_t n; size_t len = sizeof(n); mib[0] = CTL_HW; mib[1] = HW_AVAILCPU; sysctl(mib, sizeof(mib)/sizeof(int), &n, &len, NULL, 0); if (n < 1) { mib[1] = HW_NCPU; sysctl(mib, sizeof(mib)/sizeof(int), &n, &len, NULL, 0); if (n < 1) { n = 1; } } return n; #endif /* !_SC_NPROCESSORS_ONLN */ } /* * Send a message and optional file descriptor on a connected AF_UNIX * SOCKET_DGRAM socket s. Returns the return value of sendmsg(). * If fd is -1, no file descriptor is passed. */ ssize_t sys_sendmsgfd(int sock, void *buf, size_t bufsz, int fd) { struct iovec iov; struct msghdr msg; struct cmsghdr *cmsg; char cmsgbuf[CMSG_SPACE(sizeof(int))]; ssize_t n; iov.iov_base = buf; iov.iov_len = bufsz; msg.msg_name = NULL; msg.msg_namelen = 0; msg.msg_iov = &iov; msg.msg_iovlen = 1; if (fd != -1) { msg.msg_control = cmsgbuf; msg.msg_controllen = sizeof(cmsgbuf); cmsg = CMSG_FIRSTHDR(&msg); if (!cmsg) return -1; cmsg->cmsg_len = CMSG_LEN(sizeof(int)); cmsg->cmsg_level = SOL_SOCKET; cmsg->cmsg_type = SCM_RIGHTS; *((int *) CMSG_DATA(cmsg)) = fd; } else { msg.msg_control = NULL; msg.msg_controllen = 0; } do { #ifdef MSG_NOSIGNAL n = sendmsg(sock, &msg, MSG_NOSIGNAL); #else /* !MSG_NOSIGNAL */ n = sendmsg(sock, &msg, 0); #endif /* !MSG_NOSIGNAL */ } while (n == -1 && errno == EINTR); return n; } /* * Receive a message and optional file descriptor on a connected AF_UNIX * SOCKET_DGRAM socket s. Returns the return value of recvmsg()/recv() * and sets errno to EINVAL if the received message is malformed. * If pfd is NULL, no file descriptor is received; if a file descriptor was * part of the received message and pfd is NULL, then the kernel will close it. */ ssize_t sys_recvmsgfd(int sock, void *buf, size_t bufsz, int *pfd) { ssize_t n; if (pfd) { struct iovec iov; struct msghdr msg; struct cmsghdr *cmsg; unsigned char cmsgbuf[CMSG_SPACE(sizeof(int))]; iov.iov_base = buf; iov.iov_len = bufsz; msg.msg_name = NULL; msg.msg_namelen = 0; msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = cmsgbuf; msg.msg_controllen = sizeof(cmsgbuf); do { n = recvmsg(sock, &msg, 0); } while (n == -1 && errno == EINTR); if (n <= 0) return n; cmsg = CMSG_FIRSTHDR(&msg); if (cmsg && cmsg->cmsg_len == CMSG_LEN(sizeof(int))) { if (cmsg->cmsg_level != SOL_SOCKET) { errno = EINVAL; return -1; } if (cmsg->cmsg_type != SCM_RIGHTS) { errno = EINVAL; return -1; } *pfd = *((int *) CMSG_DATA(cmsg)); } else { *pfd = -1; } } else { do { n = recv(sock, buf, bufsz, 0); } while (n == -1 && errno == EINTR); } return n; } /* * Format AF_UNIX socket address into printable string. * Returns newly allocated string that must be freed by caller. */ static char * sys_afunix_str(struct sockaddr *addr, socklen_t addrlen) { struct sockaddr_un *sun = (struct sockaddr_un *)addr; char *name; if (addrlen == sizeof(sa_family_t)) { asprintf(&name, "unnmd"); } else if (sun->sun_path[0] == '\0') { /* abstract sockets is a Linux feature */ asprintf(&name, "abstr:%02x:%02x:%02x:%02x", sun->sun_path[1], sun->sun_path[2], sun->sun_path[3], sun->sun_path[4]); } else { asprintf(&name, "pname:%s", sun->sun_path); } return name; } /* * Dump all open file descriptors to stdout - poor man's lsof/fstat/sockstat */ void sys_dump_fds(void) { int maxfd = 0; #ifdef F_MAXFD if (!maxfd && ((maxfd = fcntl(0, F_MAXFD)) == -1)) { fprintf(stderr, "fcntl(0, F_MAXFD) failed: %s (%i)\n", strerror(errno), errno); } #endif /* F_MAXFD */ #ifdef _SC_OPEN_MAX if (!maxfd && ((maxfd = sysconf(_SC_OPEN_MAX)) == -1)) { fprintf(stderr, "sysconf(_SC_OPEN_MAX) failed: %s (%i)\n", strerror(errno), errno); } #endif /* _SC_OPEN_MAX */ if (!maxfd) maxfd = 65535; for (int fd = 0; fd <= maxfd; fd++) { struct stat st; if (fstat(fd, &st) == -1) { continue; } printf("%5d:", fd); switch (st.st_mode & S_IFMT) { case S_IFBLK: printf(" blkdev"); break; case S_IFCHR: printf(" chrdev"); break; case S_IFDIR: printf(" dir "); break; case S_IFIFO: printf(" fifo "); break; case S_IFLNK: printf(" lnkfil"); break; case S_IFREG: printf(" regfil"); break; case S_IFSOCK: printf(" socket"); break; default: printf(" unknwn"); break; } if ((st.st_mode & S_IFMT) == S_IFSOCK) { int lrv, frv; struct sockaddr_storage lss, fss; socklen_t lsslen = sizeof(lss); socklen_t fsslen = sizeof(fss); char *laddrstr, *faddrstr; lrv = getsockname(fd, (struct sockaddr *)&lss, &lsslen); frv = getpeername(fd, (struct sockaddr *)&fss, &fsslen); switch (lss.ss_family) { case AF_INET: case AF_INET6: { if (lrv == 0) { char *host, *port; if (sys_sockaddr_str( (struct sockaddr *)&lss, lsslen, &host, &port) != 0) { laddrstr = strdup("?"); } else { asprintf(&laddrstr, "[%s]:%s", host, port); free(host); free(port); } } else { laddrstr = strdup("n/a"); } if (frv == 0) { char *host, *port; if (sys_sockaddr_str( (struct sockaddr *)&fss, fsslen, &host, &port) != 0) { faddrstr = strdup("?"); } else { asprintf(&faddrstr, "[%s]:%s", host, port); free(host); free(port); } } else { faddrstr = strdup("n/a"); } printf(" %-6s %s -> %s", lss.ss_family == AF_INET ? "in" : "in6", laddrstr, faddrstr); free(laddrstr); free(faddrstr); break; } case AF_UNIX: { if (lrv == 0) { laddrstr = sys_afunix_str((struct sockaddr *)&lss, lsslen); } else { laddrstr = strdup("n/a"); } if (frv == 0) { faddrstr = sys_afunix_str((struct sockaddr *)&fss, fsslen); } else { faddrstr = strdup("n/a"); } printf(" unix %s -> %s", laddrstr, faddrstr); free(laddrstr); free(faddrstr); break; } case AF_UNSPEC: { printf(" unspec"); break; } default: printf(" (%i)", lss.ss_family); } } printf("\n"); } } /* vim: set noet ft=c: */