views:

426

answers:

3

Goal: Load .so or executable that has been verified to be signed (or verified against an arbitrary algorithm).

I want to be able to verify a .so/executable and then load/execute that .so/executable with dlopen/...

The wrench in this is that there seems to be no programmatic way to check-then-load. One could check the file manually and then load it after.. however there is a window-of-opportunity within which someone could swap out that file for another.

One possible solution that I can think of is to load the binary, check the signature and then dlopen/execvt the /proc/$PID/fd.... however I do not know if that is a viable solution.

Since filesystem locks are advisory in Linux they are not so useful for this purpose... (well, there's mount -o mand ... but this is something for userlevel, not root use).

A: 

This project supposedly solves this on kernel level.

DigSig currently offers:

  • run time signature verification of ELF binaries and shared libraries.
  • support for file's signature revocation.
  • a signature caching mechanism to enhance performances.
Eugene
Awesome, sadly missing the common-user usage w/o root rights...
harningt
+2  A: 

Many dynamic linkers (including Glibc's) support setting LD_AUDIT environment variable to a colon-separated list of shared libraries. These libraries are allowed to hook into various locations in the dynamic library loading process.

#define _GNU_SOURCE
#include <dlfcn.h>
#include <link.h>
unsigned int la_version(unsigned int v) { return v; }
unsigned int la_objopen(struct link_map *l, Lmid_t lmid, uintptr_t *cookie) {
    if (!some_custom_check_on_name_and_contents(l->l_name, l->l_addr))
        abort();
    return 0;
}

Compile this with cc -shared -fPIC -o test.so test.c or similar.

You can see glibc/elf/tst-auditmod1.c or latrace for more examples, or read the Linkers and Libraries Guide.


Very very specific to Glibc's internals, but you can still hook into libdl at runtime.

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>

extern struct dlfcn_hook {
    void *(*dlopen)(const char *, int, void *);
    int (*dlclose)(void *);
    void *(*dlsym)(void *, const char *, void *);
    void *(*dlvsym)(void *, const char *, const char *, void *);
    char *(*dlerror)(void);
    int (*dladdr)(const void *, Dl_info *);
    int (*dladdr1)(const void *, Dl_info *, void **, int);
    int (*dlinfo)(void *, int, void *, void *);
    void *(*dlmopen)(Lmid_t, const char *, int, void *);
    void *pad[4];
} *_dlfcn_hook;
static struct dlfcn_hook *old_dlfcn_hook, my_dlfcn_hook;

static int depth;
static void enter(void) { if (!depth++) _dlfcn_hook = old_dlfcn_hook; }
static void leave(void) { if (!--depth) _dlfcn_hook = &my_dlfcn_hook; }

void *my_dlopen(const char *file, int mode, void *dl_caller) {
    void *result;
    fprintf(stderr, "%s(%s, %d, %p)\n", __func__, file, mode, dl_caller);
    enter();
    result = dlopen(file, mode);
    leave();
    return result;
}

int my_dlclose(void *handle) {
    int result;
    fprintf(stderr, "%s(%p)\n", __func__, handle);
    enter();
    result = dlclose(handle);
    leave();
    return result;
}

void *my_dlsym(void *handle, const char *name, void *dl_caller) {
    void *result;
    fprintf(stderr, "%s(%p, %s, %p)\n", __func__, handle, name, dl_caller);
    enter();
    result = dlsym(handle, name);
    leave();
    return result;
}

void *my_dlvsym(void *handle, const char *name, const char *version, void *dl_caller) {
    void *result;
    fprintf(stderr, "%s(%p, %s, %s, %p)\n", __func__, handle, name, version, dl_caller);
    enter();
    result = dlvsym(handle, name, version);
    leave();
    return result;
}

char *my_dlerror(void) {
    char *result;
    fprintf(stderr, "%s()\n", __func__);
    enter();
    result = dlerror();
    leave();
    return result;
}

int my_dladdr(const void *address, Dl_info *info) {
    int result;
    fprintf(stderr, "%s(%p, %p)\n", __func__, address, info);
    enter();
    result = dladdr(address, info);
    leave();
    return result;
}

int my_dladdr1(const void *address, Dl_info *info, void **extra_info, int flags) {
    int result;
    fprintf(stderr, "%s(%p, %p, %p, %d)\n", __func__, address, info, extra_info, flags);
    enter();
    result = dladdr1(address, info, extra_info, flags);
    leave();
    return result;
}

int my_dlinfo(void *handle, int request, void *arg, void *dl_caller) {
    int result;
    fprintf(stderr, "%s(%p, %d, %p, %p)\n", __func__, handle, request, arg, dl_caller);
    enter();
    result = dlinfo(handle, request, arg);
    leave();
    return result;
}

void *my_dlmopen(Lmid_t nsid, const char *file, int mode, void *dl_caller) {
    void *result;
    fprintf(stderr, "%s(%lu, %s, %d, %p)\n", __func__, nsid, file, mode, dl_caller);
    enter();
    result = dlmopen(nsid, file, mode);
    leave();
    return result;
}

static struct dlfcn_hook my_dlfcn_hook = {
    .dlopen   = my_dlopen,
    .dlclose  = my_dlclose,
    .dlsym    = my_dlsym,
    .dlvsym   = my_dlvsym,
    .dlerror  = my_dlerror,
    .dladdr   = my_dladdr,
    .dlinfo   = my_dlinfo,
    .dlmopen  = my_dlmopen,
    .pad      = {0, 0, 0, 0},
};

__attribute__((constructor))
static void init(void) {
    old_dlfcn_hook = _dlfcn_hook;
    _dlfcn_hook = &my_dlfcn_hook;
}

__attribute__((destructor))
static void fini(void) {
    _dlfcn_hook = old_dlfcn_hook;
}
$ cc -shared -fPIC -o hook.so hook.c
$ cat > a.c
#include <dlfcn.h>
int main() { dlopen("./hook.so", RTLD_LAZY); dlopen("libm.so", RTLD_LAZY); }
^D
$ cc -ldl a.c
$ ./a.out
my_dlopen(libm.so, 1, 0x80484bd)

Unfortunately, my investigations are leading me to conclude that even if you could hook into glibc/elf/dl-load.c:open_verify() (which you can't), it's not possible to make this race-free against somebody writing over segments of your library.

ephemient
Sweet, this kinda-of-looks just like what I want... except it's one of those environment variables only checked at startup :-/The project needing this feature is one that is often loaded as a plugin to another product... however LD_AUDIT looks like something useful to handle when it's used in our controlled applications...
harningt
Awesome! I'd give this one of the most useful tidbit of info and mark it the answer except for the case where someone poked a hole outright in the theory.
harningt
A: 

The problem is essentially unsolvable in the form you've given, because shared objects are loaded by mmap()ing to process memory space. So even if you could make sure that the file that dlopen() operated on was the one you'd examined and declared OK, anyone who can write to the file can modify the loaded object at any time after you've loaded it. (This is why you don't upgrade running binaries by writing to them - instead you delete-then-install, because writing to them would likely crash any running instances).

Your best bet is to ensure that only the user you are running as can write to the file, then examine it, then dlopen() it. Your user (or root) can still sneak different code in, but processes with those permissions could just ptrace() you to do their bidding anyhow.

caf
Well, `mmap(,,,MAP_COPY,,)` would give a mapping which is not affected by further changes to the file on disk, but it's not widely implemented. On Linux and most other systems, `mmap(,,,MAP_PRIVATE,,)` gets used; it's unspecified by POSIX whether changes to the file change the mapping, but generally they do unless the page has already been copied-on-write.
ephemient
Ah, good catch on that one... and ptrace renders this all useless doesn't it :-/Makes DigSig look like the only option... or a filesystem that offers read-only access to data that is itself signature verified...
harningt
Marking this as the 'answer' since there seems to be no alternative that prevents a root/current-user from mucking with things.
harningt
At heart, the security boundary in UNIX is between UIDs. It's a fair bet that most of /usr/bin/* hasn't been audited against allow-user-to-execute-arbitrary-code-as-themselves "attacks".
caf