Введение
В интернете опубликовано множество статей по перехвату системных вызовов под x32. В рамках решения одной задачи появилась необходимость в перехвате системных вызовов под архитектурой x86-64 при помощи загружаемого модуля ядра. Приступим:
Перехват системных вызовов
Алгоритм:
- Поиск адреса таблицы системных вызовов
- Подмена на адреса новых системных вызовов
Поиск адреса таблицы системных вызовов
Первый вариант: можно найти через таблицу дескрипторов прерываний (IDT), IDT — служит для связи обработчика прерывания с номером прерывания. В защищённом режиме адрес в физической памяти и размер таблицы прерываний определяется 80-битным регистром IDTR.В защищённом режиме элементом IDT является шлюз прерывания длиной 10 байт, содержащий сегментный (логический) адрес обработчика прерывания, права доступа и др. Нам такой метод не интересен, т.к. мы получим адрес обработчика, который сделан для совместимости с х32
Второй вариант, более интересен.
Для начала не большой экскурс: MSR – machine state register это набор регистров процессоров Интел, используемых в семействе x86 и x86-64 процессоров. Эти регистры предоставляют возможность контролировать и получать информацию о состоянии процессора. Все MSR регистры доступны только для системных функций и не доступны из пользовательских программ. Нас в частности интересует следующий регистр:MSR_LSTAR — 0xc0000082 (long mode SYSCALL target)
(полный список можно посмотреть в /usr/include/asm/msr-index.h).
В этом регистре хранится адрес обработчика прерываний для x86-64.
Получить адрес можно следующим образом:
int i, lo, hi;
asm volatile("rdmsr" : "=a" (lo), "=d" (hi) : "c" (MSR_LSTAR));
system_call = (void*)(((long)hi<<32) | lo);
Далее найдем адрес самой таблицы. Перейдем на только что полученный адрес и найдем в памяти последовательность \xff\x14\xc5(эти магические числа берутся, если посмотреть на код ядра, в частности, на код функции system_call, в которой происходит вызов обработчика из искомой). Считав следующие за ней 4 байта, мы получим адрес таблицы системных вызовов syscall_table. Зная ее адрес, мы можем получить содержимое этой таблицы (адреса всех системных функций) и изменить адрес любого системного вызова, перехватив его.
код для нахождения адреса таблицы системных вызовов:
unsigned char *ptr;
for (ptr=system_call, i=0; i<500; i++) {
if (ptr[0] == 0xff && ptr[1] == 0x14 && ptr[2] == 0xc5)
return (void*)(0xffffffff00000000 | *((unsigned int*)(ptr+3)));
ptr++;
}
Подмена на адреса новых системных вызовов
Тут тоже есть определенные нюансы, если просто так попробовать изменить что либо в таблице, то будет выдана ошибка. К счастью это достаточно легко обходится:
- Отключаем защиту памяти
- Переписываем адрес на адрес нашего обработчика
- Включаем защиту памяти
Для снятие и установки защиты необходимо знать следующее: регистр CR0 — содержит системные флаги управления, управляющие поведением и состоянием процессора.Флаг WP — защита от записи (Write Protect), 48-й бит CR0. Когда установлен, запрещает системным процедурам запись в пользовательские страницы с доступом только для чтения (когда флаг WP сброшен — разрешает). В отличие от х32 изменился только размер регистра и номер флага
код снятия защиты:
asm("pushq %rax");
asm("movq %cr0, %rax");
asm("andq $0xfffffffffffeffff, %rax");
asm("movq %rax, %cr0");
asm("popq %rax");
код включения защиты:
asm("pushq %rax");
asm("movq %cr0, %rax");
asm("xorq $0x0000000000001000, %rax");
asm("movq %rax, %cr0");
asm("popq %rax");
Этих знаний достаточно для подмены системных вызовов в Linux x86-64. Надеюсь кому-нибудь это будет полезным.
Спасибо за внимание.
UPD: