Перехват открытого системного вызова в C при вызове через fopen

Я пытаюсь закодировать (ограниченное) перенаправление файловой системы в памяти с помощью перехвата libc/syscall и возврата созданных memfd_create файловых дескрипторов для виртуализированных файлов. На этапе подготовки у меня есть следующий test.c файл:

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <dlfcn.h>
#include <sys/stat.h>

/*
FILE* fopen(const char *path, const char *mode)
{
    typedef FILE* (*orig_fopen_func_type)(const char *path, const char *mode);
    fprintf(stderr, "log_file_access_preload: fopen(\"%s\", \"%s\")\n", path, mode);
    orig_fopen_func_type orig_func = (orig_fopen_func_type)dlsym(RTLD_NEXT, "fopen");
    return orig_func(path, mode);
}
*/

int open(const char *path, int flags)
{
    typedef int (*orig_func_type)(const char *pathname, int flags);
    fprintf(stderr, "log_file_access_preload: open(\"%s\", %d)\n", path, flags);
    orig_func_type orig_func = (orig_func_type)dlsym(RTLD_NEXT, "open");
    return orig_func(path, flags);
}
int open64(const char *path, int flags)
{
    typedef int (*orig_func_type)(const char *pathname, int flags);
    fprintf(stderr, "log_file_access_preload: open64(\"%s\", %d)\n", path, flags);
    orig_func_type orig_func = (orig_func_type)dlsym(RTLD_NEXT, "open64");
    return orig_func(path, flags);
}
//TODO: int openat(int dirfd, const char *path, int flags, mode_t mode)
int openat(int dirfd, const char *path, int flags)
{
    typedef int (*orig_func_type)(int dirfd, const char *pathname, int flags);
    fprintf(stderr, "log_file_access_preload: openat(%d, \"%s\", %d)\n", dirfd, path, flags);
    orig_func_type orig_func = (orig_func_type)dlsym(RTLD_NEXT, "openat");
    return orig_func(dirfd, path, flags);
}

int main()
{
    (void)fopen("test.txt", "r");
}

Компиляция как gcc test.c (в Ubuntu 22.04) и вызов его как ./a.out ничего не печатает (если я раскомментирую перехват fopen, то он работает, но, к сожалению, fmemopen не позволяет создать с ним соответствующий fd/fileno, в отличие от shm_open и memfd_create, поэтому я предпочитаю переопределять только вызовы open)

Если я поместлю аналогичный перехват в общую библиотеку LD_PRELOADed, она все равно не будет вызвана для вызова open (опять же, это работает, если я перехватываю fopen).

Если я strace -f ./a.out, то на выходе я получаю openat(AT_FDCWD, "test.txt", O_RDONLY) = -1 ENOENT (No such file or directory), значит, open вызывается?

