Root поселение на Unix тачке

Xakep, номер #049, стр. 049-086-2


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

Пишем local backdoor

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

Что же будет представлять собой лкм, выполняющий поставленную выше задачу? Схема следующая: необходимо узнать, каким образом ядро определяет, под каким пользователем работает конкретный процесс, и затем сделать так, чтобы ядро определило наш процесс, как работающий под рутом. Для этого рассмотрим сначала, что вообще ядро о нем знает. При создании нового процесса ядро создает непосредственно сам процесс - выделяет память для кода, стека и данных процесса. Кроме того, в своем адресном пространстве ядро создает структуру, описывающую данный процесс. Она нужна для осуществления планирования процесса, хранения данных об используемых процессом ресурсах и т.д. Во FreeBSD это struct proc (лежит в sys/proc.h), для линуха - struct task_struct (лежит в linux/sched.h). Так вот, в этой структуре и содержатся данные о владельце процесса. Для линуховой task_struct эта информация хранится в таких ее членах, как uid, euid, gid, egid. Во фрюхе же для этого существует специальная структура pcred, указатель на которую также содержится в struct proc. Выглядит этот pcred примерно так:

struct pcred {

struct ucred *pc_ucred; // указатель на структуру, содержащую текущие привилегии процесса

uid_t p_ruid; // Реальный uid

uid_t p_svuid; // Сохраненный эффективный uid. Используется в том случае, если прога делает seteuid().

gid_t p_rgid; // Реальный gid

gid_t p_svgid; // Сохраненный эффективный gid

int p_refcnt;

struct uidinfo *p_uidinfo;

};

Структура ucred, соответственно, выглядит так:

struct ucred {

u_short cr_ref;

uid_t cr_uid; // Эффективный uid

short cr_ngroups;

gid_t cr_groups[NGROUPS];

struct uidinfo *cr_uidinfo;

};

Как нетрудно догадаться, для того, чтобы наш процесс стал рутовым, нам нужно изменить соответствующие элементы его struct pcred и struct ucred (во FreeBSD) или struct task_struct (в линуксе). Вопрос только, каким образом менять эти элементы: для самого lkm это не представляет трудностей - он и так находится в ядре. Трудность в том, чтобы процесс, которому необходимо повысить свои привилегии, каким-то образом смог передать нашему модулю команду на изменение элементов соответствующих структур. Существует множество способов это сделать. Мы же рассмотрим самый "стандартный" способ доставки нужной нам инфы в ядро - посредством системных вызовов. Но для начала немного о том, как осуществляются системные вызовы в линухе и фрюхе (а осуществляются они практически одинаково). Со стороны прикладного программиста создание сокета, например, осуществляется посредством обращения к функции socket (). Дальнейшими деталями он обычно не интересуется, принимая вызов socket() уже системным вызовом. На самом деле это не совсем так. Код функции socket() располагается в библиотеке, которая, в свою очередь, преобразует переданные ей в качестве параметров данные в вид, понятный ядру. Вид этот в случае с FreeBSD и линуксом выглядит примерно одинаково: он состоит из номера системного вызова (например, во FreeBSD номер системного вызова socket() - 97, а в линуксе - 102) и набора параметров, переданных библиотечной функции. После того, как произошло преобразование в понятный для ядра вид, номер и параметры распихиваются по регистрам процессора, после чего наша библиотечная функция делает int $0x80, чем передает управление в ядро. В ядре располагается так называемая таблица системных вызовов, представляющая собой массив, элементы которого - структуры, содержащие указатели на функции-обработчики системных вызовов. Номер системного вызова - индекс в этом массиве. Вот и все, что нам необходимо знать для дальнейших действий. Суть здесь предельно проста - заменить функцию-обработчик любого валидного системного вызова на свою, которая будет выполнять нужные нам действия, а затем вызывать оригинальную функцию-обработчик. Или, скажем, нам нужно подправить результаты оригинальной функции (например, для того, чтобы убрать из листинга, который сгенерировал системный вызов, какой-нибудь файл, вследствие чего он станет невидимым для любого процесса).

Назад на стр. 049-086-1  Содержание  Вперед на стр. 049-086-3