Перейти к содержанию

Основы шифрования, отладки, обратной трассировки и сбора информации со стека


aliast

Рекомендуемые сообщения

1) Новый тутор про трейсер для CE (англ) - ссылка

А вот и перевод от меня этой статейки. Статья и правда познавательная. Разобрался, будем применять в других играх)

Основы шифрования, отладки, обратной трассировки и сбора информации со стека

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

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

Я буду пользоваться Cheat Engine 6.0, последняя версия которого доступна по адресу: http://www.cheatengine.org

Игра которую мы будем исследовать называется Chicken Invaders 4, однако статья содержит общие сведения, которые можно применять и при исследовании других программ.

Итак, для начала найдите игру Chicken Invaders 4 v4.00. Именно такую версию программы я буду описывать в этой статье. Если у Вас эта игра уже установлена, поиграйте немного, ознакомьтесь с материалом. Игра представляет собой простенький космический шутер, в котором нужно отстреливать злобных куриц. Довольно глупая концепция, но сама по себе игра (программирование, графика и музыка) довольно неплохая. Теперь настройте игру на запуск в оконном режиме (не в полноэкранном).

Миссия 1: Поиск некоторого значения

В этой статье мы будем пытаться изменить количество жизней. Если Вы уже пытались найти их, наверняка обломались и ничего не нашли. А всё потому, что игра шифрует значение количества жизней, очков, еды, ракет и ключей. Чтобы найти значение, нужно выбрать тип Byte и искать по критерию изменилось/не изменилось после смерти.

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

0044DB20 - 8B 81 04010000 - mov eax,[ecx+00000104]

0044DB26 - 33 81 80000000 - xor eax,[ecx+00000080]

0044DB5D - 89 B3 80000000 - mov [ebx+00000080],esi

0044DB63 - 33 74 24 10 - xor esi,[esp+10]

0044DB67 - 5F - pop edi

0044DB68 - 89 B3 04010000 - mov [ebx+00000104],esi

ЗАМЕТКА: Таким же образом Вы можете найти адреса еды или очков. В любом случае Вы придёте к аналогичному коду.

Миссия 2: Что такое шифрование

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

Инструкция XOR соединяет два значения с помощью операции исключающее ИЛИ (не перепутайте с обычным ИЛИ, которое является включающим ИЛИ).

Чтобы разобраться, рассмотрим пример:

1001010110

0101001101

Если применить к ним операцию ИЛИ, результат будет 1100011011

Если два бита равны, в результате получится 0. При равенстве битов получится 1. Вы можете использовать Windows калькулятор для расчёта действия операции XOR.

За подробностями обратитесь к Google. Детальное изучение булевой алгебры выходит за рамки этой статьи.

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

0044DB20 - 8B 81 04010000 - mov eax,[ecx+00000104]

0044DB26 - 33 81 80000000 - xor eax,[ecx+00000080]

Одно значение хранится по адресу [ecx+0104], а другое по адресу [ecx+0080]. Используя отладчик можно выяснить адреса, обращающиеся к этим кодам (find out what addresses accessed by these codes). Можно найти ключевое значение жизней. Отлично, теперь установите в обоих адресах 0, в итоге жизней станет 0. Одна проблема – на экране жизней 0, а на самом деле их число осталось прежним. Получается, что Вы нашли отображаемое значение, но не нашли реального числа жизней.

Миссия 3: Исследование зашифрованного кода

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

0044DB26 - 33 81 80000000 - xor eax,[ecx+00000080]

Этот код запускается очень часто, СЧИТЫВАЯ сразу несколько значений. Очевидно, код обращается не только к количеству жизней, но ещё и к другим значениям.

0044DB5D - 89 B3 80000000 - mov [ebx+00000080],esi

0044DB63 - 33 74 24 10 - xor esi,[esp+10]

0044DB67 - 5F - pop edi

0044DB68 - 89 B3 04010000 - mov [ebx+00000104],esi

Этот код запускается только когда значение изменяется. Код ЗАПИСЫВАЕТ измённое значение. Давайте разбираться. Мы можем посмотреть содержимое регистра ESI, содержащего некоторую важну информацию, и да, он же используется в операции xor с адресом [esp+10]. Ради интереса, давайте посмотрим что хранится по адресу [ESP+10].

Начните новую игру, получите 5 жизней.

Теперь Вы можете проверить адрес двумя способами.

