Перехват системного вызова путем изменения sys_call_table не работает

Я пытаюсь выполнить базовое перехват, найдя 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 модификация для перехвата системного вызова или нет?

Я постарался включить достаточно информации, чтобы помочь воспроизвести тот же результат.

Я пытался сделать это недавно, но потерпел неудачу точно так же! Есть много других способов сделать это, например, проверка ядра и ftrace. В моем случае я использовал ftrace. Мне очень интересно посмотреть, как вы решите эту проблему. Спасибо

David 10.06.2024 11:20

Да, я немного исследовал другие подходы к тому же, но на данный момент я просто хочу знать, делаю ли я что-то не так в своем коде или есть ли какие-либо ограничения в ядре Linux, которые я пропустил.

Jelal 10.06.2024 12:26

«А зацепить sys_call_table уже актуально?» Это никогда не было одобрено, и это одна из причин, почему постепенно становится все труднее. Кроме того, это по своей сути небезопасно, поскольку если какой-то другой модуль установил перехват той же функции после вашей, невозможно гарантировать, что модули отменят изменения в правильном порядке.

Ian Abbott 10.06.2024 13:50

Я понимаю все риски, связанные с использованием sys_call_table, и то, что это никогда не поощрялось. Я читал об этом и не мог заставить это работать. Можете ли вы рассказать подробнее о том, «постепенно становится сложнее». Существует ли какая-либо защита, которая не позволяет коду, который я указал в своем вопросе, не работать?

Jelal 10.06.2024 14:48
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
7
4
256
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Сюрприз Сюрприз! Вы больше не можете этого делать, начиная с 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 23.06.2024 13:06

@Jelal Я не уверен, почему вы увидите тот же адрес после его изменения... но в любом случае для этого ядра та же история. Название 5.15.0-112 может вводить в заблуждение, оно наверняка основано на версии после 5.15.154. sys_call_table бесполезен и предназначен только для отслеживания.

Marco Bonelli 23.06.2024 14:25

Извините, я не смог правильно объясниться, я просто имел в виду, что патч, похоже, применим и к 5.15.0-112.

Jelal 25.06.2024 18:06

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