Обработка системных вызовов посредством LD_PRELOAD с единой точкой входа

  • Tutorial

Делая пере-реализацию QInst на LLVM, я столкнулся с такой проблемой: QEMU в режиме эмуляции одного процесса естественным образом перехватывает все «гостевые» системные вызовы. В итоге плагин инструментации имеет единую точку входа для их предобработки, где можно по номерам SYS_* и значениям аргументов принимать решения. Это довольно удобно. Проблема в том, что все системные вызовы делает, в основном, libc и, переписывая код статически, мы просто до этой части в большинстве случаев не доберёмся. Конечно, можно было бы использовать ptrace, который как раз предназначен и для этого в том числе. Но тогда не уверен, что получилось бы обойтись без отдельного процесса, а семантика QInst предполагала тривиальный «синхронный» перехват — пришлось бы как-то инжектить вызов обработчика, а это сильно сложнее привычного LD_PRELOAD. Можно оборачивать каждый системный вызов — но это, как минимум, неудобно (к тому же, можем что-нибудь пропустить, ведь в этом случае мы перехватываем на самом не системные вызовы, а их конкретные обёртки).


Под катом — решение, не привязанное к LLVM, но заточенное под Linux на x86_64 (но адаптируемое для Linux на других архитектурах).


Примечание: в этой статье не представлено универсального готового решения — оно лишь подходит для достаточно широкого списка случаев. Зато эту статью можно считать обзорно-пятничной: интересная (надеюсь) новая (для большинства?) информация, щепотка брутфорс-программирования и рабочие примеры. И пусть нашу пятницу не омрачает даже тот факт, что сегодня четверг!


Для начала определимся: в этой статье не ставится целью обход антиотладочных механизмов, и вообще предполагается, что программа не сопротивляется перехвату системных вызовов. С другой стороны, вручную переписывать исходники, чтобы программа нам помогала, тоже так себе перспектива чисто с точки зрения трудоёмкости. Поэтому LD_PRELOAD. Впрочем, если вы захотите статически вкомпилировать такой механизм в собственную программу, то LD_PRELOAD здесь абсолютно не существенен.


Лирическое отступление: как вообще выполняются системные вызовы в Linux? Для этого есть несколько способов. Когда-то процесс (на x86) просто клал в eax номер системного вызова, в ebx первый аргумент и так далее, после чего дёргал int 0x80. В какой-то момент решили, что это то ли не очень быстро, то ли не дружественно к кешу, то ли ещё что-то, и переделали. Потом ещё раз. На данный момент используется vDSO — это вообще инжектируемый в каждый процесс честный shared object, у которого можно дёргать функции, чтобы делать частые системные вызовы, не требующие переключения контекста (например, time). Казалось бы, на этом можно заканчивать статью, но нет: там описано около четырёх отдельных системных вызовов (точное число, скорее всего, зависит от версии ядра), а глобальной точки входа что-то не наблюдается...


Можно было бы дальше искать общую точку, выбирающую, через какую инструкцию на этой машине делают системные вызовы (int 0x80 / sysenter / ...) — а может, её и нет вовсе — оставим это упражнением читателю. Давайте лучше обратим внимание на механизм seccomp, обеспечивающий ещё один штатный интерфейс фильтрации системных вызовов.


Seccomp означает secure computing и предполагается для обработки не очень доверенного кода. Цитата из man seccomp (2):


Strict secure computing mode is useful for number-crunching applications that may need to
execute untrusted byte code, perhaps obtained by reading from a pipe or socket.

Ну, после Meltdown&Co люди уже JavaScript в несколько потоков запускать боятся — не знаю, как там с недоверенным бинарным кодом. Впрочем, безопасность в частично доверенном окружении — отдельная тема, и скажу лишь, что хотя seccomp и может работать, например, на уровне отдельных потоков, я бы, естественно, не осмелился сидеть в одном адресном пространстве с запущенным недоверенным машинным кодом. В общем, безопасное «препарирование» malware тоже не является темой этой статьи.


Меня в данном случае интересует перехват системных вызовов скорее с точки зрения инструментации. К счастью, кроме режима SECCOMP_SET_MODE_STRICT, принудительно завершающего процесс при выполнении любого вызова кроме read, write, _exit и sigreturn, есть SECCOMP_SET_MODE_FILTER — он позволяет задать BPF-программу, которая будет фильтровать только интересующие системные вызовы, и для отфильтрованных решать, что сделать. Вот тут уже выбор намного больше, но меня сейчас интересует SECCOMP_RET_TRAP: при выдаче этого вердикта конкретному потоку посылается SIGSYS, то есть ровно то, что нужно: можно синхронно обработать запрос и вернуть управление, будто системный вызов произошёл штатно.


Подготовка


Для нашего обработчика создадим файл syscall-handler.c:


#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <linux/signal.h>
#include <sys/ptrace.h>

uint64_t handle_syscall(uint32_t num, uint32_t *drop_syscall,
                        uint64_t arg1, uint64_t arg2, uint64_t arg3,
                        uint64_t arg4, uint64_t arg5, uint64_t arg6);

void initialize_handler(void)
{
}

static void __attribute__((constructor))constr(void)
{
  initialize_handler();
}

