January 15, 2020

Как приручить руткит. Предотвращаем загрузку вредоносных модулей в Linux

Источник: t.me/Bureau121

Какие бывают руткиты для Linux

Руткиты помогают злоумышленнику закрепить доступ ко взломанной системе, при этом акцент сделан на максимальной незаметности вредоноса. Для этого они скрывают сетевые соединения, процессы, папки, файлы, подделывают их содержимое. Обычно руткит несет в себе хакерские утилиты для управления зараженной машиной, с помощью которых злодей может установить и прятать в системе DDoS-бота или майнер (кстати, один такой, Skidmap, обнаружили сравнительно недавно). Чаще всего в эти утилиты входят бэкдоры, и не только те, что можно запросто обнаружить внешним сканером портов, а использующие технологию Port knocking (что-то вроде «простукивания портов»), когда порт открывается только после правильной и заранее определенной последовательности запросов к закрытым портам.

Традиционно все руткиты делят на работающие в пространстве пользователя и работающие в пространстве ядра. Против первых уже есть утилиты, способные обнаружить многие из них: chkrootkit, rkhunter, Antidoto и проект с незамысловатым названием linux-malware-detect. Поэтому «ядерные» руткиты, которые сложнее обнаружить непосредственно на зараженной системе, представляют больший интерес, хотя часть из них эти утилиты тоже могут выявить (но не удалить).

Руткиты уровня ядра в Linux, как правило, реализуются в виде загружаемых модулей ядра (LKM, Loadable Kernel Modules), но есть еще более экзотические способы: вредоносный код записывается прямо в память ядра через файл устройства /dev/kmem или внедряется на ранних этапах загрузки с модификацией initrafms (если ты знаком с такими руткитами, дай знать — попадаются только теоретические описания, а найти образцы мне не удалось). Сейчас, впрочем, о случаях заражения «ядерными» руткитами пишут нечасто, но все тот же недавно выявленный Skidmap говорит о том, что об этой угрозе забывать не стоит.

Жизненно важные задачи каждого руткита

В ядре имеется множество структур, описывающих текущее состояние системы. Например, это список запущенных процессов, состоящий из указателей на дескрипторы процессов, который используется планировщиком. Другой важный объект — список загруженных модулей ядра, где каждый элемент указывает на дескриптор загруженного модуля. Он используется командами, оперирующими LKM: lsmod, rmmod, modprobe и подобными. Эти списки относятся ко внутренним объектам ядра.

Всякий вредоносный модуль первым делом удаляет себя из списка загруженных модулей, ведь если LKM в нем не описан, то ядро считает, что такой модуль не загружен. Это значит, что он не отобразится в выводе lsmod и не будет выгружен с помощью rmmod. Такая техника называется манипулированием внутренними объектами ядра (DKOM, Direct Kernel Object Manipulation) и подробно описана в статье на Хабре.

Также любой хороший руткит заботится о том, как остаться в строю после перезапуска системы. Например, Snakso, обнаруженный в 2012 году, для этого прописывает команду загрузки модуля в /etc/rc.local, rkduck предпочитает файл /etc/rc.modules, а Reptile в зависимости от целевой системы может использовать и /etc/rc.modules, и /etc/modules. Skidmap вносит разнообразие в этот список и закрепляется в сценариях планировщика задач cron. Вообще, для этой цели могут подойти и другие файлы, влияющие на загрузку Linux, в том числе загрузочные скрипты. Далее будем называть такие файлы файлами автозагрузки.

И все вроде бы просто: проверяй содержимое этих файлов на подозрительные строки, и в твоей системе все будет хорошо. Но руткит не руткит, если не скрывает еще и что эти файлы модифицированы. Он может перехватывать системные вызовы и даже функции самого ядра, которые за этими вызовами стоят: например, Snakso и Reptile перехватывают функции файловой подсистемы ядра VFS (Virtual File System). Руткиты проверяют, было ли среди прочитанного что-то, что необходимо скрыть от глаз пользователя или администратора, и при необходимости модифицируют буфер со считанными данными.

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


