Pull to refresh

Подмена обработчика системного вызова

Reading time6 min
Views9.6K
Всем доброго времени суток! Я студентка-второкурсница технического ВУЗа. Пару месяцев назад пришла пора выбирать себе тему курсового проекта. Темы типа калькулятора меня не устраивали. Поэтому я поинтересовалась, есть ли что-нибудь более интересное, и получила утвердительный ответ. «Подмена обработчика системного вызова» — вот моя тема.

Введение

Обработчик прерываний (или процедура обслуживания прерываний) — специальная процедура, вызываемая по прерыванию для выполнения его обработки. Эти обработчики вызываются либо по аппаратному прерыванию, либо соответствующей инструкцией в программе, и обычно предназначены для взаимодействия с устройствами или для осуществления вызова функций операционной системы (wiki).

Зачем?

Главная цель, пожалуй, наглядно на рабочей системе посмотреть как оно работает, а не «грызть» сухую теорию. Ну, или как раньше программисты пытались «делать многозадачность» в DOS, переопределяя обработчик событий таймера.

32-битный обработчик


Как подступиться?
Немного поискав в интернете (особенно пригодился этот пост) и «покурив» методички по архитектуре Linux, я нашла реализацию подмены 32-битного прерывания на С. Все оказалось проще, чем я думала.

Разберем по порядку.
Системный вызов (англ. system call) — обращение прикладной программы к ядру операционной системы для выполнения какой-либо операции (wiki). Адреса обработчиков системных вызовов хранятся ядром в таблице системных вызовов (sys_call_table). Обработчик, расположенный по одному из этих адресов, вызывается каждый раз, когда какая-то программа вызывает прерывание 80h с номером какого-либо системного вызова в регистре eax (например, eax=4 для системного вызова write, выполняющего запись в файл или устройство вывода). Зная адрес этой таблицы и номер нужного вызова, можно подменить его обработчик своим собственным кодом.

Итак, с 32-битным прерыванием разобрались.

Алгоритм подмены прерывания предельно прост:
  1. ищем адрес таблицы системных вызовов (sys_call_table)
  2. ищем в ней адрес нужного нам системного вызова
  3. записываем вместо этого адреса адрес нашего обработчика

После таких манипуляций, при вызове подмененого нами прерывания будет вызван наш обработчик.
Чтобы это реализовать, напишем модуль ядра. Почему модуль? Да все просто, модуль — программный код, который может быть загружен или выгружен из памяти по мере необходимости. Тем самым мы расширим функциональные возможности ядра без необходимости перезагрузки системы. Модуль будем писать на С.

«Скелет» модуля ядра:

static int init(void) {
}
static void exit(void) {
}
module_init(init);
module_exit(exit);


Модуль, как видно из вышенаписанного, должен содержать как минимум 2 функции — функцию инициализации модуля в памяти (вызывается при загрузке модуля в память) и функцию завершения работы (вызывается, соответственно, при выгрузке модуля).
Главная задача, которую требуется решить для подмены обработчика — выяснить расположение таблицы системных вызовов в оперативной памяти. Адрес таблицы можно найти в файле «System.map-версия_ядра». Найденный адрес добавим в компилируемый модуль. Для поиска адреса воспользуемся следующей командой:

grep sys_call_table /boot/System.map-$(uname -r) |awk '{print $1}'

Команда выведет на экран найденный адрес, например:

c05d3180

Для автоматизации процесса поиска таблицы системных вызовов можно написать небольшой скрипт, что я, собственно, и сделала.
Чтобы полностью заменить системный вызов своим кодом, нужно полностью реализовать его функционал. Поэтому, во избежание ненужной головной боли, поступим иначе: при подмене адреса в таблице системных вызовов сохраним прежнее значение в какой-либо переменной, и каждый раз после выполнения своих действий будем передавать управление на этот адрес. Такой подход позволяет добавить собственные действия без ущерба для уже существующего функционала и таким образом не сломать работу ОСи.
Чтобы ничего не испортить, наш первый модуль ядра будет просто выводить сообщение в лог ядра системы при вызове функции write.

Важная деталь! При подмене обработчика необходимо обойти защиту от записи для области векторов прерываний. Мы делаем это сбросом WP-бита системного регистра CR0. Этот бит действует на аппаратном уровне, разрешая (для кода, имеющего достаточные привилегии) модификации страниц памяти независимо от того, разрешена в них запись или нет. Доступ к регистру CR0 выполняется макросами write_cr0() и read_cr0().

Итоговый код модуля
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/unistd.h>
#include <asm/cacheflush.h>
#include <asm/page.h>
#include <asm/current.h>
#include <linux/sched.h>
#include <linux/kallsyms.h>

unsigned long *syscall_table = (unsigned long *)0xTABLE; // TABLE - адрес таблицы системных вызовов (в нашем случае был c05d3180)
asmlinkage int (*original_write)(unsigned int, const char __user *, size_t);

asmlinkage int new_write(unsigned int fd, const char __user *buf, size_t count) { // измененная функция write
    printk(KERN_ALERT "It works!\n");
    return (*original_write)(fd, buf, count);
}

static int init(void) {
    printk(KERN_ALERT "Module init\n");
    write_cr0 (read_cr0 () & (~ 0x10000)); // сброс WP бита
    original_write = (void *)syscall_table[__NR_write];// сохраняем адрес старого обработчика
    syscall_table[__NR_write] = new_write; // и записываем новый
    write_cr0 (read_cr0 () | 0x10000); // устанавливаем WP бит обратно
    return 0;
}