Опишем функцию, обрабатывающую системные вызовы — с этим всё довольно понятно: функция принимает номер системного вызова, (максимум) шесть его аргументов и (возвращаемый) признак, что системный вызов был не только учтён, но и проэмулирован, и реально его выполнять не нужно. Далее всё ещё проще: функция initialize_handler будет настраивать диспетчеризацию всего этого.


А что это за загадочная функция constr? Она лишь вызывает initialize_handler, но, поскольку помечена атрибутом constructor, будет вызвана в процессе инициализации библиотеки (или основного выполняемого файла), содержащего эту функцию. Если же нам захочется включить перехват уже после запуска программы, можно заifdefить constr и вызвать initialize_handler вручную.


Пишем BPF-программу


Теперь нужно написать байткод BPF, который будет просто реагировать на нужные номера системных вызовов выдачей SECCOMP_RET_TRAP. Документация на формат программ лежит, в частности, в исходниках ядра в Documentation/networking/filter.txt, при этом для seccomp нужно смотреть на старую версию, не eBPF. Сами файлы linux/seccomp.h, linux/filter.h, а также linux/bpf_common.h, на который тот ссылается, тоже будут весьма полезны. Если сгрести в кучу эти источники, пример из man seccomp и потрясти, получится что-то такое:


struct sock_filter filt[] = {
    BPF_STMT(BPF_LD  | BPF_W   | BPF_ABS, (offsetof(struct seccomp_data, args[5]))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K  , MARKER, 0, 1),
    BPF_STMT(BPF_RET           | BPF_K  , SECCOMP_RET_ALLOW),
    BPF_STMT(BPF_RET           | BPF_K  , SECCOMP_RET_TRAP),
  };

Поскольку писал я это несколько недель назад, да и тогда это был подрихтованный код из примера, то по старой традиции скажем, что это очевидно оставим читателю в качестве упражнения. В принципе, здесь явно просматриваются опкоды (LD / JMP со смещениями 0 и 1 / RET), условия (JEQ), указание использовать immediate operand (BPF_K) — подробности смотрите в уже упоминавшемся filter.txt. А макросы позволяют не задумываться об упаковке этого всего в слова команд.


Зачем нам константа MARKER? Это такой костыль, который позволит всё-таки пропустить системный вызов, если наш userspace-фильтр так решит. Коряво, костыльно — но я и не обещал показать универсальное решение. Впрочем, показанное решение тоже в большинстве случаев работает вполне успешно, при этом тривиально устроено.


Этот самый MARKER мы кладём в шестой аргумент системного вызова в надежде, что это «лишняя деталь» и все шесть аргументов используются редко. Вычитываем его при помощи самой первой инструкции нашей BPF-программы: отсюда это загадочное выражение — offsetof(struct seccomp_data, args[5]) — ядро нам передаёт структуру seccomp_data (см. man seccomp):


struct seccomp_data {
    int   nr;                   /* System call number */
    __u32 arch;                 /* AUDIT_ARCH_* value
                                   (see <linux/audit.h>) */
    __u64 instruction_pointer;  /* CPU instruction pointer */
     __u64 args[6];              /* Up to 6 system call arguments */
};

… и мы из неё вычитываем переменную по требуемому смещению: offsetof(struct seccomp_data, args[5]). Спонсор этого абзаца — aol-nnov


Используется это как-то так:


uint64_t handle_syscall(uint32_t num, uint32_t *drop_syscall,
                        uint64_t arg1, uint64_t arg2, uint64_t arg3,
                        uint64_t arg4, uint64_t arg5, uint64_t arg6)
{
  return 0;
}

#define MARKER 0x12345678

static int in_handler;

static void handle_sigsys(int num, siginfo_t *si, void *arg)
{
  ucontext_t *ctx = arg;
  greg_t *gregs = ctx->uc_mcontext.gregs;
  uint32_t drop = 0;
  uint64_t res;
  if (!in_handler) {
    in_handler = 1;
    res = handle_syscall(gregs[REG_RAX], &drop,
                         gregs[REG_RDI], gregs[REG_RSI], gregs[REG_RDX],
                         gregs[REG_R10], gregs[REG_R8], gregs[REG_R9]);
    in_handler = 0;
  }
  if (!drop) {
    res = syscall(gregs[REG_RAX],
                  gregs[REG_RDI], gregs[REG_RSI], gregs[REG_RDX],
                  gregs[REG_R10], gregs[REG_R8], MARKER);
  }
  gregs[REG_RAX] = res;
}

void initialize_handler(void)
{
  // Описываем BPF-программу фильтра
  struct sock_filter filt[] = {
    BPF_STMT(BPF_LD  | BPF_W   | BPF_ABS, (offsetof(struct seccomp_data, args[5]))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K  , MARKER, 0, 1),
    BPF_STMT(BPF_RET           | BPF_K  , SECCOMP_RET_ALLOW),
    BPF_STMT(BPF_RET           | BPF_K  , SECCOMP_RET_TRAP),
  };

  struct sock_fprog prog = {
    sizeof(filt) / sizeof(filt[0]), filt
  };

  // Регистрируем userspace-обработчик
  struct sigaction sig;
  memset(&sig, 0, sizeof(sig));
  sig.sa_sigaction = handle_sigsys;
  sig.sa_flags = SA_SIGINFO;
  sigaction(SIGSYS, &sig, NULL);

  // Собственно, включаем фильтрацию
  prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
  syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog);
}

