Skip to content
Advertisement

Compiling a program with limited library access

i want to compile a C program with gcc and glibc(or any other c library) but i want to limit the program access to certain functions for example program should not be compiled if it uses sockets or signal handling functions.

any idea how i could do this??

by the way i want to use this on a simple programming contest judge

Thanks

Advertisement

Answer

I guess I’m a bit late to the party, but I feel that none of the answers given so far are entirely correct. In fact it is possible to restrict the capabilities of a program in the way you ask, and to do so sanely.

It is true that preventing calls to arbitrary functions is, while also possible, pointless — it would be like sealing a colander hole by hole. It is also not asking the right question — you do not, I suspect, want to prevent the coder from calculating the square root of a number but to prevent him from owning the system. This means to prevent him from making the system do certain things, which would invariably involve a system call, so it makes sense to focus on them instead of functions. Which function is used to open a socket is immaterial; they all end up using the socket system call.

Access to system calls can be controlled by the kernel. The linux kernel has a mechanism for this called seccomp that is used by various large programs such as Firefox, Chrome and Adobe Flash to sandbox their code interpreters and some smaller ones such as vsftpd to minimize their attack surface in the event that an attacker manages to find a remote code execution vulnerability (on the basis that the exploit code would find itself severely limited without the ability to call exec and others).

Now, before I go into more detail: If you are going to take code from people you don’t know (and hence can’t trust), paranoia is sanity. Seccomp is good but not sufficient in this scenario because this scenario is an attacker’s wet dream. It would be best to stack defenses and not bother with subtlety. So, the first three things you have to do for this are:

  1. Use a VM
  2. Use a VM
  3. Seriously, use a VM.

Running all programs in a virtual machine makes it much harder to exploit your main system because an attacker would have to break out of the VM in addition to all the other things he would have to do otherwise. There are free implementations for this that work nicely and are not very difficult to set up. I use Virtualbox most of the time.

Once you have installed a Linux system into your VM, make a snapshot of the VM so you can go back to it if a program manages to destroy it.

Got all that set up? Good. Now, seccomp allows a process to restrict its ability to make use of system calls. By design, the restrictions are a one-way street; it is not possible to re-expand the process’s capabilities later. The restrictions seccomp can place are somewhat powerful; for example, not only can a process prevent itself from calling write, it can also prevent itself from calling write on any file descriptor other than STDOUT_FILENO. Since the kernel API is rather clunky, I will be using libseccomp in the following code examples. It has a set of very helpful man pages that should help you with the details, and your distribution is likely to have packages for it unless it is very old. A simple example to show what this is about:

#include <seccomp.h>
#include <stdio.h>
#include <unistd.h>

int main() {
  scmp_filter_ctx ctx;

  puts("foo");                // works as usual. (needed here because it forces
  fputs("barn", stderr);     // some initialisation. More on that later)

  ctx = seccomp_init(SCMP_ACT_KILL);              // default action: kill process
  seccomp_rule_add(ctx, 
                   SCMP_ACT_ALLOW,                       // allow
                   SCMP_SYS(write),                      // calls to write
                   1,                                    // under one condition:
                   SCMP_A0(SCMP_CMP_EQ, STDOUT_FILENO)); // if the first argument
                                                         // is STDOUT_FILENO
  seccomp_load(ctx);

  puts("foo");                      // this will still work
  fputs("barn", stderr);           // this will make the kernel kill the process
  fprintf(stderr, "barn");         // so would this
  fputc('b', stderr);               // and this
  write(STDERR_FILENO, "barn", 4); // and this
                                    // and any other write to anything but stdout

  return 0;
}

So we have reasonably fine-grained control over allowed system calls, which is nice. It leaves the problem of identifying the system calls that need to be allowed for the proper operation of the program, a couple of which are non-trivial to decide. This is a design question you’ll have to answer yourself. The system calls are listed in /usr/include/asm/unistd_64.h.

So how do we apply it to a piece of code from an untrustworthy source?

