Примеры реальных взломов

Вид материалаДокументы

Содержание


конец таинств или где тот trial который expired
Листинг 95 зверь типа "заяц", пойманный за его короткий хвост
Листинг 96 сердце защитного кода – формирование строки EXPIRED и иже с ней
001B:0044714d enter c901,01
Листинг 98 расшифровщик текстовых строк
таблица ординалов
Листинг 100 так выглядит расшифрованная строка
Листинг 101 скрипт для расшифровщики текстовых строк
001B:0041cfde sub edx,[eax+08]
Листинг 103 инициализация ячейки, хранящей текущую дату
001B:0040af96 mov eax,[esp]
Листинг 105 так выглядит результат взлома
Подобный материал:
1   ...   5   6   7   8   9   10   11   12   13

конец таинств или где тот trial который expired


Теперь, когда основные аспекты функционирования защиты нам стали более или менее ясны, не грех сосредоточиться непосредственно на самом взломе, – удалении надписи "trial expired", которая уже успела порядком достать нас за последнее время. Можно ли быстро и элегантно выйти на след того самого кода, который и формирует надпись "TRIAL EXPIRED", не дизассемблируя всю программу целиком? Ну конечно же можно! Достаточно поставить точку останова на API-функцию вывода строки и затем, раскручивая стек, проследить передачу строки-аргумента до того самого места, где TRIAL EXPIRED и возникает.

Прикладные программисты наверняка знают, что вывод на консоль может осуществляться двояко: либо через WriteConsoleA, либо через WriteFile. На самом деле реально существует всего лишь одна функция вывода: WriteConsoleA, а WriteFile является не более чем "оберткой" вокруг последней. ОК, устанавливаем точку останова на WriteConsoleA и… отладчик действительно всплывает! Даем команду "P RET" для выхода из функции и на экране появляется "UniLink v1.03 [beta] ( EXPIRED ) (build 17.19)".

Ага! Это как раз то, что нам и нужно! Остается выяснить кто же именно вставляет подстроку EXIRED в середину "нормальных" символов. Конечно, это не вызывающая ее функция, ибо ею является ни в чем не повинная WriteFile, не имеющая вообще никакого представления ни о защите, ни о ее создателе. (Вот, кстати, пример дикой несправедливости, Харон знает о существовании WriteFile, а WriteFile о существовании Харона – нет). В свою очередь функция, вызывающая WriteFile, так же не имеет к защитному коду не малейшего отношения (это еще одна обертка поверх WriteFile). Попробуем дать "P RET" еще один раз, может хоть на этому уровне вложенности нам повезет… Как бы не так! Еще одна обертка! Да какая!!! Не функция, а целый монстр в полкило весом (между прочим это около четверти сотни машинных команд). И долго мы так будет шататься пьяным матросом по окружающему коду? Должен же существовать способ быстро обнаружить точку входа в интересующую нас функцию, которые мы условно окрестим как "функция вывода TRIAL'a на экран"?!

Действительно, а за каким чертом вы эти обертки порываетесь анализировать? Давайте будет тупо бить по "P RET" до тех пор, пока на экран не появится остальные текстовые строки. Функция, выводящая их, очевидно и будет той самой функцией, которая вызывает hi-level функцию вывода строки с TRIAL'ом на экран. Ну так поехали? Поосторожнее на поворотах! (Шутка!) Короче говоря у нас наклевывается следующая иерархия выводов: WriteConsoleA  WriteFile 41CFADh7  41D297h  41D037h  41D007h  401329h  и… Стоп машина! После выхода из последней функции на нас обрушивается целый каскад прочих текстовых строк (справка по ключам и все такое). Следовательно, адрес 401329h и есть тот самый адрес, который следует за концом интересующей нас функции. Смотрим, что у нас здесь расположено?


001B:00401324 CALL 0041CFB0

001B:00401329 MOV EDX,EDI

001B:0040132B MOV EAX,000000DA

001B:00401330 CALL 00447148

