Теоретические основы крэкинга: Глава 4. Переменные и константы.

Дата публикации 10 мар 2004

Теоретические основы крэкинга: Глава 4. Переменные и константы. — Архив WASM.RU

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

Чем меньше изменений внесено в логику программы, тем менее вероятно, что эти изменения приведут к некорректной работе программы.

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

- Модификация константы

- Изменение значения предварительно инициализированной переменной

- Изменение условия перехода на противоположное

- Удаление линейной, то есть не содержащей ветвлений и вызовов нетривиальных подпрограмм, последовательности команд

- Дописывание в программу собственного кода; изменение значения, возвращаемого функцией

- Модификация внедренного в программу ресурса, важного для логики программы (т.е. такого, изменение которого меняет поведение программы)

- Удаление из программы логического блока (например, вызова нетривиальной функции); удаление внедренного в программу ресурса

Этот принцип, как любое другое широкое обобщение, требует достаточно осторожного отношения и в некоторых случаях может повести Вас по неоптимальному пути. Но на практике принцип минимального вмешательства обычно работает вполне успешно. Да и с точки зрения простого здравого смысла очевидно, что найти и исправить условие, которое проверяет зарегистрированность программы – прием куда более безобидный  и надежный, чем вырывание с корнем nag-screen’ов и триальных ограничений при помощи глубокого патчинга.

Как я уже говорил, многие незарегистрированные программы содержат количественные ограничения. В предыдущей главе мы как раз рассматривали некоторые разновидности таких ограничений и способы их обхода. Но, к сожалению, есть немало случаев, когда манипуляции с системным временем, файлами и реестром не помогают: это всевозможные ограничения на продолжительность одного сеанса работы с программой, на максимальное количество создаваемых или обрабатываемых документов, число записей в базе данных и т.п. Даже число жизней в компьютерной игре является одним из таких ограничений (как ни странно, крэкинг иногда применяется в качестве средства для беспроблемного прохождения компьютерных игр; «вечная жизнь» и «бесконечное оружие» - это тоже один из аспектов крэкинга, возможно даже самый старый).

Наверное, наиболее распространенным типом данных, используемым в программах, в настоящее время являются целочисленные данные. Абсолютное большинство существующих процессоров, в том числе и наши любимые x86, изначально ориентировались на работу с целыми числами определенной разрядности (в настоящее время на платформе x86 наиболее актуальны 32-разрядные целые со знаком или без). Именно в виде целых чисел чаще всего хранятся всевозможные счетчики, контрольные суммы и даже логические значения (когда я переходил от программирования под ДОС к программированию под Win32, меня сильно удивляла расточительность фирмы Microsoft, отводившей под простую булевскую переменную целых 4 байта. И только более глубокое изучение архитектуры 32-разрядных процессоров и ассемблера расставило все по своим местам).

Что Вы будете делать, если Вам потребуется найти в программе сравнение чего-либо с целочисленной константой с определенным значением? В принципе, для некоторых целых чисел достаточно обычного поиска блока двоичных данных в файле. Однако двоичный поиск хорошо работает далеко не со всеми числами: скажем, если Вы ищете число 3B9ACA00h, вероятность ложного срабатывания будет весьма небольшой, а вот если Вы попытаетесь найти в исполняемом файле число 10 или 15, Вы, скорее всего, просто устанете нажимать на кнопку «Найти далее». Кроме того, при таком способе поиска не учитывается структура исполняемого файла программы, то есть Вы ищете нужную Вам константу не только в коде программы, но и в PE-заголовке, секции данных, секции ресурсов и прочих областях, имеющих к коду программы самое отдаленное отношение.