static void exit(void) {
    write_cr0 (read_cr0 () & (~ 0x10000)); // сброс WP бита
    syscall_table[__NR_write] = original_write; // Возвращаем стандартный обработчик на место
    write_cr0 (read_cr0 () | 0x10000); // устанавливаем WP бит обратно
    printk(KERN_ALERT "Module exit\n");
    return;
}

module_init(init);
module_exit(exit);


64-битный обработчик


Первое отличие, которое я заметила — это при вводе команды

grep sys_call_table /boot/System.map-$(uname -r) |awk '{print $1}'

вывелось два адреса:

ffffffff81801300
ffffffff81805260

Несколько изменив команду, я получила такой вот результат

grep sys_call_table /boot/System.map-$(uname -r)

ffffffff81801300 R sys_call_table
ffffffff81805260 R ia32_sys_call_table

Все сразу стало ясно. В 64-битной архитектуре для совместимости с 32-битной присутствуют две таблицы системных вызовов. Как видно, одна для 64-битный вызовов, а вторая для 32-битных.
В 32-битной архитектуре __NR_write был равен 4 (оно и понятно, системный вызов write находится под номером 4), а в х64 равен 1. Так как до этого я не работала с 64-битным ассемблером, я не сразу поняла в чем дело, но потом узнала, что sys_write в 64-битной архитектуре имеет номер 1.
Собственно, на этом все интересующие меня различия между 64-битным и 32-битным обработчиком прерывания write для меня закончились.
Так как ТЗ предполагает использование ассемблера, модуль ядра мы напишем на С, а все его функции — на ассемблере.

Shell-скрипт
#!/bin/bash

TABLE=$(grep ' sys_call_table' /boot/System.map-$(uname -r) |awk '{print $1}')
echo $TABLE
sed -i s/TABLE/$TABLE/g module.c

Модуль ядра
#include <linux/init.h>
#include <linux/module.h>

unsigned long *syscall_table = (unsigned long *)0xTABLE;

extern void change(unsigned long *temp);
extern void unchange(unsigned long *temp);

static int init(void) {
    printk(KERN_ALERT "\nModule init\n");
    change(syscall_table);
    return 0;
}
static void cleanup(void) {
    unchange(syscall_table);
    printk(KERN_ALERT "Module exit\n");
}
module_init(init);
module_exit(cleanup);

Вспомогательный модуль
global unlockWP
global lockWP
global change
global unchange
extern printk

SECTION .text

newwrite:
    mov    rax, original        ; original_write
    mov    rax, QWORD[rax]      ; в rdi - 4 байта fd
    call   far rax              ; в rsi - 8 байт buf
                                ; в rdx - 8 байт count
                                ; вызов оригинального прерывания

    push rax                    ; сохраняем результат отработки прерывания

    xor rax, rax                ; обнуляем rax
    mov rdi, work               ; выводим строку "It works"
    call printk                 ; вызываем функцию printk

    pop rax                     ; возвращаем в rax то, что вернула оригинальная функция

   ret

change:

    call unlockWP               ; снимаем защиту 

                                ; rdi - параметр
    add rdi, 8                  ; rdi - syscall_table + __NR_write
    mov rax, QWORD [rdi]        ; rax - syscall_table[__NR_write]
    mov rbx , original
    mov QWORD [rbx], rax        ; сохранили адрес оригинального вызова

    mov rax, newwrite           ; и записываем в таблицу вместо оригинального
    mov QWORD [rdi], rax        ; адрес нашего вызова

    call lockWP                 ; возвращаем защиту

    ret

unchange:

    call unlockWP               ; снимаем защиту 

                                ; rdi - параметр
    add rdi, 8                  ; rdi - syscall_table + __NR_write
    mov rbx, original           ; в rbx адрес оригинального вызова
    mov rax, QWORD [rbx]        ; rax - syscall_table[__NR_write]
    mov QWORD [rdi],rax         ; записываем его обратно в таблицу

    call lockWP                 ; возвращаем защиту

    ret

unlockWP:
    mov rax, cr0
    and rax, 0xfffffffffffeffff
    mov cr0, rax
    ret

lockWP:
    mov rax, cr0
    xor rax, 0x0000000000001000
    mov cr0, rax
    ret
SECTION .data
    original: DQ 0,0
    work:     DB "It works!",10,0

Makefile
obj-m += kmod.o
kmod-objs := module.o main.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
module:
	nasm -f elf64 -o main.o main.asm	
	make -C $(KDIR) SUBDIRS=$(PWD) modules
	make clean
clean:
	rm -f *.o *mod.c *.symvers *.order


Если все прошло успешно, в каталоге с исходниками получим файл kmod.ko. Это и есть наш модуль ядра. Чтобы проверить его работу, необходимо загрузить его в память. Делается это при помощи команды insmod модуль_ядра. Чтобы выгрузить модуль — выполнить команду rmmod модуль_ядра. Для проверки работы модуля выполним команду dmesg, тем самым выведем буфера сообщений ядра в стандартный поток вывода.

Спасибо за внимание.

P.S.:
Парочка скриншотов.
Запуск скрипта

Компиляция вспомогательного модуля и модуля ядра

Загрузка модуля в память

Вывод буфера сообщений ядра в стандартный поток вывода

Выгрузка модуля из памяти

Повторный вывод буфера сообщений ядра


Ну и архивчик с исходниками.
Tags:
Hubs:
+42
Comments32

Articles

Change theme settings