1. Правый клик на коде, затем "Break and trace".

2. Нажать F5 для ручной установки бряка.

Теперь вернитесь в игру и умрите. Никого не убивайте и ничего не подбирайте, просто умрите. Если Вы выбрали первый вариант действий, то сможете увидеть вот такую важную информацию:

ESP: 0012EB10

xor esi,[esp+10]

0012EB20 = (dword)00000004(4)

Если же Вы выбрали ручной бряк, игра остановится и Вы увидите значение ESP, 0012EB10. ESP+10 будет 0012EB20 и если Вы добавите этот адрес в таблицу, то увидите значение этого адреса, равное 4. Нажмите F5 снова и удалите бряк, затем нажмите F9 для разморозки игры. Как только Вы это сделаете, увидите, что значение адреса тут же изменится.

Замечательно, у нас было 5 жизней, мы погибли и ESP+10 показал нам 4. Если Вы умрёте снова, то увидите 3.

Very good, we had 5 lives, we have died and ESP+10 shows 4. If You die again, it will show 3. Значит мы нашли расшифрованное число наших жизней.

На этой картинке виден результат трассировки выполненного кода:

ci4_die.jpg

ЗАМЕЧАНИЕ: Более продвинутые пользователи знают, что мы могли использовать бряки по условию для работы со сложным кодом, но в данном случае проще было сделать именно так.

Миссия 4: Стек

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

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

Например, примем ecx=3, edx=2 и в стеке хранятся следующие значения.

4

5

6

Теперь мы положим в стек ecx, выполнив инструкцию "push ecx". Стек примет такой вид.

3

4

5

6

Теперь положим в стек edx, выполнив "push edx". Стек станет таким.

2

3

4

5

6

А теперь мы хотим вытащить значение из стека, например "pop ecx".

В этом случае программа вытолкнет первое значение из стека, т.е. 2, и сохранит его в ecx. Теперь ecx=2, а стек примет такой вид.

3

4

5

6

Если мы выполним теперь инструкцию "pop edx", edx станет равен 3, и стек станет таким

4

5

6

Результат: Стек остался каким и был вначале, но теперь ecx=2, а edx=3, мы поменяли их значения местами.

Хорошо, теперь Вы знаете как значение кладётся и выталкивается из стека. Это не случайный процесс, это работает в строгом порядке. Следующее что Вы должны узнать это указатель стека и как его использовать. В моём примере я буду использовать стек нереального размера в 16 байт.

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

0012FF00 4 //это адрес дна стека со значением 4

0012FEFC 2 //следующий уровень стека, адрес уменьшен на 4 байта

----------------------- //после этой линии неиспользуемые уровни стека

0012FEF8 x

0012FEF4 x

Указатель стека: указатель стека указывает на вершину стека, в нашем случае это линия ------------------. Указатель стека хранится в регистре ESP. В нашем примере 0012FEFC это вершина стека, она может быть вычислена проверкой значения в ESP, которое равно 0012FEFC.

Теперь давайте положим что-нибудь в стек.

Предположим, следующей инструкцией будет:

push 8

После неё наш мини-стек примет такой вид:

0012FF00 4 // это адрес дна стека со значением 4

0012FEFC 2 // уровень стека, адрес уменьшен на 4 байта

0012FEF8 8

-------------------------------------- //после этой линии неиспользуемые уровни стека

0012FEF4 x

Мы положили значение в стек и вершина стека снова уменьшилась на 4 байта. Теперь ESP = 0012FEF8, а его значение равно 8.

Теперь положим в стек другое значение, предположим, 5.

push 5

0012FF00 4 //это адрес дна стека со значением 4

0012FEFC 2 //уровень стека, адрес уменьшен на 4 байта

0012FEF8 8

0012FEF4 5

-------------------------------------- ////после этой линии неиспользуемые уровни стека

Как Вы теперь видите, вершина стека снова была уменьшена на 4 байта. ESP = 0012FEF4, а значение в 0012FEF4 = 5.

Что случится, если нам понадобится узнать значение в 0012FEFC, например? Это очень просто, я добавлю ещё одну колонку в наш пример, чтобы показать ESP.

0012FF00 4 ESP+0C //это адрес дна стека со значением 4

0012FEFC 2 ESP+08 // уровень стека, адрес уменьшен на 4 байта

0012FEF8 8 ESP+04

0012FEF4 5 ESP

-------------------------------------- //после этой линии неиспользуемые уровни стека

Как видите из примера, значение 0012FEFC равно [ESP+08].

И наконец предположим, что нам нужно получить значение вершины стека, после чего оно нам больше не понадобится и можно удалить значение из стека. Будем использовать такую инструкцию:

pop eax

Эта инструкция скопирует 5 в регистр EAX и вершина стека снова сдвинется.

0012FF00 4 ESP+08 // это адрес дна стека со значением 4

0012FEFC 2 ESP+04 //уровень стека, адрес уменьшен на 4 байта

0012FEF8 8 ESP

-------------------------------------- //после этой линии неиспользуемые уровни стека

0012FEF4 x

Я надеюсь, что этот пример понятно объяснил назначение указателя стека (ESP) и как в стеке хранится информация. Если не разобрались, попробуйте выполнить трассировку некоего кода и проследить как стек работает с этим кодом.

Последняя миссия: Обратная трассировка

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

xor esi,[esp+10]

Если Вы напишете скрипт для изменения [ESP+10] в 99, Вы увидите, что действительно получили 99 жизней. А также 99 очков, 99 еды и 99 ракет. Это произошло потому, что код также работает и с другими значениями, не только с жизнями. И что делать? Мы не будем использовать этот код для чита бесконечных жизней.

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

Если Вы выполните код когда жизни были изменены, Вы увидите, что ESP был всегда равен 0012EB10 когда наш код запускался. Когда этот код:

xor esi,[esp+10]

используется для изменения жизней, ESP+10 = 0012EB20.

Сейчас нам нужно найти код, который будет копировать число жизней в 0012EB20.

Хотя это только временное пристанище, я думаю, Вы поняли, что мы могли бы просто написать скрипт для изменения 0012EB20 когда этот код выполняется, но мы не будем этого делать, потому что так будет неинтересно, по ламерски. Мы сделаем всё грамотно и найдём код, который кладёт значение в стек, чтобы увидеть, где оно хранится.

Нам нужно познакомиться с основами обратной трассировки. Вы могли заметить, что опция Break and trace способна проследитьзапускаемый код из некотрой точки. Но как осуществить прыжок «назад во времени» и увидеть, откуда код пришёл? Да, я вижу Вы думаете «мы просто прокрутим код вверх в дизассемблере», но не всё так просто. Вы должны узнать кое-что о функциях.

Программа состоит из множества функций. Каждая функция выполняет определённую задачу, затем возвращается в точку, откуда была вызвана. В нашем случае нужно найти функцию шифрования/расшифровки значений, которая вызывается из многих мест кода. После завершения работы функции, выполнение кода вернётся в место вызова функции и программа продолжится. Конечно некоторые задачи настолько большие, что может быть раздлена на меньшие подзадачи. Функция может иметь множество подфункций, эти подфункции могут также делиться на подфункции и т.д. Так как это работает?

Функции вызываются инструкцией "call". Когда call выполняется, программа перепрыгивает в место начала выполнения функции. Функция оканчивается инструкцией "ret". Когда выполняется ret, программа перепрыгивает назад в некоторое место после адреса, вывавшего функцию. Ret всегда возвращает программу к месту последнего call, всё это выглядит следующим образом:


некоторый код
call 2
некоторый код
ret (прыжок в место в коде, следующее за call 2)
некоторый код
ret (прыжок в место в коде, следующее за call 1)
некоторый код
call 1

Так почему всё это так важно? Давайте представим себе такой код.


некоторый код
помещаем число жизней в стек
call 2
некоторый код
call 3
некоторый код
call 4
некоторый код
call 5
некоторый код
xor esi,[esp+10] (наш код)
ret
некоторый код
ret
...
ret
...
ret
...
ret
...
call 1

Мы хотим найти код «помещаем число жизней в стек». Для этого нам нужно найти местонахождение «call 1», затем проследить оттуда и найти код. Мы начинаем с малеьнких подфункций, и нам нужно пойти «назад». Как нам это сделать? Пойти по точкам возврата. Не забудьте, если Вы знаете где находится точка возврата, Вы будете знать откуда функция была вызвана. И как дойдём до инструкции ret ПОСЛЕ нашего кода, мы можем отследить её и увидеть куда программа прыгает, так мы найдём не просто точку возврата, но точку входа в функцию.

Я думаю Вам всё ясно, так давайте начнём. Сначала добавьте адрес 0012EB20 в таблицу, по нему мы будем искать инструкцию, в которой 0012EB20 изменяется на число жизней. Теперь начните игру, правый клик на "xor esi,[esp+10]" и выберите опцию Break and trace. Теперь умрите один раз. Трассировка должна быть готова. Правый клик по окну трассировки, выберите "Expand all".

ЗАМЕЧАНИЕ: Адреса возврата запоминаются в стеке, но в CE6 имеется очень удобное дерево, показывающее точки возврата, и нам не придётся их искать. Именно поэтому новое окно трассировки одно из моих любимых опций CE6.

Посмотрите на нашу первую инструкцию, "xor esi,[esp+10]". Несколькими инструкциями ниже будет ret. Посмотрим на следующий адрес после ret:

00591215 - 8B 0D 9C7C7100 - mov ecx,[00717C9C] : [012560A8]

Картинка трассировки:

backtrace_lives1.jpg

Щёлкаем по этой инструкции, переходим в окно отладчика и прокручиваем код вверх:

00591210 - E8 1BC9EBFF - call 0044DB30

00591215 - 8B 0D 9C7C7100 - mov ecx,[00717C9C] : [012560A8]

Итак, наша функция была вызвана отсюда. Поставим бряк на этот call. Нажмите на него и потом F5. Потеряйте жизнь. Программа прервётся. Посмотрите на наш адрес в таблице, 0012EB20. Он показывает число наших жизней, значит копирование происходит перед этой функцией. Нужно вернуться ещё дальше. Нажимаем F5 снова, и F9 для запуска программы. Смотрим на нашу трассировку и ищем инструкцию ret, которая вернёт нас назад ещё на один уровень. Прокручивайте код, пока не увидите несколько подфункций, только некоторую часть кода, но нам они не нужны, мы хотим вернуться назад, чтобы найти место, где дерево возвращается с уровня.

Следующий найденный код будет таким:

00591CF7 - E8 D4F4FFFF - call 005911D0

00591CFC - C3 - ret

Другая картинка, показывающая ret данного шага:

backtrace_lives2.jpg

Снова ставим бряк на call и умираем в игре. Ок, игра прервалась. Смотрим на 0012EB20. Значение адреса явно отличается от числа оставшихся жизней. Это значит, что код, который помещает значение в этот адрес, пока не выполнен. Теперь изучим код более подробно и посмотрим что происходит со значением в 0012EB20. Нажимаем F5 для снятия бряка и нажимаем F7. Теперь Вы пошагово выполняете код и стоите на 005911D0, откуда начинается функция. Делаем ещё шаг, ещё и ещё. После каждого шага, смотрите на 0012EB20 в таблице, пока он не изменится.

0012EB20 изменится на число Ваших оставшихся жизней после выполнения этого кода:

0059120D - 50 - push eax

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

00591203 - 8B 44 24 24 - mov eax,[esp+24]

00591207 - 8D B5 E0010000 - lea esi,[ebp+000001E0]

0059120D - 50 - push eax

Из этих строк кода ясно, что eax копируется из [esp+24], из стека и затем перекладываемся в вершину стека. Ок, запоминаем эти коды и нажимаем F9, запуская прервавшуюся программу. Мы всё ещё не знаем где значение запоминается в памяти, однако у нас есть код, работающий с этим значением и мы можем изменить его здесь. В качестве финального теста установим бряк на этот код и возвращаемся в игру чтобы убедиться, что этот код запускается только когда Вы теряете или получаете жизнь. Итак, я могу сказать Вам, что эта функция не используется больше нигде, поэтому Вы можете использовать инъекцию кода для изменения EAX или [ESP+24] в этом месте.

ЗАМЕЧАНИЕ: Опытные пользователи могут рассмотреть установку бряка на данные с прерыванием на запись на изменение адреса 0012EB20.

На этом видео финальный этап статьи, обратная трассировка:

Мои поздравления, Вы сделали свой чит «Бесконечные жизни» для игры Chicken Invaders 4 и я надеюсь узнали что-то новое, что поможет Вам во взломе других игр. Конечно, есть и другие способы создания читов, но описанный способ, я надеюсь, охватывает много информации в одной статье, и всё на примере одной игры.

Мир!

Geri

-------

Перевод на русский: Aliast

  • Плюс 1
Ссылка на комментарий
Поделиться на другие сайты

  • 1 год спустя...

А вот и перевод от меня этой статейки. Статья и правда познавательная. Разобрался, будем применять в других играх)