execve("./a.out", ["./a.out"], 0x7ffff9b89518 /* 22 vars */) = 0
brk(NULL)                               = 0x7fffde75e000
arch_prctl(0x3001 /* ARCH_??? */, 0x7fffe5547b50) = -1 EINVAL (Invalid argument)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcee5410000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=103195, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 103195, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fcee53b6000
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\0P\237\2\0\0\0\0\0"..., 832) = 832
pread64(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, 64) = 784
pread64(3, "\4\0\0\0 \0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0"..., 48, 848) = 48
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\244;\374\204(\337f#\315I\214\234\f\256\271\32"..., 68, 896) = 68
newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=2216304, ...}, AT_EMPTY_PATH) = 0
pread64(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, 64) = 784
mmap(NULL, 2260560, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fcee5180000
mmap(0x7fcee51a8000, 1658880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x7fcee51a8000
mmap(0x7fcee533d000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bd000) = 0x7fcee533d000
mmap(0x7fcee5395000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x214000) = 0x7fcee5395000
mmap(0x7fcee539b000, 52816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fcee539b000
close(3)                                = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcee53b0000
arch_prctl(ARCH_SET_FS, 0x7fcee53b0740) = 0
set_tid_address(0x7fcee53b0a10)         = 5270
set_robust_list(0x7fcee53b0a20, 24)     = 0
rseq(0x7fcee53b10e0, 0x20, 0, 0x53053053) = -1 ENOSYS (Function not implemented)
mprotect(0x7fcee5395000, 16384, PROT_READ) = 0
mprotect(0x7fcee5419000, 4096, PROT_READ) = 0
mprotect(0x7fcee5408000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=8192*1024}) = 0
munmap(0x7fcee53b6000, 103195)          = 0
getrandom("\x05\x04\x5c\x57\xcc\x25\x9c\x8e", 8, GRND_NONBLOCK) = 8
brk(NULL)                               = 0x7fffde75e000
brk(0x7fffde77f000)                     = 0x7fffde77f000
openat(AT_FDCWD, "test.txt", O_RDONLY)  = -1 ENOENT (No such file or directory)
exit_group(0)                           = ?
+++ exited with 0 +++

Почему strace можно увидеть этот звонок, а мой перехват — нет? Есть ли способ заставить этот перехват работать?

Спасибо!


P.S. В конце концов, я хочу создать fd для буфера в памяти (возможно, memfd_create, а затем записать в него свой буфер, а затем seek перевести в 0).

libc fopen вызывает openat внутри себя, поэтому ваш openat не будет вызываться. Вы можете перехватывать только внешние по отношению к libc вызовы, например fopen от main.

Craig Estey 02.07.2024 00:20

Обратите внимание, что strace перехватывает системный вызов (через ptrace), а не просто функцию. Если вы хотите управлять libc, вам придется сделать то, что делает stracegdb], и запустить целевую программу под написанной вами программой, которая вызывает цель под ptrace

Craig Estey 02.07.2024 00:26

Можно ли каким-то образом заставить компоновщик или загрузчик использовать мой предоставленный/уже загруженный перехватывающий символ? Речь идет о слабых/сильных символах?

Vadim Kantorov 02.07.2024 00:27

Нет, glibc делает все возможное, чтобы использовать скрытые символы. Дерево вызовов для fopen: __fopen_internal --> _IO_new_file_fopen --> _IO_file_open --> __open_nocancel и последняя функция выполняет системный вызов. Все эти символы «спрятаны» и не экспортируются. Даже если бы они были экспортированы, glibc все равно ссылался бы на них внутри.

Craig Estey 02.07.2024 00:44

Пример использования ptrace для перехвата и/или изменения системных вызовов см. в моем ответе: Принудительное сбой pthread_create в целях тестирования

Craig Estey 02.07.2024 00:44

Вы уверены, что для этого нельзя использовать fmemopen(3) или funopen(3)?

Lorinczy Zsigmond 02.07.2024 06:56

@CraigEstey К сожалению, ptrace не вариант, поскольку я пытаюсь написать этот код для каких-то ограниченных сред, возможно, у него нет поддержки ptrace. Если вы захотите написать ответ с подробным описанием того же самого (даже кажется, что glibc даже встраивает последовательность системных вызовов), я отмечу его как ответ на этот вопрос, специально @LorinczyZsigmond fmemopen, к сожалению, создает объект потока с недействительным fileno (обычно -2) - и в моем случае клиентский код извлекает fileno, а затем повторно открывает поток распаковки zlib поверх него.

Vadim Kantorov 02.07.2024 13:32
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
7
78
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Почему strace можно увидеть этот звонок, а мой перехват — нет?

strace не видит вызов openat в libc, он видит реальный системный вызов, используя совершенно другой механизм (ptrace).

Напротив, вы пытаетесь вставить вызовы в libc.so, используя динамическое разрешение символов компоновщика.

Эта программа вообще не вызывает libc, но strace всё равно будет показывать openat в ней:

        section .text
        global _start
_start:
        mov     rax,257
        mov     rdi,0
        mov     rsi,0
        syscall

        mov     rax,231
        mov     rdi,0
        syscall
nasm -f elf64 x.s && ld x.o && strace ./a.out

execve("./a.out", ["./a.out"], 0x7fff89dee840 /* 30 vars */) = 0
openat(0, NULL, O_RDONLY)               = -1 EFAULT (Bad address)
exit_group(0)                           = ?
+++ exited with 0 +++

Есть ли способ заставить этот перехват работать?

Во-первых, давайте посмотрим, как на самом деле вызывается openat. В Fedora 40, используя int main() { fopen("test.txt", "r"); }, переходя к main и используя catch syscall openat в GDB:

Catchpoint 1 (call to syscall openat), 0x00007ffff7ed4403 in __libc_open64 (file=0x402012 "test.txt", oflag=0)
    at ../sysdeps/unix/sysv/linux/open64.c:41
Downloading source file /usr/src/debug/glibc-2.39-15.fc40.x86_64/io/../sysdeps/unix/sysv/linux/open64.c
41        return SYSCALL_CANCEL (openat, AT_FDCWD, file, oflag | O_LARGEFILE,
(gdb) bt
#0  0x00007ffff7ed4403 in __libc_open64 (file=0x402012 "test.txt", oflag=0)
    at ../sysdeps/unix/sysv/linux/open64.c:41
#1  0x00007ffff7e56adf in __GI__IO_file_open (fp=fp@entry=0x4052a0, filename=<optimized out>,
    posix_mode=<optimized out>, prot=prot@entry=438, read_write=8, is32not64=<optimized out>) at fileops.c:188
#2  0x00007ffff7e56c95 in _IO_new_file_fopen (fp=fp@entry=0x4052a0,
    filename=filename@entry=0x402012 "test.txt", mode=<optimized out>, mode@entry=0x402010 "r",
    is32not64=is32not64@entry=1) at fileops.c:281
#3  0x00007ffff7e4b046 in __fopen_internal (filename=0x402012 "test.txt", mode=0x402010 "r", is32=1)
    at iofopen.c:75
#4  0x0000000000401139 in main ()

Здесь вы можете видеть, что __libc_open64 — это место, где выполняется системный вызов, и что он вызывается из __GI__IO_file_open. Как выглядит разборка на месте звонка?

(gdb) x/i $pc-5
   0x7ffff7e56ada <__GI__IO_file_open+42>:      call   0x7ffff7ed43b0 <__libc_open64>

Вы можете видеть, что этот вызов не проходит через PLT и, следовательно, не участвует в динамическом разрешении символов.

Так что же можно с этим сделать?

Если вы контролируете целевую среду, вы можете изменить исходный код GLIBC, чтобы он вызывал open вместо __open в fileops.c и пересобрать libc.so.6.

Если вам нужно заставить это работать с существующим libc.so.6, единственный известный мне вариант — это (очень хакерский) патчинг во время выполнения.

По сути, вам придется отсканировать инструкции внутри __IO_file_open, найти вызов __open64 и исправить его, чтобы вместо этого вызвать ваш код промежуточного устройства (с дополнительной сложностью в том, что ваш промежуточный модуль не может находиться на расстоянии более 2 ГБ от места вызова).

Другие вопросы по теме