АЛЕКСАНДР БАЙРАК
Контролируем и ограничиваем системные вызовы с помощью systrace
Наверняка вы не раз задумывались о том, как именно работает та или иная используемая вами программа. Разобраться помогут исходные текcты программы. Но что делать, если они недоступны? Как всегда быть в курсе того, что происходит у вас в системе?
Systrace – инструмент для ограничения и контроля системных вызовов. Изначально systrace был написан для NetBSD, позже был портирован под OpenBSD, а в настоящий момент ведутся работы по переносу на Linux, FreeBSD и OpenDarwin.
Любая программа в процессе свой работы использует системные вызовы. Что такое системный вызов? Его можно определить как некую функцию, которая позволяет вашей программе обращаться к ядру ОС для выполнения некого действия. В современных версиях UNIX-систем реализовано около 300 различных системных вызовов. И если такие инструменты, как ktrace (kernel process tracing) и truss (tracing system call), позволяют нам выступать лишь в качестве наблюдателей, systrace позволяет нам вмешиваться в происходящее.
Пример использования
Перейдем к практике. В качестве примера напишем простую программу:
#include <stdio.h>
void proc1();
int main()
{
printf("just a message\n");
mkdir(«test»);
proc1();
}
void proc1()
{
printf("just a dumb procedure\n");
}
Как ясно из исходного текста, наша подопытная программа выполняет следующие действия:
- Выводит на экран сообщение.
- Создает каталог «test».
- Передает управление процедуре «proc1».
- Процедура proc1 выводит на экран сообщение.
Скомпилируем программу:
#gcc -o proga proga.c
На первый взгляд при выполнении нашей программы используются два системных вызова: mkdir и write. Хотя логично предположить, что, перед тем как «что-то» «куда-то» записать с помощью write, это самое «куда-то» нужно сначала открыть, а после записи закрыть, а значит, задействованы системные вызовы open и close . Также не лишено смысла предположение, что используется еще один системный вызов – exit. Итого пять вызовов. Посмотрим, как обстоят дела на самом деле:
#systrace -A /path/to/program/proga
Ключ -A указывает systrace автоматически создать политику для указанного исполняемого файла.
Автоматический режим предполагает создание правил в режиме «разрешить все».
После выполнения команды в домашнем каталоге пользователя появится подкаталог .systrace, в который будет помещен файл c правилами. Рассмотрим его:
Policy: /home/01mer/labs/systr/proga, Emulation: netbsd
netbsd-mmap: permit
netbsd-fsread: filename eq “/etc/ld.so.conf” then permit
netbsd-__fstat13: permit
netbsd-close: permit
netbsd-munmap: permit
netbsd-fsread: filename eq “/lib/libc.so.12.114.1” then permit
netbsd-__sysctl: permit
netbsd-fsread: filename eq “/etc/mallic.conf” then permit
netbsd-break: permit
netbsd-ioctl: permit
netbsd-write: permit
netbsd-fswrite: filename eq “/home/01mer/labs/systr/test” then permit
netbsd-exit: permit
|
Синтаксис правил достаточно прост:
[системный вызов] [условие] [действие]
Мы видим, что в процессе исполнения нашей программы используется несколько больше системных вызовов, чем можно было бы предположить с самого начала.
Рассмотрим полученные правила.
Policy: /home/01mer/labs/systr/proga, Emulation: netbsd |
Указывается, для какого бинарного файла политика описана ниже. Полный путь к исполняемому файлу нужен для того, чтобы исключить возможность применения политики для файла с таким же именем, но располагающимся в другом месте. Emulation: netbsd показывает, что будет использоваться ABI ОС NetBSD.
Как видно из названия, системному вызову mmap разрешено исполняться. Для позволения выполнения системного вызова используется действие – permit, для запрещения deny.
netbsd-fsread: filename eq “/etc/ld.so.conf” then permit |
Тут мы видим, что системному вызову fsread будет разрешено выполниться в том случае, если запрошенный им файл будет /etc/ld.so.conf. Стоп, скажет внимательный читатель, системного вызова fsread не существует! И будет абсолютно прав. По умолчанию в systrace применяется использование псевдонимов для системных вызовов. Например: fsread является псевдонимом для stat, lstat, readlink, access, open. Но при желании режим использования псевдонимов можно отключить. В дальнейшем комментировании всего файла конфигурации смысла не вижу – все должно быть понятно и без этого.
Что ж, политика для нашей программы создана, давайте попробуем ее выполнить под чутким руководством systrace:
#systrace -a /home/01mer/labs/systr/proga
just a message
just a dumb procedure
|
Как мы видим, все функции нашей программы выполнились. Предположим, что логика нашей программы изменилась, что будет происходить в этом случае? Давайте для примера изменим функцию proc1 в нашей программе. Для начала мы должны точно знать, что будем менять. Просто изменить пару строчек в исходном коде и перекомпилировать программу, конечно, можно, но я предлагаю более интересный подход.
#objdump -d proga > proga.lst
Программа objdump показывает информацию из бинарных/объектных файлов. Ключ -d указывает, что файл нужно дисассемблировать. Вывод информации для удобства произведем в proga.lst
В полученном листинге находим нашу процедуру proc1.
08048833 :
8048833: 55 push %ebp
8048834: 89 e5 mov %esp,%ebp
8048836: 83 ec 08 sub $0x8,%esp
8048839: 83 ec 0c sub $0xc,%esp
804883c: 68 16 89 04 08 push $0x8048916
8048841: e8 52 fc ff ff call 8048498
8048846: 83 c4 10 add $0x10,%esp
8048849: c9 leave
804884a: c3 ret
804884b: 90 nop
|
Если не вдаваться в подробности, мы видим тут «подготовку» стека, выполнение вывода на экран текста, очищение стека и возврат в «основную» программу. Итого 25 байт, которые мы должны изменить на что то свое. Негусто, но вполне хватит для классического shell-кода вызывающего /bin/sh, он занимает как раз 23 байта (не забывайте, что 2 байта нам нужны для возврата в «основную программу»). А это именно то, что нам нужно – используется системный вызов, описания которого нет в наших правилах. Читаем man 2 execve. Для запуска /bin/sh через execve мы должны написать следующие:
execve("/bin/sh/",0,0);
Что можно свести к следующему коду на ассемблере:
xorl %eax,%eax
pushl %eax
pushl $0x68732f2f
pushl $0x6e69622f
movl %esp,%ebx
pushl %eax
pushl %esp
pushl %ebx
pushl %eax
movb $0x3b,%al
int $0x80
Все это можно представить в следующих опкодах:
31 c0
50
68 6e 2f 73 68
68 2f 2f 62 69
89 e3
50
54
53
50
b0 3b
cd 80
Вооружившись hex-редактором, открываем исполняемый файл с нашей программой и меняем содержимое proc1 на представленные выше опкоды. Само собой, нам нужно оставить команды возврата в «основную» программу. После успешного редактирования проверяем результат:
# home/01mer/labs/systr/proga
Как вы видите, вместо вывода на экран «just a dumb procedure» у нас запустился /bin/sh.
А теперь попробуем выполнить то же самое, но уже под контролем systrace:
# systrace -a /home/01mer/labs/systr/proga
Just a message
Jul 11 16:53:25 darkthrone.net1 systrace: deny user: 01mer, prog:
/home/01mer/labs/systr/proga, pid: 395(0)[0], policy:
/home/01mer/labs/systr/proga, filters: 13, syscall: netbsd-execve(59),
filename: /bin/sh, argv:
#
|
Как мы видим, попытка запустить модифицированную программу провалилась.
Автоматизируем создание правил
Доверить автоматическое создание правил systrace допустимо только в том случае, когда есть уверенность на 99,9% в том, что мы запускаем. Во всех остальных случаях рекомендуется создавать правила в интерактивном режиме. Рассмотрим пример:
# systrace -t /home/01mer/labs/systr/newproga
Ключ -t указывает, чтобы интерактивное создание правил проходило в текстовом режиме. Если у вас на машине установлены x-windows, вы можете воспользоваться xsystrace.
/home/01mer/labs/systr/newproga, pid: 729(0)[0], policy:
/home/01mer/labs/systr/newproga, filters: 0, syscall: netbsd-mmap(197), args: 32
Answer:
|
Тут systrace ожидает от нас ввода permit, в случае если мы желаем разрешить системный вызов или deny для запрета. Конечно, интерактивное создание правил – процесс достаточно трудоемкий и занимает много времени, но полученный результат того стоит. Вы будете точно знать, когда и какие системные вызовы использует программа.
Дополнительные «полезности»
В синтаксисе правил вы можете использовать необязательные опции протоколирования.
Например:
netbsd-fsread: filename eq "/lib/libc.so.12.114.1" then permit log
параметр log указывает systrace протоколировать каждое обращение к указанному фильтру.
По умолчанию протоколируются лишь попытки выполнить системные вызовы, о которых нет ни слова в правилах. Для протоколирования systrace использует syslogd. А это значит, что мы можем немного поправить /etc/syslog.conf, чтобы все сообщения от systrace размещались в отдельном файле.
При желании даже можно написать маленький парсер для получаемых логов. Но это уже по желанию, все в ваших руках.
Нельзя не отметить еще одну необязательную опцию – errorcode. С её помощью мы можем указать, какая ошибка будет возвращаться при обращении к запрещенному системному вызову.
Например:
netbsd-fswrite: filename eq "/some/where/file" then deny[EIO]
В данном случае при попытке записи в файл системному вызову будет возвращена ошибка – input/outpu error. Для ознакомления с полным списком errno-кодов возврата вы можете прочитать man errno.
Заключение
Мы рассмотрели простой пример использования systrace. Изложенных сведений вам будет достаточно и для создания более сложных собственных правил для различных программ. Применение политик ограничения системных вызовов для конкретных программ поможет вам существенно повысить степень защиты ОС в целом.