Основы шифрования, отладки, обратной трассировки и сбора информации со стека

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

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

Я буду пользоваться Cheat Engine 6.0, последняя версия которого доступна по адресу: http://www.cheatengine.org

Игра которую мы будем исследовать называется Chicken Invaders 4, однако статья содержит общие сведения, которые можно применять и при исследовании других программ.

Итак, для начала найдите игру Chicken Invaders 4 v4.00. Именно такую версию программы я буду описывать в этой статье. Если у Вас эта игра уже установлена, поиграйте немного, ознакомьтесь с материалом. Игра представляет собой простенький космический шутер, в котором нужно отстреливать злобных куриц. Довольно глупая концепция, но сама по себе игра (программирование, графика и музыка) довольно неплохая. Теперь настройте игру на запуск в оконном режиме (не в полноэкранном).

Миссия 1: Поиск некоторого значения

В этой статье мы будем пытаться изменить количество жизней. Если Вы уже пытались найти их, наверняка обломались и ничего не нашли. А всё потому, что игра шифрует значение количества жизней, очков, еды, ракет и ключей. Чтобы найти значение, нужно выбрать тип Byte и искать по критерию изменилось/не изменилось после смерти.

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

0044DB20 - 8B 81 04010000 - mov eax,[ecx+00000104]

0044DB26 - 33 81 80000000 - xor eax,[ecx+00000080]

0044DB5D - 89 B3 80000000 - mov [ebx+00000080],esi

0044DB63 - 33 74 24 10 - xor esi,[esp+10]

0044DB67 - 5F - pop edi

0044DB68 - 89 B3 04010000 - mov [ebx+00000104],esi

ЗАМЕТКА: Таким же образом Вы можете найти адреса еды или очков. В любом случае Вы придёте к аналогичному коду.

Миссия 2: Что такое шифрование

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

Инструкция XOR соединяет два значения с помощью операции исключающее ИЛИ (не перепутайте с обычным ИЛИ, которое является включающим ИЛИ).

Чтобы разобраться, рассмотрим пример:

1001010110

0101001101

Если применить к ним операцию ИЛИ, результат будет 1100011011

Если два бита равны, в результате получится 0. При равенстве битов получится 1. Вы можете использовать Windows калькулятор для расчёта действия операции XOR.

За подробностями обратитесь к Google. Детальное изучение булевой алгебры выходит за рамки этой статьи.

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

0044DB20 - 8B 81 04010000 - mov eax,[ecx+00000104]

0044DB26 - 33 81 80000000 - xor eax,[ecx+00000080]

Одно значение хранится по адресу [ecx+0104], а другое по адресу [ecx+0080]. Используя отладчик можно выяснить адреса, обращающиеся к этим кодам (find out what addresses accessed by these codes). Можно найти ключевое значение жизней. Отлично, теперь установите в обоих адресах 0, в итоге жизней станет 0. Одна проблема – на экране жизней 0, а на самом деле их число осталось прежним. Получается, что Вы нашли отображаемое значение, но не нашли реального числа жизней.

Миссия 3: Исследование зашифрованного кода

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

0044DB26 - 33 81 80000000 - xor eax,[ecx+00000080]

Этот код запускается очень часто, СЧИТЫВАЯ сразу несколько значений. Очевидно, код обращается не только к количеству жизней, но ещё и к другим значениям.

0044DB5D - 89 B3 80000000 - mov [ebx+00000080],esi

0044DB63 - 33 74 24 10 - xor esi,[esp+10]

0044DB67 - 5F - pop edi

0044DB68 - 89 B3 04010000 - mov [ebx+00000104],esi

Этот код запускается только когда значение изменяется. Код ЗАПИСЫВАЕТ измённое значение. Давайте разбираться. Мы можем посмотреть содержимое регистра ESI, содержащего некоторую важну информацию, и да, он же используется в операции xor с адресом [esp+10]. Ради интереса, давайте посмотрим что хранится по адресу [ESP+10].

Начните новую игру, получите 5 жизней.

Теперь Вы можете проверить адрес двумя способами.

1. Правый клик на коде, затем "Break and trace".

2. Нажать F5 для ручной установки бряка.

Теперь вернитесь в игру и умрите. Никого не убивайте и ничего не подбирайте, просто умрите. Если Вы выбрали первый вариант действий, то сможете увидеть вот такую важную информацию:

