Всем привет! Потребовалось на старой Synology (ядро linux 3.10, нет возможности обновить) запустить несколько docker-контейнеров, требующих getrandom и/или getentropy. Но старые ядра не имеют этих системных вызовов. Напр��мер, последние версии контейнеров веб-сервера apache выдают такую ошибку: [:crit] (38)Function not implemented: AH00141: Could not initialize random number generator.
Но в старых Linux есть /dev/random и /dev/urandom. Начал думать как решить эту проблему, вспомнил про LD_PRELOAD — это переменная окружения в Linux, которая указывает, какая разделяемая библиотека должна быть загружена до любых других библиотек. Она используется для переопределения функций в библиотеках по умолчанию. Обычно используется для отладки, тестирования, разработки, etc.
При выполнении программы dynamic linker (ld.so) ищет необходимые для программы разделяемые библиотеки. Если LD_PRELOAD установлен, указанная библиотека загружается первой, даже раньше стандартных библиотек, таких как libc.
Например, если использовать библиотеку library.so,export LD_PRELOAD=/path/to/your/library.so, то функции, определенные в файле library.so, будут иметь приоритет над функциями в библиотеках по умолчанию.
В контексте решения проблемы запуска современных docker контейнеров на системах со старым ядром, создадим файл randentoldkernel.c с таким содержимым:
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/syscall.h>
static int urandom_fd = -1;
// Функция для инициализации дескриптора
static int get_fd() {
if (urandom_fd == -1) {
// Открываем с флагом O_CLOEXEC, чтобы дескриптор не наследовался
// дочерними процессами через exec()
int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
if (fd != -1) {
urandom_fd = fd;
}
}
return urandom_fd;
}
// Автоматическое закрытие при завершении работы программы
__attribute__((destructor))
static void close_urandom_fd() {
if (urandom_fd != -1) {
close(urandom_fd);
urandom_fd = -1;
}
}
// Реализация getrandom
ssize_t getrandom(void *buf, size_t buflen, unsigned int flags) {
int fd = get_fd();
if (fd == -1) {
errno = ENOSYS;
return -1;
}
return read(fd, buf, buflen);
}
// Реализация getentropy
int getentropy(void *buf, size_t buflen) {
if (buflen > 256) {
errno = EIO;
return -1;
}
int fd = get_fd();
if (fd == -1) {
errno = ENOSYS;
return -1;
}
ssize_t res = read(fd, buf, buflen);
if (res != (ssize_t)buflen) {
return -1;
}
return 0;
}Далее скомпилируем файл randentoldkernel.c. Для компиляции будем использовать временные контейнеры docker, так как для того чтобы файл корректно работал в разных дистрибутивах (например, Alpine использует musl, а Ubuntu/Debian — glibc), лучше всего скомпилировать две версии библиотеки в соответствующих Docker-контейнерах. Это гарантирует правильную линковку.
Сборка для glibc (Ubuntu, Debian, CentOS, Fedora)
docker run --rm -v "$PWD":/src -w /src debian:stable-slim /bin/sh -c "\
apt update && apt install -y gcc libc6-dev && \
gcc -shared -fPIC randentoldkernel.c -o randentoldkernel-glibc.so"Сборка для musl (Alpine Linux)
docker run --rm -v "$PWD":/src -w /src alpine:latest /bin/sh -c "\
apk add --no-cache gcc musl-dev && \
gcc -shared -fPIC randentoldkernel.c -o randentoldkernel-musl.so"Также используя кросс-компиляцию можно собрать библиотеки под архитектуры, отличные от x86-64, но это отдельная тема.
Проверим полученные 2 файла randentoldkernel-glibc.so и randentoldkernel-musl.so с помощью ldd (ниже вывод с последнего Debian-a):
еlir@debian:~/tests$ ldd randentoldkernel-glibc.so
linux-vdso.so.1 (0x00007f5bbecb4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5bbeaad000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5bbecb6000)
еlir@debian:~/tests$ ldd randentoldkernel-musl.so
/lib64/ld-linux-x86-64.so.2 (0x00007f9811318000)
linux-vdso.so.1 (0x00007f9811316000)
libc.musl-x86_64.so.1 => not found
Как видим, библиотека randentoldkernel-musl.so не запустится на Debian из-за отсутствия musl, но будет работать внутри docker-контейнера, основанного на Alpine.
Далее скопируем скомпилированную библиотеку в необходимый образ Docker и укажем LD_PRELOAD внутри образа. Создадим Dockerfile и добавим в него следующее:
Для образов основанных на Debian
FROM httpd:latest
COPY randentoldkernel-glibc.so /usr/local/lib/randentoldkernel-glibc.so
ENV LD_PRELOAD="/usr/local/lib/randentoldkernel-glibc.so"Для образов основанных на Alpine
FROM alpine:latest
COPY randentoldkernel-musl.so /usr/local/lib/randentoldkernel-musl.so
ENV LD_PRELOAD="/usr/local/lib/randentoldkernel-musl.so"Затем соберём образ: docker build -t imagename:1.0 .
Образ можно сразу же экспортировать в tar: docker save -o imagename.tar imagename:1.0
Теперь наши образы будут работать на системах со старым ядром.
Безопасность: Разница использования /dev/urandom вместо getentropy() / getrandom() в том, что системный вызов getrandom() по умолчанию блокирует выполнение, если пул энтропии ядра еще не инициализирован (например, сразу после загрузки системы), а /dev/urandom — нет. getentropy() обычно является оберткой над getrandom(). Если сервер генерирует SSL-ключи в первые секунды после загрузки старого ядра, ключи могут быть менее стойкими. Рекомендуется отложенный старт для таких приложений.
Также если злоумышленник получит доступ к записи в файл библиотеки randentoldkernel.so или сможет изменить переменную LD_PRELOAD, он сможет подменить любую другую функцию. Но в Docker-контейнере этот риск минимален, так как файловая система образа обычно неизменяема для внешнего мира, а переменные окружения задаются при старте. Этот способ не открывает прямой дыры в безопасности. Это гораздо безопаснее, чем запускать устаревшие версии Apache с известными CVE.
Основной нюанс: если приложение или его модули вызывают getrandom через прямой ассемблерный вызов syscall(), а не через стандартную библиотеку libc, то LD_PRELOAD не сработает.
