Взлом через покрытие

Автор: (c)Крис Касперски ака мыщъх

В этой статье описывается быстрый и эффективный метод взлома, основанный на анализе сравнения трасс программы до и после срабатывания защитного механизма, что позволяет мгновенно локализовать его дислокацию, даже во многомегабайтной программе.

Введение

Самое сложное во взломе - это "запеленговать" защитный код (зачастую представляющий собой тривиальный CALL CheckReg/TEST EAX,EAX/Jx unregistered), все остальное - дело техники. В хакерском арсенале имеются и точки останова, и перекрестные ссылки и многие другие противозащитные приемы, однако не всегда они оказываются эффективными и тогда приходится напрягать мозги в поисках новых путей, один из которых мыщъх выносит из своей норы, отдавая его на растерзание читателям.

Руководящая идея

Рассмотрим воображаемую программу с ограниченным сроком использования (trail'ом), которая некоторое время исправно работает, а потом выбрасывает мерзкий диалог с требованием о регистрации или выплевывает не менее грязное ругательство и завершает работу. Очевидно, что если мы найдем код, выводящий его на экран, нам останется только скорректировать Jx или добавить несколько NOP'ов. Но вот - как и что именно нужно корректировать? Можно, конечно, поставить точку останова на API-функцию или пройтись по перекрестным ссылкам на ругательные строки, но... это недостаточно эффективно. Существуют десятки API-функций, ответственных за чтение текущей даты/создание диалоговых окон, а текстовые строки часто бывают зашифрованы или хранятся в ресурсах...

А что, если сравнить трассы программы до и после истечения испытательного срока? Код, выводящий окно на экран, не выполняется в первом случае, но выполняется во втором! Таким образом, взлом сводится к анализу покрытия (coverage), измерением которого занимаются профилировщики и сопутствующие им утилиты. Покрытым называется код, хотя бы однажды получивший управление и, соответственно, наоборот.

Покрытие позволяет взламывать и другие типы защитных механизмов. Например, nag-screen'ы, выводимые через случайные или регулярные промежутки времени. Запускаем программу и тут же выходим из нее, опережая nag-screen, а в следующем прогоне терпеливо ждем его появления и сравниваем результаты покрытия (правда, это не спасет, если nag-screen выводится до запуска программы).

Защиты, основанные на ключевом файле или серийном номере, легко ломаются через покрытие при наличии хотя бы одно-единственного валидного ключа. Некоторые могут спросить: а зачем ломать защиту, если ключ уже есть? Очень просто! Многие программы пытаются подтвердить подлинность ключа через Сеть, и, если запросы на подтверждение сыплются с разных IP-адресов, ключ объявляется "пиратским" и программе посылается сигнал дезактивации. Покрытие позволяет нам мгновенно определить, где именно происходит проверка и блокировать ее. (Во многих случаях проблема решается брандмауэром, но некоторые программы уже научились определять наличие сети, например, вызовом API-функции InternetGetConnectedState и, если сеть есть, защита нагло требуют отключить брандмауэр для активации ключа).

Аналогичным образом обстоят дела и с электронными ключами. Сравнивая трассу прогона программы с ключом и без ключа мы видим все проверки, после чего либо "убиваем" их, либо пишем свой собственный эмулятор, отлаживаемый также путем сравнения трасс.

Покрытие дает в наши руки надежный способ взлома, позволяя выявить все проверки, даже если они идут из разных мест и с разной вероятностью. Мы получаем как бы "рельефный слепок" с программы, легко обнаруживающий то, что без него обнаруживается только путем утомительной трассировки или кропотливого дизассемблирования.

Выбор инструментария

Анализом покрытия занимаются многие утилиты - как коммерческие, так и бесплатные. К явным фаворитам относятся Intel Coverage Tool, NuMega TrueCoverage и т.д. вплоть до MS profile.exe, из комплекта Visual Studio. Однако все они ориентированы на работу с программами, имеющими исходные тексты, а "голый" двоичный файл обрабатывать не в состоянии. К счастью, их достаточно легко обмануть, сгенерировав всю необходимую информацию на основе дизассемблерного листинга, полученного с помощью IDA Pro.

VTune Performance Analyzer

Рисунок 1. Intel VTune Performance Analyzer - один из мощнейших коммерческих профилировщиков, работающий в паре с утилитой Intel Coverage Tool, определяющей покрытие.

Фактически, профилировщику нужна всего лишь информация о номерах строк и именах функций. С исходным текстом он не работает и для решения задачи нам достаточно добавить в ломаемый файл отладочную информацию, правда, для этого сначала придется разобраться с ее форматом. Некоторые профилировщики переваривают тривиальный map (который можно автоматически сгенерировать той же IDA Pro), но большинство работают со своими собственными форматами (как правило, недокументированными), завязанными на свои же собственные компиляторы (в случае Intel Coverage Tool это Intel Fortran Compiler или Intel C++ Compiler). К тому же, Intel Coverage Tool требует обязательного наличия Intel VTune Performance Analyzer, который весит свыше 200 Мбайт и плохо работает под VM Ware.

NuMega& True Coverage

Рисунок 2. NuMega True Coverage - отличное средство для определения покрытие, но, увы, ориентированное сугубо на драйвера.

NuMega, не имеющая собственных компиляторов, выглядит в этом плане более соблазнительной, однако версия True Coverage, входящая в состав Driver Studio, поддерживает работу только с драйверами, а прикладной версии в Сети найти не удалось (а ведь раньше, пока NuMega еще не продалась Compuware, она ведь была!).

MS Profile.exe

Рисунок 3. MS Profile.exe - простейший профилировщик, умеющий в том числе определять и покрытие, но работающий только с перемещаемыми программами.

MS Profiler.exe способен профилировать только перемещаемые программы, (т.е. такие, чья FIXUP TABLE не пуста), а большинство двоичных файлов неперемещаемы и хотя существует множество эвристических алгоритмов, восстанавливающих перемещаемые элементы (в частности, их можно встретить в дамперах), возможности MS Profile.exe не стоят того, чтобы с ним трахаться.

Алгоритмы определения покрытия

Существует, по меньшей мере, два алгоритма определения покрытия: трассировка и точки останова. Исполнение программы в пошаговом режиме дает нам полную трассу прогона, "разоблачая" адреса всех выполняемых инструкций. К сожалению, защищенные программы, как правило, не позволяют себя трассировать, к тому же это медленно и непроизводительно.

Другой алгоритм заключается во внедрении программных точек останова в начало всех машинных инструкций. Каждая, однажды сработавшая точка останова, снимается, а соответствующий ей код объявляется покрытым. Именно так большинство профилировщиков и работает (с той лишь разницей, что они внедряют точки останова в начало группы инструкций, представляющей ту или иную строку исходного кода - вот зачем им нужна информация о строках!). Быстродействие (за счет снятия точек останова) намного выше, чем при трассировке, особенно в циклах, но... программную точку останова очень легко обнаружить, поскольку она представляет собой байт CCh, записываемый поверх инструкции и выявляемый простым подсчетом контрольной суммы, что делает его малопригодным для анализа защитных механизмов.

К сожалению, аппаратных точек останова всего четыре и потому единственным надежным способом определения покрытия остается полная эмуляция процессора (чтобы не писать эмулятор с нуля, можно воспользоваться уже готовым, например, BOCHS, распространяемым в исходных текстах - легким взмахом "напильника" мы переделаем его во что угодно!). Как вариант, можно использовать грубый метод покрытия по страницам - помечаем все страницы процесса как недоступные, а потом ловим исключения и определяем покрытие. При условии, что защитный код расположен в отдельной процедуре (как чаще всего и происходит), мы с определенной степенью вероятности ее "запеленгуем", а, быть может, и нет. Все зависит от того, сколько места защитный код занимает. Во всяком случае, одиночные jx'ы данный метод не обнаруживает в принципе, но при желании его можно доработать.

Усовершенствованный алгоритм помечает все страницы недоступными, как и прежде, но вот при возникновении исключения мы не снимаем атрибут PAGE_NOACCESS, а смотрим на EIP, декодируем машинную инструкцию, устанавливаем аппаратную точку останова за ее концом (если это условный переход, мы либо анализируем флаги, определяющие условия его срабатывания, либо устанавливаем две аппаратные точки: за концом перехода и по адресу, на который он указывает). Временно снимаем атрибут PAGE_NOACCESS, выполняем инструкцию на живом процессоре, помечая ее покрытой, а затем вновь возвращаем PAGE_NOACCESS на место, который окончательно снимаем только тогда, когда покрываются все инструкции, принадлежащие данной странице. Такой подход обеспечивает приемлемую скорость выполнения и не слишком сложен в реализации (фактически нам достаточно реализовать дизассемблер длин инструкций). Правда, ломаемая программа может легко обнаружить и аппаратные точки, и атрибуты PAGE_NOACCESS, но большинство защит этим не занимаются.

Как мы будем действовать

Поскольку написание своего собственного профилировщика отнимает много сил и времени, на первых порах можно ограничиться использованием отладчика, поддерживающего трассировку в лог и обходящего большинство типовых защит. На эту роль хорошо подходит знаменитый OllyDbg для которого существует множество плагинов, скрывающих присутствие отладчика от глаз практически всех защитных механизмов.

Запуская программу до и после срабатывания защиты, мы получаем два лога трассировки. Считываем адреса машинных команд, загоняем их в массив "своего" лога, сортируем, убираем повторы, а затем сравниваем оба массива. Легко поискать, что сравнение сводится к поиску, который в отсортированном массиве быстрее всего осуществляется методом "вилки", причем сравнивать нужно не только первый массив со вторым, но и второй с первым, для удобства выводя рядом с адресами мнемоники машинных команд, которые можно вытащить прямо из лога.

Ниже приведен листинг упрощенной версии анализатора логов, не использующий сортировку и выполняющий сравнение путем последовательного поиска, а потому объемные логи обрабатывающий довольно долго (впрочем, с учетом быстродействия современных процессоров времени потребуется не так уж и много):

#define XL 1024 // максимальная длина одной строки лога
#define NX (1024 * 1024) // максимальное количество строк лога

// добавить новый адрес в массив,
// если он не встречался ранее

addnew(unsigned int *p, unsigned int x)
{
        int a;

        for (a = 1; a < *p; a++) if (p[a] == x) return 0; if (a == NX) return -1;
        p[0]++; p[a] = x; return 1;
}

// вывод результатов на экран
PRINT(unsigned int x, FILE *f)
{
        char *z; char buf[XL];
        while (fgets(buf, XL - 1, f)) if ((strtol(buf, &z, 16) == x) && (printf("%s", buf)|1)) break;
}

// сравнение двух массивов адресов на предмет различий
diff(unsigned int *p1, unsigned int *p2, FILE *f)
{
        int a, b, flag;
        for (a = 1; a < *p1; a++)
        {
                for (b = 1, flag = 0; b < *p2; b++) if ((p1[a] == p2[b]) && ++flag) break;
                if (!flag) PRINT(p1[a], f);
        }
}

main(int c, char **v)
{
        int f = 0; char buf[XL]; FILE *f1, *f2; unsigned int *p1, *p2; char *x;

        if (c < 3) return printf("USAGE: log-coverage-diff.exe file1 file2\n");

        p1 = (unsigned int *) malloc(NX * 4); p2 = (unsigned int *) malloc(NX * 4);

        f1 = fopen(v[1], "rb"); if (!f1) return printf("-ERR: open %s\x7\n", v[1]);
        f2 = fopen(v[2], "rb"); if (!f2) return printf("-ERR: open %s\x7\n", v[2]);

        fgets(buf, 1023, f1); fgets(buf, 1023, f2);

        while (f < 2 && !(f = 0))
        {
                if (fgets(buf, XL - 1, f1)) addnew(p1, strtol(buf, &x, 16)); else f++;
                if (fgets(buf, XL - 1, f2)) addnew(p2, strtol(buf, &x, 16)); else f++;
        }

        if (fseek(f1, 0, SEEK_SET)) return printf("-ERR: seek %s\x7\n", v[1]);
        if (fseek(f2, 0, SEEK_SET)) return printf("-ERR: seek %s\x7\n", v[2]);

        printf("\ndiff p1 -> p2\n"); diff(p1, p2, f1);
        printf("\ndiff p2 -> p1\n"); diff(p2, p1, f2);

        return 0;
}

Листинг 1. Листинг программы log-coverage-diff.c, сравнивающий протоколы трассировки отладчика OllyDbg и определяющий разницу в покрытии.

Компиляция осуществляется как обычно, то есть с ключами по умолчанию, в частности, при использовании Microsoft Visual C++ командная строка выглядит так: "cl.exe log-coverage-diff.c", а для создания оптимизированного варианта - "cl.exe /Ox log-coverage-diff.c" Только не надо выделять весь текст и копировать его в среду разработки, а потом нажимать F7 (Build) и удивляться, почему это программа не компилируется! Она и не должна компилироваться, поскольку по умолчанию Microsoft Visual Studio создает проект на Си++, а это Си. Как говорится, почувствуйте разницу!

Результат компиляции log-coverage-diff.c

Рисунок 4. Результат компиляции log-coverage-diff.c в Microsoft Visual Studio IDE - 3 ошибки, 6 предупреждений.

А из командной строки компиляция проходит нормально

Рисунок 5. А из командной строки компиляция проходит нормально!

Демонстрационный пример взлома

Давайте в качестве упражнения напишем простейшую триальную защиту и попробуем ее взломать методом сравнения покрытий до истечения испытательного срока и после. Исходный код подопытной программы может выглядеть, например, так, как показано в листинге 2. Срок "годности" задается константами ye (год), me (месяц), md (день), которые должны быть установлены в соответствии с текущей датой (так, чтобы программа еще работала).

int expired = 0;

#define md 3
#define me 8
#define ye 2006

foo()
{
        SYSTEMTIME sy;

        GetSystemTime(&sy);
        if ((sy.wYear & 0xF) * sy.wMonth * sy.wDay > (ye & 0xF) * me * md) expired = 1;
}

main()
{
        printf("coverage trial\n");
        foo();
        if (expired)
        {
                printf("trial has expired!\n");
        }
        else
        {
                printf("trial ok\n");
        }
}

Листинг 2. Листинг программы coverage.c, защищенной испытательным сроком, которую мы будем ломать.

Компиляция осуществляется так же, как и в предыдущем случае.

Запускаем coverage.exe, убеждаемся, что она работает (программа пишет: trial ok), а затем переводим дату за конец испытательного срока и запускаем вновь. Ага! trial has expired! Значит, надо ломать!

Демонстрационная программа coverage.exe

Рисунок 6. Демонстрационная программа coverage.exe, с ограниченным сроком использования.

Возвращаем дату на прежнее место и загружаем coverage.exe в OllyDbg. Затем в "View" выбираем "Run trace" и нажимаем <SHFT-F10> для вызова контекстного меню, в котором говорим "log to file" и в появившемся диалоговом окне вводим имя файла - "coverage1.txt". После чего нажимаем <CTRL-F11> (Trace Into) и дожидаемся завершения работы программы. На диске образуется файл coverage1.txt размером ~3 Мбайта. В том же самом окне "Run trace" нажимаем <SHIFT-F10> еще раз и закрываем лог-файл ("close log file").

Перезапускаем программу по <CTRL-F2>, переводим системную дату вперед (из командной строки командой date или щелкнув по часам в правом нижнем углу), возвращаемся в окно "Run Trace", нажимаем <SHIFT-F10>, "log to file" и вводим имя "coverage2.txt". Запускаем трассировку по <CTRL-F11> и по завершении выполнения программы закрываем log-файл (<SHIFT-F10>, "close log file").

Определение покрытия (Run Trace)

Рисунок 7. Определение покрытия (Run Trace) с помощью отладчика OllyDbg.

Теперь у нас есть два log-файла, которые можно скормить утилите log-coverage-diff.exe, чтобы увидеть различия в покрытии до истечения испытательного срока и после:

$log-coverage-diff.exe coverage1.txt coverage2.txt
diff p1 -> p2
00401076 Main        PUSH coverage.00406054
0040107B Main        CALL coverage.00401085
00401080 Main        ADD ESP,4

diff p2 -> p1
0040103B Main        MOV DWORD PTR DS:[4068F0],1
00401067 Main        PUSH coverage.00406040
0040106C Main        CALL coverage.00401085
00401071 Main        ADD ESP,4
00401074 Main        JMP SHORT coverage.00401083

Листинг 3. Результат сравнения покрытия программы до и после истечения испытательного срока.

Проанализировав полученный результат (см. листинг 3), даже начинающий хакер может догадаться, что двойное слово по адресу 4068F0h представляет собой глобальный флаг истечения испытательного срока, а команда "MOV DWORD PTR DS:[4068F0], 1" - как раз и есть та редиска, которая его взводит, когда испытательный срок заканчивается. Чтобы заполучить программу в бессрочное использование, достаточно заменить "MOV DWORD PTR DS:[4068F0], 1" на "MOV DWORD PTR DS:[4068F0], 0".

Загружаем coverage.exe в hiew, дважды нажимаем <ENTER> для перехода в дизассемблерный режим, давим <F5> (Goto) и вводим адрес команды MOV, предварив его символом точки, чтобы указать hiew'у, что это именно адрес, а не смещение от начала файла - ".40103B", затем нажимаем <F3> для активации режима редактирования и <ENTER> для ввода ассемблерной команды. hiew автоматически копирует текущую инструкцию в строку редактирования и нам остается всего лишь заменить "1" на "0", нажать <F9> для сохранения изменений в файле и после выхода из hiew'а его можно запускать, невзирая на текущую дату.

Взлом программы coverage.exe в hiew

Рисунок 8. Взлом программы coverage.exe в hiew.

Как вариант, можно открыть coverage.exe в IDA Pro и посмотреть, что находится по адресам, выданным утилитой log-coverage-diff.exe и вокруг них. Возможно, мы найдем более быстрый путь взлома:

0401000 sub_401000        proc near
0401000                   push    ebp
0401001                   mov     ebp, esp
0401003                   sub     esp, 10h
0401006                   lea     eax, [ebp+SystemTime]
0401009                   push    eax                   ; lpSystemTime
040100A                   call    ds:GetSystemTime
0401010                   mov     ecx, dword ptr [ebp+SystemTime.wYear]
0401013                   and     ecx, 0FFFFh
0401019                   and     ecx, 0Fh
040101C                   mov     edx, dword ptr [ebp+SystemTime.wMonth]
040101F                   and     edx, 0FFFFh
0401025                   imul    ecx, edx
0401028                   mov     eax, dword ptr [ebp+SystemTime.wDay]
040102B                   and     eax, 0FFFFh
0401030                   imul    ecx, eax
0401033                   cmp     ecx, 90h
0401039                   jle     short loc_401045
040103B                   mov     dword_4068F0, 1
0401045 loc_401045:
0401045                   mov     esp, ebp
0401047                   pop     ebp
0401048                   retn
0401048 sub_401000        endp
0401048
0401049 _main        proc near
0401049                   push    ebp
040104A                   mov     ebp, esp
040104C                   push    offset aCoverageTrial  ; "coverage trial\n"
0401051                   call    _printf
0401056                   add     esp, 4
0401059                   call    sub_401000
040105E                   cmp     dword_4068F0, 0
0401065                   jz      short loc_401076
0401067                   push    offset aTrialHasExpire ; "trial has expired!\n"
040106C                   call    _printf
0401071                   add     esp, 4
0401074                   jmp     short loc_401083
0401076 loc_401076:
0401076                   push    offset aTrialOk        ; "trial ok\n"
040107B                   call    _printf
0401080                   add     esp, 4
0401083
0401083 loc_401083:
0401083                   pop     ebp
0401084                   retn

Листинг 4. Фрагмент дизассемблерного листинга ломаемой программы coverage.exe, различия в покрытии до и после истечения испытательного срока выделены подчеркиванием.

Как можно видеть, сердце защитного механизма сосредоточено в процедуре sub_401000, которая сравнивает текущую дату с жестко прошитой (hard-coded) константой, после чего делает jle на loc_401045 (испытательный срок еще не истек) или... не делает и тогда выполняется команда mov dword_4068F0, 1, устанавливающая глобальный флаг истечения испытательного срока, проверяемый лишь в одном-единственном месте - по адресу 040105Eh, за которым идет условный переход 0401065 jz short loc_401076, выбирающий - какое из двух сообщений выводить на экран (в реальных программах флаг регистрации обычно проверяется многократно). Если мы заменим jle short loc_401045 на jmp short loc_401045, флаг истечения испытательного срока никогда не будет взведен!

Наглядное отображение расхождения в покрытии

Рисунок 9. Наглядное отображение расхождения в покрытии исследуемой программы в IDA Pro, выполненное специальным скриптом и оформленное в виде комментариев.

Для наглядности можно написать несложный скрипт на IDA-Си, считывающий результат работы log-coverage-diff.exe и отмечающий различия в покрытиях каким-нибудь символом, например, "*1*" будет означать, что данная машинная команда выполнялась только в первом прогоне, а "*2*" - только во втором (см. рис. 9). Подробности о синтаксисе IDA-Си и технике написания скриптов - в книге "Образ мышления - IDA PRO" электронную копию которой можно бесплатно скачать с мыщъх'иного сервера ftp://nezimu.org.ru/.

Заключение

Подведем итог - мы познакомились с новым способом взлома, написали несколько полезных утилит и обогатились свежими идеями. В общем, неплохо провели время и теперь можем уверенно двигаться вперед, переходя к взлому реальных защит!