Как перехватить ВСЕ системные вызовы Linux во время бинарного выполнения
Я пытаюсь изменить поведение системного вызова Linux по умолчанию. В данный момент я пытаюсь подключить простое выражение для печати до того, как они действительно будут вызваны. Я знаю о стандартной опции "обтекания" компоновщика GCC и о том, как ее можно использовать для перехватывания упаковщиков. Это прекрасно работает для open (), fstat (), fwrite () и т. Д. (Где я на самом деле перехватываю оболочки libc).
ОБНОВИТЬ:
Ограничением является то, что НЕ все системные вызовы подключаются с таким подходом. Чтобы проиллюстрировать это, давайте возьмем простой статически скомпилированный двоичный файл. Когда мы пытаемся добавить оболочки, на них влияют вызовы, которые мы вводим после main () (см. Вывод strace, показанный ниже)
> strace ./sample
execve("./sample", ["./sample"], [/* 72 vars */]) = 0
uname({sys="Linux", node="kumar", ...}) = 0
brk(0) = 0x71f000
brk(0x7201c0) = 0x7201c0
arch_prctl(ARCH_SET_FS, 0x71f880) = 0
readlink("/proc/self/exe", "/home/admin/sample"..., 4096) = 41
brk(0x7411c0) = 0x7411c0
brk(0x742000) = 0x742000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fbcc54d1000
write(1, "Hello from the wrapped readlink "..., 36Hello from the wrapped readlink :з
) = 36
readlink("/usr/bin/gnome-www-browser", "/etc/alternatives/gnome-www-brow"..., 255) = 35
write(1, "/etc/alternatives/gnome-www-brow"..., 36/etc/alternatives/gnome-www-browser
) = 36
exit_group(36) = ?
+++ exited with 36 +++
Если мы обратим внимание на двоичный файл, первый "не перехваченный" вызов readlink () (системный вызов 89, то есть 0x59) происходит из этих строк - некоторая часть кода, связанная с компоновщиком (т.е. _dl_get_origin), выполняет readlink () для своего функционирования. Эти неявные системные вызовы (хотя они присутствуют в двоичном коде) никогда не зацикливаются на нашем подходе "обертки".
000000000051875c <_dl_get_origin>:
51875c: b8 59 00 00 00 mov $0x59,%eax
518761: 55 push %rbp
518762: 53 push %rbx
518763: 48 81 ec 00 10 00 00 sub $0x1000,%rsp
51876a: 48 89 e6 mov %rsp,%rsi
51876d: 0f 05 syscall
Как распространить идею обёртывания на системные вызовы, такие как readlink () (включая все неявные, вызываемые)?
1 ответ
У меня есть опция для упаковки, цитата из руководства:
- символ переноса
Используйте функцию-обертку для символа. Любая неопределенная ссылка на символ будет преобразована в __wrap_symbol. Любая неопределенная ссылка на __real_symbol будет преобразована в символ. Это можно использовать для предоставления оболочки для системной функции. Функция-обертка должна называться __wrap_symbol. Если он хочет вызвать системную функцию, он должен вызвать __real_symbol.
Он отлично работает и с системными вызовами. Вот пример с readlink
:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
ssize_t __real_readlink(const char *path, char *buf, size_t bufsiz);
ssize_t __wrap_readlink(const char *path, char *buf, size_t bufsiz) {
puts("Hello from the wrapped readlink :з");
__real_readlink(path, buf, bufsiz);
}
int main(void) {
const char testLink[] = "/usr/bin/gnome-www-browser";
char buf[256];
memset(buf, 0, sizeof(buf));
readlink(testLink, buf, sizeof(buf)-1);
puts(buf);
}
Чтобы передать опцию компоновщику из компилятора, используйте -Wl
опция:
$ gcc test.c -o a -Wl,--wrap=readlink
$ ./a
Hello from the wrapped readlink :з
/etc/alternatives/gnome-www-browser
Идея в том, что __wrap_func
ваша оболочка функции __real_func
компоновщик будет связывать с реальной функцией func
, И каждый звонок func
в коде будет заменен на __wrap_func
,
UPD: можно заметить, что бинарный файл статически вызывает другой readlink
, которые не перехватываются. Чтобы понять причину, просто проведите небольшой эксперимент - скомпилируйте код в объектный файл и перечислите символы, например:
$ gcc test.c -c -o a.o -Wl,--wrap=readlink
$ nm a.o
0000000000000037 T main
U memset
U puts
U readlink
U __real_readlink
U __stack_chk_fail
0000000000000000 T __wrap_readlink
Интересно то, что вы не увидите ссылки на набор функций, которые были просмотрены с помощью strace до входа в основную функцию - например, uname()
, brk()
, access()
и т. д. Это потому, что основная функция не первый код, который вызывается в вашем двоичном файле. Немного исследований с objdump
покажет вам, что первая функция называется _start
,
Теперь давайте сделаем еще один пример - переопределить _start
функция:
$ cat test2.c
#include <stdio.h>
#include <unistd.h>
void _start() {
puts("Hello");
_exit(0);
}
$ gcc test2.c -o a -nostartfiles
$ strace ./a
execve("./a", ["./a"], [/* 69 vars */]) = 0
brk(0) = 0x150c000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece55d000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=177964, ...}) = 0
mmap(NULL, 177964, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f3ece531000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", 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\320\37\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1840928, ...}) = 0
mmap(NULL, 3949248, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f3ecdf78000
mprotect(0x7f3ece133000, 2093056, PROT_NONE) = 0
mmap(0x7f3ece332000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1ba000) = 0x7f3ece332000
mmap(0x7f3ece338000, 17088, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f3ece338000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece530000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece52e000
arch_prctl(ARCH_SET_FS, 0x7f3ece52e740) = 0
mprotect(0x7f3ece332000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ) = 0
mprotect(0x7f3ece55f000, 4096, PROT_READ) = 0
munmap(0x7f3ece531000, 177964) = 0
fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 10), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece55c000
write(1, "Hello\n", 6Hello
) = 6
exit_group(0) = ?
+++ exited with 0 +++
$
Что это было?! Мы просто переопределяем первую функцию в двоичном файле и по-прежнему видим системные вызовы - почему?
На самом деле это потому, что вызовы выполняются не вашим приложением, а ядром, прежде чем ваше приложение будет загружено в память и разрешено запускаться.
UPD: как мы видели ранее, функции не вызываются вашим приложением. Честно говоря, я не смог найти, что делается для статических двоичных файлов после вызова оболочки execve
для вашего приложения, но из списка это выглядит так, как будто каждый вызов, который вы видите, выполняется самим ядром - без каких-либо сторонних приложений, таких как динамический компоновщик, который не нужен для статических двоичных файлов (и потому что есть такие функции, как brk
это работает с сегментами данных).
Как бы то ни было, вы, конечно, не можете так легко изменить это поведение, вам понадобится взлом. Потому что, если бы вы могли легко переопределить функцию для кода, который выполняется перед вашим двоичным запуском - то есть из другого двоичного файла - это была бы большая черная дыра в безопасности, просто представьте: как только вам нужны права root, вы переопределяете функцию с одним, чтобы выполнить ваш код, и немного подождать, пока какой-нибудь демон с правами root выполнит скрипт и, таким образом, запустит ваш код в игру.