Листинг 95 зверь типа "заяц", пойманный за его короткий хвост

И минуты не ушло на выяснение адреса защитной функции (в листинге, приведенном выше, она выделена жирным шрифтом, а для пущей наглядности взята в рамочку). Остается лишь дизассемблировать ее тело (между прочим, очень стройное и худенькое такое тельце, как у российской курочки с птицефабрики).


001B:0041CFB0 ADD ESP,-28 ; резервируем память для local variable

001B:0041CFB3 TEST BYTE PTR [044B016],01 ; "не все то груша, что висит"

001B:0041CFBA JNZ 0041D00A ; хорошие хакеры сюда не прыгают!

001B:0041CFBC OR BYTE PTR [044B016],01 ; тут был Харон

001B:0041CFC3 MOV EDX,ESP ; буфер для расшифровщика

001B:0041CFC5 MOV EAX,0000007D ; индекс сообщения для расшифровки

001B:0041CFCA CALL 00447148 ; расшифровка сообщения



001B:0041D007 ADD ESP,1C

001B:0041D00A ADD ESP,28

001B:0041D00D RET

Листинг 96 сердце защитного кода – формирование строки EXPIRED и иже с ней

Условный переход, "шунтирующий" функцию (то есть, попросту говоря, прыгающий из начала функции в ее конец) буквально сам бросается нам в глаза, вот он: TEST byte ptr [44B016h] ,01/JNZ to_ret. Ну прямо будто специально для хакеров приготовлен, так и проситься "ну хакните пожалуйста меня!". А вот хрен! (А вот как бы не так!) Если заменить JNZ на JMP, то вместе с "EXPIRED" уйдет и вся прилегающая к ней строка, что никак не входит в наши планы. Да, программа будет взломана, но какой ценой?! Поэтому, преодолев соблазн упорно продираемся сквозь тернистые заросли Харонового кода дальше. Довольно скоро дорогу нам преграждает загадочный вызов CALL 447148h. Ну что, заглянем внутрь него?


001B:00447148 CALL 00449B7B

001B:0044714D ENTER C901,01

001B:00447151 FLD REAL4 PTR [ECX]

001B:00447153 OUT 01,EAX

001B:00447155 STI

Листинг 97 Ошибка?! Нет! Это – защитная функция!

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


001B:00449B7B XCHG ESI,[ESP] ; на массив ординалов

001B:00449B7E CLD ; флаг направления

001B:00449B7F MOVZX EAX,WORD PTR [EAX*2+ESI] ; ординал зашифрованной строки

001B:00449B83 ADD ESI,EAX ; эффективный адрес строки

001B:00449B85 LODSB ; читаем первый байт (длина)

001B:00449B86 ROR AL,03 ; циклический сдвиг на 3 вправо

001B:00449B89 MOVZX ECX,AL ; перегоняем длину в счетчик

001B:00449B8C OR AL,AL ; длина влезает в один байт?

001B:00449B8E JNS 00449B9C ;  длина влезает в один байт

001B:00449B90 SHL ECX,08 ; перегоняем длину в старший байт

001B:00449B93 AND CH,7F ; обнуляем сигнальный бит

001B:00449B96 LODSB ; читаем младший байт длины

001B:00449B97 ROR AL,03 ; расшифровываем младший байт

001B:00449B9A MOV CL,AL ; перегоняем в счетчик

001B:00449B9C LODSB ; читаем очередной байт строки

001B:00449B9D ROR AL,03 ; расшифровываем

001B:00449BA0 MOV [EDX],AL ; записываем расшифрованный

001B:00449BA2 INC EDX ; на следующий байт

001B:00449BA3 LOOP 00449B9C ; мотаем цикл

001B:00449BA5 MOV EAX,EDX ; рвем когти

001B:00449BA7 MOV BYTE PTR [EAX],00 ; пишем завершающий ноль

001B:00449BAA POP ESI ; сдираем адрес возврата

001B:00449BAB RET ; здравствуй бабушка!