handle_sigsys — это new-style обработчик сигналов, так сказать, используемый вместе с функцией sigaction. Про его третий параметр в man sigaction говорится:


    ucontext
        This is a pointer to a ucontext_t structure, cast to void *.  The structure
        pointed to by this field contains signal context information that was saved
        on the user-space stack by the kernel; for details, see sigreturn(2). Further
        information  about the ucontext_t structure can be found in getcontext(3).
        Commonly, the handler function doesn't make any use of the third argument.

Что ж, у нас не совсем common case, поэтому для разнообразия будем неиспользовать второй аргумент — там тоже очень много всего интересного, но нет аргументов системного вызова. Здесь начинаются пляски с бубном, причём бубен нужен от правильной архитектуры. Вполне возможно, libseccomp успешно инкостылирует эти архитектурные детали, и просто выдаёт массив аргументов — а если она сейчас этого и не делает, то, возможно, будет потом — но раз уж у нас ознакомительная статья, то будем считать, что такая низкоуровневость — не баг, а фича… Поэтому просто сверимся со второй таблицей из man syscall для требуемой архитектуры, в моём случае — x86_64.


В этом же обработчике мы видим ещё одну забавную функцию: syscall — собственно, именно от неё мы man только что и читали. Ей просто передаётся номер системного вызова и аргументы, и она его выполняет. На некоторых архитектурах бывают всякие хитрые ABI с «выравниванием» 64-битных значений в 32-битных регистрах (вычитано всё в той же man-странице) — будем надеяться, на x86_64 это не актуально, иначе бы нам пришлось это парсить в зависимости от номера вызова, вычитывая регистры, поскольку syscall() уже скрывает в себе эту логику. Кроме таких вот странных случаев, как здесь, когда номер вызова определяется динамически, она может использоваться, если у нас по какой-то причине нет libc-шной обёртки для системного вызова. Например, именно так я вызываю seccomp в следующей функции:


  syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog);

Этот странный prctl нужен по соображениям безопасности: в man seccomp приводится пример, когда без этого требования можно было бы поместить незадачливый set-user-ID бинарник в виртуальную реальность, в которой setuid вернёт ноль, но ничего не сделает, что будет очень опасно с точки зрения повышения привилегий.


Скомпилируем и воспользуемся нашей библиотечкой:


$ gcc -fPIC --shared syscall-handler.c -o syscall-handler.so
$ LD_PRELOAD=syscall-handler.so ls
ERROR: ld.so: object 'syscall-handler.so' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored.
article.md  example.c  syscall-handler.c  syscall-handler.so
$ LD_PRELOAD=./syscall-handler.so ls
article.md  example.c  syscall-handler.c  syscall-handler.so

Как видим, как минимум на моей системе нужно указывать путь к библиотеке. Хоть относительный, но путь. Хорошо хоть, что загрузчик предупредил об ошибке. Во втором случае всё вроде бы работает.


Отладка


Как мы отлаживаем системные вызовы? Можно, конечно, через команду catch syscall в gdb, но в данном случае (как и в большинстве других, честно говоря) нам поможет чудесный инструмент strace:


strace -E LD_PRELOAD=./syscall-handler.so ls

Параметр -E говорит о том, что нужно выставить указанную переменную для исследуемого процесса. В противном случае пришлось бы вгружать отлаживаемую библиотеку в сам отладчик — так себе перспектива.