Однако есть и другой, более эффективный метод поиска в программе известных заранее значений. Как это ни странно, но в реальных программах широко используется лишь сравнительно небольшой набор целочисленных констант: это, прежде всего, небольшие положительные числа от 0 до 7 (а также небольшие отрицательные от -3 до -1) и степени двойки: 8, 16, 32 и т.д. Другие константы в программах встречаются значительно реже. Попробуйте сами провести эксперимент – дизассемблируйте какую-нибудь достаточно большую программу и попробуйте найти в ней какое-нибудь число, например, 32h. Для демонстрации этого я написал программку на Delphi 7, вся функциональность которой концентрировался в следующем коде, имитирующем простейшее ограничение на число строк в документе:

Код (Text):
  1.  
  2. procedure TForm1.Button1Click(Sender: TObject);
  3. begin
  4.  if Memo1.Lines.Count>50 then
  5.    begin
  6.     Application.MessageBox('More than 50 items not available','Demo version');
  7.     Close;
  8.    end
  9.   else Memo1.Lines.Add(Edit1.Text);
  10. end;

В результате компиляции этот весьма нехитрый текст превратился в исполняемый файл размером более 350 килобайт (я намеренно использовал режим компиляции без использования runtimepackages, и потому мой собственный код составлял в исполняемом файле очень малую долю по сравнению с библиотеками Delphi). Затем я дизассемблировал откомпилированную программу при помощи W32Dasmи получил листинг текст длиной более 180 000 строк. Казалось бы, обнаружить область, где происходит сравнение с числом 50 в этом листинге ничуть не проще, чем найти иголку в стоге сена. Но я воспользовался функцией поиска в тексте строки, в качестве параметра поиска указав 00000032 (так в W32Dasmотображается число 50; заодно это позволило отсеять команды типа moveax,[ebx+50h], обычно использующиеся для доступа к элементам массивов и полям структур). Реальность превзошла самые смелые ожидания: четырехбайтное число 32hвстретилось в листинге всего два (!!!) раза:

Код (Text):
  1.  
  2. :004478C6 6A32                    push 00000032
  3. и
  4. :004505BB 83F832                  cmp eax, 00000032

Чтобы догадаться, что нужное нам сравнение происходит во второй строке, достаточно самых минимальных познаний в ассемблере. Из этого следует вывод: поиск нужной константы в дизассемблированной программе вполне реален, даже несмотря на огромный объем листинга.

Далее: Вам наверняка интересен не сам факт наличия константы где-то в недрах кода, а то, в каком контексте эта постоянная используется. Иными словами, если Вы знаете, что программа сравнивает число записей в базе данных с некоторым значением (в нашем случае - 32h), то среди всех строк, в которых присутствует эта константа, в первую очередь следует рассматривать команды сравнения (cmp) и вычисления разности (subи sbc), и лишь затем – все прочие, менее очевидные способы сравнения вроде

Код (Text):
  1.  
  2. mov ebx,50
  3. cmp eax, ebx

или

Код (Text):
  1.  
  2. push 50
  3. push eax
  4. call Compare2dwords

Ну и, раз уж речь зашла о сравнениях, нельзя не упомянуть об альтернативных вариантах реализации этой, казалось бы, нехитрой операции. Поразмыслим над приведенным выше примером сравнения содержимого регистра eax с числом 50. В самом деле, условия eax>50 и eax>=51 в приложении к целым числам имеют один и тот же смысл, а код

Код (Text):
  1.  
  2. cmp  eax,50
  3. jg my_label

работает совершенно аналогично коду

Код (Text):
  1.  
  2. cmp eax,51
  3. jge my_label

Если же eaxнеобходимо сравнить с числом 31, проверка может выглядеть даже так:

Код (Text):
  1.  
  2. and eax, 0FFFFFFE0h
  3. jnz my_label

То есть, проверка одного и того же условия может быть реализована несколькими разными способами, и когда Вы будете искать константы в дизассемблированном тексте, этот факт тоже надо учитывать.