Листинг 98 расшифровщик текстовых строк

Благодаря незатейливому алгоритму защиты, а так же характерному сочетанию LODSB/ROR/LOOP, мы легко распознаем в этом коде расшифровщик текстовых строк (а они, действительно, зашифрованы! попробуйте отыскать хоть одну из них в исполняемом файле, – там ничего этого не будет!).

Первая же машинная команда функции уже интересна: XCHG ESI, [ESP]. Зачем это Харону понадобился адрес возврата? Сейчас узнаем! Так, смотрим: полученное значение используется… вот это да! для хитрого приема с адресаций MOVZX EAX, word ptr [EAX*2 + ESI]/ADD ESI, EAX/LODSB. Ну, в EAX судя по всему находится индекс (ориднал) текстовой строки, выводимой на экран, тогда… вплотную к вызову функции-расшифровщика должна примыкать словный массив – таблица ординалов. Да, Харон знает толк в извращениях!!!


.text:00447148 call DecryptStr

.text:0044714D dw 1C8h

.text:0044714F dw 1C9h

.text:00447151 dw 1D9h

.text:00447153 dw 1E7h

.text:00447161 …

.text:00447237 dw 1308h

.text:00447239 …

Листинг 99 так выглядит таблица ординалов, примыкающая к вызову расшифровщика

Каждый элемент этой таблицы представляет собой смещение соответствующей ему строки, считая от первого байта следующего за концом машинной команды CALL DecryptStr. В данном случае, как хорошо видно под отладчиком, строка EXPIRED имеет порядковый номер 7Dh (да хоть и без отладчика, – регистр EAX явно инициализируется перед вызовом функции 41CFCA:CALL 447148), следовательно, эффективный адрес строки равен: 44714Dh + [44714Dh + 7D*2] == 44714Dh + [447237h] == 448525h.

Из ячейки, расположенный по данному адресу, извлекается первый байт, содержимое которого тут же на три бита проворачивается вправо (LODSB/ROR EAX,3). Затем, если знаковый бит равен единице, Харон извлекает второй байт, сместив уже декодированный байт на восемь бит влево, проделывает над содержимым младшего байта туже самую операцию. Попросту говоря, Харон стремиться втиснуть поле с длиной строки как можно в меньше количество байт.

Дальше уже совсем неинтересно. Каждый символ строки расшифровывается путем циклического сдвига на три бита вправо (как вы помните имена API – функций расшифровывались с точностью до наоборот, не считая поля длины, которое декодировалось вообще иначе), а результат расшифровки записывается в область памяти на которую указывает регистр EDX. Да, а на что он кстати указывает?!


0023:0012DEEC 5B 62 65 74 61 5D 20 28-20 45 58 50 49 52 45 44 [beta] ( EXPIRED

0023:0012DEFC 20 29 00 00 00 00 00 00-00 00 00 00 00 00 00 00 )..............

0023:0012DF0C 00 00 00 00 00 00 00 00-29 13 40 00 00 00 00 00 ........).@.....

0023:0012DF1C 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................

Листинг 100 так выглядит расшифрованная строка

Хм! Это совсем не то, что мы ожидали! И "beta" и "EXPIRED" идут одной строкой! Выходит, отключение EXPIRED повлечет за собой и "бэту"? Не будет спешить, ведь мы знаем какой Харон извращенец! (в хорошем смысле этого слова). Кстати, а почему бы нам не написать скрпт для расшифровки всех текстовых строк, ведь мы уже знаем алгоритм!


// расшифровщик строк

static main()

{

auto _beg, _end, a, count, p, x, x1, x2, s0;

_beg = ScreenEA(); p = _beg; s0 = "";

x1 = (Byte(p) >> 3); x2 = (Byte(p) << 5); x = x1 | x2;

count = (x & 0xFF); //PatchByte(p, count);

for (a = 0; a < count; a++)

{

x1 = (Byte(p+a+1) >> 3); x2 = (Byte(p+a+1) << 5); x = x1 | x2;

s0 = s0 + form("%c", x); //PatchByte(p + a +1, x);

}

Message("%s",s0 ); //MakeComm(p, s0);

}

Листинг 101 скрипт для расшифровщики текстовых строк

Остается лишь выяснить как происходит возврат из функции, ведь возвращается в материнскую функции мы не имеем права (там вообще никакого кода нет). Разгадка лежит в машинной команде POP ESI, которой не соответствует никакой инструкции PUSH! Так вот оно что! POP ESI сдирает адрес возврата в материнскую процедуру с верхушки стека и в результате команда RET выбрасывает нас в про-материнскую функцию ("бабушку").


001B:0041CFCF MOV EAX,[0044CAF8]

001B:0041CFD4 TEST EAX,EAX

001B:0041CFD6 JZ 0041CFEB

001B:0041CFD8 MOV EDX,[0044B030]

001B:0041CFDE SUB EDX,[EAX+08]

001B:0041CFE1 SHR EDX,16

001B:0041CFE4 JNZ 0041CFEB

001B:0041CFE6 MOV BYTE PTR [ESP+06],00

001B:0041CFEB PUSH 13

001B:0041CFED PUSH 11

Листинг 102 проверка демонстрационного периода на истечение

Прогон под отладчиком полностью подтверждает нашу гипотезу, но вот следующая машинная инструкция – MOV EAX, [44CAF8h] ставит нас в тупик. Что же такое в этой ячейке находится? Тем более, что дальше события разворачиваются просто с головокружительной быстротой. Если результат ([44B030h] – [EAX + 08])>>16h равен нулю, то следующая машинная команда MOV byte prt [ESP + 06], 0h вставляет… да! вставляет завершающий ноль в шестой считая от нуля байт строки "[beta] ( EXPIRED )" (как мы помним, расшифрованная строка лежит на вершине стека, – см. команду 41CFBC:MOV EDX,ESP). А в этой позиции находится… Невероятно! Но здесь действительно находится тот самый символ, что разделяет строки "[beta]" и "( EXPIRED )". Короче, если условный переход по адресу 41CFE4h не выполняется, то защита усекает расшифрованную строку и противное ругательство по поводу EXPIRED уже не появляется на экране.

Как нетрудно догадаться, в ячейке [EAX + 08] содержится "опорная" дата, а в ячейке [44B030h] – текущая. Весь вопрос в том, кто именно и как именно эти ячейки изменяет! И хотя, в принципе, во всех этих подробностях можно и не разбираться – достаточно лишь заменить JNZ на NOP/NOP (самоконтроля целостности у защиты нет), но… разве ж это будет интересно?!

Устанавливаем точку останова на ячейку 44B030h (bpm 44B030) и дожидаемся всплытия отладчика. Последствия не заставляют себя долго ждать и…


001B:00403664 CALL 0040AF8C

001B:00403669 MOV [0044B030],EAX

001B:0040366E CALL 004151F0

001B:00403673 CALL [KERNEL32!GetACP]

Листинг 103 инициализация ячейки, хранящей текущую дату

Ага! В ячейку 44B030h заносится результат выполнения функции sub_40AF8Ch. Но что же содержит сама функции sub_49AF8Ch? Даем команду "u 49AF8C" и смотрим:


001B:0040AF8C ADD ESP,-08

001B:0040AF8F PUSH ESP

001B:0040AF90 CALL [KERNEL32!GetSystemTimeAsFileTime]

001B:0040AF96 MOV EAX,[ESP]

Листинг 104 чтение текущего времени

Все точно как мы и говорили, и ячейка [44B030h] это действительно ячейка с текущей датой, а [EAX +08], соответственно, с опорной. Под отладчиком хорошо видно, что указатель (EAX +08) нацелен на ячейку 4001A0h, которая содержит… Стоп! Откуда здесь вообще взялось 4001A0h, ведь адрес первого байта файла (если верить IDA) лежит значительно выше и равен 401000h, что вполне соответствует базовому адресу его загрузке, указанному в PE-заголовке.