ESP: 0012EB10

xor esi,[esp+10]

0012EB20 = (dword)00000004(4)

Если же Вы выбрали ручной бряк, игра остановится и Вы увидите значение ESP, 0012EB10. ESP+10 будет 0012EB20 и если Вы добавите этот адрес в таблицу, то увидите значение этого адреса, равное 4. Нажмите F5 снова и удалите бряк, затем нажмите F9 для разморозки игры. Как только Вы это сделаете, увидите, что значение адреса тут же изменится.

Замечательно, у нас было 5 жизней, мы погибли и ESP+10 показал нам 4. Если Вы умрёте снова, то увидите 3.

Very good, we had 5 lives, we have died and ESP+10 shows 4. If You die again, it will show 3. Значит мы нашли расшифрованное число наших жизней.

На этой картинке виден результат трассировки выполненного кода:

ci4_die.jpg

ЗАМЕЧАНИЕ: Более продвинутые пользователи знают, что мы могли использовать бряки по условию для работы со сложным кодом, но в данном случае проще было сделать именно так.

Миссия 4: Стек

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

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

Например, примем ecx=3, edx=2 и в стеке хранятся следующие значения.

4

5

6

Теперь мы положим в стек ecx, выполнив инструкцию "push ecx". Стек примет такой вид.

3

4

5

6

Теперь положим в стек edx, выполнив "push edx". Стек станет таким.

2

3

4

5

6

А теперь мы хотим вытащить значение из стека, например "pop ecx".

В этом случае программа вытолкнет первое значение из стека, т.е. 2, и сохранит его в ecx. Теперь ecx=2, а стек примет такой вид.

3

4

5

6

Если мы выполним теперь инструкцию "pop edx", edx станет равен 3, и стек станет таким

4

5

6

Результат: Стек остался каким и был вначале, но теперь ecx=2, а edx=3, мы поменяли их значения местами.

Хорошо, теперь Вы знаете как значение кладётся и выталкивается из стека. Это не случайный процесс, это работает в строгом порядке. Следующее что Вы должны узнать это указатель стека и как его использовать. В моём примере я буду использовать стек нереального размера в 16 байт.

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

0012FF00 4 //это адрес дна стека со значением 4

0012FEFC 2 //следующий уровень стека, адрес уменьшен на 4 байта

----------------------- //после этой линии неиспользуемые уровни стека

0012FEF8 x

0012FEF4 x

Указатель стека: указатель стека указывает на вершину стека, в нашем случае это линия ------------------. Указатель стека хранится в регистре ESP. В нашем примере 0012FEFC это вершина стека, она может быть вычислена проверкой значения в ESP, которое равно 0012FEFC.

Теперь давайте положим что-нибудь в стек.

Предположим, следующей инструкцией будет:

push 8

После неё наш мини-стек примет такой вид:

0012FF00 4 // это адрес дна стека со значением 4

0012FEFC 2 // уровень стека, адрес уменьшен на 4 байта

0012FEF8 8

-------------------------------------- //после этой линии неиспользуемые уровни стека

0012FEF4 x

Мы положили значение в стек и вершина стека снова уменьшилась на 4 байта. Теперь ESP = 0012FEF8, а его значение равно 8.

Теперь положим в стек другое значение, предположим, 5.

push 5

0012FF00 4 //это адрес дна стека со значением 4

0012FEFC 2 //уровень стека, адрес уменьшен на 4 байта

0012FEF8 8

0012FEF4 5

-------------------------------------- ////после этой линии неиспользуемые уровни стека

Как Вы теперь видите, вершина стека снова была уменьшена на 4 байта. ESP = 0012FEF4, а значение в 0012FEF4 = 5.

Что случится, если нам понадобится узнать значение в 0012FEFC, например? Это очень просто, я добавлю ещё одну колонку в наш пример, чтобы показать ESP.

0012FF00 4 ESP+0C //это адрес дна стека со значением 4

0012FEFC 2 ESP+08 // уровень стека, адрес уменьшен на 4 байта

0012FEF8 8 ESP+04

0012FEF4 5 ESP

-------------------------------------- //после этой линии неиспользуемые уровни стека

Как видите из примера, значение 0012FEFC равно [ESP+08].

И наконец предположим, что нам нужно получить значение вершины стека, после чего оно нам больше не понадобится и можно удалить значение из стека. Будем использовать такую инструкцию:

pop eax