Матчасть: как LKM-руткиты заметают следы

Прежде чем приступать к борьбе с вредоносными модулями, нужно более детально разобрать механизмы, с помощью которых они скрываются в системе сами и скрывают своих протеже, — чтобы знать, с чем конкретно бороться.

Перехват системных вызовов и функций

Этот шаг необходим руткиту для последующих действий. Перехват (hook) функций ядра и перехват системных вызовов мало чем отличаются и позволяют выполнять руткиту свои функции под видом перехваченных, проводя для маскировки данных проверки. Вариантов перехвата не так много: либо подменить адрес функции, либо установить в самом ее коде инструкцию безусловного перехода jmp.

Информацию о местонахождении кода системных функций и вызовов ядро берет из двух таблиц: символьной таблицы ядра System.map (откуда при загрузке экспортируемые функции ядра отображаются в псевдофайл /proc/kallsyms) и таблицы syscall’ов sys_call_table, адрес которой есть в System.map.

Когда найден адрес нужной функции, руткит заменяет его адресом своей функции. Таблица системных вызовов находится в защищенной области памяти, помеченной только для чтения в регистре CR0 (x86), но это ограничение — ерунда для руткита уровня ядра, ведь есть write_cr0(), с помощью которой оно легко обходится. Замена адреса самой таблицы возможна, но мне пока не встречалась.

Другой вариант, получивший название сплайсинг (от англ. splice — стык, склейка), заключается в установке инструкции jmp в коде функции и выполняется одинаково для системных функций и syscall’ов. Как правило, переписываются первые пять байт пролога функции, которых достаточно для размещения опкода 0xe9 и адреса перехода. Оригинальные байты пролога сохраняются, чтобы была возможность корректно вызвать перехваченную функцию. Однако умный хакер может значительно усложнить задачу белым шляпам, размещая хук не в начале перехватываемой функции, а где-нибудь в ее середине.

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


Маскировка файлов и их содержимого

На самом деле еще и папок, ведь со времен Unix в Linux действует философия «все есть файл». Для этой задачи вредоносные LKM перехватывают функции файловой подсистемы ядра VFS, в первую очередь vfs_read(). После ее выполнения руткит просматривает прочтенные данные в поисках того, что нужно скрыть от пользователя.

К примеру, Reptile ищет в буфере vfs_read() свои теги (по умолчанию это <reptile> и закрывающий в пару к нему) и удаляет оттуда их, а также все, что между ними. Чтобы спрятать файлы и папки, руткиты проверяют листинг директории на заданные заранее имена и при совпадении убирают их. При этом они не всегда перехватывают обращения к скрытым папкам, и, зная эти имена, можно туда зайти и оперировать файлами в них.


Маскировка процессов

Так руткиты прячут майнеры и прочую нечисть. Один из способов аналогичен скрытию файлов: список процессов доступен через интерфейс ядра /proc/<PID>/ для каждого процесса. Эти файлы используются, например, программами ps, top, и если руткит скроет соответствующую папку, то они не отобразят такой процесс.

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


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

Чтобы скрыть бэкдор, руткиты либо применяют технику Port knocking, либо подделывают информацию об открытых сокетах. Пользовательские программы, в числе которых netstat, для получения информации о сетевых соединениях используют псевдофайлы /proc/net/tcp и /proc/net/tcp6, служащие отображением данных из памяти ядра. Перехват vfs_read() позволяет руткиту фильтровать соединения, доступные глазу юзера из этого файла. Можно также перехватить tcp4_seq_show() и tcp6_seq_show(), с помощью которых реализуются эти интерфейсы ядра в /proc. Впрочем, утилита ss работает немного иначе и в некоторых случаях может отобразить скрытые из /proc/net/tcp и tcp6 соединения.