Вывод strace
$ strace -E LD_PRELOAD=./syscall-handler.so ls
execve("/bin/ls", ["ls"], 0x5652b17024d0 /* 61 vars */) = 0
brk(NULL)                               = 0x55a2f556d000
arch_prctl(0x3001 /* ARCH_??? */, 0x7fffe8e949b0) = -1 EINVAL (Недопустимый аргумент)
openat(AT_FDCWD, "./syscall-handler.so", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 \21\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0775, st_size=16504, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f29f6baf000
getcwd("/home/trosinenko/some/path", 128) = 63
mmap(NULL, 16480, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f29f6baa000
mmap(0x7f29f6bab000, 4096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1000) = 0x7f29f6bab000
mmap(0x7f29f6bac000, 4096, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7f29f6bac000
mmap(0x7f29f6bad000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7f29f6bad000
close(3)                                = 0
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (Нет такого файла или каталога)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=254851, ...}) = 0
mmap(NULL, 254851, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f29f6b6b000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300p\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=163240, ...}) = 0
mmap(NULL, 174640, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f29f6b40000
mprotect(0x7f29f6b46000, 135168, PROT_NONE) = 0
mmap(0x7f29f6b46000, 102400, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x6000) = 0x7f29f6b46000
mmap(0x7f29f6b5f000, 28672, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1f000) = 0x7f29f6b5f000
mmap(0x7f29f6b67000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x26000) = 0x7f29f6b67000
mmap(0x7f29f6b69000, 6704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f29f6b69000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360r\2\0\0\0\0\0"..., 832) = 832
lseek(3, 64, SEEK_SET)                  = 64
read(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784) = 784
lseek(3, 848, SEEK_SET)                 = 848
read(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32) = 32
lseek(3, 880, SEEK_SET)                 = 880
read(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0!U\364U\255V\275\207\34\202%\274\312\205\356%"..., 68) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=2025032, ...}) = 0
lseek(3, 64, SEEK_SET)                  = 64
read(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784) = 784
lseek(3, 848, SEEK_SET)                 = 848
read(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32) = 32
lseek(3, 880, SEEK_SET)                 = 880
read(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0!U\364U\255V\275\207\34\202%\274\312\205\356%"..., 68) = 68
mmap(NULL, 2032984, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f29f694f000
mmap(0x7f29f6974000, 1540096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x25000) = 0x7f29f6974000
mmap(0x7f29f6aec000, 303104, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0x7f29f6aec000
mmap(0x7f29f6b36000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e6000) = 0x7f29f6b36000
mmap(0x7f29f6b3c000, 13656, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f29f6b3c000
close(3)                                = 0
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libpcre2-8.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200!\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=539176, ...}) = 0
mmap(NULL, 541448, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f29f68ca000
mmap(0x7f29f68cc000, 376832, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7f29f68cc000
mmap(0x7f29f6928000, 151552, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x5e000) = 0x7f29f6928000
mmap(0x7f29f694d000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x82000) = 0x7f29f694d000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 \22\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=18816, ...}) = 0
mmap(NULL, 20752, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f29f68c4000
mmap(0x7f29f68c5000, 8192, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1000) = 0x7f29f68c5000
mmap(0x7f29f68c7000, 4096, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f29f68c7000
mmap(0x7f29f68c8000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f29f68c8000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360\201\0\0\0\0\0\0"..., 832) = 832
lseek(3, 824, SEEK_SET)                 = 824
read(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\09V\4W\221\35\226\215\236\6\10\215\240\25\227\v"..., 68) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=158288, ...}) = 0
lseek(3, 824, SEEK_SET)                 = 824
read(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\09V\4W\221\35\226\215\236\6\10\215\240\25\227\v"..., 68) = 68
mmap(NULL, 140448, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f29f68a1000
mmap(0x7f29f68a8000, 69632, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x7000) = 0x7f29f68a8000
mmap(0x7f29f68b9000, 20480, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18000) = 0x7f29f68b9000
mmap(0x7f29f68be000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c000) = 0x7f29f68be000
mmap(0x7f29f68c0000, 13472, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f29f68c0000
close(3)                                = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f29f689f000
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f29f689c000
arch_prctl(ARCH_SET_FS, 0x7f29f689c800) = 0
mprotect(0x7f29f6b36000, 12288, PROT_READ) = 0
mprotect(0x7f29f68be000, 4096, PROT_READ) = 0
mprotect(0x7f29f68c8000, 4096, PROT_READ) = 0
mprotect(0x7f29f694d000, 4096, PROT_READ) = 0
mprotect(0x7f29f6b67000, 4096, PROT_READ) = 0
mprotect(0x7f29f6bad000, 4096, PROT_READ) = 0
mprotect(0x55a2f4670000, 4096, PROT_READ) = 0
mprotect(0x7f29f6bdd000, 4096, PROT_READ) = 0
munmap(0x7f29f6b6b000, 254851)          = 0
set_tid_address(0x7f29f689cad0)         = 31949
set_robust_list(0x7f29f689cae0, 24)     = 0
rt_sigaction(SIGRTMIN, {sa_handler=0x7f29f68a8c50, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7f29f68b6540}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {sa_handler=0x7f29f68a8cf0, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO, sa_restorer=0x7f29f68b6540}, NULL, 8) = 0
rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
statfs("/sys/fs/selinux", 0x7fffe8e94900) = -1 ENOENT (Нет такого файла или каталога)
statfs("/selinux", 0x7fffe8e94900)      = -1 ENOENT (Нет такого файла или каталога)
brk(NULL)                               = 0x55a2f556d000
brk(0x55a2f558e000)                     = 0x55a2f558e000
openat(AT_FDCWD, "/proc/filesystems", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
read(3, "nodev\tsysfs\nnodev\ttmpfs\nnodev\tbd"..., 1024) = 425
read(3, "", 1024)                       = 0
close(3)                                = 0
access("/etc/selinux/config", F_OK)     = -1 ENOENT (Нет такого файла или каталога)
rt_sigaction(SIGSYS, {sa_handler=0x7f29f6bab1ff, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7f29f6995470}, NULL, 8) = 0
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)  = 0
seccomp(SECCOMP_SET_MODE_FILTER, 0, {len=4, filter=0x7fffe8e94930}) = 0
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 257
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6579c, si_syscall=__NR_openat, si_arch=AUDIT_ARCH_X86_64} ---
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
rt_sigreturn({mask=[]})                 = 3
fstat(3, {st_mode=000, st_size=0, ...}) = 5
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a5f7b9, si_syscall=__NR_fstat, si_arch=AUDIT_ARCH_X86_64} ---
fstat(3, {st_mode=S_IFREG|0644, st_size=8994080, ...}) = 0
rt_sigreturn({mask=[]})                 = 0
mmap(NULL, 8994080, PROT_READ, MAP_PRIVATE, 3, 0) = 0x9
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6aaf6, si_syscall=__NR_mmap, si_arch=AUDIT_ARCH_X86_64} ---
mmap(NULL, 8994080, PROT_READ, MAP_PRIVATE, 3, 0x12345678) = -1 EINVAL (Недопустимый аргумент)
rt_sigreturn({mask=[]})                 = -1 EPERM (Операция не позволена)
close(3)                                = 3
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a655bb, si_syscall=__NR_close, si_arch=AUDIT_ARCH_X86_64} ---
close(3)                                = 0
rt_sigreturn({mask=[]})                 = 0
openat(AT_FDCWD, "/usr/share/locale/locale.alias", O_RDONLY|O_CLOEXEC) = 257
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6579c, si_syscall=__NR_openat, si_arch=AUDIT_ARCH_X86_64} ---
openat(AT_FDCWD, "/usr/share/locale/locale.alias", O_RDONLY|O_CLOEXEC) = 3
rt_sigreturn({mask=[]})                 = 3
fstat(3, {st_mode=037, st_size=0, ...}) = 5
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a5f7b9, si_syscall=__NR_fstat, si_arch=AUDIT_ARCH_X86_64} ---
fstat(3, {st_mode=S_IFREG|0644, st_size=2995, ...}) = 0
rt_sigreturn({mask=[]})                 = 0
read(3, "", 4096)                       = 0
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a658d8, si_syscall=__NR_read, si_arch=AUDIT_ARCH_X86_64} ---
read(3, "# Locale name alias data base.\n#"..., 4096) = 2995
rt_sigreturn({mask=[]})                 = 2995
read(3, "", 4096)                       = 0
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a658d8, si_syscall=__NR_read, si_arch=AUDIT_ARCH_X86_64} ---
read(3, "", 4096)                       = 0
rt_sigreturn({mask=[]})                 = 0
close(3)                                = 3
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a655bb, si_syscall=__NR_close, si_arch=AUDIT_ARCH_X86_64} ---
close(3)                                = 0
rt_sigreturn({mask=[]})                 = 0
openat(AT_FDCWD, "/usr/lib/locale/ru_RU.UTF-8/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = 257
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6579c, si_syscall=__NR_openat, si_arch=AUDIT_ARCH_X86_64} ---
openat(AT_FDCWD, "/usr/lib/locale/ru_RU.UTF-8/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = -1 ENOENT (Нет такого файла или каталога)
rt_sigreturn({mask=[]})                 = -1 EPERM (Операция не позволена)
openat(AT_FDCWD, "/usr/lib/locale/ru_RU.utf8/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = 257
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6579c, si_syscall=__NR_openat, si_arch=AUDIT_ARCH_X86_64} ---
openat(AT_FDCWD, "/usr/lib/locale/ru_RU.utf8/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = -1 ENOENT (Нет такого файла или каталога)
rt_sigreturn({mask=[]})                 = -1 EPERM (Операция не позволена)
openat(AT_FDCWD, "/usr/lib/locale/ru_RU/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = 257
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6579c, si_syscall=__NR_openat, si_arch=AUDIT_ARCH_X86_64} ---
openat(AT_FDCWD, "/usr/lib/locale/ru_RU/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = -1 ENOENT (Нет такого файла или каталога)
rt_sigreturn({mask=[]})                 = -1 EPERM (Операция не позволена)
openat(AT_FDCWD, "/usr/lib/locale/ru.UTF-8/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = 257
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6579c, si_syscall=__NR_openat, si_arch=AUDIT_ARCH_X86_64} ---
openat(AT_FDCWD, "/usr/lib/locale/ru.UTF-8/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = -1 ENOENT (Нет такого файла или каталога)
rt_sigreturn({mask=[]})                 = -1 EPERM (Операция не позволена)
openat(AT_FDCWD, "/usr/lib/locale/ru.utf8/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = 257
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6579c, si_syscall=__NR_openat, si_arch=AUDIT_ARCH_X86_64} ---
openat(AT_FDCWD, "/usr/lib/locale/ru.utf8/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = -1 ENOENT (Нет такого файла или каталога)
rt_sigreturn({mask=[]})                 = -1 EPERM (Операция не позволена)
openat(AT_FDCWD, "/usr/lib/locale/ru/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = 257
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6579c, si_syscall=__NR_openat, si_arch=AUDIT_ARCH_X86_64} ---
openat(AT_FDCWD, "/usr/lib/locale/ru/LC_IDENTIFICATION", O_RDONLY|O_CLOEXEC) = -1 ENOENT (Нет такого файла или каталога)
rt_sigreturn({mask=[]})                 = -1 EPERM (Операция не позволена)
ioctl(1, TCGETS, {B4000000 opost -isig icanon -echo ...}) = 16
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a65cea, si_syscall=__NR_ioctl, si_arch=AUDIT_ARCH_X86_64} ---
ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
rt_sigreturn({mask=[]})                 = 0
ioctl(1, TIOCGWINSZ, {ws_row=45567, ws_col=63162, ws_xpixel=32553, ws_ypixel=0}) = 16
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6667b, si_syscall=__NR_ioctl, si_arch=AUDIT_ARCH_X86_64} ---
ioctl(1, TIOCGWINSZ, {ws_row=58, ws_col=271, ws_xpixel=0, ws_ypixel=0}) = 0
rt_sigreturn({mask=[]})                 = 0
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 257
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6579c, si_syscall=__NR_openat, si_arch=AUDIT_ARCH_X86_64} ---
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
rt_sigreturn({mask=[]})                 = 3
fstat(3, {st_mode=037777777777, st_size=139818209035479, ...}) = 5
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a5f7b9, si_syscall=__NR_fstat, si_arch=AUDIT_ARCH_X86_64} ---
fstat(3, {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
rt_sigreturn({mask=[]})                 = 0
getdents64(3, /* d_reclen < offsetof(struct dirent64, d_name) */ /* 0 entries */, 32768) = 217
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a3007b, si_syscall=__NR_getdents64, si_arch=AUDIT_ARCH_X86_64} ---
getdents64(3, /* 7 entries */, 32768)   = 232
rt_sigreturn({mask=[]})                 = 232
getdents64(3, /* 7 entries */, 32768)   = 217
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a3007b, si_syscall=__NR_getdents64, si_arch=AUDIT_ARCH_X86_64} ---
getdents64(3, /* 0 entries */, 32768)   = 0
rt_sigreturn({mask=[]})                 = 0
close(3)                                = 3
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a655bb, si_syscall=__NR_close, si_arch=AUDIT_ARCH_X86_64} ---
close(3)                                = 0
rt_sigreturn({mask=[]})                 = 0
fstat(1, {st_mode=000, st_size=0, ...}) = 5
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a5f7b9, si_syscall=__NR_fstat, si_arch=AUDIT_ARCH_X86_64} ---
fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(0x88, 0x7), ...}) = 0
rt_sigreturn({mask=[]})                 = 0
write(1, "article.md  example.c  syscall-h"..., 61) = 1
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a60317, si_syscall=__NR_write, si_arch=AUDIT_ARCH_X86_64} ---
write(1, "article.md  example.c  syscall-h"..., 61article.md  example.c  syscall-handler.c  syscall-handler.so
) = 61
rt_sigreturn({mask=[]})                 = 61
close(1)                                = 3
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a655bb, si_syscall=__NR_close, si_arch=AUDIT_ARCH_X86_64} ---
close(1)                                = 0
rt_sigreturn({mask=[]})                 = 0
close(2)                                = 3
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a655bb, si_syscall=__NR_close, si_arch=AUDIT_ARCH_X86_64} ---
close(2)                                = 0
rt_sigreturn({mask=[]})                 = 0
exit_group(0)                           = 231
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a34fe6, si_syscall=__NR_exit_group, si_arch=AUDIT_ARCH_X86_64} ---
exit_group(0)                           = ?
+++ exited with 0 +++

