Обнаружение компрометаций ядра с помощью gdb

Обнаружение компрометаций ядра с помощью gdb

Назначение этой статьи - описание полезных способов обнаружения скрытых модификаций ядра Linux. Более известный как руткит (rootkit), этот скрытный тип злонамеренного ПО устанавливается в ядро операционной системы и для своего обнаружения требует от системных администраторов и обработчиков инцидентов использования специальных техник.

Мариус Бурдач, перевод Владимир Куксенок

Назначение этой статьи - описание полезных способов обнаружения скрытых модификаций ядра Linux. Более известный как руткит (rootkit), этот скрытный тип злонамеренного ПО устанавливается в ядро операционной системы и для своего обнаружения требует от системных администраторов и обработчиков инцидентов использования специальных техник.

В этой статье для обнаружения компрометаций ОС Linux мы будем использовать только одну утилиту - gdb (GNU Debugger). Эта утилита по умолчанию присутствует почти в каждом дистрибутиве Linux. Вторая задача этой статьи - описания популярных методов модификации ядра операционной системы Linux. Поняв принципы атаки, мы сможем легко обнаружить скомпрометированную машину или выбрать подходящие средства для мониторинга наших критических компьютеров.

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

Введение в угрозу руткитов

Все больше и больше злонамеренных программ режима пользователя, таких как трояны, бэкдоры или руткиты модифицируют существующее ПО операционной системы. Для установки одной из этих программ, атакующий должен заменить или изменить нормальные программы, входящие в состав операционной системы. Для примера давайте рассмотрим замену вездесущей команды ls. Обычные пользователи и администраторы используют команду ls для отображения содержимого директории, но модифицированная версия скроет все файлы атакующего. Утилиты, которые могут обнаруживать такой тип модификаций, называются средства проверки целостности (integrity checkers) файлов.

Давайте предположим, что атакующий не изменяет и не заменяет существующие программы, такие как ls, в файловой системе. Предположим, что вместо этого атакующий изменяет или заменяет различные компоненты ядра. Мы знаем, что многие программы режима пользователя, типа ps, ls или lsof используют системные вызовы для выполнения некоторых своих задач. Например, когда администратор исполняет команду ls, чтобы получить листинг директории, вызывается системные вызов sys_getdents. Таким образом, атакующий может изменить этот компонент ядра для сокрытия некоторых файлов или процессов.

Теперь давайте рассмотрим другой пример, где атакующий модифицирует системные вызовы sys_open и sys_read для блокирования доступа к некоторым файлам. Эти же системные вызовы используются средствами проверки целостности для проверки важных системных файлов, таких как образ ядра и загружаемые модули ядра. Когда эти утилиты пытаются сравнить хэши файлов с их исходными значениями, они не изменятся, даже если файлы изменены. Другими словами, средства проверки целостности не обнаружат модификаций критически важных файлов. Для этого нужно всего лишь перехватить два системных вызова. Весьма очевидно, что если компоненты ядра изменены атакующим, пользователи и администраторы не могут доверять результатам, полученным от ядра или от любых имеющих отношение к защите утилит, запущенных пользователем.

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

Понимание принципов атаки

Основные цели атаки – системные вызовы. Этот метод выбран злоумышленниками по двум причинам: потому что это самый простой способ получить управления над скомпрометированной машиной, а также потому, что системные вызовы дают большие возможности атакующему. Системные вызовы – основные функции, используемые операционной системой. Например, они используются для чтения и записи данных в файлы, для получения доступа к различным устройствам, для запуска исполняемых файлов и т.д.

В текущей стабильной версии ядра Linux около 230 системных вызовов, и примерно 290 в ядре 2.6.9. Обратите внимание, что количество системных вызовов зависит от версии ядра. Полный список всех системных вызовов вашего ядра всегда доступен в файле file /usr/include/asm/unistd.h. Также нужно заметить, что обычно не все системные вызовы модифицируются злоумышленником, однако есть несколько популярных. Это системные вызовы, представленные в таблице 1. Они должны быть внимательно проверены администраторами, и конечно, средствами обнаружения вторжений. Те, кого интересуют подробности, могут найти полное описание каждого системного вызова в “Linux Programmers Manual”.

Имя системного вызова

Краткое описание

ID

sys_read

Используется для чтения из файлов

3

sys_write