Эта инструкция скопирует 5 в регистр EAX и вершина стека снова сдвинется.

0012FF00 4 ESP+08 // это адрес дна стека со значением 4

0012FEFC 2 ESP+04 //уровень стека, адрес уменьшен на 4 байта

0012FEF8 8 ESP

-------------------------------------- //после этой линии неиспользуемые уровни стека

0012FEF4 x

Я надеюсь, что этот пример понятно объяснил назначение указателя стека (ESP) и как в стеке хранится информация. Если не разобрались, попробуйте выполнить трассировку некоего кода и проследить как стек работает с этим кодом.

Последняя миссия: Обратная трассировка

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

xor esi,[esp+10]

Если Вы напишете скрипт для изменения [ESP+10] в 99, Вы увидите, что действительно получили 99 жизней. А также 99 очков, 99 еды и 99 ракет. Это произошло потому, что код также работает и с другими значениями, не только с жизнями. И что делать? Мы не будем использовать этот код для чита бесконечных жизней.

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

Если Вы выполните код когда жизни были изменены, Вы увидите, что ESP был всегда равен 0012EB10 когда наш код запускался. Когда этот код:

xor esi,[esp+10]

используется для изменения жизней, ESP+10 = 0012EB20.

Сейчас нам нужно найти код, который будет копировать число жизней в 0012EB20.

Хотя это только временное пристанище, я думаю, Вы поняли, что мы могли бы просто написать скрипт для изменения 0012EB20 когда этот код выполняется, но мы не будем этого делать, потому что так будет неинтересно, по ламерски. Мы сделаем всё грамотно и найдём код, который кладёт значение в стек, чтобы увидеть, где оно хранится.

Нам нужно познакомиться с основами обратной трассировки. Вы могли заметить, что опция Break and trace способна проследитьзапускаемый код из некотрой точки. Но как осуществить прыжок «назад во времени» и увидеть, откуда код пришёл? Да, я вижу Вы думаете «мы просто прокрутим код вверх в дизассемблере», но не всё так просто. Вы должны узнать кое-что о функциях.

Программа состоит из множества функций. Каждая функция выполняет определённую задачу, затем возвращается в точку, откуда была вызвана. В нашем случае нужно найти функцию шифрования/расшифровки значений, которая вызывается из многих мест кода. После завершения работы функции, выполнение кода вернётся в место вызова функции и программа продолжится. Конечно некоторые задачи настолько большие, что может быть раздлена на меньшие подзадачи. Функция может иметь множество подфункций, эти подфункции могут также делиться на подфункции и т.д. Так как это работает?

Функции вызываются инструкцией "call". Когда call выполняется, программа перепрыгивает в место начала выполнения функции. Функция оканчивается инструкцией "ret". Когда выполняется ret, программа перепрыгивает назад в некоторое место после адреса, вывавшего функцию. Ret всегда возвращает программу к месту последнего call, всё это выглядит следующим образом:


некоторый код
call 2
некоторый код
ret (прыжок в место в коде, следующее за call 2)
некоторый код
ret (прыжок в место в коде, следующее за call 1)
некоторый код
call 1

Так почему всё это так важно? Давайте представим себе такой код.


некоторый код
помещаем число жизней в стек
call 2
некоторый код
call 3
некоторый код
call 4
некоторый код
call 5
некоторый код
xor esi,[esp+10] (наш код)
ret
некоторый код
ret
...
ret
...
ret
...
ret
...
call 1

Мы хотим найти код «помещаем число жизней в стек». Для этого нам нужно найти местонахождение «call 1», затем проследить оттуда и найти код. Мы начинаем с малеьнких подфункций, и нам нужно пойти «назад». Как нам это сделать? Пойти по точкам возврата. Не забудьте, если Вы знаете где находится точка возврата, Вы будете знать откуда функция была вызвана. И как дойдём до инструкции ret ПОСЛЕ нашего кода, мы можем отследить её и увидеть куда программа прыгает, так мы найдём не просто точку возврата, но точку входа в функцию.

Я думаю Вам всё ясно, так давайте начнём. Сначала добавьте адрес 0012EB20 в таблицу, по нему мы будем искать инструкцию, в которой 0012EB20 изменяется на число жизней. Теперь начните игру, правый клик на "xor esi,[esp+10]" и выберите опцию Break and trace. Теперь умрите один раз. Трассировка должна быть готова. Правый клик по окну трассировки, выберите "Expand all".