Встроенный в ядро Linux межсетевой экран NetFilter обеспечивает фильтрацию пакетов, трансляцию адресов и прочие преобразования пакетов. Эта подсистема представляет собой набор хуков над стеком сетевых протоколов Linux. С их помощью можно регистрировать в ядре функции для работы с пакетами на одной из пяти стадий их обработки: PREROUTING, INPUT (LOCAL IN), FORWARD, POSTROUTING и OUTPUT (LOCAL OUT). «Ядерный» руткит легко может зарегистрировать свою функцию для модификации сетевого трафика на любом из этапов.


Способы борьбы с LKM-руткитами

Несмотря на изощренные способы маскировки руткитов, установить, что система заражена, зачастую не так сложно. Руткиты перехватывают функции, а тут, как показано ранее, вариантов немного.

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

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

Еще есть интересная задача — поискать в памяти ядра дескриптор модуля, отвязанного от списка загруженных модулей. Найдя, можно вернуть его в список, чтобы затем использовать rmmod (если, конечно, руткит не подменил и его на всякий случай). Существует решение (см. раздел 6), позволяющее обнаружить в памяти объекты, похожие на дескрипторы LKM, но оно работает на уже очень старых и только 32-битных ядрах. Перебор же памяти современных систем затруднен из-за огромного адресного пространства, но оттого эта задача становится лишь интересней.

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


Начинаем собственное расследование

Итак, что нам известно? Что LKM-руткит непременно должен где-то прописать строчку, ведущую к его загрузке при старте ОС. Также мы знаем, что он старается это скрыть.

Предположим, в нашей системе установлен Reptile. Открываем в любом текстовом редакторе /etc/modules и видим, что он будто бы чист, но мы-то знаем — там есть скрытое содержимое. Что будет, если попробовать, не внося никаких изменений в файл, сохранить его? Сохранится именно то, что мы видим, — то есть после перезагрузки ОС вредоносный LKM уже активен не будет. Правда, здорово? Автор руткита EnyeLKM тоже предлагал такой способ предотвращения его загрузки. Описанный эффект наблюдается потому, что сохраняется ровно то, что открыто в редакторе, то есть часть файла, свободная от данных руткита.

Что ж, уже неплохо. Но что делать, если неизвестно, в какой именно файл автозагрузки прописался руткит? Не перебирать же все задействованные файлы (хотя можно, но это слишком просто). К тому же такой способ уничтожает в файле все зацепки, которые помогли бы быстрее идентифицировать вредонос. Нужно найти модифицированный файл автозагрузки, поместить вместо него свободную от данных руткита копию, но при этом как-то оставить эти данные, чтобы было проще найти вредоносный LKM. Еще было бы полезно определить, какой именно файл был модифицирован руткитом, чтобы не пересохранять и анализировать их все.

Таким образом, наша задача предотвращения загрузки LKM-руткита сводится к двум вопросам:

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

И как же все-таки понять, что перед нами файл, который знает больше, чем показывает? Оказывается, все просто: лишнее содержимое — значит, лишние данные на диске. Да, я клоню к тому, что, скрывая содержимое, руткиты пока не догадались фальсифицировать размер файла. Как я покажу далее, не очень-то просто это провернуть.

Со вторым вопросом тоже несложно разобраться. Переименуем найденный на предыдущем шаге файл, чтобы система не читала из него при следующем запуске, а затем сохраним копию (которая уже не будет содержать данные руткита) под оригинальным именем. Вуаля!


Разрабатываем арсенал

Теперь мы готовы автоматизировать поиск и восстановление модифицированного файла автозагрузки. Необходим лишь список этих файлов, чтобы программа знала, где именно искать подвох. Далее для каждого такого файла программа сравнивает количество байтов, прочитанных с помощью fread() (за которой стоит системный вызов read(), а за ним, в свою очередь, перехваченная функция ядра vfs_read()), с размером файла, полученным из структуры, описывающей файл в файловой системе (i-node). Так как данная структура недоступна из пространства юзера, необходимо воспользоваться системным вызовом fstat().

