commit eb80d5cd10d10158b39c344ad035afe8d31a899f Author: David Goulet dgoulet@ev0ke.net Date: Wed Oct 22 15:25:23 2014 -0400
Fix: improve Unix socket passing detection
This commit adds the support for the torsocks recvmsg wrapper to detect multiple FDs being passed through a Unix socket.
Furthermore, we now don't exit anymore but simply fire a debug message and return EACCES to the caller.
Finally, a test is added for inet socket passing detection called test_fd_passing.
Signed-off-by: David Goulet dgoulet@ev0ke.net --- .gitignore | 1 + src/lib/recv.c | 132 +++++++++--- tests/Makefile.am | 5 +- tests/test_fd_passing.c | 520 +++++++++++++++++++++++++++++++++++++++++++++++ tests/test_list | 1 + 5 files changed, 632 insertions(+), 27 deletions(-)
diff --git a/.gitignore b/.gitignore index ff6012e..c836b9b 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ src/bin/torsocks tests/test_connect tests/test_dns tests/test_socket +tests/test_fd_passing tests/unit/test_onion tests/unit/test_connection tests/unit/test_utils diff --git a/src/lib/recv.c b/src/lib/recv.c index 036fa91..b034d72 100644 --- a/src/lib/recv.c +++ b/src/lib/recv.c @@ -26,21 +26,60 @@ TSOCKS_LIBC_DECL(recvmsg, LIBC_RECVMSG_RET_TYPE, LIBC_RECVMSG_SIG)
/* + * This is the maximum hardcoded amount of fd that is possible to pass through + * a Unix socket in the Linux kernel. On FreeBSD for instance it's MLEN which + * is defined to MSIZE (256) minus the msg header size thus way below this + * Linux limit. Such a shame there is no way to dynamically get that value or + * get it in an exposed ABI... + */ +#define SCM_MAX_FD 253 + +/* + * Close all fds in the given array of size count. + */ +static void close_fds(int *fds, size_t count) +{ + int i; + + for (i = 0; i < count; i++) { + tsocks_libc_close(fds[i]); + } +} + +/* * Torsocks call for recvmsg(2) * * We only hijack this call to handle the FD passing between process on Unix * socket. If an INET/INET6 socket is recevied, we stop everything because at * that point we can't guarantee traffic going through Tor. + * + * Note that we don't rely on the given "msg" structure since it's controlled + * by the user and might not have been zeroed thus containing wrong values for + * ancillary data. Thus, we are going to expect SCM_MAX_FD and see what we can + * get from that if any. */ LIBC_RECVMSG_RET_TYPE tsocks_recvmsg(LIBC_RECVMSG_SIG) { - int fd; + int sock_domain; + socklen_t optlen; ssize_t ret = 0; - char dummy, recv_fd[CMSG_SPACE(sizeof(fd))]; + char dummy, recv_fd[CMSG_SPACE(SCM_MAX_FD)]; struct iovec iov[1]; struct cmsghdr *cmsg; struct msghdr msg_hdr;
+ /* Don't bother if the socket family is NOT Unix. */ + optlen = sizeof(sock_domain); + ret = getsockopt(sockfd, SOL_SOCKET, SO_DOMAIN, &sock_domain, &optlen); + if (ret < 0) { + DBG("[recvmsg] Fail getsockopt() on sock %d", sockfd); + errno = EBADF; + goto error; + } + if (sock_domain != AF_UNIX) { + goto libc; + } + memset(&msg_hdr, 0, sizeof(msg_hdr));
/* Prepare to receive the structures */ @@ -55,42 +94,83 @@ LIBC_RECVMSG_RET_TYPE tsocks_recvmsg(LIBC_RECVMSG_SIG) /* Just peek the data to inspect the payload for fd. */ ret = tsocks_libc_recvmsg(sockfd, &msg_hdr, MSG_PEEK); } while (ret < 0 && errno == EINTR); - + if (ret < 0) { + /* Use the current errno set by the call above. */ + goto error; + } cmsg = CMSG_FIRSTHDR(&msg_hdr); if (!cmsg) { - goto end; + /* No control message header, safe to pass to libc. */ + goto libc; + } + if (msg_hdr.msg_flags & MSG_CTRUNC) { + /* + * This means there are actually *more* data in the control thus + * exceeding somehow our hard limit of SCM_MAX_FD. In that case, return + * an error since we can't guarantee anything for socket passing + */ + errno = EMSGSIZE; + goto error; }
/* * Detecting FD passing, the next snippet of code will check if we get a - * inet/inet6 socket. If so, everything stops immediately before going - * further. + * inet/inet6 socket. If so, we are going to close the received socket, + * wipe clean the cmsg payload and return an unauthorized access code. */ if (cmsg->cmsg_type == SCM_RIGHTS || cmsg->cmsg_level == SOL_SOCKET) { - struct sockaddr addr; - socklen_t addrlen; - sa_family_t family = AF_UNSPEC; - - memcpy(&fd, CMSG_DATA(cmsg), sizeof(fd)); - - /* Get socket protocol family. */ - addrlen = sizeof(addr); - ret = getsockname(fd, &addr, &addrlen); - if (ret < 0) { - /* Use the getsockname() errno value. */ - goto end; - } - - family = addr.sa_family; - if (family == AF_INET || family == AF_INET6) { - ERR("[recvmsg] Inet socket passing detected. Aborting everything! " - "A non Tor socket could be used thus leaking information."); - exit(EXIT_FAILURE); + /* + * The kernel control that len value and there is a hard limit so no + * chance here of having a crazy high value that could exhaust the + * stack memory. + */ + size_t sizeof_fds = (cmsg->cmsg_len - sizeof(*cmsg)) / sizeof(int); + int i, fds[sizeof_fds]; + + memcpy(&fds, CMSG_DATA(cmsg), sizeof(fds)); + + /* + * For each received fds, we will inspect them to see if there is an + * inet socket in there and if so, we have to stop, close everything to + * avoid fd leak and return an error. + */ + for (i = 0; i < sizeof_fds; i++) { + struct sockaddr addr; + socklen_t addrlen = sizeof(addr); + + memset(&addr, 0, addrlen); + + /* Get socket protocol family. */ + ret = getsockname(fds[i], &addr, &addrlen); + if (ret < 0) { + /* Either a bad fd or not a socket. */ + continue; + } + + if (addr.sa_family == AF_INET || addr.sa_family == AF_INET6) { + DBG("[recvmsg] Inet socket passing detected. Denying it."); + /* We found socket, close everything and return error. */ + close_fds(fds, sizeof_fds); + /* + * The recv(2) man page does *not* mention that errno value + * however it's acceptable because Linux LSM can return this + * code if the access is denied in the application by a + * security module. We are basically simulating this here. + */ + errno = EACCES; + ret = -1; + goto error; + } } }
-end: + /* At this point, NO socket was detected, continue to the libc safely. */ + +libc: return tsocks_libc_recvmsg(LIBC_RECVMSG_ARGS); + +error: + return ret; }
/* diff --git a/tests/Makefile.am b/tests/Makefile.am index 141ac5e..b916134 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -6,7 +6,7 @@ LIBTAP=$(top_builddir)/tests/utils/tap/libtap.la
LIBTORSOCKS=$(top_builddir)/src/lib/libtorsocks.la
-noinst_PROGRAMS = test_dns test_socket test_connect +noinst_PROGRAMS = test_dns test_socket test_connect test_fd_passing
test_dns_SOURCES = test_dns.c test_dns_LDADD = $(LIBTAP) $(LIBTORSOCKS) @@ -17,6 +17,9 @@ test_socket_LDADD = $(LIBTAP) $(LIBTORSOCKS) test_connect_SOURCES = test_connect.c test_connect_LDADD = $(LIBTAP) $(LIBTORSOCKS)
+test_fd_passing_SOURCES = test_fd_passing.c +test_fd_passing_LDADD = $(LIBTAP) $(LIBTORSOCKS) -lpthread + check-am: ./run.sh test_list
diff --git a/tests/test_fd_passing.c b/tests/test_fd_passing.c new file mode 100644 index 0000000..1803126 --- /dev/null +++ b/tests/test_fd_passing.c @@ -0,0 +1,520 @@ +/* + * Copyright (C) 2014 - David Goulet dgoulet@ev0ke.net + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License, version 2 only, as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <arpa/inet.h> +#include <netinet/in.h> +#include <pthread.h> +#include <stdio.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <limits.h> +#include <sys/un.h> + +#include <lib/torsocks.h> + +#include <tap/tap.h> + +#define NUM_TESTS 5 + +/* + * Indicate if the thread recv is ready. 0 means no, 1 means yes and -1 means + * error occured. + */ +static volatile int thread_recv_ready; + +/* Unix socket for this test. */ +static const char *sockpath = "/tmp/torsocks-unix-fd-passing.sock"; + +/* Order libtap output. */ +static pthread_mutex_t tsocks_test_log = PTHREAD_MUTEX_INITIALIZER; +#define OK(cond, args...) \ + do { \ + pthread_mutex_lock(&tsocks_test_log); \ + ok(cond, ## args); \ + pthread_mutex_unlock(&tsocks_test_log); \ + } while (0); + +/* + * Send buf data of size len. Using sendmsg API. + * + * Return the size of sent data. + */ +static ssize_t send_unix_sock(int sock, void *buf, size_t len) +{ + struct msghdr msg; + struct iovec iov[1]; + ssize_t ret = -1; + + memset(&msg, 0, sizeof(msg)); + + iov[0].iov_base = buf; + iov[0].iov_len = len; + msg.msg_iov = iov; + msg.msg_iovlen = 1; + + ret = sendmsg(sock, &msg, 0); + if (ret < 0) { + /* + * Only warn about EPIPE when quiet mode is deactivated. + * We consider EPIPE as expected. + */ + if (errno != EPIPE) { + perror("sendmsg"); + } + } + + return ret; +} + +static ssize_t send_fds_unix_sock(int sock, int *fds, size_t nb_fd) +{ + struct msghdr msg; + struct cmsghdr *cmptr; + struct iovec iov[1]; + ssize_t ret = -1; + unsigned int sizeof_fds = nb_fd * sizeof(int); + char tmp[CMSG_SPACE(sizeof_fds)]; + char dummy = 0; + + memset(&msg, 0, sizeof(msg)); + + msg.msg_control = (caddr_t)tmp; + msg.msg_controllen = CMSG_LEN(sizeof_fds); + + cmptr = CMSG_FIRSTHDR(&msg); + cmptr->cmsg_level = SOL_SOCKET; + cmptr->cmsg_type = SCM_RIGHTS; + cmptr->cmsg_len = CMSG_LEN(sizeof_fds); + memcpy(CMSG_DATA(cmptr), fds, sizeof_fds); + /* Sum of the length of all control messages in the buffer: */ + msg.msg_controllen = cmptr->cmsg_len; + + iov[0].iov_base = &dummy; + iov[0].iov_len = 1; + msg.msg_iov = iov; + msg.msg_iovlen = 1; + + do { + ret = sendmsg(sock, &msg, 0); + } while (ret < 0 && errno == EINTR); + if (ret < 0) { + /* + * Only warn about EPIPE when quiet mode is deactivated. + * We consider EPIPE as expected. + */ + if (errno != EPIPE) { + perror("sendmsg"); + } + } + return ret; +} + +/* + * Receive data of size len in put that data into the buf param. Using recvmsg + * API. + * + * Return the size of received data. + */ +static ssize_t recv_unix_sock(int sock, void *buf, size_t len) +{ + struct msghdr msg; + struct iovec iov[1]; + ssize_t ret = -1; + size_t len_last; + + memset(&msg, 0, sizeof(msg)); + + iov[0].iov_base = buf; + iov[0].iov_len = len; + msg.msg_iov = iov; + msg.msg_iovlen = 1; + + do { + len_last = iov[0].iov_len; + ret = recvmsg(sock, &msg, 0); + if (ret > 0) { + iov[0].iov_base += ret; + iov[0].iov_len -= ret; + assert(ret <= len_last); + } + } while ((ret > 0 && ret < len_last) || (ret < 0 && errno == EINTR)); + if (ret < 0) { + perror("recvmsg"); + } else if (ret > 0) { + ret = len; + } + /* Else ret = 0 meaning an orderly shutdown. */ + + return ret; +} + +/* + * Recv a message accompanied by fd(s) from a unix socket. + * + * Returns the size of received data, or negative error value. + * + * Expect at most "nb_fd" file descriptors. Returns the number of fd + * actually received in nb_fd. + */ +static ssize_t recv_fds_unix_sock(int sock, int *fds, size_t nb_fd) +{ + struct iovec iov[1]; + ssize_t ret = 0; + struct cmsghdr *cmsg; + size_t sizeof_fds = nb_fd * sizeof(int); + char recv_fd[CMSG_SPACE(sizeof_fds)]; + struct msghdr msg; + char dummy; + + memset(&msg, 0, sizeof(msg)); + + /* Prepare to receive the structures */ + iov[0].iov_base = &dummy; + iov[0].iov_len = 1; + msg.msg_iov = iov; + msg.msg_iovlen = 1; + msg.msg_control = recv_fd; + msg.msg_controllen = sizeof(recv_fd); + + do { + ret = recvmsg(sock, &msg, 0); + } while (ret < 0 && errno == EINTR); + if (ret < 0) { + goto end; + } + if (ret != 1) { + fprintf(stderr, "Error: Received %zd bytes, expected %d\n", + ret, 1); + goto end; + } + if (msg.msg_flags & MSG_CTRUNC) { + fprintf(stderr, "Error: Control message truncated.\n"); + ret = -1; + goto end; + } + cmsg = CMSG_FIRSTHDR(&msg); + if (!cmsg) { + fprintf(stderr, "Error: Invalid control message header\n"); + ret = -1; + goto end; + } + if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) { + fprintf(stderr, "Didn't received any fd\n"); + ret = -1; + goto end; + } + if (cmsg->cmsg_len != CMSG_LEN(sizeof_fds)) { + fprintf(stderr, "Error: Received %zu bytes of ancillary data, expected %zu\n", + (size_t) cmsg->cmsg_len, (size_t) CMSG_LEN(sizeof_fds)); + ret = -1; + goto end; + } + memcpy(fds, CMSG_DATA(cmsg), sizeof_fds); + ret = sizeof_fds; +end: + return ret; +} + +/* + * Connect to unix socket using the path name. + */ +static int connect_unix_sock(const char *pathname) +{ + struct sockaddr_un sun; + int fd, ret, closeret; + + fd = socket(PF_UNIX, SOCK_STREAM, 0); + if (fd < 0) { + perror("socket"); + ret = fd; + goto error; + } + + memset(&sun, 0, sizeof(sun)); + sun.sun_family = AF_UNIX; + strncpy(sun.sun_path, pathname, sizeof(sun.sun_path)); + sun.sun_path[sizeof(sun.sun_path) - 1] = '\0'; + + ret = connect(fd, (struct sockaddr *) &sun, sizeof(sun)); + if (ret < 0) { + /* + * Don't print message on connect error, because connect is used in + * normal execution to detect if sessiond is alive. + */ + goto error_connect; + } + + return fd; + +error_connect: + closeret = close(fd); + if (closeret) { + perror("close"); + } +error: + return ret; +} + +/* + * Creates a AF_UNIX local socket using pathname bind the socket upon creation + * and return the fd. + */ +static int create_unix_sock(const char *pathname) +{ + struct sockaddr_un sun; + int fd; + int ret = -1; + + /* Create server socket */ + if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) { + perror("socket"); + goto error; + } + + memset(&sun, 0, sizeof(sun)); + sun.sun_family = AF_UNIX; + strncpy(sun.sun_path, pathname, sizeof(sun.sun_path)); + sun.sun_path[sizeof(sun.sun_path) - 1] = '\0'; + + /* Unlink the old file if present */ + (void) unlink(pathname); + ret = bind(fd, (struct sockaddr *) &sun, sizeof(sun)); + if (ret < 0) { + perror("bind"); + goto error; + } + + return fd; + +error: + return ret; +} + +/* + * Do an accept(2) on the sock and return the new file descriptor. The socket + * MUST be bind(2) before. + */ +static int accept_unix_sock(int sock) +{ + int new_fd; + struct sockaddr_un sun; + socklen_t len = 0; + + /* Blocking call */ + new_fd = accept(sock, (struct sockaddr *) &sun, &len); + if (new_fd < 0) { + perror("accept"); + } + + return new_fd; +} + +void *thread_recv(void *data) +{ + int ret, new_sock, sock, fds[3] = {-1, -1, -1}; + char buf[4]; + ssize_t len; + + sock = create_unix_sock(sockpath); + if (sock < 0) { + fail("Create unix socket at %s", sockpath); + goto error; + } + + OK(sock >= 0, "Unix socket %d created at %s", sock, sockpath); + + ret = listen(sock, 10); + if (ret < 0) { + fail("Listen on unix socket %d", sock); + goto error; + } + + /* Notify we are ready to test. */ + thread_recv_ready = 1; + + new_sock = accept_unix_sock(sock); + if (new_sock < 0) { + fail("Accept on unix sock %d", sock); + close(sock); + goto error; + } + + /* First receive a normal message saying "hello" to make sure the recvmsg + * call is not borked. */ + len = recv_unix_sock(new_sock, buf, sizeof(buf)); + if (len < 0) { + fail("Recv normal data failed"); + goto error; + } + OK(len == sizeof(buf) && + strncmp(buf, "hello", sizeof(buf)) == 0, + "Data received successfully"); + + len = recv_fds_unix_sock(new_sock, fds, 3); + if (len < 0) { + /* This is suppose to fail with a errno set to EACCESS. */ + OK(errno == EACCES, + "Passing INET socket denied."); + } else { + fail("Received INET socket through the unix socket"); + } + + close(sock); + close(new_sock); + close(fds[0]); + close(fds[1]); + close(fds[2]); + +error: + thread_recv_ready = -1; + return NULL; +} + +void *thread_send(void *data) +{ + int sock, fds[3], pipe_fds[2]; + ssize_t len; + + sock = connect_unix_sock(sockpath); + if (sock < 0) { + fail("Unable to connect to unix socket at %s", sockpath); + goto error; + } + + if (pipe(pipe_fds) < 0) { + fail("Unable to create pipe"); + goto error; + } + + /* First send regular data. */ + len = send_unix_sock(sock, "hello", 4); + if (len < 0) { + fail("Sending regular data."); + goto error; + } + + /* + * We are going to pass 3 fds, two of them are pipse in position 0 and 2 + * and the inet socket is at position 1. + */ + fds[0] = pipe_fds[0]; + fds[1] = *((int *)data); + fds[2] = pipe_fds[1]; + + len = send_fds_unix_sock(sock, fds, 3); + if (len < 0) { + fail("Send inet socket through Unix sock"); + goto error; + } + OK(len == 1, "Inet socket %d sent successfully.", fds[1]); + +error: + if (sock >= 0) { + close(sock); + } + if (pipe_fds[0] >= 0) { + close(pipe_fds[0]); + } + if (pipe_fds[1] >= 0) { + close(pipe_fds[1]); + } + return NULL; +} + +/* + * This test will spawn two thread, one accepting a Unix socket connection + * which will recv the fd(s). The second thread will connect and send the fds. + * Usually this is between processes but for the sake of the test threads are + * enough. + */ +static void test_inet_socket(void) +{ + int ret, i, inet_sock = -1; + void *status; + pthread_t th[2]; + struct sockaddr_in addr; + const char *ip = "93.95.227.222"; + + /* + * First of all, we are going to try to create an inet socket to a public + * known IP being www.torproject.org --> 93.95.227.222. + */ + inet_sock = socket(AF_INET, SOCK_STREAM, 0); + if (inet_sock < 0) { + fail("Creating inet socket"); + goto error; + } + + addr.sin_family = AF_INET; + addr.sin_port = htons(443); + inet_pton(addr.sin_family, ip, &addr.sin_addr); + memset(addr.sin_zero, 0, sizeof(addr.sin_zero)); + + ret = connect(inet_sock, (struct sockaddr *) &addr, sizeof(addr)); + if (ret < 0) { + fail("Unable to connect inet socket"); + goto error; + } + + OK(!ret, "Inet socket %d created connected to %s", inet_sock, ip); + + ret = pthread_create(&th[0], NULL, thread_recv, NULL); + if (ret < 0) { + fail("pthread_create thread recv"); + goto error; + } + + /* Active wait for the thread recv to be ready. */ + while (thread_recv_ready == 0) { + continue; + } + + if (thread_recv_ready == -1) { + goto error; + } + + ret = pthread_create(&th[1], NULL, thread_send, (void *) &inet_sock); + if (ret < 0) { + fail("pthread_create thread send"); + goto error; + } + + for (i = 0; i < 2; i++) { + ret = pthread_join(th[i], &status); + if (ret < 0) { + perror("pthread_join"); + } + } + +error: + if (inet_sock >= 0) { + close(inet_sock); + } + unlink(sockpath); + return; +} + +int main(int argc, char **argv) +{ + /* Libtap call for the number of tests planned. */ + plan_tests(NUM_TESTS); + + test_inet_socket(); + + return 0; +} diff --git a/tests/test_list b/tests/test_list index bb812b1..fb320db 100644 --- a/tests/test_list +++ b/tests/test_list @@ -1,5 +1,6 @@ ./test_connect ./test_dns +./test_fd_passing ./test_socket ./unit/test_onion ./unit/test_connection
tor-commits@lists.torproject.org