Я пытаюсь закодировать (ограниченное) перенаправление файловой системы в памяти с помощью перехвата 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_PRELOAD
ed, она все равно не будет вызвана для вызова 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).
Обратите внимание, что strace
перехватывает системный вызов (через ptrace
), а не просто функцию. Если вы хотите управлять libc, вам придется сделать то, что делает strace
[и gdb
], и запустить целевую программу под написанной вами программой, которая вызывает цель под ptrace
Можно ли каким-то образом заставить компоновщик или загрузчик использовать мой предоставленный/уже загруженный перехватывающий символ? Речь идет о слабых/сильных символах?
Нет, glibc
делает все возможное, чтобы использовать скрытые символы. Дерево вызовов для fopen
: __fopen_internal --> _IO_new_file_fopen --> _IO_file_open --> __open_nocancel
и последняя функция выполняет системный вызов. Все эти символы «спрятаны» и не экспортируются. Даже если бы они были экспортированы, glibc все равно ссылался бы на них внутри.
Пример использования ptrace
для перехвата и/или изменения системных вызовов см. в моем ответе: Принудительное сбой pthread_create в целях тестирования
Вы уверены, что для этого нельзя использовать fmemopen(3)
или funopen(3)
?
@CraigEstey К сожалению, ptrace
не вариант, поскольку я пытаюсь написать этот код для каких-то ограниченных сред, возможно, у него нет поддержки ptrace. Если вы захотите написать ответ с подробным описанием того же самого (даже кажется, что glibc даже встраивает последовательность системных вызовов), я отмечу его как ответ на этот вопрос, специально @LorinczyZsigmond fmemopen
, к сожалению, создает объект потока с недействительным fileno
(обычно -2) - и в моем случае клиентский код извлекает fileno, а затем повторно открывает поток распаковки zlib поверх него.
Почему
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 ГБ от места вызова).
libc
fopen
вызываетopenat
внутри себя, поэтому вашopenat
не будет вызываться. Вы можете перехватывать только внешние по отношению к libc вызовы, напримерfopen
отmain
.