Делая пере-реализацию 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();
}
aol-nnov
годно, спасибо!
однако, долго всматривался в код, но так и не нашел что такое args[5] и вообще, откуда оно…
Семён Семеныч, оне в seccomp_data… вопрос снимается! :)ведь (offsetof(struct seccomp_data, args[5]))) будет вычисляться в момент выполнения, а в void initialize_handler(void) эти самые args никак не фигурируют… глобальной переменной тоже не видать..
atrosinenko Автор
Хы-хы, слона-то я и не задокументировал, спасибо за баг-репорт :) Добавил абзац в статью с описанием, откуда оно взялось.
aol-nnov
во, теперь гораздо понятнее, почему «один широко известный в узких кругах системный вызов с шестью аргументами» является проблемой! :))