Сначала мы видим просто процесс линковки динамических библиотек (да-да, в userspace), собственно же обработчик устанавливается где-то здесь, и начинается веселье...


...
rt_sigaction(SIGSYS, {sa_handler=0x7f29f6bab1ff, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7f29f6995470}, NULL, 8) = 0
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)  = 0
seccomp(SECCOMP_SET_MODE_FILTER, 0, {len=4, filter=0x7fffe8e94930}) = 0
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 257
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6579c, si_syscall=__NR_openat, si_arch=AUDIT_ARCH_X86_64} ---
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
rt_sigreturn({mask=[]})                 = 3
fstat(3, {st_mode=000, st_size=0, ...}) = 5
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a5f7b9, si_syscall=__NR_fstat, si_arch=AUDIT_ARCH_X86_64} ---
fstat(3, {st_mode=S_IFREG|0644, st_size=8994080, ...}) = 0
rt_sigreturn({mask=[]})                 = 0
mmap(NULL, 8994080, PROT_READ, MAP_PRIVATE, 3, 0) = 0x9
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6aaf6, si_syscall=__NR_mmap, si_arch=AUDIT_ARCH_X86_64} ---
mmap(NULL, 8994080, PROT_READ, MAP_PRIVATE, 3, 0x12345678) = -1 EINVAL (Недопустимый аргумент)
rt_sigreturn({mask=[]})                 = -1 EPERM (Операция не позволена)
close(3)                                = 3
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a655bb, si_syscall=__NR_close, si_arch=AUDIT_ARCH_X86_64} ---
close(3)                                = 0
rt_sigreturn({mask=[]})                 = 0
openat(AT_FDCWD, "/usr/share/locale/locale.alias", O_RDONLY|O_CLOEXEC) = 257
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a6579c, si_syscall=__NR_openat, si_arch=AUDIT_ARCH_X86_64} ---
openat(AT_FDCWD, "/usr/share/locale/locale.alias", O_RDONLY|O_CLOEXEC) = 3
rt_sigreturn({mask=[]})                 = 3
fstat(3, {st_mode=037, st_size=0, ...}) = 5
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a5f7b9, si_syscall=__NR_fstat, si_arch=AUDIT_ARCH_X86_64} ---
fstat(3, {st_mode=S_IFREG|0644, st_size=2995, ...}) = 0
rt_sigreturn({mask=[]})                 = 0
read(3, "", 4096)                       = 0
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f29f6a658d8, si_syscall=__NR_read, si_arch=AUDIT_ARCH_X86_64} ---
read(3, "# Locale name alias data base.\n#"..., 4096) = 2995
...

