Я пытаюсь выполнить базовое перехват, найдя sys_call_table и изменив запись для системного вызова sys_read на функцию в моем собственном модуле ядра. Я пробовал kprobes, мне просто интересно это сделать sys_call_table.
Ниже мой код:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/syscalls.h>
#include <linux/version.h>
typedef asmlinkage long (*t_syscall)(const struct pt_regs *);
unsigned long cr0;
unsigned long **__sys_call_table;
typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
typedef asmlinkage int (*orig_getdents64_t)(unsigned int,
struct linux_dirent64 *, unsigned int);
asmlinkage long (*original_syscall)(const struct pt_regs *);
static struct kprobe kp = {
.symbol_name = "kallsyms_lookup_name"
};
static kallsyms_lookup_name_t kallsyms_lookup_name_ptr;
static struct kprobe kp2 = {
.symbol_name = "__x64_sys_read"
};
unsigned long *get_syscall_address(unsigned long *sys_call_table, int syscall_number);
asmlinkage long hooked_syscall(const struct pt_regs *regs);
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
static inline void
write_cr0_forced(unsigned long val)
{
unsigned long __force_order;
asm volatile(
"mov %0, %%cr0"
: "+r"(val), "+m"(__force_order));
}
#endif
static inline void
unprotect_memory(void)
{
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
write_cr0_forced(cr0 & ~0x00010000);
#else
write_cr0(cr0 & ~0x00010000);
#endif
#elif IS_ENABLED(CONFIG_ARM64)
update_mapping_prot(__pa_symbol(start_rodata), (unsigned long)start_rodata,
section_size, PAGE_KERNEL);
#endif
}
static inline void
protect_memory(void)
{
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
write_cr0_forced(cr0);
#else
write_cr0(cr0);
#endif
#elif IS_ENABLED(CONFIG_ARM64)
update_mapping_prot(__pa_symbol(start_rodata), (unsigned long)start_rodata,
section_size, PAGE_KERNEL_RO);
#endif
}
asmlinkage long hooked_syscall(const struct pt_regs *regs) {
printk(KERN_INFO "Syscall hooked!\n");
return original_syscall(regs);
}
static unsigned long **find_sys_call_table(void) {
unsigned long **sct;
sct = (unsigned long **)kallsyms_lookup_name_ptr("sys_call_table");
return sct;
}
static int __init kprobe_init(void)
{
int ret;
cr0 = read_cr0();
ret = register_kprobe(&kp);
if (ret < 0)
return ret;
kallsyms_lookup_name_ptr = (kallsyms_lookup_name_t)kp.addr;
__sys_call_table = find_sys_call_table();
if (!__sys_call_table) {
printk(KERN_ERR "Couldn't find sys_call_table.\n");
return -1;
}
printk("__sys_call_table address : %px\n", __sys_call_table);
unprotect_memory();
original_syscall = (void *)__sys_call_table[__NR_read];
printk("__NR_READ : %px\n", original_syscall);
printk("HOOKED FUNCTION : %px\n", (unsigned long *)hooked_syscall);
__sys_call_table[__NR_read] = (unsigned long *)hooked_syscall;
/// Double check
original_syscall = (void *)__sys_call_table[__NR_read];
printk("__NR_READ : %px\n", original_syscall);
protect_memory();
// Extra check
int ret2 = register_kprobe(&kp2);
if (ret2 < 0)
return ret2;
printk("%px\n", kp2.addr);
unregister_kprobe(&kp);
unregister_kprobe(&kp2);
return 0;
}
static void __exit kprobe_exit(void)
{
}
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");
и Makefile,
# Name of the kernel module
obj-m += sct.o
# List of source files for the module
hello_world-objs := sct.c
# Path to the kernel source tree
KDIR := /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
Я получаю адрес kallsyms_lookup_name(), установив kprobe, и после его регистрации получаю поле .addr. Как только я получу адрес sys_call_table, я смогу прочитать
адрес системного вызова sys_read. Я проверил адрес чтения, набрав /proc/kallsyms, и, кажется, получил правильный адрес.
Затем я меняю запись __NR_read на функцию в моем файле lkm. После этого у меня есть несколько отладочных отпечатков, и я могу подтвердить, что запись sys_call_table изменилась.
printk("__sys_call_table address : %px\n", __sys_call_table);
unprotect_memory();
original_syscall = (void *)__sys_call_table[__NR_read];
printk("__NR_READ : %px\n", original_syscall);
printk("HOOKED FUNCTION : %px\n", (unsigned long *)hooked_syscall);
__sys_call_table[__NR_read] = (unsigned long *)hooked_syscall;
/// Double check
original_syscall = (void *)__sys_call_table[__NR_read];
printk("__NR_READ : %px\n", original_syscall);
К сожалению, после изменения записи sys_call_table я не вижу ни printk, ни каких-либо сбоев или чего-то еще!
Для дополнительной проверки я установил kprobe на sys_read и получил addr, но даже после изменения sys_call_table kprobe по-прежнему показывает исходный адрес sys_read.
У меня Ubuntu 24.04, 6.8.0-35-generic. Я также попробовал Ubuntu 22.04, но получил тот же результат! Оба со стандартным ядром и конфигурацией по умолчанию. Пробовал как на виртуальной машине VMware, так и на физическом оборудовании.
Я немного поискал, может ли какой-либо механизм безопасности вызвать проблемы с этим, но ничего не нашел :(
Меня довольно сбивает с толку, почему мое изменение sys_call_table не вступает в силу.
Можете ли вы сказать мне, что мне здесь не хватает? Зацепить sys_call_table уже актуально?
Я новичок и изучаю различные функции ядра Linux, мне нужно знать, актуальна ли sys_call_table модификация для перехвата системного вызова или нет?
Я постарался включить достаточно информации, чтобы помочь воспроизвести тот же результат.
Да, я немного исследовал другие подходы к тому же, но на данный момент я просто хочу знать, делаю ли я что-то не так в своем коде или есть ли какие-либо ограничения в ядре Linux, которые я пропустил.
«А зацепить sys_call_table уже актуально?» Это никогда не было одобрено, и это одна из причин, почему постепенно становится все труднее. Кроме того, это по своей сути небезопасно, поскольку если какой-то другой модуль установил перехват той же функции после вашей, невозможно гарантировать, что модули отменят изменения в правильном порядке.
Я понимаю все риски, связанные с использованием sys_call_table, и то, что это никогда не поощрялось. Я читал об этом и не мог заставить это работать. Можете ли вы рассказать подробнее о том, «постепенно становится сложнее». Существует ли какая-либо защита, которая не позволяет коду, который я указал в своем вопросе, не работать?





Сюрприз Сюрприз! Вы больше не можете этого делать, начиная с Linux v6.9. Коммит 1e3ad78334a69b36e107232e337f9d693dcc9df2 ввел меры безопасности против спекулятивного выполнения на x86, которые полностью исключили использование таблиц системных вызовов, которые были перенесены в версии 6.8.5+, v6.6.26+, v6.1.85+, v5.15.154+ .
Ubuntu 24.04 использует стабильную ветку v6.8, а Ubuntu 22.04 использует стабильную ветку v6.1, поэтому патч присутствует и там. То же самое касается Debian и дистрибутивов на его основе, таких как Kali. Большинство основных дистрибутивов Linux также включили это изменение, поскольку они просто следуют стабильной ветке ядра.
Символ sys_call_table все еще существует и содержит действительные указатели на функции, но используется только для целей трассировки (CONFIG_FTRACE_SYSCALLS=y). Фактический код отправки системного вызова теперь реализован в виде огромного встроенного switch случая (источник):
#define __SYSCALL(nr, sym) case nr: return __x64_##sym(regs);
long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
{
switch (nr) {
#include <asm/syscalls_64.h>
default: return __x64_sys_ni_syscall(regs);
}
};
Я вижу, вы уже упомянули, что пробовали kprobes (реальное решение), так что я предполагаю, что вы знаете, как их использовать. Я просто оставлю это здесь для тех, кто встретит этот пост и может найти его полезным. Использовать kprobes значительно проще, чем делать что-то вручную и «грязным» способом путем редактирования sys_call_table.
Чтобы найти подходящий символ для перехвата, вы можете просмотреть символы ядра непосредственно с помощью readelf -s и grep для интересующего вас имени системного вызова. Обычно они имеют префикс, специфичный для арки. В случае x86 это __x64_sys_ для 64-битных системных вызовов.
Я также поддерживаю syscalls.mebeim.net, где вы можете найти список имен символов системных вызовов для различных архитектур и версий ядра, которые могут оказаться вам полезными.
Вот пример того, как это можно сделать:
#include <linux/kprobes.h>
#include <linux/ptrace.h>
// ...
static int sys_read_kprobe_pre_handler(struct kprobe *p, struct pt_regs *regs)
{
// Do something here...
return 0;
}
struct kprobe syscall_kprobe = {
.symbol_name = "__x64_sys_read",
.pre_handler = sys_read_kprobe_pre_handler,
};
static int __init my_module_init(void)
{
int err;
err = register_kprobe(&syscall_kprobe);
if (err) {
pr_err("register_kprobe() failed: %d\n", err);
return err;
}
return 0;
}
static void __exit my_module_exit(void)
{
unregister_kprobe(&syscall_kprobe);
}
Примечание. Я не проверял приведенный выше код, поэтому не ждите, что он будет работать идеально, как есть, но вы можете использовать его в качестве отправной точки.
Важно: помните, что .pre_handler kprobe получит регистры ядра в struct pt_regs, переданном в качестве второго аргумента, а не регистры пользовательского пространства. Вам нужно будет получить struct pt_regs регистры пользовательского пространства из регистра, содержащего первый аргумент функции (это будет отличаться в зависимости от архитектуры, на x86 это regs->di для RDI). Существуют также особые случаи, когда системный вызов не определен с помощью макроса SYSCALL_DEFINEn и аргументы пользовательского пространства не передаются как struct pt_regs. Вам следует проверить исходные коды ядра того системного вызова, который вы пытаетесь перехватить. FWIF, я размещаю ссылку на таблицу системных вызовов здесь, которая может помочь.
Я только что протестировал перехват sys_call_table в Ubuntu 22.04.4 LTS с 5.15.0-112-generic, и у меня было такое же поведение, как описано в примечании № 1. Я мог изменить адрес системного вызова, но после изменения адрес функции остался прежним, как будто моего изменения никогда не происходило.
@Jelal Я не уверен, почему вы увидите тот же адрес после его изменения... но в любом случае для этого ядра та же история. Название 5.15.0-112 может вводить в заблуждение, оно наверняка основано на версии после 5.15.154. sys_call_table бесполезен и предназначен только для отслеживания.
Извините, я не смог правильно объясниться, я просто имел в виду, что патч, похоже, применим и к 5.15.0-112.
Я пытался сделать это недавно, но потерпел неудачу точно так же! Есть много других способов сделать это, например, проверка ядра и ftrace. В моем случае я использовал ftrace. Мне очень интересно посмотреть, как вы решите эту проблему. Спасибо