Working Around Skype's Privacy Hole with LD_PRELOAD

This post will explain how to use LD_PRELOAD on Linux to solve a problem with a program we don't have the source for. LD_PRELOAD is a Linux feature that lets you override library functions in programs.

Skype has an issue that exposes any user's IP address publicly to anyone on the internet, not just your contacts list, with no way to block this as long as you're signed in. This was publicly pointed out more than a year ago, and there has been no progress in the bug being fixed since then. A variety of DDOS-for-hire ("network stresser") services have popped up where you can simply give them your target's Skype username and they'll resolve the IP and DDOS them to take them offline. (2018 update: I haven't been able to find any still-working Skype user IP resolvers online any more, so maybe the root issue has been addressed since. Anyway, I've entirely replaced my use of Skype with Discord since this post. I'm leaving this post mostly as-is since the specifics of how to use LD_PRELOAD still apply.)

I understand Skype partially uses peer-to-peer technology, so it makes sense that people I accept to my contacts list or people I chat with may be able to resolve my IP address, but there is no need for it to be completely public.

I've managed servers on the receiving end of DDOS attacks, and I've known people hit directly by DDOS attacks after having their IP resolved via Skype. The last thing I need is a trojan disguised as a chat program exposing my IP so someone can launch a DDOS attack against my internet connection while I'm trying to defend and reconfigure a server! But I have too many people I can only contact over Skype, and Skype's group text conversation support is pretty good and I don't know of any good alternatives. (No, IRC with a bouncer service is not a solution I can convince all of my contacts to set up!)

So my only choice is to somehow work around the issue. Looking through Skype's settings, it appears to have proxy support. If I could tunnel my Skype connection through my VPS, then only my VPS's IP would possibly be exposed, and its IP is already necessarily public. (Skype might have a bit more latency being proxied, but I mainly use Skype's text chat so I don't mind.) Having my VPS DDOSed isn't quite as bad as getting my personal connection attacked. I configured an SSH dynamic tunnel through my VPS (ssh -D7070 myvps; this opens a SOCKS proxy on my local computer which forwards its traffic through the SSH connection), and set it as a SOCKS proxy in Skype's settings.

...and then the Skype resolver tools I tried out still managed to resolve my personal IP. Turns out, Skype only obeys the proxy settings configured in it if it can't establish a direct connection! It completely ignores those settings if a regular connection works!

If the Skype application itself won't honor proxy settings, then we'll force it to, by blocking it from connecting to anything besides the proxy. (Disclaimer: I'm on Linux, so everything from this point on is Linux-specific.)

My first thought was to see if I could use iptables to do this, but as far as I can tell, it doesn't appear that iptables can have rules specific to an application. (I think it can have rules specific to a user or group, but I do not want to lock down my whole account, and I prefer not to create some goofy solution where I somehow run Skype as a different user from within my main account.)

My next thought was to use AppArmor. While a lot of traditional system security is based on restricting users from accessing each other's things, AppArmor is built for restricting applications from accessing files (even belonging to the same user) that they shouldn't be. If your PDF viewer is buggy and you view a PDF file with an exploit, then AppArmor can prevent the exploit from tricking your PDF viewer into infecting executables in your ~/bin directory for example. But after spending a long time going through AppArmor's documentation and trying to get its examples to work, I find out that the features to restrict access by host to applications are only planned and not yet implemented. (While I was at it, I found an older decent AppArmor profile for restricting Skype's file access to the minimum, and patched it up a bit for the current version, so that exploration wasn't a total loss! Make sure to give Skype mr permission to the .so file you make later!)

Enter LD_PRELOAD

Dynamically linked executables load a lot of their code from shared libraries installed on the system. The C and C++ standard libraries and various GUI libraries are very common. Usually these libraries are found and loaded automatically by the linker, but you can customize this by using some environment variables. For example, the LD_LIBRARY_PATH environment variable can be used to add more directories to search for libraries.

Another interesting environment variable is LD_PRELOAD. It can specify a list of shared libraries (usually .so files, separated by spaces) which are loaded before any other libraries when an executable is started. These preloaded libraries then can override functions defined in other libraries. We can use LD_PRELOAD to make a simple library which overrides and intercepts Skype's calls to internet-related functions.

First, we have to make sure the Skype executable is dynamically linked:

$ which skype
/usr/bin/skype
$ file /usr/bin/skype
/usr/bin/skype: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x06628ce1adec6427dabb21f1ab71194752a638ee, stripped

Looks good! If we were told it was statically linked, then we would have to try modifying the executable directly (which would be especially painful because it's stripped of debugging symbols), or go back to exploring kernel-based options like iptables or AppArmor.

Now we need to figure out what functions we need to override. As most C socket tutorials will show, the function that starts a TCP connection is connect. The connect function is passed a socket file descriptor, and a sockaddr struct which describes the address we're trying to connect to. We want to intercept all calls to connect, and return -1 for connections not to localhost (where ssh has my tunnel opened; alternatively, another address for a remote proxy could be made allowed. Allowing localhost too is still probably a good idea just in case Skype wants to communicate with itself on localhost). For the allowed connections, we'll want our custom connect function to fall through and call the old real connect function. We can get a function pointer to the old connect function by using the dlsym function with the RTLD_NEXT parameter, which causes the linker to skip the current library (or else we would just get a pointer back to our own custom connect function again!). Note that according to the man page, we'll need to #define _GNU_SOURCE before #include <dlfcn.h> in order to use the RTLD_NEXT parameter.

One more thing: UDP is connectionless, so it doesn't use the connect function. Instead, all of its packets are sent with the sendto function, which specifies its destination in a sockaddr struct just like connect, so we'll want to override sendto similarly. (I don't know if Skype supports UDP over SOCKS or if SSH SOCKS tunnels support UDP, but Skype appears to use sendto for localhost connections or local non-IP endpoints, so we still want sendto to fuction for those cases, rather than completely blocking it.)

Here's the completed library code for blocking non-localhost connections. Notice that it never blocks connections where the sockaddr's family is neither AF_INET nor AF_INET6, because we don't ever want to block local things like AF_UNIX connections. (Feel free to use this code for any purpose.)

wrapper.c

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <dlfcn.h>

int addr_is_not_localhost(const struct sockaddr *addr) {
	if (addr->sa_family == AF_INET) {
		const struct sockaddr_in *addr4 = (struct sockaddr_in*) addr;
		if (addr4->sin_addr.s_addr != htonl(INADDR_LOOPBACK))
			return 1;
	} else if (addr->sa_family == AF_INET6) {
		const struct sockaddr_in6 *addr6 = (struct sockaddr_in6*) addr;
		if (memcmp(&addr6->sin6_addr, &in6addr_loopback, sizeof(struct in6_addr)) != 0)
			return 1;
	}
	return 0;
}

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
	fprintf(stderr, "connect(%d, %p, %d) called\n", sockfd, addr, (int) addrlen);

	static int (*oldconnect)(int sockfd, const struct sockaddr *addr, socklen_t addrlen) = NULL;
	if (!oldconnect) {
		dlerror();
		char *error;
		oldconnect = dlsym(RTLD_NEXT, "connect");
		if ((error = dlerror()) != NULL) {
			fprintf(stderr, "%s\n", error);
			exit(EXIT_FAILURE);
		}
	}

	if (addr_is_not_localhost(addr)) {
		fprintf(stderr, "Dropping non-localhost connection.\n");
		errno = EACCES;
		return -1;
	}

	return oldconnect(sockfd, addr, addrlen);
}

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
	       const struct sockaddr *dest_addr, socklen_t addrlen) {
	fprintf(stderr, "sendto(%d, %p, %d, %d, %p, %d) called\n",
		sockfd, buf, (int) len, flags, dest_addr, (int) addrlen);

	static ssize_t (*oldsendto)(int sockfd, const void *buf, size_t len, int flags,
				    const struct sockaddr *dest_addr, socklen_t addrlen) = NULL;
	if (!oldsendto) {
		dlerror();
		char *error;
		oldsendto = dlsym(RTLD_NEXT, "sendto");
		if ((error = dlerror()) != NULL) {
			fprintf(stderr, "%s\n", error);
			exit(EXIT_FAILURE);
		}
	}

	if (addr_is_not_localhost(dest_addr)) {
		fprintf(stderr, "Dropping non-localhost connection.\n");
		errno = EACCES;
		return -1;
	}

	return oldsendto(sockfd, buf, len, flags, dest_addr, addrlen);
}

And here's a Makefile, just because there's a few parameters you need to pass when building a library that you don't want to forget. Linking with -ldl in order to get dlsym, and -fPIC, -shared, and -rdynamic because we're compiling a library.

Makefile

CFLAGS=-Wall -m32
CC=gcc

all: wrapper.so

wrapper.o: wrapper.c
	$(CC) $(CFLAGS) -c -fPIC wrapper.c

wrapper.so: wrapper.o
	$(CC) $(CFLAGS) -shared -rdynamic -o wrapper.so wrapper.o -ldl

clean:
	rm -f *.o
	rm -f *.so

And using it:

$ make
gcc -Wall -m32 -c -fPIC wrapper.c
gcc -Wall -m32 -shared -rdynamic -o wrapper.so wrapper.o -ldl
$ LD_PRELOAD=./wrapper.so skype

Placing the library in a shared directory and editing Skype's .desktop launcher to always LD_PRELOAD it, auto-starting any necessary SSH SOCKS tunnels, or adding more allowed hosts are left as exercises for the reader.

Once Skype is proxied, it seems like Skype IP resolvers no longer list any IP address for my username. (I assume it's because of the lack of a direct UDP connection; I doubt Skype supports UDP over SOCKS and that my SSH tunnel supports UDP.) Success!

Further Resources

This article was discussed on reddit's /r/programming, and an alternate solution using setgid on the Skype executable was discussed among other things.