Безусловно, LKM-руткиту не составит особого труда перехватить и его, но мне хотелось показать, что нынешним «ядерным» руткитам можно успешно противостоять из пространства пользователя, поэтому я предлагаю программу пользовательского уровня. При необходимости ее можно портировать в ядро. Там подделать размер файла руткит сможет, только подменив значение f->inode->i_size (для ядер старше 3.9.0 — f->f_mapping->host->i_size), но это как минимум чревато проблемами при чтении файла — все-таки серьезная низкоуровневая структура. В любом случае известные ныне руткиты не влияют ни на эти данные, ни на fstat() (смею предположить, что теперь начнут).

Код разработанной мной программы можно найти на гитхабе. Она предлагает минимальный набор функций, демонстрирующий, что справляется со своей задачей; при желании его можно и нужно расширить. Программа содержит массив строк с именами файлов, наиболее уязвимых к модификации LKM-руткитами (список еще далеко не полный), и проверяет каждый из них на наличие маскируемого содержимого.

Основная часть этой проверки заключена в функции cmp_size(). При подозрении на руткит выводится соответствующее сообщение с информацией о том, на какое количество байтов отличается фактическое содержимое от прочитанного. get_fsize() получает размер проверяемого файла из его дескриптора:

off_t get_fsize(FILE *f)
{
    int res;
    struct stat fst;
    errno = 0;
    res = fstat(fileno(f), &fst);
    if(res){
        perror("In get_fsize(): couldn't get fstat");
        return 0;
    }
    return fst.st_size;
}

short cmp_size(FILE *f)
{
    unsigned int i_size, read;
    char *fbuf;

    i_size = (unsigned int)get_fsize(f);
    if (errno){         
         printf("\x1b[1;31m***WARN***\x1b[0m Some problems with %s.\n",
                start_files[i]);
         return 1;
    }

    fbuf = (char*)malloc((i_size+1) * sizeof(char));
    memset(fbuf, 0, i_size+1);

    read = fread(fbuf, 1, i_size, f);

    if (i_size != read){
        printf("\x1b[1;31m***WARN***\x1b[0m Something performs file tampering of %s : "
               "read %u bytes instead of %u.\n", start_files[i], read, i_size);
        lets_talk(f, i_size, read);
        free(fbuf);
        return 1;
    }else{
        printf("\x1b[32m%s\x1b[0m looks fine to the userland\n", start_files[i]);
        free(fbuf);
        return 0;
    }   
}

Если найдено несоответствие, программа предложит возможные действия: попытку прочесть действительное содержимое файла (побайтово, чтобы руткит не смог найти свои маркеры в считанном буфере. Это не дает никаких гарантий, но почему бы не попробовать?) и заменить его безопасной копией. К старому файлу с данными руткита при этом добавляется суффикс .old.

Теперь осталось перезагрузиться, ведь руткит еще сидит в памяти и занимается своими грязными делами. Ну а дальше — ищем то, что скрывалось в ныне чистом файле автозагрузки, находим вредоносный бинарник, проверяем его на VirusTotal или где-нибудь еще и радуемся, какие мы молодцы.

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

Напоследок

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

Как ты понимаешь, самым надежным способом выявить руткит все равно будет анализ диска, хотя бы с помощью Live CD. И лишь после, имея на руках вредонос и изучив все его механизмы, можно браться за разработку защиты непосредственно для взломанной оси, что мы и попробовали сделать на одном конкретном примере. Против будущих и неизвестных сейчас LKM-руткитов наша программа, вероятно, окажется бесполезна, но это лишний повод вновь почувствовать себя супергероем и совершенствовать методы противодействия.

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

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

ПОДПИСАТЬСЯ - Бюро121