Поиск и анализ "троянских коней" под UNIX
Mixter mixter@newyorkoffice.com
Этот документ - попытка дать представление о методах анализа
исполняемых файлов ОС UNIX для предсказания действий, которые они могут
произвести в системе. Эти методы применимы для исследования обнаруженных
"троянских коней" и другого вредоносного программного обеспечения. Они также
будут полезны для анализа прекомпилированного программного обеспечения с целью
убедиться в его надёжности.
При использовании программного обеспечения с открытым исходным
кодом пользователь может получить исходный код программы и откомпилировать свою,
надёжную версию. Однако, исходный код может содержать возможности троянского
коня, которые нелегко заметить. Некоторые из изощрённых способов производства
скрытых действий используют системные вызовы system() или exec для
передачи команд, вызывающих преднамеренные переполнения или небезопасные
ситуации, интерпретатору shell. Используется также непосредственное выполнение
инструкций ассемблера путём создания указателя ("void (*fp)()") на
двоичную строку с последующим его исполнением. Однако, бывает, что программы
прекомпилированы, например, как часть rpm или подобного двоичного пакета, или
части коммерческого программного обеспечения, или двоичные файлы скомпилированы
из непроверенного исходного кода, к тому же удалённого после компиляции.
К счастью, большинство UNIX-систем предлагают множество
инструментов для разработки и отладки, которые облегчают анализ двоичных файлов.
Прежде всего, всё должно делаться в "чистой", то есть, надёжной среде, где
обнаруженный двоичный файл исследуется, но не был ещё исполнен. Естетственно,
нужно использовать непривилегированную учётную запись (account). Если Вам
действительно нужно искать и анализировать возможных "троянцев" в ненадёжной
системе, то нужно использовать автономный интерпретатор shell (sash), который
должен быть объединён статически (statically linked). В таком случае
единственной программой, играющей роль "троянца" может быть модуль ядра,
отвественный за упаковку системных вызовов open и read, но такие
"троянцы" достаточно редки. При использовании автономного интерпретатора shell
наиболее значимыми являются команды ls, more и ed.
Первое, что должно быть сделано для поиска "троянца" - поиск
явных кодограмм в двоичном файле. Это может быть сделано с использованием
strings или просмотром при помощи less. Автор первоисточника
предпочитает редактор joe, который позволяет просматривать и
редактировать почти все не-ascii символы. Кодограммы обычно включают жёстко
запрограмированные имена используемых файлов, ascii-строки, которые программа
записывает в другие файлы или статические строки, которые она может искать, или
имена используемых библиотечных функций, если из файла не удалены символы. Они
могут также содержать имена необычных библиотек или библиотек, котрые они не
намереваются использовать будучи "троянцем". Лучше всего проверить это,
используя ldd для определения зависимостей от библиотек и file для
определения были ли удалены символы, была ли программа статически связана и
других специальных форматов.
На следующем этапе анализа программы нужно проследить вызовы
функций, выполняемые программой и сравнить их с функциями, которые программа, по
предположению, должна выполнять. Системные вызовы могут быть прослежены в
большинстве систем с помощью strace, ktrace в BSD, или
truss в Solaris. Следует обратить внимание на все попытки доступа к
файлам (open/stat/access/read/write), вызова гнёзд (socket calls),
особенно, вызовы listen() и fork(). Порождённые процессы могут
быть прослежены при помощи опции -f во всех этих программах.
Заслуживает интереса подобный инструмент для Linux -
ltrace, который распознаёт все библиотечные вызовы, производимые
программой в обход системных программ и позволяет создать очень подробный список
параметров программы.
Наконец, немаловажно то, что программа может и должна быть
дизассемблирована, предпочтительно, с использованием gdb.
Дизассемблирование, в основном, означает обнаружение функций в двоичном файле и
перевод двоичного кода обратно в команды ассемблера. Сначала должна быть
определена точка входа в программу. Это - адрес начала функции, которая будет
выполнена при запуске программы и которая, если из программы не была
модифицирована или из неё не были удалены символы, всегда называется main
или _start. Следуя за этой функцией можно проследить процесс выполнения
программы и увидеть, что может, а чего не может сделать программа. Особенно
интересны вызовы инструкций (function calls) и инструкции jmp/int,
если они используют небиблиотечные вызовы ядра или скомпилированы статически.
Типичные точки входа для двоичных файлов архитектуры x86 выглядят примерно так: 0x8048f97 <_start+7>: call 0x8048eac <atexit>
0x8048f9d <_start+13>: call 0x8048dcc <__libc_init_first>
0x69662 <__open+18>: int $0x80
Последней деталью рассмотрения являются строки, находящиеся в
определённых частях программы и аргументы, передаваемые функциям. Строки
(содержащиеся в символьном или другом буферах) упоминаются в программе
определёнными общими способами, например: void do_something (char *y) {};
char *h = "hello world";
int main() {
char *text = h;
int x = getchar();
do_something(text);
return 0;
}
Это соответствует следующим командам ассемблера: 0x804847e <main+6>: movl 0x804950c,%eax
0x8048483 <main+11>: movl %eax,0xfffffffc(%ebp)
Сохранить указатель по относительному адресу. Указатель
ссылается на статическую, жёстко запрограммированную строку "hello world"
в коде. 0x8048486 <main+14>: call 0x80483cc <getchar>
0x804848b <main+19>: movl %eax,%eax
0x804848d <main+21>: movl %eax,0xfffffff8(%ebp)
Вызвать функцию getchar и поместить результат (видимо,
целое. хранящееся в регистре EAX) в стек. EBP - базовый указатель, используемый
для ссылки на адреса относительно текукщей функции в стеке. 0x8048490 <main+24>: movl 0xfffffffc(%ebp),%eax
0x8048493 <main+27>: pushl %eax
0x8048494 <main+28>: call 0x8048470 <do_something>
Здесь полученный обратно указатель на строку отправляется как
первый и единственный аргумент функции do_something в стеке.
Следовательно, очевидно, строка, на которую ссылается указатель, передаётся
функции.
Теперь мы можем вручную разыменовать указатель при помощи
команды x : (gdb) x/a 0x804950c
/* x option /a displays the memory content as an address, to
see which address a pointer actually points to */
0x804950c <h>: 0x80484fc <_fini+28>
(gdb) x/s 0x80484fc
/* x option /s displays the memory content as string up to the
point where a terminating is found */
0x80484fc <_fini+28>: "hello world"
В больших и сложных программах (автор допускает, что это может
потребовать времени на поиск всех возможных действий) это достаточно полный
метод определения, что в действительности делает программа, безотносительно к
тому, была ли она прекомпилирована, статически связана, лишена символов или
что-нибудь ещё.
|