Если рассматривать области возможного практического применения вышеописанного приема, то лучше всего поиск известной константы в дизассемблированном тексте работает на триальных ограничениях типа «не допускается создание больше Nэлементов в базе данных». Как правило, Nбольше 7 и является целым числом, что облегчает поиск нужной константы. Исходя из принципа минимального вмешательства, для обезвреживания таких ограничений я предпочитаю исправлять не команды сравнения, а сами константы. Действительно, если программа для проектирования интерьера комнаты не способна работать более, чем с 20 объектами, для практического применения она вряд ли будет пригодна. Но вот та же программа, где максимальное количество обрабатываемых объектов увеличено до двух с хвостиком миллиардов наверняка удовлетворит даже самого взыскательного пользователя.

Одним из наиболее частых вопросов, возникающих у начинающего крэкера, звучит так: «Программа работает 30 дней, но я так и не нашел в листинге сравнения с числом 30. Что делать?». Один из факторов я уже описал выше – там могло быть сравнение не с числом 30, а с числом 31. Однако этим список  возможных причин неудачи не исчерпывается. Как мы все знаем, день состоит из 24 часов, каждый из которых состоит из 60 минут, каждая из которых состоит из 60 секунд. Более того, продолжительность секунд также может измеряться во всевозможных «условных единицах», например, в миллисекундах. Тысячные доли секунд, в частности, используются в таких функциях WinAPI, как SetTimer (таймеры Windowsчасто используются для установки ограничений на продолжительность одного сеанса работы с программой) или Sleep. А вот в функциях, возвращающих время в виде структуры типа FILETIME, используются уже другие «условные единицы», равные ста наносекундам. Так что пресловутые 30 дней – это не только 30 дней, но еще и 720 часов, 43200 минут, 2592000 секунд, ну и так далее. И каждое из этих значений может быть использовано в программе как один из аргументов операции сравнения. Надо отметить, что в «условных единицах» может быть представлено не только время, но и многие другие величины: масса, географические координаты, денежные единицы и т.д.

Раз уж речь зашла о представлении временных отрезков внутри программ, уместно будет рассказать и о тонкостях использования таймеров. Наверняка Вы встречали программы, в которых после запуска окно с предложением о регистрации висит на экране в течение некоторого времени (иногда еще в этом окне идет обратный отсчет секунд), и при этом его невозможно закрыть. Подобные «спецэффекты» по усмотрению разработчика могут сопровождать вызов каких-либо функций программы, сохранение файлов или завершение приложения – это не суть важно. Важно другое: все эти временные задержки так или иначе используют средства измерения времени. В ОС Windowsсуществует два наиболее популярных способа отсчета отрезков времени: использование функций задержки (в частности, функции Sleep) и использование всевозможных таймеров.

Вообще в Windowsсуществует несколько разновидностей таймеров – кроме обычного таймера, создаваемого функцией SetTimer, существует еще высокоточный мультимедийный таймер и специфические таймерные функции DirectX. Эти таймеры срабатывают с некоторой заданной частотой, вызывая функцию-обработчик (она же callback-функция), внутри которой и выполняются необходимые действия, например, тот же обратный отсчет секунд до исчезновения окна с предложением зарегистрироваться. Периодичность срабатывания таймера почти всегда является константой, однако взаимосвязь между тем, что происходит внутри программы и тем, что Вы можете видеть на экране, не всегда очевидна. Чтобы пояснить эту мысль и заодно продемонстрировать на практике, как можно обращаться с таймерами, приведу несколько примеров.