ЗАМЕЧАНИЕ: Адреса возврата запоминаются в стеке, но в CE6 имеется очень удобное дерево, показывающее точки возврата, и нам не придётся их искать. Именно поэтому новое окно трассировки одно из моих любимых опций CE6.

Посмотрите на нашу первую инструкцию, "xor esi,[esp+10]". Несколькими инструкциями ниже будет ret. Посмотрим на следующий адрес после ret:

00591215 - 8B 0D 9C7C7100 - mov ecx,[00717C9C] : [012560A8]

Картинка трассировки:

backtrace_lives1.jpg

Щёлкаем по этой инструкции, переходим в окно отладчика и прокручиваем код вверх:

00591210 - E8 1BC9EBFF - call 0044DB30

00591215 - 8B 0D 9C7C7100 - mov ecx,[00717C9C] : [012560A8]

Итак, наша функция была вызвана отсюда. Поставим бряк на этот call. Нажмите на него и потом F5. Потеряйте жизнь. Программа прервётся. Посмотрите на наш адрес в таблице, 0012EB20. Он показывает число наших жизней, значит копирование происходит перед этой функцией. Нужно вернуться ещё дальше. Нажимаем F5 снова, и F9 для запуска программы. Смотрим на нашу трассировку и ищем инструкцию ret, которая вернёт нас назад ещё на один уровень. Прокручивайте код, пока не увидите несколько подфункций, только некоторую часть кода, но нам они не нужны, мы хотим вернуться назад, чтобы найти место, где дерево возвращается с уровня.

Следующий найденный код будет таким:

00591CF7 - E8 D4F4FFFF - call 005911D0

00591CFC - C3 - ret

Другая картинка, показывающая ret данного шага:

backtrace_lives2.jpg

Снова ставим бряк на call и умираем в игре. Ок, игра прервалась. Смотрим на 0012EB20. Значение адреса явно отличается от числа оставшихся жизней. Это значит, что код, который помещает значение в этот адрес, пока не выполнен. Теперь изучим код более подробно и посмотрим что происходит со значением в 0012EB20. Нажимаем F5 для снятия бряка и нажимаем F7. Теперь Вы пошагово выполняете код и стоите на 005911D0, откуда начинается функция. Делаем ещё шаг, ещё и ещё. После каждого шага, смотрите на 0012EB20 в таблице, пока он не изменится.

0012EB20 изменится на число Ваших оставшихся жизней после выполнения этого кода:

0059120D - 50 - push eax

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

00591203 - 8B 44 24 24 - mov eax,[esp+24]

00591207 - 8D B5 E0010000 - lea esi,[ebp+000001E0]

0059120D - 50 - push eax

Из этих строк кода ясно, что eax копируется из [esp+24], из стека и затем перекладываемся в вершину стека. Ок, запоминаем эти коды и нажимаем F9, запуская прервавшуюся программу. Мы всё ещё не знаем где значение запоминается в памяти, однако у нас есть код, работающий с этим значением и мы можем изменить его здесь. В качестве финального теста установим бряк на этот код и возвращаемся в игру чтобы убедиться, что этот код запускается только когда Вы теряете или получаете жизнь. Итак, я могу сказать Вам, что эта функция не используется больше нигде, поэтому Вы можете использовать инъекцию кода для изменения EAX или [ESP+24] в этом месте.

ЗАМЕЧАНИЕ: Опытные пользователи могут рассмотреть установку бряка на данные с прерыванием на запись на изменение адреса 0012EB20.

На этом видео финальный этап статьи, обратная трассировка:

Мои поздравления, Вы сделали свой чит «Бесконечные жизни» для игры Chicken Invaders 4 и я надеюсь узнали что-то новое, что поможет Вам во взломе других игр. Конечно, есть и другие способы создания читов, но описанный способ, я надеюсь, охватывает много информации в одной статье, и всё на примере одной игры.

Мир!

Geri

-------

Перевод на русский: Aliast

ОГРОМНОЕ СПАСИБО за урок :rolleyes: !очень полезная статья...я как рах ищю телепорт на z треекторию + - на игре танки онлайн.нашол с помошью вашего урока. )))спасибо :grin:

Ссылка на комментарий
Поделиться на другие сайты

×
×
  • Создать...

Важная информация

Находясь на нашем сайте, Вы автоматически соглашаетесь соблюдать наши Условия использования.