Проверка подписи общего объекта Linux перед загрузкой

Цель: загрузить .so или исполняемый файл, который был проверен на предмет подписи (или проверен на соответствие произвольному алгоритму).

Я хочу иметь возможность проверить .so/executable, а затем загрузить/выполнить этот .so/executable с помощью dlopen/...

Проблема в том, что, похоже, нет программного способа проверить, а затем загрузить. Можно проверить файл вручную, а затем загрузить его после... однако есть окно возможностей, в рамках которого кто-то может заменить этот файл на другой.

Одно из возможных решений, которое я могу придумать, - загрузить двоичный файл, проверить подпись, а затем dlopen/execvt /proc/$PID/fd.... однако я не знаю, является ли это жизнеспособным решением.

Поскольку блокировки файловой системы в Linux являются рекомендательными, они не так полезны для этой цели... (ну, есть mount -o mand... но это что-то для уровня пользователя, а не для использования root).


person harningt    schedule 21.07.2009    source источник
comment
Похоже, что общая проблема неразрешима без вмешательства на уровне ядра :-/ Сегменты могут быть перезаписаны после проверки... ptrace может напрямую изменить способ работы... Ожидаются любые ответы, которые могут сделать что-то "волшебное"... это похоже, что нет способа сделать это без привилегий уровня root и какого-либо способа отключить внешнюю отладку.   -  person harningt    schedule 22.07.2009
comment
Я думаю, что здесь все сильно зависит от того, чего именно вы хотите добиться с точки зрения сценария использования. Например, если вы хотите отправить подписанную общую библиотеку своим пользователям, вы можете относительно безопасно предположить, что нет проблемы окна возможности — на машине уже должно быть установлено какое-то вредоносное ПО, и в этом случае вы не так уж много можете сделать. делать. РЕДАКТИРОВАТЬ: вопрос от 2009 года? Майн Готт! Разговор о тредовомантии!   -  person Jędrzej Dudkiewicz    schedule 18.09.2018


Ответы (4)


Проблема практически неразрешима в той форме, которую вы дали, потому что общие объекты загружаются с помощью mmap() для обработки пространства памяти. Таким образом, даже если вы могли убедиться, что файл, над которым работала dlopen(), был тем, который вы проверили и объявили в порядке, любой, кто может писать в файл, может изменить загруженный объект в в любое время после загрузки. (Вот почему вы не обновляете работающие двоичные файлы, записывая в них — вместо этого вы удаляете, а затем устанавливаете, потому что запись в них, скорее всего, приведет к сбою всех запущенных экземпляров).

Лучше всего убедиться, что только пользователь, под которым вы работаете, может писать в файл, затем проверять его, а затем использовать dlopen(). Ваш пользователь (или root) по-прежнему может внедрить другой код, но процессы с такими разрешениями могут просто выполнить ptrace() вас, чтобы выполнять свои приказы.

person caf    schedule 22.07.2009
comment
Что ж, mmap(,,,MAP_COPY,,) даст отображение, на которое не повлияют дальнейшие изменения в файле на диске, но оно широко не применяется. В Linux и большинстве других систем используется mmap(,,,MAP_PRIVATE,,); POSIX не указывает, изменяют ли изменения в файле отображение, но обычно они изменяются, если только страница уже не была скопирована при записи. - person ephemient; 22.07.2009
comment
Ах, хороший улов на этом... и ptrace делает все это бесполезным, не так ли :-/ Делает DigSig похожим на единственный вариант... или файловую систему, которая предлагает доступ только для чтения к данным, которые сами проверены по подписи. .. - person harningt; 22.07.2009
comment
Отмечаем это как «ответ», поскольку, похоже, нет альтернативы, которая не позволяла бы пользователю root/current-user возиться с вещами. - person harningt; 22.07.2009
comment
По сути, граница безопасности в UNIX проходит между UID. Справедливо поспорить, что большая часть /usr/bin/* не подвергалась аудиту против атак, позволяющих пользователям выполнять произвольный код как самих себя. - person caf; 23.07.2009
comment
Рассмотрим int memfd = memfd_create(...), который затем можно запечатать, в сочетании с dlopen("/proc/self/fs/<memfd>". Это должно сделать невозможным запись в него других процессов. - person WorldSEnder; 24.07.2018

Многие динамические компоновщики (включая Glibc) поддерживают установку переменной среды LD_AUDIT в список разделяемых библиотек, разделенных двоеточиями. Этим библиотекам разрешено подключаться к различным местам в процессе загрузки динамической библиотеки.

#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;
}

Скомпилируйте это с помощью cc -shared -fPIC -o test.so test.c или подобного.

Вы можете увидеть glibc/elf/tst-auditmod1.c или latrace для получения дополнительных примеров или прочитать Руководство по компоновщикам и библиотекам.


Очень специфично для внутреннего устройства Glibc, но вы все равно можете подключиться к libdl во время выполнения.

#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)

К сожалению, мои исследования привели меня к выводу, что даже если бы вы могли подключиться к glibc/elf/dl-load.c:open_verify() (чего вы не можете), невозможно сделать это свободным от гонки против кого-то, переписывающего сегменты вашей библиотеки.

person ephemient    schedule 21.07.2009
comment
Сладко, это похоже на то, что я хочу ... за исключением того, что это одна из тех переменных среды, которые проверяются только при запуске :-/ Проект, нуждающийся в этой функции, часто загружается как плагин к другому продукту ... однако LD_AUDIT выглядит как что-то полезное для обработки, когда оно используется в наших контролируемых приложениях... - person harningt; 21.07.2009
comment
Потрясающий! Я бы дал этот один из самых полезных лакомых кусочков информации и пометил его как ответ, за исключением случая, когда кто-то прямо проткнул дыру в теории. - person harningt; 22.07.2009

Этот проект предположительно решает эту проблему на уровне ядра.

В настоящее время DigSig предлагает:

  • проверка подписи во время выполнения двоичных файлов ELF и общих библиотек.
  • поддержка отзыва подписи файла.
  • механизм кэширования сигнатур для повышения производительности.
person Eugene    schedule 21.07.2009

Предлагаю следующее решение, которое должно работать без библиотек *)

int memfd = memfd_create("for-debugging.library.so", MFD_CLOEXEC | MFD_ALLOW_SEALING);
assert(memfd != -1);

// Use any way to read the library from disk and copy the content into memfd
// e.g. write(2) or ftruncate(2) and mmap(2)
// Important! if you use mmap, you have to unmap it before the next step
// fcntl( , , F_SEAL_WRITE) will fail if there exists a writeable mapping

int seals_to_set = F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL;
int sealing_err = fcntl(memfd, F_ADD_SEALS, seals_to_set);
assert(sealing_err == 0);

// Only now verify the contents of the loaded file
// then you can safely *) dlopen("/proc/self/fd/<memfd>");

*) На самом деле не проверял его против атак. Не использовать в производстве без дальнейшего изучения.

person WorldSEnder    schedule 24.07.2018