Как противостоять трассировке
Принципиальная возможность создания подлинно "невидимых" отладчиков большей частью просто возможностью и остается – большинство из них позволяют обнаружить себя даже непривилегированному коду.
Наибольшие нарекания вызывает использование однобайтового кода 0xCC для создания точки останова вместо поручения той же задачи специально на то предназначенным отладочным регистрам. Так поступают SoftIce, Turbo Debugger, Code Viewer и отладчик, интегрированный в Microsoft Visual Studio. Причем последний неявно использует точки останова при пошаговом прогоне программы – помещая в начало следующей инструкции этот пресловутый байт 0xCC.
Тривиальная проверка собственной целостности позволяет обнаружить факт установки точек останова, свидетельствующий об отладке. Не стоит использовать конструкции наподобие if (CalculateMyCRC()!=MyValidCRC) {printf("Hello, Hacker!\n");return;} их слишком легко обнаружить и нейтрализовать, подправив условный переход так, чтобы он всегда передавал управление нужной ветке программы. Лучше расшифровывать полученным значением контрольной суммы критические данные или некоторый код.
Простейшая защита может выглядеть, например, так:
int main(int argc, char* argv[])
{
// зашифрованная
строка Hello, Free World!
char s0[]="\x0C\x21\x28\x28\x2B\x68\x64\x02\x36\
\x21\x21\x64\x13\x2B\x36\x28\x20\x65\x49\x4E";
__asm
{
BeginCode: ; //начало контролируемого кода
pusha ; //сохранение всех регистров общего назначения
lea ebx,s0 ; // ebx=&s0[0]
GetNextChar: ; // do
XOR eax,eax ; // eax = 0;
LEA esi,BeginCode;// esi = &BeginCode
LEA ecx,EndCode ; // выислиление длины...
SUB ecx,esi ; // ...контролируемого кода
HarvestCRC: ; // do
LODSB ; // загрузка очередного байта в al
ADD eax,eax ; // выисление контрольной суммы
LOOP HarvestCRC ; // until(--cx>0)
xor [ebx],ah ; // расшифровка очередного символа s0
inc ebx ; // указатель на след. симв.
cmp [ebx],0 ; // until (пока не конец строки)
jnz GetNextChar ; // продолжить расшифровку
popa ; // восстановить все регистры
EndCode: ; // конец контролируемого кода
NOP ; // Safe BreakPoint here
}
printf(s0); // вывод строки на экран
return
0;
}
Листинг 221
При нормальном запуске на экране должна появиться строка "Hello, Free World!", но при прогоне под отладчиком при наличии хотя бы одной точки останова, установленной в пределах от BeginCode до EndCode на экране появится бессмысленный мусор наподобие: "Jgnnm."Dpgg"Umpnf#0"
Значительно усилить защиту можно, если поместить процедуру подсчета контрольной суммы в отдельный поток, занимающийся (для сокрытия свой деятельности) еще чем-нибудь полезным так, чтобы защитный механизм по возможности не бросался в глаза.
Потоки – вообще великая вещь, требующая к себе особого подхода. Человеку очень трудно смирится с тем, что программа может исполняться во множестве мест одновременно. Распространенные отладчики грешат тем, что отлаживают каждый поток по отдельности, но никогда два и более сразу. Приведенный ниже пример показывает, как это можно использовать для защиты.
// Эта функция будет выполняться в отдельном потоке
// ее назначение незаметно изменять регистр символов в строке,
// содержащей имя пользователя
void My(void *arg)
{
int p=1; // Указатель на шифруемый байт
// обратите внимание, шифровка выполняется
// не с первого байта, - это позволяет обойти
// контрольную точку, установленную на начало
// буфера
// выполнять до тех пор, пока не встретится перенос строки
while ( ((char *) arg)[p]!='\n')
{
// ожидать, пока очередной символ не будет инициализирован
while( ((char *) arg)[p]<0x20 );
// инвертировать пятый бит
// это приводит к изменению регистра латинских
// символов на противоположный
((char *) arg)[p] ^=0x20;
// указатель на следующий обрабатываемый байт
p++;
}
}
int main(int argc, char* argv[])
{
char name[100]; // буфер, содержащий имя пользователя
char buff[100]; // буфер, содержащий пароль
// забивка буфера имени пользователя нулями
// некоторые компиляторы это делают за нас, но не все!
memset(&name[0],0,100);
// выполнять процедуру My
в отдельном потоке
_beginthread(&My,NULL,(void *) &name[0]);
// запрос имени
пользователя
printf("Enter name:");fgets(&name[0],66,stdin);
// запрос пароля
// Важно: пока пользователь вводит пароль, второй поток
// получает достаточно квантов времени, чтобы изменить
// регистр всех символов имени пользователя
// Это обстоятельсво не так очевидно и не вытекает из
// беглого анализа программы, особенно при ее исследовании
// под отдадчиком, слабо показывающим взамного влияение
// отдельных компонентов программы друг на друга
printf("Enter password:");fgets(&buff[0],66,stdin);
// сравнение имени и пароля c
эталонными значениями
if (!(strcmp(&buff[0],"password\n")
// Важно: поскольку, введенное пользователем имя было
// преобразовано, фактически происходит сранение не
// strcmp(&name[0],"KPNC\n") а strcmp(&name[0],"Kpnc\n"),
// что далеко не очевидно на первый взгляд
|| strcmp(&name[0],"KPNC\n")))
// правильные имя и пароль
printf("USER OK\n");
else
// ошибка в вводе имени или пароля
printf("Wrong user or password!\n");
return
0;
}
Листинг 222
На первый взгляд программа ожидает "услышать" "KPNC:password" Но так ли это на самом деле? А вот и нет! Верный ответ – "Kpnc:password". В то время пока пользователь вводит свой пароль, второй поток обрабатывает буфер, содержащий его имя, меняет регистр всех символов, кроме первого, на противоположный. Весь фокус в том, что при пошаговой трассировке одного потока все остальные потоки выполняются независимо от него и могут произвольным образом вклиниваться в работу отлаживаемого потока, например, модифицировать его код.
Взять потоки под контроль можно введением в каждый из них точки останова, но если потоков окажется больше четырех (а что мешает разработчику защиты их создать?) отладочных регистров на всех не хватит и придется прибегать к использованию опкода 0xCC, который защитному механизму ничего не стоит обнаружить!
Ситуация усугубляется тем, что большинство отладчиков, в том числе и хваленый SoftIce очень плохо переносят программы со структурной обработкой исключений
(SEH). Инструкция, вызывающая обрабатываемое исключение, либо "срывает" отладчик, выходя из-под его контроля, либо передает управление на библиотечный фильтр исключений, который прежде чем передать управление прикладному обработку вызывает множество своих служебных функций, в которых взломщику немудрено и "утонуть".
Впрочем, по сравнению с ранними версиями SoftIce даже это большой прогресс, т.к. раньше он жестко держал некоторые прерывания, не позволяя программе самостоятельно обрабатывать, скажем, деление на нуль.
Если попытаться прогнать приведенный пример под SoftIce вплоть до версии 4.05 включительно (остальные не проверял, ввиду их отсутствия, но, скорее всего, они будут вести себя точно так же), он, достигнув строки int c=c/(a-b) внезапно "слетит", теряя контроль над отлаживаемым приложением. Теоретически исправить ситуацию можно заблаговременной установкой точки останова на первую команду блока __except, но, попробуй-ка вычислить, где расположен этот блок, не заглядывая в исходный текст, которого у хакера заведомо нет!
// Пример защиты, построенный на обработке структурных исключений
int main(int argc, char* argv[])
{
// Защищенный блок
__try{
int a=1; // Попытка деления на ноль
int b=1; // многословность объясняется тем,
// при выполнении следующей инструии отладчик SoftIce
//
теряет контроль над отлаживаемой программой и "слетает"
int c=c/(a-b); // что большинсвтво компиляторов
// выдают ошибку, встретив конструкцию
// наподобие
int a=a/0;
// некий код, который никогда не получит управления,
// но может быть вставлен для "отвода глаз". Если значение
// переменным a и b присваивается не непосредственно, а
// из результата, возращенного некими функциями, то при
// дизассемблировании программы их равенство будет не так
// очевидно. В результате взломщик может потратить много
// времени на анализ совершенно бесполезного кода
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
// этот код получит управление при возникновении
// исключения "деление на ноль"
// но отладчик SoftIce не распознает такой ситации
// и требует ручной установки точки останова на первую
// инструкцию блока __except
// а, что бы определить, адрес блока __except
требуется
// разобраться каким именно образом реализованна поддержка
// SEH в конктерном компиляторе
}
}
Листинг 223
Прежде чем справиться с такой защитой, взломщику придется основательно изучить реализацию механизма обработки структурных исключений, как на уровне операционной системы, так и на уровне конкретного компилятора. В подавляющем большинстве существующей литературы этот вопрос обходится стороной. И не спроста – реализация SEH действительно очень сложна, громоздка, многословна. Все это приводит к тому, что большинство программистов и технических писателей совершенно не представляют, что находится у нее "под капотом".
Поскольку, SEH по-разному реализована в каждом компиляторе, нет ничего удивительно, что SoftIce отказывается ее поддерживать. Поэтому, предложенный вариант защиты очень стоек к взлому и, в то же время, крайне прост в реализации. А самое важное – он одинаково хорошо работает во всех операционных системах семейства Windows от 95 до 2000.