Как получить limbo

Обновлено: 14.05.2024

Большинство способностей Лимбо связаны с использованием его способностей Ущелья. Вот что игроки должны помнить о том, как максимально использовать эти уникальные навыки.

Пассивная способность

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

Изгнание

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

Стазис

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

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

Rift Surge

Cataclysm

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

Strengths amp; Слабые стороны

Как построить лимбо

Благодаря способности Лимбо управлять толпой лучше всего создать сборку, которая увеличивает диапазон и продолжительность его способности . Это поможет увеличить диапазон способностей Лимбо и его способности управлять толпами врагов.


Всем привет. Многие знают об этой замечательной игре — LIMBO! Вы даже наверняка покупали ее в Стиме, или качали с торрентов…
Я тоже ее купил когда-то (что и вам советую!), и прошел). Но, как всегда, мне было этого мало, и я, из спортивного интереса, решил изучить ее защиту. Так и появился кейген к игре LIMBO.

В этой статье я расскажу и покажу вам, как я это делал.

Прежде, чем начинать, помните: все действия вы выполняете на свой страх и риск. Уважайте работу геймдевелоперов.
Этап первый: Поверхностный осмотр пациента

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


Видим: Visual Studio 2008. IDA Pro прекрасно с ней справляется, поэтому туда и отправим. Я буду пользоваться связкой IDA Pro + HexRays, т.е. с декомпилятором — для ускорения работы.

Этап второй: что мы ищем?

Первым делом, дадим Иде проанализировать limbo.exe — главный исполняемый файл игры.

Далее, нужно определить, что именно мы, собственно, хотим найти здесь. Запустим игру:


Видим волшебную надпись "UNLOCK FULL GAME". На нее и нажмем. Далее нас ожидает нежданчик (по крайней мере, я, когда первый раз выбирал этот пункт меню, я ожидал увидеть поле ввода на графическом движке игры, или типа того, а оказалось все гораздо проще. ):


Да, да! Именно обычное окошко! Нам же легче. Попробуем что-нибудь ввести, и нажать Unlock. Как-то так:


Этап третий: Нажми меня

Мы поступим следующим образом. Откроем любой удобный Вам редактор ресурсов, затащим в него ехе-шник, найдем окошко диалога ввода ключа, а в нем — кнопку Unlock. Сказано — сделано:


На скрине я выделил ID нашей кнопки. По нему мы и будем искать, где именно обрабатывается нажатие. Откроем Иду, нажмем Alt+I (Search -> immediate value. ), введем число 203 (без 0x, т.к. десятичное), и посмотрим, что найдется. А нашлось вот это:


Видите те строчки, которые Ида пометила как ; nIDDlgItem? С них и начнем. Двойным кликом переходим на первый из таких результатов:


Зеленой стрелкой я обозначил место, на которое указала Ида, а чуть ниже (привычка: прокручивать выше/ниже искомого места) — стрелкой обозначено место вызова одной интересной API-функции: GetDlgItemTextA. Судя по названию по MSDN, эта функция получает текст указанного элемента окна в буфер.

Почему я сразу не искал по ID поля ввода? Можно, конечно и так было сделать. Но, мало ли какие действия происходят после нажатия кнопки, еще до вычитывания текста из поля.

Итак, проследим, куда уходит полученный серийник. Прокручиваем листинг, чтобы видеть место вызова API-функции целиком:


Мой "намыленный" взгляд подсказывает мне, что полученный буфер (Ида обозначила его как var_134) передается прямиком в следующую за вызовом GetDlgItemTextA функцию, которая возвращает в al нулевое, либо ненулевое значение (похоже на результат проверки ключа). Давайте проверим догадку…

Этап четыре: Декомпиляция

Заходим в функцию. Видим там прыжок на еще один адрес — переходим по нему. Видим нормальный код, поэтому смело жмем там F5 (вызываем HexRays Decompiler).

Теперь можно попытаться привести этот код к более адекватному.

Первым делом, замечаем, что входной параметр имеет тип int, что не совсем правда. Обозначим его как "char *". Для этого становимся на имя функции и жмем там клавишу Y (Set item type). Исправляем тип и имя входного параметра (я обозвал его как key).

Далее… Видим строчку:


Т.к. наш входной параметр — строка, давайте в тех местах, где символы ключа сверяются с числами, исправим на сравнение с символами. Для этого на каждом из таких чисел нажмем R (Char). Уже лучше:

Для наглядности дадим v3 имя i, т.к. похоже, что она используется как итератор. Переименовываем нажатием на имени клавиши N (Name).

Замечаем, что в цикле происходит взятие каждого символа из ключа, и передача его в пока неизвестную нам функцию. Предлагаю выяснить, что это за функция. Двойным щелчком переходим в нее. Видим там вызов еще одной функции, переходим туда. И, вот оно — обработка одиночного символа! (Здесь есть куча работы для клавиши R, но я лишь покажу сразу результат обработки).

Прекрасно! Теперь возвращаемся назад клавишей Esc до основной функции. Замечаем, что IDA сама переопределила для нас тип результата возвращаемого функцией обработки символа. Именуем дальше, обозначаем типы, и получаем следующий код цикла:

Если вы заметили, то тут есть одна интересная бага декомпилера. Видим, что переменная, обозначенная у меня как itr, совершенно не инкрементируется. Чтобы выяснить, что на самом деле происходит, жмем ПКМ -> Copy to assembly, и смотрим, где же используется наша itr. Выясняем: она инкрементируется прямо в этом цикле (чего и стоило ожидать), а до цикла — обнуляется. Учтем это при написании кейгена.

Теперь вторая часть функции проверки ключа… У нас осталась одна неисследованная функция, которая, кстати, очень похожа на функцию подсчета CRC32. Результат обработки (пусть и на скорую руку, но читаемый):

Оставшийся кусок (преобразованный):

Этап пять: написание кейгена

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

  1. Игре нужен ключ в 32 символа без дефисов (37 — с дефисами).
  2. Берется по четыре символа из ключа (не учитываются дефисы). Каждый из них пропускается через функцию convert_char и суммируется по формуле: sum += new_c << 5 * (3 — itr);
  3. Каждая такая сумма преобразовывается в lower-case хекс-строку (5 символов) и доклеивается до имеющейся (итого 40 символов);
  4. Берется CRC32 от первых 32-х символов получившейся строки и сравнивается с оставшимися восемью символами полученной в предыдущем пункте строки;
  5. Если строки не совпали — наш ключ неправильный.
  1. Т.к. входной хэш был получен из восьми5-символьных хэш-кусков, будем обрабатывать его так же, по "пятеркам";
  2. Каждая "пятерка" была получена из четырех символов ключа;
  3. Т.к. при каждом вычислении "пятерки", она сдвигалась на 5 бит влево, получается, что на каждый символ ключа приходится 5 бит;
  4. Внимательное рассмотрение кода функции convert_char приводит нас к такой мысли, что набор символов ключа ограничивается лишь символами набора "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
  5. Итого: 32 символа хэша генерятся из "пятерок". 32 % 5 = 24 целых символа и 2 в остатке — т.е. два символа нам придется просто догенерить.
Итоговый вариант функции, генерирующей хэш (Delphi)

Далее считаем CRC32 от хэша:

Ну и, наконец, результирующее получение лицензионного ключа:


Проверяем, и… Ввод сгенеренного ключа активировал игру, пункт активации исчез!

Итоги

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

P.S.

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

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

По большому счёту мне давным давно пофиг, на каком языке писать (я программирую с 1989 года, и за это время перепробовал кучу языков). Но… всё таки на одних языках работать приятнее, чем на других — и здесь дело не в том, что одни языки лучше других, а в том, что для разных стилей мышления лучше подходят разные языки.

Переход от Perl к Limbo — очень контрастный. Языки совершенно разные: Perl — не типизированный вообще, Limbo — сильно типизированный; в Perl нет нормальной поддержки нитей и асинхронности приходится добиваться через мультиплексирование, Limbo — чуть ли не вынуждает писать именно многопоточные программы (если вы смотрели презентацию Роба Пайка, то там был прикольный пример с многопоточным поиском простых чисел); etc. И, тем не менее, Limbo мне очень понравился и писать работающий код я на нём начал практически сразу.

Я уже не очень хорошо помню C, но попробую описать Limbo именно в плане отличий от C — думаю, так будет проще для большей части аудитории (и ни слова про PHP! :)).

Общая информация

Про такие особенности Limbo как схожесть синтаксиса с C, высокая портабельность байт-кода, заточенность под параллельное программирование, динамическая подгрузка/выгрузка модулей, проверку типов и границ массивов в том числе и в процессе выполнения и наличие сборщика мусора я уже упоминал.

Ещё можно добавить, что для Limbo написано значительное кол-во разнообразных библиотек (идут в комплекте с Inferno), облегчающих работу с графикой, математикой, базами данных, etc.

  • : — объявление
  • = — присваивание
  • := — объявление с одновременным присваиванием, тип определяется по типу присваиваемого объекта

Типы данных

Помимо обычных числовых типов, структур и union, Limbo поддерживает строки и несколько более специфических типов данных: списки, массивы, tuples и каналы. (Ещё есть специальный тип «модуль», я его упоминал ранее когда описывал интерфейсы, но с точки зрения особенностей языка он интереса не представляет.) Все эти типы данных это first-class variables, т.е. их можно сохранять в переменных, передавать через каналы, etc.

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

Строки

string можно преобразовывать в массивы байт, и наоборот.

Кроме этого строки поддерживают срезы, т.е. можно обратиться к конкретному символу или последовательности символов, например: my_string[5:15] .

Списки

list это последовательность элементов одного типа оптимизированная для стеко-подобных операций (добавить элемент в начало списка, получить первый элемент списка, получить остаток списка (кроме первого элемента)).

  • :: — создание нового списка, левый операнд это один элемент, правый это список элементов того же типа
  • hd — возвращает первый элемент списка не меняя сам список
  • tl — возвращает список состоящий из второго и последующих элементов заданного списка — т.е. «выкусывает» первый элемент

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

Массивы

array содержит фиксированное кол-во элементов одного типа.

Размер массива указывается при его создании/инициализации, а не при объявлении типа переменной — т.е. массивы можно динамически создавать в любой момент (когда стал известен требуемый размер массива).

Фактически в Limbo только два способа динамически выделить память: создать array указав требуемый размер через переменную, и добавить новый элемент в начало list.

Естественно, массивы тоже поддерживают срезы.

Tuples (кортежи)

tuple это что-то вроде списка из 2-х и более элементов любых типов. И это не просто список, а такой же тип данных, как и другие — тип самого tuple фактически определяется по тому, каких типов элементы и в каком порядке он содержит. Пример:

Причём tuple можно «разбирать» на составляющие присваивая его в список обычных переменных:

Кстати, обмен значений двух переменных на Limbo делается примерно так:

Каналы

Каналы ( chan ) позволяют организовывать IPC между локальными процессами передавая атомарно объекты заданного типа.

Чтение/запись канала это блокирующая операция. Операторы чтения/записи выглядят как стрелки:

Каналы бывают буферизированные (размер буфера вы указываете примерно так же, как размер массива). Запись в буферизованные каналы не блокируется пока не будет заполнен буфер. Буфер работает как FIFO очередь.

Для мультиплексирования каналов в Limbo есть целых два средства — можно читать из массива каналов, а можно использовать специальный оператор alt для выбора канала.

Собственно каналы это единственный способ IPC в Limbo, они используются и для передачи данных, и для синхронизации потоков, в общем полная замена всяким mutexes, semaphores, shared memory, etc…

Что касается их производительности… при передаче чего-то через канал передаётся просто его адрес в памяти, т.е. никакого копирования на самом деле не происходит и всё просто летает.

Составные типы

это массив хранящий каналы, по которым передаются tuple состоящие из int и списка строк. Размер массива здесь не определяется, он будет задан в процессе выполнения, при инициализации массива.

Unicode

Limbo использует UTF8 для I/O, и UTF16 для представления строк в памяти.

Т.е., например при считывании исходника модуля с диска в нём может использоваться UTF8 в комментариях, строках и символьных константах.

Если есть массив байт ( array of byte ) и он конвертируется в строку, то байты из массива обрабатываются как UTF8 и конвертируются в строке в UTF16; а при преобразовании строки в массив байт происходит обратное преобразование и в массиве оказывается UTF8.

Функции

Функциям можно передавать параметрами ссылки на функции.

Объекты симулируются через тип данных структура ( adt ), элементами которых помимо обычных типов данных могут быть функции. На самом деле это, безусловно, очень кастрированное ООП — наследования нет, ничего нет, населена роботами. (с) :) Впрочем, вру. Полиморфизм — есть. Но немного странный, больше напоминает templates в C++: смотрите сами.

Для запуска заданной функции в отдельной нити в Limbo используется встроенный оператор spawn.

Ошибки и исключения

Поддержка исключений есть, как обычных строковых, так и пользовательских типов. К моему сожалению большинство системных и библиотечных функций вместо исключений для возврата ошибок используют tuple: (errcode, result) . Безусловно, tuple это большой шаг вперед относительно POSIX-овского возврата информации об ошибке в виде результата -1, но… хотелось бы чтобы вместо этого использовались исключения.

Читайте также: