Техника и философия хакерских атак II (фрагмент [2/3]) часть 3
Переведем окно дампа в режим отображения двойных слов командой "DD" и командой "d ss:esp + 8" заставим его отобразить искомый адрес. Запомним его (запишем на бумажке) или выделим мышью и скопируем в буфер (последние версии soft-ice поддерживают мышь). В частности, на компьютере автора содержимое стека выглядело так:
:dd :d ss:esp+8 0023:0012F9EC 002F4018 0000000F 00402310 004015D8 .@/......#@...@. 0023:0012F9FC 0012FA04 0012FE14 002F4018 6C361C58 .........@/.X.6l 0023:0012FA0C 6C361C58 0012F9F8 0012FB44 00401C48 X.6l....D...H.@. 0023:0012FA1C 00000002 6C2923D8 00402310 00000111 .....#)l.#@.....
Листинг 15 определение значения указателя lpString
Выделенное жирным шрифтом число и есть адрес буфера, готового принять прочитанную из окна строку. Посмотрим, что у нас там? Переключившись из режима двойных слов в режим байтов командой "DB", мы говорим отладчику "D SS:2F4018" и: ну конечно же видим вокруг себя один мусор, что и не удивительно, ведь функция GetWindowTextA еще и начинала своего выполнения! Что ж, приказываем Айсу выйти из функции ("P RET") и: вот она, наша строка!
:db :d ss:2f4018 :p ret 0023:002F4018 4B 72 69 73 20 4B 61 73-70 65 72 73 6B 79 00 00 Kris Kaspersky.. 0023:002F4028 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0023:002F4038 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0023:002F4048 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
Листинг 16 строка, считанная функцией GetWindowText
Теперь установим точку останова на адрес начала строки (в листинге, приведенном выше он обведен рамкой) или на всю строку целиком. Заметим, что обоим решениям присущи свои недостатки: если защита игнорирует несколько первых символов имени, то первый примем просто не сработает. С другой стороны, точки останова на диапазон адресов аппаратно не поддерживаются и отладчик вынужден прибегать к хитрым манипуляциям с атрибутами страницы, заставляя процессор генерировать исключение при всякой попытке доступа к ней, а затем вручную анализировать - произошло ли обращение к контролируемой области или нет. Естественно, это значительно снижает производительность и отлаживаемое приложение исполняется со скоростью, которой не позавидует и черепаха! Поэтому, к этому трюку имеет смысл прибегать лишь тогда, когда не сработал первый (а не срабатывает он крайне редко).
Уничтожив ставшей ненужной точку останова на GetWindowText (команда "bc *") мы устанавливаем новую точку останова "bpm ss:2F4018" (разумеется, на вашем компьютере адрес строки может быть и другим) и покидаем отладчик нажатием <Ctrl-D>. Не желая коротать свои дни в одиночестве, отладчик тут же всплывает, сигнализируя нам о том, что некий код попытался обратиться к нашей строке:
001B:77E9736D REPNZ SCASB 001B:77E9736F NOT ECX 001B:77E97371 DEC ECX 001B:77E97372 OR DWORD PTR [EBP-04],-01
Листинг 17 перехват обращения к регистрационной строке
Судя по адресу, мы имеем дело с некоторой системной функцией (ибо они традиционно размешаются в верхних адресах), но вот с какой именно? Сейчас выясним! Долго ли умеючи! Наскоро набив на клавиатуре трехбуквенное сочетание "mod" мы заставляем отладчик вывести список всех модулей системы на экран:
:mod hMod Base PEHeader Module Name File Name 80400000 804000C8 ntoskrnl \WINNT\System32\ntoskrnl.exe 77E10000 77E100D8 user32 \WINNT\system32\user32.dll 77E80000 77E800D0 kernel32 \WINNT\system32\kernel32.dll 77F40000 77F400C8 gdi32 \WINNT\system32\gdi32.dll 77F80000 77F800C0 ntdll \WINNT\system32\ntdll.dll 78000000 780000D8 msvcrt \WINNT\system32\msvcrt.dll
Листинг 18 определение принадлежности адреса к модулю
Очевидно, что адрес 77E9736Dh принадлежит динамической библиотеке kernel32.dll, а точнее, - функции lstrlenA, которая, как и следует из ее названия определяет длину строки. Поскольку, в определении длины для нас нет ничего интересного, мы безо всякого зазрения совести оставляем этот код жить на бозе и вновь выходим из отладчика, позволяя ему продолжить поиски защитного кода.
Следующее всплытие отладчика оказывается более информативным, смотрите (внимание: в силу архитектурных особенностей x86 процессоров, отладочное исключение возникает не до, а после выполнения команды, "зацепившей" точку останова, а потому отладчик подсвечивает не ее саму, а следующую за ней команду):
001B:004015F7 MOV CL,[EAX+ESI] ;эта команда "зацепила" breakpoint 001B:004015FA MOVSX AX,BYTE PTR [EAX+ESI+01] ;здесь отладчик получил управление 001B:00401600 MOVSX CX,CL 001B:00401604 IMUL EAX,ECX 001B:00401607 AND EAX,0000FFFF 001B:0040160C AND EAX,8000001F ; STATUS_BEGINNING_OF_MEDIA 001B:00401611 JNS 00401618 001B:00401613 DEC EAX
Листинг 19 ловля защитного кода за длинные уши и короткий хвост
Используемая адресация наталкивает нас на мысль, что EAX, возможно, параметр цикла, а вся эта конструкция посимвольно читает строку. Очень похоже, что в мы находимся самом "сердце" защитного механизма - генераторе серийного номера. Если мы посмотрим чуть-чуть ниже, то в глаза бросится очень любопытная строка (в тексте она выделена жирным шрифтом) :
001B:0040164E PUSH ECX 001B:0040164F PUSH EDX 001B:00401650 CALL [MSVCRT!_mbscmp] 001B:00401656 ADD ESP,08 001B:00401659 TEST EAX,EAX 001B:0040165B POP ESI 001B:0040165C PUSH 00 001B:0040165E PUSH 00 001B:00401660 JNZ 00401669 001B:00401662 PUSH 00403030 001B:00401667 JMP 0040166E
Листинг 20 в недрах генератора регистрационных номеров
Вероятно, здесь-то защита и сравнивает введенный пользователем регистрационный номер с только что сгенерированным эталоном! Переведем курсор на строку 401650h и дадим команду "HERE", обозначающую буквально "сюда!" Теперь последовательно дадим команды "D DS:ECX" и "D DS:EDX", посредством которых мы сможем подсмотреть содержимое указателей, передаваемых функции в качестве аргументов. Скорее всего, один из них принадлежит введенной нами строки, а другой - сгенерированному защитой регистрационному номеру.
:d ecx 0023:002F40B8 36 36 36 00 00 00 00 00-00 00 00 00 00 00 00 00 666............. 0023:002F40C8 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ :d edx 0023:002F4068 47 43 4C 41 41 4C 54 51-51 5B 57 52 54 00 35 38 GCLAALTQQ[WRT.58 0023:002F4078 44 44 32 44 36 39 2E 2E-2E 00 00 00 00 00 00 00 DD2D69..........
Листинг 21 просмотр аргументов, передаваемых компаратору
Итак, наше предположение на счет "введенного регистрационного номера" полностью подтверждается, и шансы на то, что абракадабра "GCLAALTQQ[WRT" и есть эталонный регистрационный номер весьма велики (обратите внимание на завершающий ее нуль, отсекающий остаток строки ":58DD2D69", который по невнимательности можно принять за саму строку.
Выйдем из отладчика и попытаемся ввести "GCLAALTQQ[WRT" в программу: Защита, благополучно проглотив регистрационный номер, выводит диалог с победной надписью "ОК". Получилось! Нас признали зарегистрированным пользователем! Вся операция не должна была занять порядка пары-тройки минут. Обычно для подобных защит большего и не требуется. С другой стороны, на их написание автор потратил как минимум полчаса. Это очень плохой баланс между накладными расходами на создание защиты и ее стойкостью. Тем не менее, использование таких защит вовсе не лишено смысла (ведь не все же пользователи - хакеры). Нельзя сказать, что создатели защит совсем уж не представляют насколько их легко вскрыть. Косвенным это подтверждением этого являются убедительные просьбы не ломать защиту, а зарегистрироваться и способствовать развитию отечественного рынка (что особенно характерно для российских программистов). Иной раз они бывают настолько красноречивы и длинны, что за время, потраченное на сочинение подобных опусов, можно было бы значительно усилить защиту.
Вышеописанная технология взлома доступна невероятно широкому кругу людей и не требует даже поверхностного знания ассемблера и операционной системы. Просто ставим точку останова на GetWindowText, затем еще одну на строковой буфер и, дождавшись всплытия отладчика, пытаемся найти в каком месте происходит сравнение введенного регистрационного номера со сгенерированным на основе имени эталоном. Любопытно, но большинство кракеров довольно смутно представляют себе "внутренности" операционной системы и знают API куда хуже прикладных программистов. Воистину "умение снять защиту еще не означает умения ее поставить". Чего греха таить! И автор этой книги сначала научился ломать и лишь затем программировать.
Однако мы не закончили взлом программы. Да, мы узнали регистрационный код для нашего имени, но понравится ли это остальным пользователям? Ведь каждый из них хочет зарегистрировать программу на себя. Кому приятно видеть чужое имя?! Вернемся к коду, сравнивающему строки введенного и эталонного регистрационного номера. Если мы заменим в строке 0040164Eh команду PUSH ECX (опкод 52h) на команду PUSH EDX (опкод 51h), то защита станет сравнивать эталонный регистрационный номер с: самим эталонным регистрационным номером! Разумеется, не совпадать с самим собой регистрационный номер просто не может и какие бы строки мы не вводили, защита воспримет их как правильные. Другой путь - заменить условный переход JNZ в строке 401660h (в тексте он выделен квадратиком) на безусловный переход JZ (тогда защита будет "проглатывать" любые регистрационные номера, кроме правильных), или же забить его любой незначащей командой подходящего размера, например SUB EAX, EAX (тогда будут "проглатываться" любые регистрационные номера, включая правильные), хотя последнее и неоригинально. Запускаем HIEW, переводим его в ASM-режим двойным нажатием <Enter>, переходим по адресу 401660h (<F5>, ".401660") и меняем "jne 1669" на "je 1669", скидываем изменения в файл <F9> и запускаем программу. Вводим в нее любую понравившуюся вам комбинации и: это работает!!!
Замечу, что это не самый лучший способ взлома и в ряде случаев он не срабатывает. Типичные защитные механизмы имеют как минимум два уровня обороны. На первом осуществляется проверка корректности введенного регистрационного номера и, если он воспринимается защитой как правильный, то данные пользователя заносятся в реестр или дисковый файл. Затем, при перезапуске программы, защитный механизм извлекает пользовательские данные из места их постоянного хранения и проверяет: а соответствует ли имя пользователя его регистрационному номеру?
Блокировав первую проверку, мы добьемся лишь того, что позволим защите сохранить неверные данные, но наш обман будет немедленно раскрыт как только программа попытается загрузить поддельные данные! Конечно, второй "укрепрайон" защитного механизма можно разбить тем же самым способом, которым мы воспользовались для захвата первого (только на этот раз вместо перехвата функции GetWindowText следует установить точки останова на функции, манипулирующие с файлом и реестром), однако это очень утомительно. Другой, и все такой же утомительный, путь - отследить все вызовы процедуры генерации регистрационного номера по перекрестным ссылкам (если одна и та же процедура вызывалась из разных мест защитного механизма), либо же по ее сигнатуре (если создатель защиты дублировал процедуру генерации). Действительно, крайне маловероятно, чтобы разработчик использовал не один, а несколько независимых вариантов генератора. Но даже в последнем случае очень трудно избежать отсутствия совпадающих фрагментов (во всяком случае на языках высокого уровня). Далеко не каждый программист знает, что "(!a) ? b = 0 : b = 1" и "if (a) b=1; els b=0" в общем случае компилируются в идентичный код. Реализовать один и тот же алгоритм так, чтобы ни в одном из вариантов не присутствовало повторяющихся фрагментов кода, представляется достаточно нетривиальной задачей! Тем не менее, выделение уникальной последовательности, присущей одному лишь защитному коду, - задача ничуть не менее нетривиальная, особенно если в защите присутствует множество проверок, расположенных в самых неожиданных местах.
К счастью, помимо изменения двоичного кода программы (которое, кстати, не очень-то приветствуется законом), существует и другая стратегия взлома: создание собственного генератора регистрационных номеров или, в просторечии, ключеделки. Для осуществления своего замысла хакеру необходимо проанализировать алгоритм оригинального генератора и затем написать аналогичный самостоятельно. Преимущества такого подхода очевидны: во-первых, ключеделка вычисляет действительно правильный регистрационный номер и сколько бы раз защита его ни проверяла - менее правильным он все равно не станет. Во-вторых, с юридической точки зрения создание собственного генератора регистрационных номеров более мягкое преступление, чем модификация защитного кода программы. Правда, возможность наказания за нелегальное использование ПО у законников все равно остается, так что, право же, не стоит так рисковать. Но не будем углубляться в дебри юриспруденции, - пусть трактовкой законов занимаются судьи и адвокаты, нам же - хакерам - лучше сосредоточить свои усилия на машинном коде. Вернемся немного назад, в то самое место, где отладчик зафиксировал обращение к первому байту строки, содержащей имя пользователя, и прокрутим экран дизассемблера немного вверх, до тех пор, пока не встретим начало цикла генератора, определяющееся наименьшим адресом условного (безусловного) перехода, направленного назад (подробнее см. "Фундаментальные основы хакерства" by me главы "Идентификация циклов" и "Идентификация условных операторов").
001B:004015EF PUSH ESI 001B:004015F0 XOR ESI,ESI 001B:004015F2 DEC ECX 001B:004015F3 TEST ECX,ECX 001B:004015F5 JLE 00401639 001B:004015F7 MOV CL,[EAX+ESI] ; эта команда обратилась к строке 001B:004015FA MOVSX AX,BYTE PTR [EAX+ESI+01] 001B:00401600 MOVSX CX,CL 001B:00401604 IMUL EAX,ECX 001B:00401607 AND EAX,0000FFFF 001B:0040160C AND EAX,8000001F 001B:00401611 JNS 00401618 ; адрес направлен "вниз", это не цикл 001B:00401611 ; а оператор "IF" 001B:00401613 DEC EAX 001B:00401614 OR EAX,-20 001B:00401617 INC EAX 001B:00401618 ADD AL,41 001B:0040161A LEA ECX,[ESP+0C] 001B:0040161E MOV [ESP+14],AL 001B:00401622 MOV EDX,[ESP+14] 001B:00401626 PUSH EDX 001B:00401627 CALL 0040192E 001B:0040162C MOV EAX,[ESP+08] 001B:00401630 INC ESI 001B:00401631 MOV ECX,[EAX-08] 001B:00401634 DEC ECX 001B:00401635 CMP ESI,ECX 001B:00401637 JL 004015F7 ; "наивысший" адрес из всех 001B:00401637 ; 4015F7 - начало цикла генератора 001B:00401637 ; 401637 - конец цикла генератора 001B:00401639 LEA EAX,[ESP+10] 001B:0040163D LEA ECX,[EDI+60] 001B:00401640 PUSH EAX 001B:00401641 CALL 00401934 001B:00401646 MOV ECX,[ESP+10] 001B:0040164A MOV EDX,[ESP+0C] 001B:0040164E PUSH ECX 001B:0040164F PUSH EDX 001B:00401650 CALL [MSVCRT!_mbscmp] ; _ тут сравниваются строки ; очевидно это конец генератор
Листинг 22 дизассемблерный код генератора регистрационных номеров
Прежде нем приступать к восстановлению алгоритма генерации регистрационных номеров, отметим, что отладчики вообще-то не предназначены для декомпиляции кода и нам лучше прибегнуть к помощи дизассемблера. Найти же в дизассемблерном листинге требуемый фрагмент очень просто, - ведь адрес процедуры генератора нам уже известен. Для быстрого перемещения к исследуемому коду в IDA достаточно отдать к консоли команду Jump(0x4015EF) , а в HIEW'e - <F5>, ".4015EF". Так или иначе мы встретим следующие строки (а еще лучше, если из мазохистских соображений, мы будем анализировать этот код под отладчиком, поскольку дизассемблер - особенно IDA - доступен не всем):
001B:004015EF PUSH ESI 001B:004015F0 XOR ESI,ESI 001B:004015F2 DEC ECX 001B:004015F3 TEST ECX,ECX 001B:004015F5 JLE 00401639
Листинг 23 фронтовая часть генератора регистрационных номеров
Регистр ESI здесь инициализируется явно (ESI ^ ESI := 0), а вот чему равен ECX?! Прокручиваем экран отладчика вверх до тех пор, пока не встретим машинную команду, присваивающую ECX то или иное значение:
001B:004015D8 MOV EAX,[ESP+04] 001B:004015DC MOV ECX,[EAX-08] 001B:004015DF CMP ECX,0A 001B:004015E2 JGE 004015EF
Листинг 24 определение значение, присваиваемого регистру ECX в листинге $ -- 2
Ага, здесь в ECX пересылается значение ячейки по адресу [EAX-08], но что это за ячейка и куда указывает сам EAX? Что ж, под отладчиком (в отличии от дизассемблера) его содержимое очень просто подсмотреть! Достаточно дать команду "D EAX" и область памяти на которую указывает EAX немедленно отобразится в окне дампа:
:d eax 0023:002F4018 4B 72 69 73 20 4B 61 73-70 65 72 73 6B 79 00 00 Kris Kaspersky.. 0023:002F4028 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0023:002F4038 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0023:002F4048 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
Листинг 25 текстовая строка на которую указывает регистр EAX (и эта та самая строка, которая только что была введена нами с клавиатуры)
Да это же только что введенная нами строка! А в регистр ECX тогда загружается что? Смотрим: так, значение ECX равно 0Eh или 14 в десятичной системе исчисления. Очень похоже на длину этой строки (как известно, MFC -строки, точнее объекты класса CString хранят свою длину в специальном 32-разрядном поле, "родимым пятном" которого как раз и является смещение на 8 байт влево относительно начала самой строки). Действительно, имя "Kris Kaspersky" как раз и насчитывает ровно 14 символов (считая вместе с пробелом). Тогда становятся понятными две следующие машинные команды: CMP ECX,0Ah/JGE 4015EFh, осуществляющие контроль строк на соответствие минимально допустимой длине. При попытке ввода имени, состоящего из девяти или менее символов, программа откинет его как непригодное для регистрации. Это важный момент! Многие хакеры игнорируют подобные тонкости алгоритма и создают не вполне корректные генераторы, не осуществляющие таких проверок вообще. Как следствие - пользователь вводит свое короткое имя в генератор (например, "KPNC"), получает регистрационный код, подсовывает его защите и: обложив матом хакера, вводит в генератор другое имя - на сей раз подлиннее. А если защита имеет ограничение на предельно допустимую длину? Сколько так пользователю придется мотаться между защитой и генератором?
Ладно, оставим вопросы профессиональной этики и вернемся к коду генератора, черкнув в лежащем справа от Клавы листке белой бумаги, что EAX указывает на имя пользователя, а ECX содержит его длину.
001B:004015F2 DEC ECX 001B:004015F3 TEST ECX,ECX 001B:004015F5 JLE 00401639
Листинг 26 заголовок цикла обработки введенной пользователем строки
Здесь: мотаем цикл до тех пор, пока не будут обработаны все символы строки (читатели, знакомые с "Фундаментальными основами хакерства" уже наверняка распознали в этой конструкции цикл for).
Теперь заглянем в тело цикла, спустившись еще на одну строчку вниз:
001B:004015F7 MOV CL,[EAX+ESI]
Здесь происходит загрузка очередного символа строки (и именно этот код вызвал всплытие отладчика при установленной точке останова, так что, надеюсь, вы его все еще помните). Поскольку, EAX - указатель на имя, то ESI с большой степени вероятности - параметр цикла. Правда, немного странно, что очередной символ строки помешается в младший байт регистра ECX, который судя по всему представляет собой счетчик цикла, но это все потом: Пока же мы нам известно лишь то, что начальное значение ESI равно нулю, а потому строка скорее всего обрабатывается от первого до последнего символа (хотя некоторые защиты поступают и наоборот).
001B:004015FA MOVSX AX,BYTE PTR [EAX+ESI+01]
MOVe whith Signed eXtension (пересылка со знаковым расширением) загружает следующий байт строки в регистр AX, автоматически расширяя его до слова и загаживая тем самым указатель на саму строку с именем. На редкость уродливый код! Но дальше - больше.
001B:00401600 MOVSX CX,CL
Преобразуем первый прочитанный символ строки к слову (обратим внимание, что здесь и далее под "первым" и "вторым" символом мы будем понимать отнюдь не NameString[0] и NameString[0], а NameString[ESI] и NameString[ESI + 1] соответственно, а сам ESI условно обозначим как index или, сокращенно, idx). Обратим внимание на несовершенство компилятора. Эту команду можно было записать более экономно как MOVSX CX, [ESI+EAX]
001B:00401604 IMUL EAX,ECX
Подставим вместо регистров их смысловые значения, мы получаем: EDX:EAX := NameString[idx] * String[idx + 1]
001B:00401607 AND EAX,0000FFFF
Преобразуем EAX к машинному слову, откидывая старшие 16 бит.
001B:0040160C AND EAX,8000001F
Выделяем пять младших бит от оставшегося слова (почему именно пять? просто переведите 1Fh в двоичную форму и сами увидите). Так же, выделяется и старший, знаковый, бит слова, однако, он всегда равен нулю, так как его принудительно сбрасывает предыдущая команда. Зачем же тогда его компилятор так старательно выделает? Осел он - вот почему. Программист присваивает результат беззнаковой переменной, вот компилятор и понимает его буквально!
001B:00401611 JNS 00401618
Если знаковый бит не установлен (ха! а с какой такой радости ему быть установленным?!), то прыгаем на 401618h. Ну что ж! Прыгаем, так прыгаем, избавляя себя от "радости" анализа нескольких никогда не исполняющихся команд защитного кода:
001B:00401618 ADD AL,41 001B:0040161A LEA ECX,[ESP+0C] 001B:0040161E MOV [ESP+14],AL 001B:00401622 MOV EDX,[ESP+14]
Листинг 27 код, знакомящий нас с плавающими фреймами
Первая машинная команда добавляет к содержимому регистра AL константу 41h (литера 'А' в символьном представлении) и полученная сумма перегоняется в регистр EDX, минуя по пути локальную переменную [ESP + 14].
С конструкцией LEA ECX, [ESP + 0Ch] разобраться несколько сложнее. Во-первых, ячейка [ESP +0Ch] явным образом не инициализируется в программе, а, во-вторых, значение регистра ECX ни здесь, ни далее не используются. Если бы оптимизирующие компиляторы не выкидывали все лишние операции присвоения (т .е. такие, чей результат не используется), мы бы просто списали эту команду на ляп разработчика защитного механизма, но сейчас такая стратегия уже не проходит. К тому же это удачный повод для знакомства с плавающими фреймами, без умения работать с которыми невозможно побороть практически ни одну современную защиту.
Для начала давайте вспомним устройство "классического" кадра стека. При выходе в функцию компилятор сохраняет в стеке прежнее значение регистра EBP (а так же при желании и всех остальных регистров общего назначения, если они действительно должны быть сохранены), а затем приподнимает регистр ESP немного "вверх", резервируя тем самым то или иное количество памяти для локальных переменных. Область памяти, расположенная между сохраненным значением регистра EBP и новой вершиной стека, и называется кадром. Начальный адрес только что созданного кадра копируется в регистр EBP, и этот регистр используется в качестве опорной точки для доступа ко всем локальным переменным. По мере разбухания стека поверх кадра могут громоздиться и другие данные, заталкиваемые туда машинными командами PUSH и PUSHF (например: аргументы функций, временные переменные, сохраняемые регистры и т. д.). Достоинство этой системы заключается в том, что для доступа к локальным переменным нам достаточно знать всего лишь одно число - смещение переменной относительно вершины кадра стека. Благодаря этому, машинные команды, обращающиеся к одной и той же локальной переменной, из какой бы точки функции они ни шли, выглядят одинаково. То есть, нам не требуется никаких усилий, чтобы догадаться, что MOV EAX, [EBP + 69h] и MOV [EBP + 69h], ECX в действительности обрабатывают одну локальную переменную, а не две. Между прочим, вы зря смеетесь! Хотите получить кукурузный початок в зад? Ну так получайте! (Знаю, что больно, но ведь я же предупреждал!).
Поскольку регистров общего назначения в архитектуре IA-32 всего семь, то отдавать даже один из них на организацию поддержки фиксированного кадра стека по меньшей мере не логично, тем более, что локальные переменные можно адресовать и через ESP. Ну и в чем же разница? - спросите вы. А разница между тем принципиальна! В отличии от EBP, жестко держащего верхушку кадра за хвост, значение ESP изменяется всякий раз, когда в стек что-то вложат или, наоборот, что-то вытащат оттуда. Рассмотрим это на следующем примере: MOV EAX, [ESP+10h]/PUSH EAX/MOV ECX, [ESP + 10h]/PUSH ECX/MOV [ESP + 18h], EBP, - как вы думаете, к каким локальным переменным здесь происходит обращение? На первый взгляд, значение ячейки [ESP + 10h] дважды засылается в стек, а затем в ячейку [ESP +18h] копируется содержимое регистра EBP. На самом же деле тут все не так! После засылки в стек содержимого регистра EAX, указатель вершины стека приподнимается на одно двойное слово вверх и дистанция между ним и локальными переменными неотвратимо увеличивается! Следующая машинная команда - MOV ECX, [ESP + 10h] на самом деле копирует в регистр ECX содержимое совсем другой ячейки! А вот [ESP + 18h] после засылки ECX указывает на ту же самую ячейку, что вначале копировалась в регистр EAX. Ну и как теперь насчет "посмеяться"?
Такие оптимизированные кадры стека по-русски называются "плавающими", а в англоязычной литературе обычно обозначаются аббревиатурой FPO - Frame Pointer Omission. Это едва ли не самое страшное проклятие для хакеров. Основной камень преткновения заключается в том, что для определения смещения переменной в кадре мы должны знать текущее состояние регистра ESP, а узнать его можно лишь путем отслеживания всех предшествующих ему машинных команд, манипулирующих с указателем верхушки стека и, если мы случайно упустим хоть одну из них, вычисленный таким трудом адрес локальной переменной окажется неверным! Следовательно, неверным окажется и результат дизассемблирования!!! Вернемся к нашему примеру LEA ECX, [ESP + 0Ch]. Будем прокручивать экран "CODE" отладчика вверх до тех пор, пока не обнаружим пролог функции или не накопим по меньшей мере 0Ch байт, закинутых на стек командами PUSH (в квадратных скобках показано смещение соответствующих ячеек относительно вершины стека на момент вызова нашего LEA).
001B:00401580 PUSH FF [ +24h] 001B:00401582 PUSH 00401C48 [ +20h] 001B:00401587 MOV EAX,FS:[00000000] 001B:0040158D PUSH EAX [ +1Сh] 001B:0040158E MOV FS:[00000000],ESP 001B:00401595 SUB ESP,10 [ +18h] (40161A:04h) 001B:00401598 PUSH EDI [ +08h] 001B:00401599 MOV EDI,ECX : 001B:004015CD PUSH EAX [ +04h] : 001B:004015EF PUSH ESI [ +00h]
Листинг 28 отслеживание манипуляций с вершиной стека
Ну, что Шура, я Вам могу сказать, - если считать, что SUB ESP, 10h открывает фрейм функции, то LEA ECX, [ESP + 0Ch] лежит по смещению 04h от его начала, - аккурат посередине. А что у нас здесь? Листаем код ниже (в квадратных скобках показано смещение соответствующих ячеек относительно начала кадра стека):
001B:00401595 SUB ESP,10 [ +00h] 001B:00401598 PUSH EDI [ +20h] 001B:00401599 MOV EDI,ECX 001B:0040159B LEA ECX,[ESP+04] [ +00h] 001B:0040159F CALL 40190Ah 001B:004015A4 LEA ECX,[ESP+0C] [ +08h] 001B:004015A8 MOV DWORD PTR [ESP+1C],00h 001B:004015B0 CALL 40190Ah 001B:004015B5 LEA ECX,[ESP+08] [ +04h] 001B:004015B9 MOV BYTE PTR [ESP+1C],01 001B:004015BE CALL 40190Ah
Листинг 29 инициализация локальных переменных
Ага! Вот теперь мы видим, что указатель на локальную переменную, расположенную по смещению 04h от начала кадра стека (далее просто var_04h) передается функции 40190Ah очевидно для ее, переменной, инициализации. Но вот что делает эта загадочная функция? Если, находясь в отладчике, нажать <F8> для входа в ее тело, мы обнаружим следующий код:
001B:0040190A JMP [00402164h]
Узнаете? Ну да, это характерный способ вызова функций из динамических библиотек. Но вот какая функция какой именно библиотеки сейчас вызывается? Ответ хранит ячейка 402164h, содержащая непосредственно сам вызываемый адрес. Посмотрим ее содержимое?
:dd :d 402164 0010:00402164 6C29198E 6C294A70 6C2918DD 6C298C74 ..)lpJ)l..)lt.)l
Листинг 30 просмотр содержимого ячейки 402164h (двойное слово, выделенное квадратиком)
Остается только узнать какому модулю принадлежит адрес 6C9198Eh. Не выходя из soft-ice даем ему команду "mod" и смотрим (протокол, приведенный ниже по понятным соображениям сильно сокращен):
Base PEHeader Module Name File Name 10000000 10000100 pdshell \WINNT\system32\pdshell.dll 6C120000 6C1200A8 mfc42loc \WINNT\system32\mfc42loc.dll 6C290000 6C2900F0 mfc42 \WINNT\system32\mfc42.dll 6E380000 6E3800C8 indicdll \WINNT\system32\indicdll.dll
Листинг 31 определение принадлежности адреса 6C9198Eh
Легко видеть, что адрес 6C29199Eh принадлежит модулю MFC42.DLL, что совершенно неудивительно ввиду того, что данная программа действительно интенсивно использует библиотечку MFC. Чтобы не вычислять принадлежность всех остальных функций вручную давайте просто загрузим символьную информацию из MFC42.DLL в отладчик. Запустив NuMega "Symbol Loader" (если только вы еще не сделали этого ранее), выберите команду "Load Exports" в меню "File", а затем, перейдя в папку "\WINNT\System32\" дважды щелкните по строке с именем "MFC42.DLL". Теперь, тот же самый код под отладчиком будет выглядеть так:
001B:004015B5 LEA ECX,[ESP+08] 001B:004015B9 MOV BYTE PTR [ESP+1C],01 001B:004015BE CALL MFC42!ORD_021B
Листинг 32 определение ординала функций
Умница soft-ice определил не только название динамической библиотеки, экспортирующей вызываемую функцию, но и ее ординал! Что же касается имени функции, его можно вычислить с помощью DUMPBIN и библиотеки MFC42.lib. Даем команду "DUMPBIN /HEADRES MFC42.LIB >MFC42.headrs.txt" и затем в образовавшемся файле простым контекстным поиском ищем строку "Ordinal : 539", где "539" наш ординал 021Bh записанный в десятичном виде (именно так выдает оридиналы этот dumpbin). Если все идет пучком, мы должны получить следующую информацию:
Version : 0 Machine : 14C (i386) TimeDateStamp: 35887C4E Thu Jun 18 06:32:46 1998 SizeOfData : 00000020 DLL name : MFC42.DLL Symbol name : ??0CString@@QAE@PBG@Z (__thiscall CString::CString(unsigned short *)) Type : code Name type : ordinal Ordinal : 539
Листинг 33 определение символьного имени функции MFC42!ORD_021B
Так, это конструктор объекта типа CString, а указатель, передаваемый ему, стало быть и есть тот самый this, что указывает на свой экземпляр CString! Следовательно, var_4 - это локальная переменная типа "MFC-строка". Теперь, не грех вернуться к изучению прерванной темы (а прервали мы ее на строке 40161Ah, где осуществлялась загрузка указателя на var_4 в регистр ECX посредством машинной команды LEA; регистр же EDX, как мы помним, содержит в себе результат умножения двух символов исходной строки, преобразованный в литерал):
001B:00401626 PUSH EDX 001B:00401627 CALL MFC42!ORD_03AB
Листинг 34 передача результата умножения двух символов функции MFC42!ORD_03AB
Следующими двумя командами мы заталкиваем полученный литерал в стек, передавая его в качестве второго аргумента функции MFC42!ORD_03AB (первый аргумент функций типа __thiscall передается через регистр ECX, содержащий указатель на экземпляр соответствующего объекта, с которым мы сейчас и манипулируем). Преобразовав ординал в символьное имя функции, мы получаем "оператор +=", что очень хорошо вписывается в обстановку окружающей действительности. Другими словами, здесь осуществляется посимвольное наращивание строки var_4 генерируемыми налету литералами.
001B:0040162C MOV EAX,[ESP+08]
Что у нас в [ESP + 8]? Прокручивая экран с дизассемблерным листингом вверх, находим, что здесь лежит самая первая ячейка из принадлежащих кадру стека. Условимся называть ее var_0. Давайте определим, что же за информация в ней находится?
001B:00401595 SUB ESP,10 ; [ +00h] 001B:00401598 PUSH EDI ; [ +04h] : 001B:004015C3 LEA EAX, [ESP+04] ; var_0 001B:004015C7 LEA ECX,[EDI+000000A0] 001B:004015CD PUSH EAX ; [ +08h] 001B:004015CE MOV BYTE PTR [ESP+20],02 001B:004015D3 CALL MFC42!ORD_0F21 ; CWnd::GetWindowText
Листинг 35 определение содержимого ячейки [ESP + 8]
Кое-что начинает уже проясняться. Переменная var_0 содержит указатель на MFC-строку, бережно хранящую в себе регистрационное имя пользователя.
001B:00401630 INC ESI
Указатель текущего символа перемещается на одну позицию вправо (ведь вы помните, что в ESI содержится именно указатель на текущий обрабатываемый символ регистрационной строки, верно?).
001B:00401631 MOV ECX,[EAX-08] ; EAX := var_4 001B:00401634 DEC ECX 001B:00401635 CMP ESI,ECX 001B:00401637 JL 004015F7
Листинг 36 хвост цикла
Первая машинная команда из четырех загружает длину регистрационной MFC-строки в регистр ECX, команда "DEC" уменьшает ее на единицу, а "CMP ESI, ECX" сравнивает полученное значение с индексом текущего обрабатываемого символа регистрационной строки. И, до тех пор, пока индекс не достигнет предпоследнего символа строки, условный переход "JL" прыгает на адрес 4015F7h, мотая цикл.
001B:00401639 LEA EAX,[ESP+10] 001B:0040163D LEA ECX,[EDI+60] 001B:00401640 PUSH EAX 001B:00401641 CALL MFC42!ORD_0F21 001B:00401646 MOV ECX,[ESP+10] 001B:0040164A MOV EDX,[ESP+0C] 001B:0040164E PUSH ECX 001B:0040164F PUSH EDX 001B:00401650 CALL [MSVCRT!_mbscmp]
Листинг 37 сравнение сгенерированной строки с регистрационным номером, введенным пользователем
По факту завершения цикла, защита сравнивает только что сгенерированную ей строку с регистрационным номером, введенным пользователем и, в зависимости от результатов этого сравнения, пользователь либо признается легальным чувяком, либо получает от ворот поворот.
Брр! Вы еще не запутались?! Что ж, тогда давайте подытожим все вышесказанное краткими комментариями к защитному коду:
:ESI = 0 (индекс) [index]; :[ESP+08h], EAX - на регистрационную строку [NameString]; :[ESP+0Ch] - на генерируемую строку [GenString] 001B:004015F7 MOV CL,[EAX+ESI] ; CL := (char) NameString[index] ;AX := (uint)((char) NameString[index+1]) 001B:004015FA MOVSX AX,BYTE PTR [EAX+ESI+1] 001B:00401600 MOVSX CX,CL ; 001B:00401604 IMUL EAX,ECX ; EAX := EAX * ECX 001B:00401607 AND EAX,0000FFFF ; EAX := LOW_WORD(EAX) 001B:0040160C AND EAX,8000001F ; EAX := EAX ^ 1Fh 001B:00401611 JNS 00401618 ; GOTO 401618h 001B:00401618 ADD AL,41 ; EAX := EAX + 'A' 001B:0040161A LEA ECX,[ESP+0C] ; ECX := &GenString 001B:0040161E MOV [ESP+14],AL ; tmp := AL 001B:00401622 MOV EDX,[ESP+14] ; EDX := tmp 001B:00401626 PUSH EDX ; 001B:00401627 CALL 0040192E ; GetString += EDX 001B:0040162C MOV EAX,[ESP+08] ; EAX := &NameString 001B:00401630 INC ESI ; index++ ; ECX := NameString->GetLength() 001B:00401631 MOV ECX,[EAX-08] 001B:00401634 DEC ECX ; ECX-- 001B:00401635 CMP ESI,ECX ; 001B:00401637 JL 004015F7 ; if (index < ECX) GOTO 4015F7h
Листинг 38 сводный дизассемблерный листинг генератора регистрационных номеров
Вот теперь - другое дело и нам уже ничего не стоит восстановить исходный код генератора.