Как мы видим, теперь каждый системный вызов печатается дважды, а между ними — пришедший SIGSYS, причём возвращаемые значения в первом напечатанном вызове заполнены мусором. Обратите внимание, например, на fstat: возвращаемые значения — это не только код возврата!


Усложняем эксперимент...


Итак, что-то запустить удалось. Попробуем теперь что-то осмысленное: обработать openat, через который libc открывает файлы: впишем в handle_syscall


  if (num == SYS_openat) {
    fprintf(stderr, "openat: %s\n", (const char *)arg2);
  }

Перекомпилируем и запустим:


$ gcc -fPIC --shared syscall-handler.c -o syscall-handler.so
$ LD_PRELOAD=./syscall-handler.so ls
Неверный системный вызов
$ strace -E LD_PRELOAD=./syscall-handler.so ls
...
rt_sigaction(SIGSYS, {sa_handler=0x7f55ef2cb24e, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7f55ef0b5470}, NULL, 8) = 0
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)  = 0
seccomp(SECCOMP_SET_MODE_FILTER, 0, {len=4, filter=0x7ffca47b36e0}) = 0
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 257
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f55ef18579c, si_syscall=__NR_openat, si_arch=AUDIT_ARCH_X86_64} ---
write(2, "openat: /usr/lib/locale/locale-a"..., 39) = 1
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7f55ef180317, si_syscall=__NR_write, si_arch=AUDIT_ARCH_X86_64} ---
+++ killed by SIGSYS +++
Неверный системный вызов

Причина оказалась проста: мы не указали флаг SA_NODEFER при регистрации обработчика сигнала, поэтому второй SIGSYS должен был бы прийти после выхода из предыдущего обработчика, и получился бы deadlock. Укажем этот флаг в initialize_handler:


  sig.sa_flags = SA_SIGINFO | SA_NODEFER;

$ LD_PRELOAD=./syscall-handler.so ls
openat: /usr/lib/locale/locale-archive
openat: /usr/share/locale/locale.alias
openat: /usr/lib/locale/ru_RU.UTF-8/LC_IDENTIFICATION
openat: /usr/lib/locale/ru_RU.utf8/LC_IDENTIFICATION
openat: /usr/lib/locale/ru_RU/LC_IDENTIFICATION
openat: /usr/lib/locale/ru.UTF-8/LC_IDENTIFICATION
openat: /usr/lib/locale/ru.utf8/LC_IDENTIFICATION
openat: /usr/lib/locale/ru/LC_IDENTIFICATION
openat: .
article.md  example.c  syscall-handler.c  syscall-handler.so

Но есть одна проблема...


Дело в том, что есть один широко известный в узких кругах системный вызов с шестью аргументами. mmap называется :) Как известно, используется он даже для выделения памяти, если запрошено больше какого-то определённого размера.


example.c:


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

int main()
{
  fprintf(stderr, "Allocating 1MB of memory\n");
  void *ptr = malloc(1 << 20);
  fprintf(stderr, "Allocated: %p\n", ptr);
}

$ LD_PRELOAD=./syscall-handler.so ./example
Allocating 1MB of memory
Allocated: 0x564fc07012a0

Эм… Оно работает?!?


$ strace -E LD_PRELOAD=./syscall-handler.so ./example
...
rt_sigaction(SIGSYS, {sa_handler=0x7fd7065cd24e, sa_mask=[], sa_flags=SA_RESTORER|SA_NODEFER|SA_SIGINFO, sa_restorer=0x7fd7063e2470}, NULL, 8) = 0
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)  = 0
seccomp(SECCOMP_SET_MODE_FILTER, 0, {len=4, filter=0x7fff8ea97810}) = 0
write(2, "Allocating 1MB of memory\n", 25) = 1
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7fd7064ad317, si_syscall=__NR_write, si_arch=AUDIT_ARCH_X86_64} ---
write(2, "Allocating 1MB of memory\n", 25Allocating 1MB of memory
) = 25
rt_sigreturn({mask=[]})                 = 25
brk(NULL)                               = 0xc
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7fd7064b355b, si_syscall=__NR_brk, si_arch=AUDIT_ARCH_X86_64} ---
brk(NULL)                               = 0x55e31f74f000
rt_sigreturn({mask=[]})                 = 94433973694464
brk(0x55e31f770000)                     = 0xc
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7fd7064b355b, si_syscall=__NR_brk, si_arch=AUDIT_ARCH_X86_64} ---
brk(0x55e31f770000)                     = 0x55e31f770000
rt_sigreturn({mask=[]})                 = 94433973829632
mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x9
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7fd7064b7af6, si_syscall=__NR_mmap, si_arch=AUDIT_ARCH_X86_64} ---
mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0x12345678) = -1 EINVAL (Недопустимый аргумент)
rt_sigreturn({mask=[]})                 = -1 EPERM (Операция не позволена)
brk(0x55e31f870000)                     = 0xc
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7fd7064b355b, si_syscall=__NR_brk, si_arch=AUDIT_ARCH_X86_64} ---
brk(0x55e31f870000)                     = 0x55e31f870000
rt_sigreturn({mask=[]})                 = 94433974878208
write(2, "Allocated: 0x55e31f74f2a0\n", 26) = 1
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7fd7064ad317, si_syscall=__NR_write, si_arch=AUDIT_ARCH_X86_64} ---
write(2, "Allocated: 0x55e31f74f2a0\n", 26Allocated: 0x55e31f74f2a0
) = 26
rt_sigreturn({mask=[]})                 = 26
exit_group(0)                           = 231
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7fd706481fe6, si_syscall=__NR_exit_group, si_arch=AUDIT_ARCH_X86_64} ---
exit_group(0)                           = ?
+++ exited with 0 +++

Не получилось: когда mmap вернул ошибку, libc просто воспользовалась brk. А если так...


