Linux - статьи

Модификация таблицы системных вызовов


Текущие адреса системных вызовов находятся в таблице системных вызовов, в памяти, выделенной для ядра ОС. Адреса расположены в том же порядке, что и их функции и представлены в файле /usr/include/asm/unistd.h. Системные вызовы идентифицируются номером точки входа (ID), как мы видели в таблице выше.

Давайте начнем с примера. Когда вызывается системный вызов sys_write, его ID, равный 4, помещается в регистр eax и генерируется программное прерывание (int 0x80). Есть специальный обработчик прерывания, помещающий этот адрес в таблицу дескрипторов прерываний и отвечающий за обработку прерывания (снова int 0x80). Затем вызывается обработчик системных вызовов system_call. Этот обработчик, зная адрес таблицы системных вызовов и ID системного вызова (который находится в регистре eax), может определить реальный адрес запрошенного системного вызова. В реальности вызов обработчика системных вызовов более сложен, но я опустил некоторые детали, чтобы упростить эту статью.

Первый метод, используемый злоумышленником для получения контроля над желаемым системным вызовом, состоит в перезаписи адреса оригинального системного вызова в таблице системных вызовов. При запросе системного вызова, обработчик вызывает подмененную функцию. Мы можем легко проверить эти адреса в таблице системных вызовов, используя утилиту gdb. Поэтому gdb очень полезен при обнаружений таких типов вредоносного ПО.

Конечно, есть и другие проблемы. Мы должны убедиться, что текущие адреса системных вызовов не изменены, что мы в момент проверки уже не компрометированы. Как мы можем это проверить? Адреса системных вызовов всегда постоянны и не изменяются после перезагрузки операционной системы. Эти адреса определяются во время компиляции ядра, поэтому, зная оригинальные адреса, мы можем сравнить их с текущими адресами из таблицы системных вызовов. Эта информация об оригинальных адресах во время компиляции записывается в два файла. Первый из них, это файл System.map. Этот файл содержит таблицу символов и их соответствующие адреса. Второй файл это образ ядра, загружающийся в память ядра во время инициализации системы. Несжатая версия образа ядра находится в файле vmlinux-2.4.x и обычно находится в директории /boot или в директории, в которой происходит сборка ядра.


Иногда может быть доступна только сжатая версия ядра (называющаяся vmlinuz-2.4.x). В этом случае, перед началом нашего исследования мы должны распаковать образ ядра. Если злоумышленник не изменил эти файлы (сжатый/распакованный образ ядра и System.map) или если у нас есть их копии, которым мы можем доверять, мы можем сравнить исходные адреса с теми, которые в данный момент присутствуют в таблице системных вызовов. Думаю, не нужно говорить, что после компиляции ядра мы должны сохранить копии этих файлов или хотя бы их хэши.

Также мы можем использовать простой модуль ядра, ссылка на который есть в разделе "Ссылки" этой статьи, для печати виртуального адреса каждого системного вызова. Чтобы сделать это, мы скомпилируем исходный код следующим образом: gcc -c scprint.c -I/usr/src/linux/include/. После загрузки собранного модуля (scprint.o), адрес каждого системного вызова будет автоматически записан в файл syslog. Время от времени, мы должны запускать этот модуль для сравнения оригинальных адресов с текущим состоянием ядра.

В большинстве случаев, ядро модифицируется руткитом после инициализации системы. Это осуществляется путем загрузки злонамеренного модуля ядра или записи некоторого вредоносного кода непосредственно в объект /dev/kmem. Руткиты обычно не изменяет образ ядра или файла System.map. Поэтому для обнаружения любых модификаций таблицы системных вызовов мы должны печатать все адреса, в настоящее время присутствующие в таблице системных вызовов и затем сравнивать их с адресами сохраненного образа ядра (в нашем случае vmlinux-2.4.x). Память операционной системы доступна через объект kcore, который находится в файловой системе /proc.

Вначале нужно найти адрес таблицы системных вызовов. Это простая задача, так как символ sys_call_table представлен в файле System.map.

