Настольная книга компьютерного исследователя


Трюки с ядром FreeBSD

Приколы с sysctl.

Необходимость в изменении стандартной схемы работы ядра операционной системы возникает достаточно часто. Особо остро эта проблема встаёт, когда нам надо совершить различные «незаконные» действия: внедрить в систему бэкдор; глубоко и далеко прятать какую-либо важную для нас информацию от пытливых взоров системных администраторов или, наоборот, взломщиков; сделать своё пребывание в операционной системе как можно менее заметным и т.д. Ниже будут приведены некоторые приёмы, при помощи которых это можно сделать в ядре операционной системы FreeBSD. Я думаю и надеюсь, что этот документ продолжит своё существование и будет пополняться новой информацией с каждым выпуском журнала. Так что, если у вас сть какие-либо свои разработки в области kernel-программирования под FreeBSD, то можете посодействовать общему делу и поделиться данной информацией :-). Очень надеюсь, что люди, готовые на это, найдутся.

1.
Предположим, нам надо добавить в ядро код, повышающий привилегии процесса, вызвавшего его. Процесс, как всем известно, взаимодействует с операционной системой при помощи системных вызовов. При вызове нужного API процесс передаёт операционной системе номер системного вызова и его параметры (номера системных вызовов и названия, соответстующие им, находятся в sys/syscall.h). После совершения вызова ядро выбирает из массива структур sysent[], находящегося у него в памяти, структуру sysent[номер_системного_вызова], затем берёт из неё указатель на функцию-обработчик системного вызова и вызывает её с параметрами, переданными процессом. Все kernel-бэкдоры, виденные мною в сети (смотрел я их не так уж и много, есть ещё и куча других), просто-напросто производили замену указателя на функцию-обработчик на свой указатель, соответствующий другой, «хакерской» функции. В настоящее время некоторые администраторы серверов на основе FreeBSD да и вообще всех UNIX-систем (механизм с sysent[] используется практически во всех UNIX-клонах) успешно борятся с такими бэкдорами: просто при загрузке системы создают копию массива sysent[] и время от времени сверяют её с настоящим массивом с целью выявить несоответствия. Если кто-либо произведёт модификацию таблицы системных вызовов, то это сразу будет выявлено. На самом деле, помимо модификации таблицы системных вызовов, можно придумать ещё великое множество способов для интеграции в ядро нужного нам кода. Конечно, в некоторых случаях перехват системных вызовов очень удобная штука. Например, для чтения из STDIN какого-либо процесса, удобно перехватить системный вызов read(), и когда процесс вызовет API read(), пытаясь прочитать из STDIN, то новая функция-обработчик легко сможет сохранить эту информацию и затем предоставить её взломщику. Но на данный момент это нас не интересует: нам нужен просто код, находящийся в адресном пространстве ядра и могущий в любой момент времени быть вызванным внешним процессом.

Один из приёмов выглядит следующим образом: многие системные вызовы при своей работе используют связанные списки структур. Некоторые списки содержат структуры, в которых есть указатели на функции. Когда пользовательский процесс делает вызов такого API, его обработчик, основываясь на данных, переданных пользовательским процессом (параметры системного вызова), перебирает структуры из списка, пока не находит нужную, а затем вызывает функцию, указатель на которую содержится в этой структуре. От нас только остаётся добавить свою структуру с указателем на нашу функцию + написать програмку, которая будет работать вне ядра и делать вызов данного затрояненого API с нужными нам параметрами. Как вы видите, механизм троянизации по своей идеологии точно такой же, как и при модификации таблицы системных вызовов. Только здесь модифицируется не сама таблица, а обработчик конкретного системного вызова. Для примера рассмотрю системный вызов sysctl, так как он как раз работает со связанным списком структур. Этот системный вызов предназначен для получения информации о внутренних структурах ядра, а также для их модификации. При вызове этого API ядру передаётся массив из элементов типа int, длина которого не должна превышать CTL_MAXNAME. На основе данных в этом массиве ядро выполняет определённое действие. Рассмотрим механизм выбора этого действия. В ядре существует множество структур типа sysctl_oid. Вот, как выглядит структура этого типа:

struct sysctl_oid {
	struct sysctl_oid_list *oid_parent;
	SLIST_ENTRY(sysctl_oid) oid_link;
	int		oid_number;
	int		oid_kind;
	void		*oid_arg1;
	int		oid_arg2;
	const char	*oid_name;
	int 		(*oid_handler)(SYSCTL_HANDLER_ARGS);
	const char	*oid_fmt;
	int		oid_refcnt;
};

Допустим, пользовательский процесс делает вызов API sysctl() и передаёт в качестве параметра массив из элементов типа int. Что происходит в ядре? Сначала из связанного списка sysctl__children (список из структур sysctl_oid) берётся самая первая структура и происходит сравнение элемента oid_number этой структуры с первым элементом int-массива, переданного пользовательским процессом. Заметим, что каждая структура связанного списка обладает уникальным значением элемента oid_number. Если значения элементов не соответствуют друг другу, то берётся следующая структура из списка и над ней проделывается такая же операция и т.д.. Если же значения элементов совпадают, то ядро сначала смотрит, есть ли у этой структуры вложенный список (если есть, то oid_kind должен содержать флаг CTLTYPE_NODE) и затем берёт этот новый связанный список, указатель на который располагается в элементе oid_name, и начинает с ним производить точно такие же действия, как и с предыдущим списком (sysctl_children). Только здесь с элементом oid_number сравнивается уже второй элемент int-массива. Снова перебирается весь список. При нахождении соответствия берётся ещё один список. Здесь сравнение идёт уже с третим элементом int-массива и т.д. Этот процесс продолжается до тех пор, пока не будет найдена структура, соответствующая последнему элементу int-массива или же пока не будет найдена структура, в которой указатель oid_handler не равен нулю. Когда это произойдёт, ядро выполнит функцию, адресуемую указателем oid_handler. Для более подробной информации можете заглянуть в файл /sys/kern/kern_sysctl.c. Ниже приведены исходники функций, которые выполняют вышеназванные операции.