Первый пример – простейший: программа, которая при запуске в течение пяти секунд показывала баннер, при этом поверх баннера выводился обратный счетчик секунд. Регистрация в программе не предусматривалась. Дизассемблирование показало, что таймер срабатывает каждые 1000 миллисекунд, при каждом вызове callback-функции значение переменной, изначально равной пяти, уменьшалось на единицу, и результат проверялся на равенство с нулем. В той конкретной программе баннер можно было просто «выломать», убрав функцию создания и отображения рекламного окна, но в общем случае это решение было бы не лучшим (вспомните принцип минимального вмешательства). И вот почему: на последнее срабатывание таймера могло быть «подвешено» не только закрытие окна с баннером, но и инициализация каких-либо объектов внутри программы или другие критичные действия, без которых программа могла бы работать некорректно. Так что немного усложним задачу – будем считать, что полностью убирать вызов окна с рекламой нельзя. Первое, что нам приходит в голову – уменьшить число секунд, в течение которых показывается баннер. Сказано – сделано, цифру 5 исправляем на единицу. Однако баннер все равно висит целую секунду – ведь первое срабатывание таймера наступает только через секунду после его создания. Теперь уменьшим период таймера до нуля (хотя лучше все-таки до одной миллисекунды, «таймер с периодом 0 миллисекунд», согласитесь, штука довольно странная). В результате мы получили баннер, появляющийся при запуске программы лишь на мгновение и не заставляющий тратить целых пять секунд на праздное разглядывание рекламных лозунгов.

В качестве второго примера я возьму одну из старых версий TVTools. В справке к программе было четко указано, что незарегистрированная версия работает только 10 минут; дизассемблирование и анализ листинга выявили, что программа создает два таймера с периодами 60 секунд (что навело меня на мысли о защитном назначении этого таймера) и 2 секунды. Без особых сложностей обезвредив первый таймер, я запустил программу и обнаружил, что она все равно больше 10 минут не работала. Тогда я более пристально изучил callback-функцию второго таймера, и наткнулся в ней на такой код:

Код (Text):
  1.  
  2. inc     dword_40D5A7
  3. cmp     dword_40D5A7, 136h
  4. jbe     short loc_405CAA

Нетрудно догадаться, что это увеличение некоего счетчика, который затем сравнивается с числом 310. Поскольку период таймера – 2 секунды, а 310*2=620 (т.е. чуть больше 10 минут), логично было предположить, что это и есть второй уровень защиты, дублировавший первый. Очевидно, что если бы я принял на веру, что программа перестает работать ровно через 10 минут (а не через 10 минут 20 секунд, как это оказалось в действительности) и стал бы искать сравнение с числом 300, я бы не смог обнаружить таким способом вторую проверку времени работы программы. Этот пример демонстрирует один из неочевидных приемов, который может быть использован для реализации такой, казалось бы, простой операции, как отсчет 10-минутного интервала. Также из этих примеров следует и другой, не менее важный вывод: далеко не всегда следует искать известную константу, чтобы найти код, в которой она используется. Иногда следует поступать прямо противоположным образом – сначала искать код, выполняющий нужные действия, и лишь затем выяснять, какая константа внутри этого кода ответственна за интересующие нас действия.

Поиск констант с плавающей точкой – занятие с одной стороны более сложное, чем поиск целочисленной константы, но с другой – куда более простое. В чем сложность и в чем простота этого занятия? По традиции начнем с плохого. Во-первых, формат представления чисел с плавающей точкой весьма нетривиален, и  Вы вряд ли сможете в уме привести шестнадцатиричный дамп такого числа в «человеческий» вид (возьмите документацию по процессорам Intel и попробуйте перевести число 1.23 в машинное представление, а затем проделать обратную операцию – Вы сами убедитесь, насколько сложна эта задача). Более того, даже целые числа в представлении с плавающей точкой выглядят весьма неординарно: к примеру, дамп самого что ни на есть обычного числа 123, приведенного к типу Double, выглядит как 00 00 00 00 00 C0 5E 40. Если Вы способны с первого взгляда отличить число с плавающей точкой от кода программы или каких-либо иных данных и оценить величину этого числа – я рад за Вас, но большинство людей, к сожалению, такими способностями не обладают.

Во-вторых, при работе с дробными числами нередко возникают проблемы, связанные с машинным округлением и потерей точности. Самым ярким примером, наверное, может служить особенность математических программ ПЗУ некоторых моделей Spectrum: с точки зрения такого Спектрума выражение 1/2=0.5 было ложным. Это, конечно, было давно, но не следует считать, что современные компьютеры полностью свободны от этой проблемы. И вот практическое тому подтверждение.