[root@rh8 boot]# cat System.map-2.4.18-13 | grep sys_call_table c0302c30 D sys_call_table

Теперь, мы можем найти адрес таблицы системных вызовов, используя команду nm. Эта команда позволяет нам печатать все символы образа ядра: [root@rh8 boot]# nm vmlinux-2.4.18-13 | grep sys_call_table c0302c30 D sys_call_table



Используя gdb, мы можем получить полное содержание таблицы системных вызовов образа ядра, как показано ниже. Напечатанные адреса соответствуют системным вызовам, объявленным в файле entry.S в исходных кодах ядра. Например, вхождение 0 (0xc01261a0) это системный вызов sys_ni_syscall, вхождение 1 (0xc011e1d0) это sys_exit, вхождение 2 (0xc01078a0) это sys_fork и так далее.

#gdb /boot/vmlinux-2.4.*
(gdb) x/255 0xc0302c30

0xc0302c30 <sys_call_table>: 0xc01261a0 0xc011e1d0 0xc01078a0 0xc013fb70
0xc0302c40 <sys_call_table+16>: 0xc013fcb0 0xc013f0e0 0xc013f230 0xc011e5b0
0xc0302c50 <sys_call_table+32>: 0xc013f180 0xc014cb10 0xc014c670 0xc0107940
0xc0302c60 <sys_call_table+48>: 0xc013e620 0xc011f020 0xc014bcd0 0xc013e9a0
...

Мы также можем печатать адрес каждого системного вызова, вводя его имя, как показано ниже: (gdb) x/x sys_ni_syscall 0xc01261a0 <sys_ni_syscall>: 0xffffdab8 ((gdb) x/x sys_fork 0xc01078a0 <sys_fork>: 0x8b10ec83

Теперь используя утилиту gdb (или модуль scprint.o, который мы скомпилировали) мы должны снять дамп текущих значений таблицы системных вызовов. И, наконец, мы сравниваем полученные значения со значениями, сохраненными после компиляции ядра.

Чтобы напечатать текущее состояние ядра (адреса системных вызовов) мы должны запустить gdb с двумя параметрами. Первый параметр это образ ядра (vmliux-2.4.x), второй - объект /proc/kcore. Затем, мы используем адрес таблицы системных вызовов, полученный из файла System.map, чтобы вывести значения таблицы системных вызовов.

#gdb /boot/vmlinux-2.4.* /proc/kcore
(gdb) x/255x 0xc0302c30
0xc0302c30 <sys_call_table>: 0xc01261a0 0xc011e1d0 0xc01078a0 0xc88ab11a
0xc0302c40 <sys_call_table+16>: 0xc013fcb0 0xc013f0e0 0xc013f230 0xc011e5b0
0xc0302c50 <sys_call_table+32>: 0xc013f180 0xc014cb10 0xc014c670 0xc0107940
0xc0302c60 <sys_call_table+48>: 0xc013e620 0xc011f020 0xc014bcd0 0xc013e9a0
...

Как мы можем заметить из вывода выше, один из адресов системных вызовов был изменен. Это элемент 3 в таблице системных вызовов (счет начинается с 0) и для ясности он выделен в выводе выше. В файле /usr/include/asm/unistd.h мы можем найти имя этого подозрительного системного вызова, который называется sys_read.

Другой признак компрометации системы это то, что новый виртуальный адрес этой функции (sys_read) располагается выше 0xc8xxxxxx. Это очень подозрительно. ОС Linux по умолчанию может адресовать до 4 Гб памяти. Виртуальные адреса располагаются с 0x00000000 по 0xffffffff. Верхняя часть этой виртуальной области зарезервирована под код ядра (диапазон значений с 0xc0000000 по 0xffffffff). Когда загружается новый модуль ядра, функция vmalloc выделяет часть этой памяти под код модуля. Она выделяет область памяти обычно начинающейся с 0xc8800000. Таким образом, каждый раз, когда мы видим адрес системного вызова, находящийся выше этого адреса, что мы и видим в нашем примере, это показывает, что наше ядро могло быть компрометировано. Теперь посмотрим поближе на этот системный вызов.


Содержание раздела