Вот сама функция-обработчик системного вызова sysctl():

#ifndef _SYS_SYSPROTO_H_
struct sysctl_args {
	int	*name;
	u_int	namelen;
	void	*old;
	size_t	*oldlenp;
	void	*new;
	size_t	newlen;
};
#endif

int
__sysctl(struct proc *p, struct sysctl_args *uap)
{
	int error, i, name[CTL_MAXNAME];
	size_t j;

	if (uap->namelen > CTL_MAXNAME || uap->namelen < 2)
		return (EINVAL);

 	error = copyin(uap->name, &name, uap->namelen * sizeof(int));
 	if (error)
		return (error);

	error = userland_sysctl(p, name, uap->namelen,
		uap->old, uap->oldlenp, 0,
		uap->new, uap->newlen, &j);
	if (error && error != ENOMEM)
		return (error);
	if (uap->oldlenp) {
		i = copyout(&j, uap->oldlenp, sizeof(j));
		if (i)
			return (i);
	}
	return (error);
}

Как видно, в процессе своей работы она вызывает функцию userland_sysctl(). Внутри userland_sysctl() происходит вызов функции sysctl_root(), а внутри sysctl_root() вызывается ещё одна: sysctl_find_oid(), которая и реализует алгоритм поиска нужной структуры. Я внёс небольшие комментарии, так что изучайте на здоровье:

int
sysctl_find_oid(int *name, u_int namelen, struct sysctl_oid **noid,
    int *nindx, struct sysctl_req *req)
{
	struct sysctl_oid *oid;
	int indx;

	oid = SLIST_FIRST(&sysctl__children); /* теперь oid - первая 
	                                       структура из списка 
	                                       sysctl__children */
	indx = 0;
	while (oid && indx < CTL_MAXNAME) {
		if (oid->oid_number == name[indx]) { 
		/* сравнение oid_number с элементом int-массива */
			indx++;
			if (oid->oid_kind & CTLFLAG_NOLOCK)
				req->lock = 0;
			if ((oid->oid_kind & CTLTYPE) == CTLTYPE_NODE) {
				if (oid->oid_handler != NULL ||
				    indx == namelen) { 
		/* если достигнут последний элемент int-массива или 
		существует указатель oid_handler, то завершаем поиск */
					*noid = oid;
					if (nindx != NULL)
						*nindx = indx;
					return (0);
				}
				oid = SLIST_FIRST(
				    (struct sysctl_oid_list *)oid->oid_arg1);
				/* здесь берётся новый список структур и 
				выбирается первый элемент из него */
			} else if (indx == namelen) {
				*noid = oid;
				if (nindx != NULL)
					*nindx = indx;
				return (0);
			} else {
				return (ENOTDIR);
			}
		} else {
			oid = SLIST_NEXT(oid, oid_link); 
		/* если oid_number текущей структуры не соответствует 
		 элементу int-массива, то берётся следующая структура из 
		 списка */
		}
	}
	return (ENOENT);
}

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

Как несложно догадаться, чтобы затроянить ядро, нам нужно внедрить свою структуру sysctl_oid в один из списков и поместить в элемент oid_handler указатель на нужную нам функцию. Затем, в тот момент, когда нам нужно исполнить код этой функции, мы просто создаём програмку, в которой происходит вызов sysctl(), и передаём в ядро int-массив, идентифицирующий нашу структуру. Пример kernel-модуля, проделывающего эту операцию, прилагается.

Помимо простого внедрения нашего кода в ядро, при помощи механизма с sysctl() можно сделать ещё ряд интересных вещей. К примеру, такую вещь, как скрытие процессов от комманды ps. В своё время неким pragmatic’ом был описан один способ. Суть его заключалась в следующем: комманда ps для получения списка работающих в системе процессов использует системный вызов sysctl(). В этом способе просто происходил перехват системного вызова (производилась замена его функции-обработчика). Новая функция вызывала оригинальный обработчик sysctl, но перед этим проверяла int-массив, передаваемый процессом в качестве параметра. Если элементы массива соответствовали определённым числам (числа, обозначающие, что это — запрос информации о процессах), то происходила проверка того, что возвратила оригинальная функция-обработчик. Если в возвращаемых данных встречалась какая-либо информация о скрываемом процессе, то эта инфа просто исключалась из данных. После такой обработки данные отправлялись наружу, то есть в процесс, вызвавший sysctl(). Вот и всё. На самом деле, то же самое можно реализовать немного элегантнее и безопаснее (естественно для взломщика). Для этого мы будем заменять не функцию-обработчик API sysctl(), а спустимся немного глубже. Как было уже сказано выше, при помощи sysctl можно узнать список работающих в системе процессов, то есть в памяти ядра существует структура sysctl_oid, содержащая указатель на функцию, которая «вытягивает» из ядра всю информацию о процессах. Что теперь зависит от нас? Есть 2 способа: либо заменить указатель на функцию, либо заменить всю структуру, предварительно подредактировав указатели соседних структур связанного списка. Функция, адресуемая новым указателем, по принципу своей работы аналогична той, которая в предыдущем способе служила заменой системному вызову sysctl(). Как вы видите, здесь не происходит явного перехвата системного вызова, поэтому стандартные средства, применяемые админами для детектирования в ядре враждебного кода, не работают.



©2015 Журнал Хакера Entries (RSS) and Comments (RSS)