PE-заголовок?! Знаете, а это мысль! Ведь он проецируется системным загрузчиком прямиком на адресное пространство загружаемого процесса и потому свободно доступен защитному коду программы. Остается выяснить какому именно полю принадлежит ячейка 4001A0h (cм. описание структуры PE-файла в статье "Microsoft Portable Executable and Common Object File Format Specification", входящей в состав MSDN).

Вы конечно будете смеяться, но это поле – дата создания PE-файла (или, говоря другими словами, Time Stamp). Сколько лет живу, а такую защиту первый раз вижу! Во-первых, защита предельно корректна, во-вторых, она проста и элегантна, в-третьих, Time Stamp проставляется линкером автоматически и нет нужды в каждой новой версии программы его править вручную. Наконец, для корректного продления демонстрационного периода достаточно просто обновить Data Stamp и все! Обратите внимание: ни 0x00000000, ни 0xFFFFFFFF будучи записанными в качестве временной метки не дадут желаемый результат, – при вычислении разницы между датами наступи переполнение и результат не будет равен нулю! Чтобы не мучаться с изучением системы кодировки даты/времени давайте просто "перекинем" Data Stamp с любого только что созданного файла (или подсмотрим значение текущей даты в отладчике и тут же запишем ее в качестве опорной).

Короче, заносим начиная с физического смещения 1A0h последовательность "3Eh 9Bh 1Ah 19h" (если, конечно, к моменту публикации данного материла она еще не устареет) и запускаем UniLink…


UniLink v1.03 [beta] (build 17.19)

Листинг 105 так выглядит результат взлома

Держи всех тигров мира за хвост! Это сработало!!! Нет больше ругательству TRIAL EXPIRED! Ну и классную же головоломку поникнул нам Харон! Какое же удовольствие от ее анализа мы получили! Взлом как средство самоутверждения, самоутверждение как средство самопознания, самопознание как средство отождествления себя со строками кода! Вот это и есть настоящее хакерство!!! И главное, заметьте, никакого нарушения закона (Харон сам санкционировал взлом) и никакой прибыли! Ибо, где начинается прибыль там кончается хакерство и начинается скучное и невыразительное ремесло. Все хакеры немного дети, даже если биологически они глубокие старики…

1 самое смешное, что когда я все-таки скачал компилятор через своих московских знакомых (ну, для Москвы 45 мегабайт это вообще ни что) он наотрез отказался работать, мотивируя свое поведение тем, что срок демонстрационной лицензии уже истек…

2 рискнул. см. Intel C++ 7.0 compiler

3 к слову сказать, "лучший" еще не обозначает "просто хороший. Алкоголик крайне болезненно относится к искажению TOC зачастую теряя при этом всякую ориентацию – врезается в Lead-Out, виснет, выдает большое количество ошибок чтения секторов, хотя в действительности эти сектора нормально читаются и т.д., в своем умении прожигать нестандартные диски он значительно уступает Clone CD. Мной было разработано большое количество защит не копируемых ни Алкоголиком, ни Clone CD. Подробнее о них можно прочитать в книге "Техника защиты лазерных дисков"

4 как выяснилось позже это глюк конкретной версии 4.1.7 – ни более ранние, ни более поздние версии не исчезают

5 Вообще-то, анализировать PE-заголовок руками я ринулся чисто с перепугу. Тот же EXEVIEW от Randy Kath пусть и не совсем корректно обрабатывает защищенный файл, но по крайней мере не виснет и не завершает свою работу. К тому же он распространяется вместе с исходниками (см. MSDN) и у нас есть возможность оперативно исправить баг.

6 практически все современные отладчики либо трассируют программу через аппаратных точки останова, либо эмулируют выполнение инструкции PUSHF, засылая в стек подложные данные со сброшенным флагом трассировки

7 здесь и далее указываются адреса первого байта, следующего за вызовом дочерней функции