Откомпилируйте под Delphi следующий код: i:=sin(1); i:=arcsin(i) и посмотрите, как будет меняться результат при изменении типа переменной Iот Singleдо Extended. Например, если I имеет тип single, в результате вычислений получим, что arcsin(sin(1))=0,999999940395355. Такие «спецэффекты» – следствие все той же потери точности в процессе вычислений.

В-третьих, округлением чисел процессор может заниматься не только по собственному желанию, но и по велению программы. К примеру, в большинстве бухгалтерских программ всевозможные ставки налогов выводятся с точностью до копеек. Однако из того, что Вы видите на экране число 10.26, совершенно не следует, что результат расчетов представлен в памяти ЭВМ именно как 10.26. Реальное значение соответствующей переменной может быть равно 10.258 или 10.26167, которое и участвует в реальных расчетах, и лишь при выводе на экран для удобства пользователя было произведено округление до двух знаков после запятой.

Я не случайно столько места уделил округлению и точности представления чисел – именно эти особенности чисел с плавающей точкой в наибольшей мере затрудняют поиск нужных значений в памяти программы. Программисты знают, что при работе с действительными числами для проверки условия равенства некоторой вычисляемой величины другой величине не рекомендуется использовать сравнения вида f(a)=b. Причина этого лежит все в той же проблеме округления и потери точности расчетах – вспомните вышеприведенные примеры со Спектрумом или арксинусом синуса единицы. Вместо простой проверки равенства обычно используется условие «значения считаются равными, если абсолютная величина разности между ними не превышает некоторой величины»: abs(f(a)-b)<=delta, где delta – максимально допустимая величина разности, после которой числа не считаются равными. Поэтому если Вы хотите найти в памяти некоторое число с плавающей точкой F, Вы в действительности должны искать все числа из промежутка [F-delta; F+delta], причем определить значение deltaчаще всего можно лишь опытным путем. Это утверждение распространяется и на тот случай, когда Вы знаете округленное значение переменной, но в этом случае величина deltaбудет зависеть от того, до скольки знаков округлено значение переменной. Так, если число округлено до сотых, нетрудно догадаться, что delta=0.005.

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

И, наконец, нельзя забывать, что кроме стандартных для платформы x86 типов Single, Doubleи Extended (32-, 64- и 80-битных соответственно) существует еще несколько довольно экзотических, но все еще используемых форматов. Это, к примеру, Currency (64-битные, с фиксированным положением десятичной точки) или 48-битные паскалевские Real. Возможно также использование «самодельных» форматов; особенно часто встречаются числа с фиксированным положением десятичной точки (обычно такое делается для повышения скорости работы программы и применяется в основном в процедурах кодирования/декодирования аудио- и видеоинформации). Знать о таких вещах совсем не лишне, хотя, конечно, вероятность столкнуться с такими числами в современных программах довольно низка.

Теперь немного поговорим о том хорошем, что есть в числах с плавающей точкой. Как известно, изначально в процессорах x86 встроенных аппаратных и программных средств для обработки чисел с плавающей точкой не предусматривалось. Низкая скорость расчетов, в которых использовались действительные числа, вызвала к жизни математические сопроцессоры, как традиционные x87, так и весьма экзотические девайсы Weitek. Победившая линейка сопроцессоров x87 (они с некоторых пор стали интегрироваться в ядро процессора и потому перестали существовать как отдельные устройства) имела следующую особенность: новые «математические» команды активно использовали для обмена информацией оперативную память. Посмотрите, к примеру, на важнейшие команды сопроцессора fstи fld – в качестве параметра этих команд могут выступать указатели на области памяти, которые предполагается использовать для чтения/записи данных. Более того, использование указателей в качестве одного из параметров характерно и для многих других команд сопроцессора. Поэтому ищите ссылки, используемые командами сопроцессора в качестве параметров – и Вы легко доберетесь до данных, на которые эти ссылки указывают.