Используется для записи в файлы

4

sys_open

Используется для создания или открытия файлов

5

sys_getdents/sys_getdents64

Используется для получения листинга содержимого директории (также и /proc)

141/220

sys_socketcall

Используется для управления сокетами

102

sys_query_module

Используется для запроса загруженных модулей

167

sys_setuid/sys_getuid

Используется для управления UIDs

23/24

sys_execve

Используется для исполнения бинарных файлов

11

sys_chdir

Используется для изменения текущей директории

12

sys_fork/sys_clone

Используется для создания процесса потомка

2/120

sys_ioctl

Используется для работы с устройствами

54

sys_kill

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

37

Обратите внимание, что в таблице выше, ID это точка входа в таблице системных вызовов. Здесь приведены ID, используемые в ядре 2.4.18-3.

Несмотря на то, что все примеры, представленные в этой статье было проверены на Red Hat 7.3 с ядром 2.4.18-3, они могут быть воспроизведены на других версиях ядра, включая последние версии 2.6.x. Однако могут быть некоторые различия во внутренних структурах ядра 2.6.x. Например, адрес таблицы системных вызовов хранится внутри функции syscall_call, вместо обработчика системного вызова system_call.

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

Текущие адреса системных вызовов находятся в таблице системных вызовов, в памяти, выделенной для ядра ОС. Адреса расположены в том же порядке, что и их функции и представлены в файле /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. Таким образом, каждый раз, когда мы видим адрес системного вызова, находящийся выше этого адреса, что мы и видим в нашем примере, это показывает, что наше ядро могло быть компрометировано. Теперь посмотрим поближе на этот системный вызов.

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

Теперь рассмотрим метод обнаружения перехвата системных выводов. Ни один из элементов таблицы системных вызовов не модифицирован таким способом. Вместо этого, первые несколько команд оригинальной функции перезаписываются безусловным переходом на заменяемую функцию. Давайте представим, что злоумышленник хочет перехватить системный вызов sys_read. Вначале он должен загрузить свою функцию в память, а затем поместить адрес этой функции в первых нескольких битах оригинальной функции. Потом злоумышленник должен переадресовать выполнение оригинальной функции к своей функции. Для этого обычно используются такие ассемблерные команды, как call или jmp.

Чтобы определить, был ли системный вызов перехвачен, нам нужно распечатать все команды целевой функции. Потом мы запускаем gdb с двумя параметрами (образ ядра и объект /proc/kcore). Далее, мы должны дизассемблировать оригинальную функцию, используя внутреннюю команду disass, как показано ниже.

#gdb /boot/vmlinux-2.4.* /proc/kcore 
(gdb) disass sys_read 
Dump of assembler code for function sys_read: 
0xc013fb70 <sys_read>:               mov     $0xc88ab0a6,%ecx 
0xc013fb73 <sys_read+3>:              jmp     *%ecx 
0xc013fb77 <sys_read+7>:               mov     %esi,0x1c(%esp,1) 
0xc013fb7b <sys_read+11>:              mov     %edi,0x20(%esp,1) 
0xc013fb7f <sys_read+15>:              mov     $0xfffffff7,%edi 
...
В выводе выше мы можем увидеть, что первая команда помещает значение (адрес функции злоумышленника) в регистр ecx. Вторая инструкция выполняет безусловный переход по этому виртуальному адресу - 0xc88ab0a6.

Чтобы удостовериться, что системный вызов sys_red перехвачен, нам нужно дизассемблировать оригинальную функцию. Оригинальная функция находится в образе ядра vmlinux-2.4.x.

#gdb /boot/vmlinx-2.4.* 
(gdb) disass sys_read 
Dump of assembler code for function sys_read: 
0xc013fb70 <sys_read>:          	sub     $0x28,%esp 
0xc013fb73 <sys_read+3>:              mov     0x2c(%esp,1),%eax 
0xc013fb77 <sys_read+7>:              mov     %esi,0x1c(%esp,1) 
0xc013fb7b <sys_read+11>:             mov     %edi,0x20(%esp,1) 
0xc013fb7f <sys_read+15>:             mov     $0xfffffff7,%edi 
...
Вывод подтверждает, что системный вызов sys_read был изменен. Чтобы понять, что делает новая функция, мы должны дизассемблировать ее, используя утилиту gdb.

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