#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
  fprintf(stderr, "Allocating 1MB of memory\n");
  void *ptr = malloc(1 << 20);
  fprintf(stderr, "Allocated: %p\n", ptr);

  const char *fname = "test.bin";
  int fd = open(fname, O_RDONLY);
  fprintf(stderr, "Mapping first 1MB of %s\n", fname);
  void *ptr2 = mmap(NULL, 1 << 20, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, fd, 0);
  fprintf(stderr, "Mapped: %p\n", ptr2);
  return 0;
}

$ gcc example.c -o example
$ ./example
Allocating 1MB of memory
Allocated: 0x7f1aa7d09010
Mapping first 1MB of test.bin
Mapped: 0x7f1aa7c09000
$ LD_PRELOAD=./syscall-handler.so ./example
Allocating 1MB of memory
Allocated: 0x556a51df52a0
openat: test.bin
Mapping first 1MB of test.bin
Mapped: 0xffffffffffffffff

«Ага! — Сказали суровые сибирские лесорубы...» Получили 0xffffffffffffffff, он же -1, он же MAP_FAILED.


Поэтому давайте просто представим, что нам неинтересен перехват mmap, и перепишем программу следующим образом:


#define ALLOW(sys) \
    BPF_STMT(BPF_LD  | BPF_W   | BPF_ABS, (offsetof(struct seccomp_data, nr))), \
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K  , sys, 0, 1), \
    BPF_STMT(BPF_RET           | BPF_K  , SECCOMP_RET_ALLOW),

  struct sock_filter filt[] = {
    BPF_STMT(BPF_LD  | BPF_W   | BPF_ABS, (offsetof(struct seccomp_data, args[5]))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K  , MARKER, 0, 1),
    BPF_STMT(BPF_RET           | BPF_K  , SECCOMP_RET_ALLOW),
    ALLOW(SYS_mmap)
    BPF_STMT(BPF_RET           | BPF_K  , SECCOMP_RET_TRAP),
  };

Теперь всё работает:


$ LD_PRELOAD=./syscall-handler.so ./example
Allocating 1MB of memory
Allocated: 0x7fad45a6f010
openat: test.bin
Mapping first 1MB of test.bin
Mapped: 0x7fad4596f000

Финальный исходный код
#define _GNU_SOURCE

#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <syscall.h>
#include <signal.h>
#include <string.h>

uint64_t handle_syscall(uint32_t num, uint32_t *drop_syscall,
                        uint64_t arg1, uint64_t arg2, uint64_t arg3,
                        uint64_t arg4, uint64_t arg5, uint64_t arg6)
{
  if (num == SYS_openat) {
    fprintf(stderr, "openat: %s\n", (const char *)arg2);
  }
  return 0;
}

#define MARKER 0x12345678

static int in_handler;

static void handle_sigsys(int num, siginfo_t *si, void *arg)
{
  ucontext_t *ctx = arg;
  greg_t *gregs = ctx->uc_mcontext.gregs;
  uint32_t drop = 0;
  uint64_t res;
  if (!in_handler) {
    in_handler = 1;
    res = handle_syscall(gregs[REG_RAX], &drop,
                         gregs[REG_RDI], gregs[REG_RSI], gregs[REG_RDX],
                         gregs[REG_R10], gregs[REG_R8], gregs[REG_R9]);
    in_handler = 0;
  }
  if (!drop) {
    res = syscall(gregs[REG_RAX],
                  gregs[REG_RDI], gregs[REG_RSI], gregs[REG_RDX],
                  gregs[REG_R10], gregs[REG_R8], MARKER);
  }
  gregs[REG_RAX] = res;
}

void initialize_handler(void)
{
#define ALLOW(sys) \
    BPF_STMT(BPF_LD  | BPF_W   | BPF_ABS, (offsetof(struct seccomp_data, nr))), \
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K  , sys, 0, 1), \
    BPF_STMT(BPF_RET           | BPF_K  , SECCOMP_RET_ALLOW),
  struct sock_filter filt[] = {
    BPF_STMT(BPF_LD  | BPF_W   | BPF_ABS, (offsetof(struct seccomp_data, args[5]))),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K  , MARKER, 0, 1),
    BPF_STMT(BPF_RET           | BPF_K  , SECCOMP_RET_ALLOW),
    ALLOW(SYS_mmap)
    BPF_STMT(BPF_RET           | BPF_K  , SECCOMP_RET_TRAP),
  };

  struct sock_fprog prog = {
    sizeof(filt) / sizeof(filt[0]), filt
  };

  struct sigaction sig;
  memset(&sig, 0, sizeof(sig));
  sig.sa_sigaction = handle_sigsys;
  sig.sa_flags = SA_SIGINFO | SA_NODEFER;
  sigaction(SIGSYS, &sig, NULL);

  prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
  syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog);
}

static void __attribute__((constructor))constr(void)
{
  initialize_handler();
}
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 3

    +1
    годно, спасибо! однако, долго всматривался в код, но так и не нашел что такое args[5] и вообще, откуда оно…

    ведь (offsetof(struct seccomp_data, args[5]))) будет вычисляться в момент выполнения, а в void initialize_handler(void) эти самые args никак не фигурируют… глобальной переменной тоже не видать..
    Семён Семеныч, оне в seccomp_data… вопрос снимается! :)
      0

      Хы-хы, слона-то я и не задокументировал, спасибо за баг-репорт :) Добавил абзац в статью с описанием, откуда оно взялось.

        +1
        во, теперь гораздо понятнее, почему «один широко известный в узких кругах системный вызов с шестью аргументами» является проблемой! :))

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое