Поиск и анализ "троянских коней" под 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>:call0x8048eac <atexit>
0x8048f9d <_start+13>:call0x8048dcc
<__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>:movl0x804950c,%eax
0x8048483 <main+11>:movl%eax,0xfffffffc(%ebp)
Сохранить указатель по относительному адресу. Указатель ссылается на статическую, жестко запрограммированную строку "hello world" в коде.
0x8048486 <main+14>:call0x80483cc <getchar>
0x804848b <main+19>:movl%eax,%eax
0x804848d <main+21>:movl%eax,0xfffffff8(%ebp)
Вызвать функцию getchar и поместить результат (видимо, целое, хранящееся в регистре EAX) в стек. EBP — базовый указатель, используемый для ссылки на адреса относительно текукщей функции в стеке.
0x8048490 <main+24>:movl0xfffffffc(%ebp),%eax
0x8048493 <main+27>:pushl%eax
0x8048494 <main+28>:call0x8048470 <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 \0 is found */
0x80484fc <_fini+28>: "hello world"
В больших и сложных программах (автор допускает, что может потребоваться много времени на поиск всех возможных действий) это достаточно полный метод определения того, что в действительности делает программа, безотносительно к тому, была ли она прекомпилирована, статически связана, лишена символов или что-нибудь еще.
Mixter
Перевод: Василий Кондрашов
Сетевые решения. Статья была опубликована в номере 03 за 2001 год в рубрике save ass…