Издательский дом ООО "Гейм Лэнд"СПЕЦВЫПУСК ЖУРНАЛА ХАКЕР #51, ФЕВРАЛЬ 2005 г.

Несетевая защита

Крис Касперски ака мыщъх

Спецвыпуск: Хакер, номер #051, стр. 051-100-6


Дерево процессов

В Linux при нормальном исполнении программы идентификатор родительского процесса (Ppid) всегда равен и идентификатору сессии (Sid), а при запуске под отладчиком Ppid и Sid различны (см. таблицу). Однако в других операционных системах (например, во FreeBSD) это не так, и Sid отличается от Ppid даже вне отладчика. Как следствие, программа, защищенная по этой методике, дает течь, отказывается выполняться даже у честных пользователей.

Личное наблюдение: при нормальном исполнении программы под FreeBSD идентификатор текущего процесса существенно отличается от идентификатора родительского, а при запуске из-под отладчика идентификатор родительского процесса оказывается меньше ровно на единицу. Таким образом, законченный пример реализации может выглядеть так:

main ()

{

if ( (getppid() != getsid(0)) && ((getppid() + 1) != getppid())

printf("get out, debugger!\n");

else

printf("all ok!\n");

}

Сигналы, дампы и исключения

Следующий прием основан на том факте, что большинство отладчиков жестко держат SIGTRAP-сигналы (trace/breakpoint trap) и не позволяют отлаживаемой программе устанавливать свои собственные обработчики. Как можно использовать это для защиты? Устанавливаем обработчик исключительной ситуации посредством вызова Signal (SIGTRAP, handler) и спустя некоторое время выполняем инструкцию INT 03. При нормальном развитии событий управление получает Handler, а при прогоне программы под GDB происходит аварийный останов с возвращением в отладчик. При возобновлении выполнения программа продолжает исполняться с прерванного места, при этом Handler так и не получает управления. Имеет смысл повесить на него расшифровщик или любую другую "отпирающую" процедуру.

Это очень мощный антиотладочный прием, единственный недостаток которого заключается в привязанности к конкретной аппаратной платформе – в данном случае к платформе Intel.

Конкретный пример реализации выглядит так:

#include <signal.h>

void handler(int n) { /* обработчик исключения */ }

main()

{

// устанавливаем обработчик на INT 03

signal(SIGTRAP, handler);

//…

// вызываем INT 03, передавая управление handler'у

// или отладчику (если он есть)

__asm__("int3");

// зашифрованная часть программы,

// расшифровываемая handler'ом

printf("hello, world!\n")

}

Распознавание программных точек останова

Программные точки останова (машинная команда INT 03h с опкодом CCh) распознаются обычным подсчетом контрольной суммы собственного кода программы. Поскольку порядок размещения функций в памяти в общем случае совпадает с порядком их объявления в исходном тесте, адрес конца функции равен указателю на начало следующей функции.

Контроль целостности своего кода как средство обнаружения программных точек останова

foo() {/* контролируемая функция 1 */ }

bar() {/* контролируемая функция 1 */ }

main()

{

int a; unsigned char *p; a = 0;

for (p = (unsigned char*)foo; p < (unsigned char*)main; p++)

a += *p;

if (a != _MY_CRC)

printf ("get out, debugger!\n");

else

printf ("all ok\n");

}

Мы трассируем, нас трассируют

Назад на стр. 051-100-5  Содержание  Вперед на стр. 051-100-7