Patching the code with sed or something similar is an idea one might have, but it is too unreliable to make sense for security-critical applications. A “safe loader” that prohibits system calls before calling the program with execv runs into the problem that it cannot prohibit the execve system call, which is one of those one would want to prohibit most. Moreover, execv requires a bunch of other system calls (such as access, mmap, open, fstat, close, mprotect, and arch_prctl) before the main function of that program is even entered. So what to do?

IMPORTANT UPDATE: This section originally included an attempt that used LD_PRELOAD to load the seccomp code; @virusdefender correctly pointed out that this had a glaring hole in it because user code could control whether the function was actually run. The new approach makes the runtime linker call our function, closing that hole.

One way is to use a shared library that has nothing in it except a constructor and destructor which are run at load and unload time, respectively. The linker will load the library before running code from the binary, so the constructor will be run and the filter installed before the user code takes control.

Code as follows:

#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static scmp_filter_ctx ctx;

// Macro just to make error handling simple. Error handling is
// very important here. You don't want this to silently fail.
#define ADD_SECCOMP_RULE(ctx, ...)                      
  do {                                                  
    if(seccomp_rule_add(ctx, __VA_ARGS__) < 0) {        
      perror("Could not add seccomp rule");             
      seccomp_release(ctx);                             
      exit(-1);                                         
    }                                                   
  } while(0)

// Constructor. This sets up the seccomp filter.
static void __attribute__((constructor)) seccomp_load_init(void) {
  ctx = seccomp_init(SCMP_ACT_KILL);

  if(ctx == NULL) {
    perror("Could not open seccomp context");
    exit(-1);
  }

  // Rules for system calls here.
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit      ), 0);
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write     ), 1, SCMP_A0(SCMP_CMP_EQ, STDOUT_FILENO));
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write     ), 1, SCMP_A0(SCMP_CMP_EQ, STDERR_FILENO));
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read      ), 1, SCMP_A0(SCMP_CMP_EQ, STDIN_FILENO));

  // This is needed for dynamic memory allocation
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk       ), 0);

  // These are needed for stdio initialisation. Workarounds to this are ugly, and the
  // syscalls are not terribly critical because they require file descriptors. We
  // restrict the program's ability to obtain those.
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap      ), 0);
  ADD_SECCOMP_RULE(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat     ), 0);

  if(seccomp_load(ctx) < 0) {
    perror("Could not load seccomp context");
    exit(-1);
  }
}

// Destructor; run at unload time. Just cleanup here.
static void __attribute__((destructor)) seccomp_load_free(void) {
  seccomp_release(ctx);
}

This will need to be compiled into a shared library:

gcc -fPIC -shared -o libmyfilter.so myfilter.c

And it will need to be linked to the untrustworthy code, so that the linker loads it when the program is started:

gcc -o untrustworthy_program untrustworthy_code.c -L/path/to/myfilter -lmyfilter -lseccomp

And then you can call the untrustworthy program less unsafely (inside your VM!) with

LD_LIBRARY_PATH=/path/to/myfilter ./untrustworthy_program

Where /path/to/myfilter is the directory that contains libmyfilter.so.

Because the filter library uses functions from libc (and libseccomp), the libc startup stuff will be done before the seccomp filter is installed. This is intentional (and was part of the rationale behind the original attempt), because libc does a number of things at startup, such as opening files, that we may want to prevent the user code from doing. In the event that you want to allow use of another library that does things at startup that the filter should later prevent, you can use LD_PRELOAD to make the linker load it before the filter.

I’m not going to go out so far on a limb as to say that this will make exploits impossible, but an attacker would, if you design your syscall filters sanely, have to find exploitable bugs both in the Linux kernel (either in seccomp or the subset of the kernel you allowed it to use) and your VM, which is very likely to be exceedingly difficult. In the more likely case that I neglected to think of something (again), the VM will still be a useful line of defense.

User contributions licensed under: CC BY-SA
2 People found this is helpful
Advertisement