Из этого следует вывод: хороший дизассемблер или отладчик способен «догадаться», что по адресу, указанному в аргументах этих команд, находится число с плавающей точкой и отобразить это число. Если же Ваш дизассемблер/отладчик об этом не догадывается – Вам придется вручную (точнее говоря, при помощи соответствующих программ) вычислить значение, которое находится по этому адресу. И пока Вы будете копировать байтики из одной программы в другую, у Вас будет достаточно времени подумать об обновлении инструментария.

Но и здесь не обошлось без ложки дегтя – компиляторы фирмы Borland, видимо, ради особой оригинальности, для загрузки констант в стек сопроцессора могут воспользоваться комбинациями вроде

Код (Text):
  1.  
  2. mov [i],$9999999a
  3. mov [i+$4],$c1999999
  4. mov word ptr [i+$8],$4002
  5. fld tbyte ptr [i]

Хотя, казалось бы, ничто не мешало положить несчастное число в секцию инициализированных данных… Тут уж не до «умного» поиска – разобраться бы, чего и куда вообще загружается. Хотя, при желании и умении обращаться с регулярными выражениями (или умении программировать) можно искать даже в таком коде.

Другим свойством действительных чисел, облегчающим автоматический поиск известной величины, является само их внутреннее устройство. Достаточно большая длина этих чисел (32 бита, а чаше всего – 64 или 80) и сложный формат хранения позволяет искать числа с плавающей точкой в любых файлах, в том числе и в исполняемых файлах программ, непосредственно в двоичном виде, причем вероятность ложного срабатывания будет незначительной. Даже существование нескольких различных форматов представления действительных чисел не представляет серьезного препятствия – соответствующая программа очень проста и пишется за считанные минуты. Народная мудрость гласит: «лучше один раз увидеть, чем сто раз услышать», поэтому в качестве практики я рекомендую Вам самим написать и отладить такую программу – это не только усовершенствует Ваши навыки в программировании, но и позволит поближе познакомиться с миром действительных чисел. Затем, если захотите, Вы сможете доработать эту программу таким образом, чтобы она могла осуществлять нечеткий поиск в файле, о котором я говорил выше, то есть поиск всех значений, подходящих под заданный пользователем интервал.

И, наконец, рассмотрим третий из наиболее часто встречающихся простых типов данных: текстовые данные. Вообще, методы представления текстовых строк и массивов в коде программ имеют давние и богатые традиции. Наиболее старым способом является выделение под строку участка фиксированного размера, причем неиспользуемая часть блока заполняется «нулевыми» символами. В чистом виде этот прием уже давно не встречается (из примеров вспоминаются разве что старые реализации классического Паскаля), но нечто подобное иногда используется в программах на Си для хранения массивов строк – под хранение каждого из элементов массива отводится блок фиксированного размера, хотя сами элементы, по сути, являются ASCIIZ-строками.

Хранение строк в блоках фиксированного размера имело два принципиальных недостатка: неэффективное расходование памяти при хранении большого числа строк различной длины и жесткое ограничение на максимальную длину строки. Всех этих недостатков были лишены строки с завершающим символом. Идея была проста – выбирается какой-либо малоиспользуемый символ, который интерпретируется программой как признак конца строки. В языке Си таким символом стал символ с кодом 0 (а строки, оканчивающиеся нулем, окрестили ASCIIZ-строками); некоторые системные функции MS-DOSв качестве завершающего символа использовали символ “$”. Несмотря на ряд недостатков, строки с завершающим символом претерпели ряд усовершенствований и используются до сих пор. С началом активного использования UNICODEпоявилась модификация строк с завершающим символом и для этой кодировки. Зная образ мышления программистов на Си, нетрудно догадаться, что в качестве завершающего символа была использована пара нулевых байтов: (0,0). Нужно отметить, что если возникает необходимость укоротить такую строку в тексте программы на несколько символов, то обычно для этого достаточно всего лишь вписать в нужную позицию завершающий символ. То есть, если у Вас есть программа, написанная на C/С++, в заголовке окна которой написано что-то вроде «CoolProgram - Unregistered», и Вы не хотите видеть напоминание о том, что она «Unregistered», просто замените в файле программы пробел после слова Programна символ с кодом 0. После этого слово «Unregistered» Вы почти наверняка больше не увидите. Этим же способом ненужную строку можно вообще превратить в пустую, просто поставив в ее начало завершающий символ!

