Всем доброго времени суток! Я студентка-второкурсница технического ВУЗа. Пару месяцев назад пришла пора выбирать себе тему курсового проекта. Темы типа калькулятора меня не устраивали. Поэтому я поинтересовалась, есть ли что-нибудь более интересное, и получила утвердительный ответ. «Подмена 64-битного обработчика прерывания» — вот моя тема.
Введение
Обработчик прерываний (или процедура обслуживания прерываний) — специальная процедура, вызываемая по прерыванию для выполнения его обработки. Эти обработчики вызываются либо по аппаратному прерыванию, либо соответствующей инструкцией в программе, и обычно предназначены для взаимодействия с устройствами или для осуществления вызова функций операционной системы (wiki).
Зачем?
Главная цель, пожалуй, наглядно на рабочей системе посмотреть как оно работает, а не «грызть» сухую теорию. Ну, или как раньше программисты пытались «делать многозадачность» в DOS, переопределяя обработчик событий таймера.
32-битный обработчик
Как подступиться?
Немного поискав в интернете (особенно пригодился этот пост) и «покурив» методички по архитектуре Linux, я нашла реализацию подмены 32-битного прерывания на С. Все оказалось проще, чем я думала.
Разберем по порядку.
Системный вызов (англ. system call) — обращение прикладной программы к ядру операционной системы для выполнения какой-либо операции (wiki). Адреса обработчиков системных вызовов хранятся ядром в таблице системных вызовов (sys_call_table). Обработчик, расположенный по одному из этих адресов, вызывается каждый раз, когда какая-то программа вызывает прерывание 80h с номером какого-либо системного вызова в регистре eax (например, eax=4 для системного вызова write, выполняющего запись в файл или устройство вывода). Зная адрес этой таблицы и номер нужного вызова, можно подменить его обработчик своим собственным кодом.
Итак, с 32-битным прерыванием разобрались.
Алгоритм подмены прерывания предельно прост:
- ищем адрес таблицы системных вызовов (sys_call_table)
- ищем в ней адрес нужного нам системного вызова
- записываем вместо этого адреса адрес нашего обработчика
После таких манипуляций, при вызове подмененого нами прерывания будет вызван наш обработчик.
Чтобы это реализовать, напишем модуль ядра. Почему модуль? Да все просто, модуль — программный код, который может быть загружен или выгружен из памяти по мере необходимости. Тем самым мы расширим функциональные возможности ядра без необходимости перезагрузки системы. Модуль будем писать на С.
«Скелет» модуля ядра:
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 initn");
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 exitn");
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 для меня закончились.
Так как ТЗ предполагает использование ассемблера, модуль ядра мы напишем на С, а все его функции — на ассемблере.
#!/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 initn");
change(syscall_table);
return 0;
}
static void cleanup(void) {
unchange(syscall_table);
printk(KERN_ALERT "Module exitn");
}
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
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.:
Компиляция вспомогательного модуля и модуля ядра
Загрузка модуля в память
Вывод буфера сообщений ядра в стандартный поток вывода
Выгрузка модуля из памяти
Повторный вывод буфера сообщений ядра
Ну и архивчик с исходниками.
Автор: aleksandra_pyshko