Используя описанный выше метод, мы можем также дизассемблировать другие критические функции в памяти ядра. Одна из них это обработчик системных вызовов, имеющий имя system_call и использующийся для определения адреса и вызова функций системных вызовов. Обработчик использует таблицу системных вызовов для определения адреса нужного системного вызова. Дизассемблировав этот обработчик, мы можем проверить используется ли правильный адрес таблицы системных вызовов или обработчик был модифицирован. Для осуществления атаки, злоумышленник может создать свою собственную таблицу системных вызовов, использующую его системные вызовы. Затем он может разместить новый адрес таблицы системных вызовов в обработчике системных вызовов.
(gdb) disass system_call 
Dump of assembler code for function system_call: 
0xc01090dc <system_call>:        push    %eax 
0xc01090dd <system_call+1>:      cld 
0xc01090de <system_call+2>:      push    %es 
0xc01090df <system_call+3>:      push    %ds 
0xc01090e0 <system_call+4>:      push    %eax 
0xc01090e1 <system_call+5>:      push    %ebp 
0xc01090e2 <system_call+6>:      push    %edi 
0xc01090e3 <system_call+7>:      push    %esi 
0xc01090e4 <system_call+8>:      push    %edx 
0xc01090e5 <system_call+9>:      push    %ecx 
0xc01090e6 <system_call+10>:     push    %ebx 
0xc01090e7 <system_call+11>:     mov     $0x18,%edx 
0xc01090ec <system_call+16>:     mov     %edx,%ds 
0xc01090ee <system_call+18>:     mov     %edx,%es 
0xc01090f0 <system_call+20>:     mov     $0xffffe000,%ebx 
0xc01090f5 <system_call+25>:     and     %esp,%ebx 
0xc01090f7 <system_call+27>:     testb   $0x2,0x18(%ebx) 
0xc01090fb <system_call+31>:     jne     0xc010915c <tracesys> 
0xc01090fd <system_call+33>:     cmp     $0x100,%eax 
0xc0109102 <system_call+38>:     jae     0xc0109189 <badsys> 
0xc0109108 <system_call+44>:     call    *0xc0302c30 (,%eax,4) 
0xc010910f <system_call+51>:     mov     %eax,0x18(%esp,1) 
0xc0109113 <system_call+55>:     nop 
End of assembler dump.
Обратите внимание, что дизассемблированный обработчик содержит адрес оригинальной таблицы системных вызовов.

Полезные утилиты

Конечно, было бы лучше автоматизировать все проверки, описанные в этой статье. Один из способов осуществления этого – использования системы обнаружения атак (СОА), контролирующей критический структуры ядра в реальном времени. Например, может быть использована утилита Samhain. Это приложение было кратко описано в статье Host Integrity Monitoring: Best Practices for Deployment. Samhain может следить за таблицей системных вызовов, несколькими первыми командами каждого системного вызова, включая некоторые обработчики, таблицей прерываний и многим другим.

Доверяя мониторинг вашего ядра системе обнаружения атак нужно помнить одно простое правило: все утилиты, осуществляющие мониторинг, должны быть установлены на “чистую” операционную систему, так как это единственный способ быть уверенным, что вы уже не были компрометированы. Если вы не используете СОА, вы должны, по крайней мере, создать хэш образа ядра.

Итоги

Как мы убедились, утилита gdb может быть очень полезна для обнаружения компрометаций ядра операционной системы.

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

Ссылки

[1] Daniel P. Bovet, Marco Cesati. "Understanding the Linux Kernel", 2nd Edition. O'Reilly; 2002.
[2] "Host Integrity Monitoring: Best Practices for Deployment", http://www.securityfocus.com/infocus/1771.
[3] "Linux on-the-fly kernel patching without LKM", http://www.phrack.org/phrack/58/p58-0x07
[4] System manual (2), "Linux Programmer's Manual".
[5] The GNU Project Debugger, http://www.gnu.org/software/gdb/gdb.html .
[6] scprint.c is the loadable kernel module that allows one to print system calls from the kernel memory. This is available for download from SecurityFocus.
[7] The samhain file integrity / intrusion detection system, http://la-samhna.de/samhain/

Ваша цифровая безопасность — это пазл, и у нас есть недостающие детали

Подпишитесь, чтобы собрать полную картину