Делая пере-реализацию 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 -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(); }