Описанная техника укорачивания и «обнуления» строк пригодна не только для того, чтобы убирать неэстетичные надписи в заголовках программ, в действительности ее возможности гораздо шире. Приведу пару примеров из собственной практики. Один раз мне в руки попалась некая программа, которая очень любила при печати документов в заголовок вставлять надпись «Thisreportcreatedbydemoversion …» шрифтом аршинного размера. Разумеется, мне это не понравилось и при помощи нехитрых манипуляций в шестнадцатиричном редакторе я «обнулил» строку с надписью, оскорблявшей мои эстетические чувства. В другом случае я подобным же образом расправился с одной программой-генератором справок, которая считала, что незарегистрированность – это повод вставлять рекламный текст в каждую статью справочной системы. Небольшой memorypatch, исправлявший в программе «на лету» несколько байт, смог убедить капризную программу в ее принципиальной неправоте.

Более эффективными по сравнению с ASCIIZ-строками являются строки с указанием длины. Такие строки позволяют использовать в тексте все 256 ASCII-символов, хранить не только текстовые, но и любые другие двоичные данные, а также применять по отношению к этим данным строковые функции. Кроме того, вычисление длины строки требует лишь одной операции чтения данных по ссылке, в отличие от ASCIIZ-строк, где для определения длины необходимо последовательно сканировать все символы строки до тех пор, пока не встретится завершающий символ. Как такового, стандарта на строки с указанием длины не существует – можно лишь говорить о конкретных реализациях таких строк в различных компиляторах и библиотеках. В частности, в коде программ на Delphi 7 строковые константы хранятся следующим образом:

  • 4 байта: длина строки в байтах (для UNICODE-текстов это значение в два раза больше длины строки в символах).
  • Содержимое строки.
  • Завершающий символ (#0 для ANSI-строк, #0#0 для UNICODE-строк). Завершающий символ никак не используется в «родных» функциях и процедурах Delphi, но значительно упрощает вызов функций WinAPI (которые используют строки с завершающим символом) и использование сторонних библиотек.

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

Поиск текстовых констант в программе – совсем не такое простое дело, как это могло бы показаться с первого взгляда. Прежде всего, не следует полагаться на «интеллектуальность» дизассемблера: поиск текстовых строк при дизассемблировании обычно основан на анализе ссылок, встречающихся в коде программы. Поэтому, если в коде программы нет прямой ссылки на строку, дизассемблер эту строку может просто «не увидеть». Чтобы убедиться в этом, рассмотрим несложный пример:

Код (Text):
  1.  
  2. .data
  3. line1 db "Line 1",0
  4. line2 db "Line 2",0
  5. line3 db "Line 3",0
  6. LineArr dd OFFSET line1, OFFSET line2, OFFSET line3
  7.  
  8. .code
  9. GetMsgAddr proc MessageIndex:DWORD
  10. mov ebx,MessageIndex
  11. mov eax, OFFSET LineArr
  12. mov eax,[eax+ebx*4]
  13. ret
  14. GetMsgAddr endp

Этот код представляет собой максимально упрощенную реализацию списка сообщений и функции, получающей адрес текстовой строки по номеру сообщения. Откомпилировав этот пример, загрузим его в W32Dasmи посмотрим, что получится. Получилось следующее: дизассемблер успешно распознал строку «Line 1», но строки «Line 2» и «Line 3» не обнаружил. А вот IDAуспешно распознал все три строки, и создал для них именованные метки. Впрочем, и IDAпри большом желании можно обмануть: достаточно лишь вписать перед текстом самой строки ее длину в байтах (именно так хранит строки Delphi). После этого IDAхотя и обнаруживает сам факт наличия текстовых строк в программе (в окне Stringsэти строки видны), но в дизассемблированном тексте программы эти строки выглядят как последовательность db… , которые нужно приводить в желаемый вид вручную. Кстати, W32Dasmпосле этой модификации не увидит вообще ни одной строки. Если же Вам и этого мало, вместо «Line 1» напишите «Строка 1» - все тексты на русском языке знаменитый дизассемблер гордо проигнорирует. И это только начало. А ведь текстовые строки могут находиться не только в сегменте кода/инициализированных данных, но и в секции ресурсов программы…

Здесь могут помочь специализированные программы, сканирующие указанный файл и вычленяющие из него все текстовые строки (или то, что похоже на текстовые строки). Кроме того Вам потребуются смещения этих строк от начала файла, поэтому Ваш инструмент должен предоставлять и такой сервис. Однако использование таких программ (и самостоятельное их написание) осложняется двумя факторами: разнообразием существующих кодировок текста и существованием национальных символов в некоторых языках (классический strings.exeи многие другие подобные программы «не понимают» русскую секцию UNICODE). Те же проблемы с UNICODEи национальными кодировками характерны и для программного обеспечения, осуществляющего поиск в текстовых файлах. К тому же русские тексты  в UNICODEсовершенно нечитабельны в шестнадцатиричных редакторах и просмотрщиках. Все это необходимо учитывать при выборе инструментов поиска текстовых строк, а выбранный инструмент перед использованием желательно проверить на подходящем «пробном камне».

Напоследок расскажу про весьма простой, но весьма эффективный в некоторых случаях способ поиска численных переменных в работающей программе. Этот способ основан на многократном сканировании адресного пространства программы, отслеживании и анализе всех изменений в этом пространстве. Лучше всего этот прием работает на программах, в которых установлено ограничение на количество тех или иных действий, вроде ограничения на число записей, добавляемых в документ. И используется для этого совсем не крэкерский инструментарий. Вы, наверное, знакомы с программами типа GameWizardили ArtMoney, которые позволяют искать в работающей компьютерной игре количество денег или оставшихся жизней. Для тех, кто не сталкивался с такими программами, вкратце опишу алгоритм их работы:

1. Пользователь выбирает из списка работающих в данный момент программ подопытную игру.

2. Пользователь вводит в программу поиска начальное количество денег (хитов и т.п.), которое в данный момент существует в игре.

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

4. Пользователь выполняет в игре какое-либо действие, в результате которого количество денег изменяется.

5. В программу поиска вводится новое количество денег.

6. Программа проверяет все значения из построенного списка и исключает из него те значения, которые не соответствуют введенному пользователем.

7. Пункты 4-6 повторяются до тех пор, пока список адресов не укоротится настолько, чтобы можно было проверить назначение каждого элемента списка вручную.

8. Пользователь проверяет каждый элемент списка, записывая по найденным адресам новые значения и наблюдая, как это повлияет на количество денег в игре.

Проницательный читатель наверняка уже догадался, что защищенная программа ничем в принципе не отличается от компьютерной игры, а число добавленных в документ записей – это то же самое, что количество виртуальных «золотых монет». И потому, воспользовавшись соответствующей программой (например, все той же ArtMoney), можно определить адреса всех переменных, которые могут хранить счетчик добавленных в документ записей. Дальнейшие действия зависят исключительно от Вашего желания – можно поставить аппаратную точку останова на чтение из этой переменной и попытаться добраться до команды сравнения существующего числа записей с максимальным. Можно погрузиться в изучение дизассемблерного листинга в поисках команды увеличения переменной и сделать так, чтобы значение счетчика не увеличивалось. Можно даже попробовать модифицировать счетчик из ArtMoney, посмотреть, что из этого получится, и если из этого получится что-то хорошее – написать memorypatch, каждые 100 миллисекунд обнуляющий счетчик.

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

© CyberManiac

0 1.496
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532