Lev Goncharov

DevOps Engineer

View My GitHub Profile

System call interception in Linux-kernel module (kernel 2.6.34.7-61.fc13.x86_64)

Introduction

В интернете опубликовано множество статей по перехвату системных вызовов под x32. В рамках решения одной задачи появилась необходимость в перехвате системных вызовов под архитектурой x86-64 при помощи загружаемого модуля ядра. Приступим.

Перехват системных вызовов

Алгоритм:

Поиск адреса таблицы системных вызовов

Первый вариант: можно найти через таблицу дескрипторов прерываний (IDT - Interrupt Description Table), IDT — служит для связи обработчика прерывания с номером прерывания. В защищённом режиме адрес в физической памяти и размер таблицы прерываний определяется 80-битным регистром IDTR.В защищённом режиме элементом IDT является шлюз прерывания длиной 10 байт, содержащий сегментный (логический) адрес обработчика прерывания, права доступа и др. Нам такой метод не интересен, т.к. мы получим адрес обработчика, который сделан для совместимости с х32

ля начала не большой экскурс: MSR (Model-Specific Register) это набор регистров процессоров Интел, используемых в семействе x86 и x86-64 процессоров. Эти регистры предоставляют возможность контролировать и получать информацию о состоянии процессора. Все MSR регистры доступны только для системных функций и не доступны из пользовательских программ. Нас в частности интересует следующий регистр: MSR_LSTAR0xc0000082 (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");

Conclusion

Этих знаний достаточно для подмены системных вызовов в Linux x86-64. Надеюсь кому-нибудь это будет полезным.