Русский
Русский
English
Статистика
Реклама

Dnspy

Модифицируем Last Epoch От dnSpy до Ghidra

09.08.2020 16:21:03 | Автор: admin

Last Epoch это однопользовательская ARPG на Unity и C#. В игре присутствует система крафта игрок находит модификаторы, которые затем применяет к экипировке. С каждым модификатором накапливается "нестабильность", которая увеличивает шанс поломки предмета


Я преследовал две цели:


  • Убрать "поломку" предмета в результате применения модификаторов
  • Не расходовать модификаторы при крафте

cut


Вот так выглядит окно крафта в игре
Окно крафта в Last Epoch


Часть первая, где мы редактируем .NET код без регистрации и смс


Для начала я опишу процесс модификации старой версии игры (0.7.8)


После компиляции C# превращается в IL (Intermediate Language) код. IL-код напоминает ассемблер высокого уровня абстракции и замечательно декомпилируется. В Unity проектах IL-код игры как правило находится в <GameFolder>/Managed/Assembly-CSharp.dll


Для редактирования IL-кода мы будем использовать dnSpy лучший из всех инструментов подобного рода для .NET, что я встречал. dnSpy делает работу со скомпилированными .NET приложениями настолько легкой, что ее можно спутать с разработкой в обычной IDE


Ищем логику крафта


Итак, запускаем dnSpy и открываем Assembly-CSharp.dll


dnSpy с открытым Assembly-CSharp.dll


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


После непродолжительных поисков мы нашли искомое класс CraftingSlotManager
dnSpy x CraftingSlotManager


А именно метод Forge() в данном классе
ndSpy x CraftingSlotManager.Forge()


Полная версия кода метода для желающих
// CraftingSlotManager// Token: 0x06002552 RID: 9554 RVA: 0x0015E958 File Offset: 0x0015CB58public void Forge(){    if (!this.forging)    {        this.forging = true;        base.StartCoroutine(this.ForgeBlocker(10));        bool flag = false;        int num = -1;        if (this.main.HasContent())        {            int num2 = 0;            int num3 = 0;            if (this.debugNoFracture)            {                num3 = -10;            }            float num4 = 1f;            int num5 = -1;            bool flag2 = false;            ItemData data = this.main.GetContent()[0].data;            ItemData itemData = null;            if (this.support.HasContent())            {                itemData = this.support.GetContent()[0].data;                num5 = (int)itemData.subType;                if (itemData.subType == 0)                {                    num3--;                    flag2 = true;                }                else if (itemData.subType == 1)                {                    num4 = UnityEngine.Random.Range(0.4f, 1f);                    flag2 = true;                }            }            if (this.appliedAffixID >= 0)            {                Debug.Log("applied ID: " + this.appliedAffixID.ToString());                if (this.forgeButtonText.text == "Forge")                {                    if (data.AddAffixTier(this.appliedAffixID, Mathf.RoundToInt((float)(5 + num2) * num4), num3))                    {                        num = this.appliedAffixID;                        flag = true;                    }                    GlobalDataTracker.instance.CheckForShard(this.appliedAffixID);                    if (flag2)                    {                        this.support.Clear();                    }                    if (!GlobalDataTracker.instance.CheckForShard(this.appliedAffixID))                    {                        this.DeselectAffixID();                    }                }            }            else if (this.modifier.HasContent())            {                Debug.Log("modifier lets go");                ItemData data2 = this.modifier.GetContent()[0].data;                if (data2.itemType == 102)                {                    if (data2.subType == 0)                    {                        Debug.Log("shatter it");                        Notifications.CraftingOutcome(data.Shatter());                        if (num5 == 0)                        {                            flag2 = false;                        }                        this.main.Clear();                        flag = true;                        this.ResetAffixList();                    }                    else if (data2.subType == 1)                    {                        Debug.Log("refine it");                        if (data.AddInstability(Mathf.RoundToInt((float)(2 + num2) * num4), num3, 0))                        {                            data.ReRollAffixRolls();                        }                        flag = true;                    }                    else if (data2.subType == 2 && data.affixes.Count > 0)                    {                        Debug.Log("remove it");                        if (data.AddInstability(Mathf.RoundToInt((float)(2 + num2) * num4), num3, 0))                        {                            ItemAffix affixToRemove = data.affixes[UnityEngine.Random.Range(0, data.affixes.Count)];                            data.RemoveAffix(affixToRemove);                        }                        flag = true;                    }                    else if (data2.subType == 3 && data.affixes.Count > 0)                    {                        Debug.Log("cleanse it");                        List<ItemAffix> list = new List<ItemAffix>();                        foreach (ItemAffix item in data.affixes)                        {                            list.Add(item);                        }                        foreach (ItemAffix affixToRemove2 in list)                        {                            data.RemoveAffix(affixToRemove2);                        }                        if (num5 == 0)                        {                            flag2 = false;                        }                        data.SetInstability((int)((Mathf.Clamp(UnityEngine.Random.Range(5f, 15f), 0f, (float)data.instability) + (float)num2) * num4));                        flag = true;                    }                    else if (data2.subType == 4 && data.sockets == 0)                    {                        Debug.Log("socket it");                        data.AddSocket(1);                        data.SetInstability((int)((Mathf.Clamp(UnityEngine.Random.Range(5f, 15f), 0f, (float)data.instability) + (float)num2) * num4));                        flag = true;                    }                }            }            if (flag)            {                UISounds.playSound(UISounds.UISoundLabel.CraftingSuccess);                if (this.modifier.HasContent())                {                    ItemData data3 = this.modifier.GetContent()[0].data;                    this.modifier.Clear();                    if (num >= 0 && GlobalDataTracker.instance.CheckForShard(num))                    {                        this.PopShardToModifierSlot(num);                    }                    else if (data3.itemType == 102)                    {                        foreach (SingleSubTypeContainer singleSubTypeContainer in ItemContainersManager.instance.materials.Containers)                        {                            if (singleSubTypeContainer.CanAddItemType((int)data3.itemType) && singleSubTypeContainer.allowedSubID == (int)data3.subType && singleSubTypeContainer.HasContent())                            {                                singleSubTypeContainer.MoveItemTo(singleSubTypeContainer.GetContent()[0], 1, this.modifier, new IntVector2?(IntVector2.Zero), Context.SILENT);                                break;                            }                        }                    }                    if (num >= 0 && this.prefixTierVFXObjects.Length != 0 && this.suffixTierVFXObjects.Length != 0)                    {                        ItemData itemData2 = null;                        if (this.main.HasContent())                        {                            itemData2 = this.main.GetContent()[0].data;                        }                        if (itemData2 != null && this.main.HasContent())                        {                            List<ItemAffix> list2 = new List<ItemAffix>();                            List<ItemAffix> list3 = new List<ItemAffix>();                            foreach (ItemAffix itemAffix in itemData2.affixes)                            {                                if (itemAffix.affixType == AffixList.AffixType.PREFIX)                                {                                    list2.Add(itemAffix);                                }                                else                                {                                    list3.Add(itemAffix);                                }                            }                            for (int i = 0; i < list2.Count; i++)                            {                                if ((int)list2[i].affixId == num && this.prefixTierVFXObjects[i])                                {                                    this.prefixTierVFXObjects[i].SetActive(true);                                }                            }                            for (int j = 0; j < list3.Count; j++)                            {                                if ((int)list3[j].affixId == num && this.suffixTierVFXObjects[j])                                {                                    this.suffixTierVFXObjects[j].SetActive(true);                                }                            }                        }                    }                }                if (!flag2)                {                    goto IL_6B3;                }                this.support.Clear();                using (List<SingleSubTypeContainer>.Enumerator enumerator2 = ItemContainersManager.instance.materials.Containers.GetEnumerator())                {                    while (enumerator2.MoveNext())                    {                        SingleSubTypeContainer singleSubTypeContainer2 = enumerator2.Current;                        if (singleSubTypeContainer2.CanAddItemType((int)itemData.itemType) && singleSubTypeContainer2.allowedSubID == (int)itemData.subType && singleSubTypeContainer2.HasContent())                        {                            singleSubTypeContainer2.MoveItemTo(singleSubTypeContainer2.GetContent()[0], 1, this.support, new IntVector2?(IntVector2.Zero), Context.SILENT);                            break;                        }                    }                    goto IL_6B3;                }            }            this.modifier.Clear();            this.support.Clear();        }        IL_6B3:        if (!flag)        {            UISounds.playSound(UISounds.UISoundLabel.CraftingFailure);        }        this.slamVFX.SetActive(true);        this.UpdateItemInfo();        this.UpdateFractureChanceDisplay();        this.UpdateForgeButton();        ShardCountText.UpdateAll();    }}

С моей точки зрения данный код выглядит не так уж и плохо. Даже и не скажешь сразу, что это декомпелированная версия (выдают только названия переменных в духе num1, num2...). Есть даже логирование, которое позволяет легче понять назначение веток


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


Отключаем расходование ресурсов при крафте


Нас интересуют две переменные: this.modifier и this.support
Это слоты для модификаторов, которые используются во время крафта


Как оказалось уничтожение модификаторов происходит в процессе следующих вызовов:


this.modifier.Clear();this.support.Clear();

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


Удаляем все вызовы this.modifier.Clear(); и this.support.Clear(); из кода функции и радуемся


Процесс редактирования в dnSpy просто фантастика просто поправили код и сохранили в .dll все изменения будут скомпилированы автоматически
dnSpy edit method


Убираем поломку предмета в процессе крафта


В игре поломка предмета в процессе крафта называется Fracture, поэтому мне сразу бросился в глаза данный кусок кода
dnSpy x CraftingSlotManager.Forge()


И действительно модификация вида int num3 = -10; полностью отключает поломку спасибо разработчикам за оставленный дебаг флаг :)


Часть вторая, где мы испытываем боль и страдания


Начиная с версии 0.7.9 разработчики начали использовать IL2CPP для того, чтобы собирать проект напрямую в нативный бинарник под нужную платформу. В результате у нас больше нет IL-кода, а есть лишь хардкорный ассемблер но кого это останавливало?


Дисклеймер

Автор статьи не является реверс-инженером, а имеет за спиной только остатки университетского курса Ассемблера и один No-CD, сделанный в OllyDbg 10 лет назад. Вполне возможно, что какие-то вещи были сделаны неправильно или их можно было сделать намного лучше и проще


Ищем иголку в стоге Гидры


Итак, из папки игры пропали все старые .dll-ки и мы взамен получили один огромный GameAssembly.dll весом в 55 мегабайт. Наши цели не изменились, но теперь все будет намного сложнее.


Первым делом загружаем dll-ку в Ghidra'у и соглашаемся на все виды анализа, которые она предлагает (анализ занимает довольно много времени и в дальнейшем я останавливал его на стадии Analyze Address Table)


Ghidra


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


К счастью погуглив на тему IL2CPP я нашел утилиту Il2CppDumper, которая позволяет частично восстановить информацию на основе метадата-файла (который обнаружился по пути <GameFolder>/il2cpp_data/Metadata/global-metadata.dat). Не знаю является ли данный файл необходимостью или разработчики просто забыли убрать его, но он сильно облегчил нашу задачу


Скармливаем утилите наши файлы dll и метадаты и получаем набор восстановленных данных
Il2CppDumper output


В папке DummyDll находятся восстановленные dll-ки с частично восстановленным IL-кодом. Загружаем восстановленный Assembly-CSharp.dll в dnSpy и идем в наш любимый CraftingSlotManager


dnSpy (restored) x CraftingSlotManager


Ну что же, кода у нас больше нет, зато у нас есть адрес! В аннотации


Address(RVA = "0x5B9FC0", Offset = "0x5B89C0", VA = "0x1805B9FC0")

Нам нужно значение VA это оффест, по которому мы найдем нашу функцию в Гидре


Ghidra forge offset


Теперь мы хотя бы нашли начало нашей функции, что уже неплохо


Можно ли сделать лучше? Вспоминаем, что Il2CppDumper генерирует данные, котрые можно импортировать в Гидру копируем скрипт в папку скриптов Гидры, запускаем ghidra.py и скармливаем ему script.json, сгенерированный из нашей метадаты. Теперь у всех функций, которые были объявлены в исходном коде, появились имена


Отключаем расходование ресурсов при крафте


Мы уже знаем, что нам достаточно убрать вызовы this.modifier.Clear(); и this.support.Clear();. Осталось найти их в коде. К счастью восстановленные имена функций помогают решить эту задачу довольно просто


Ghidra


Ломать не строить. Чтобы убрать вызов функции нам достаточно заменить все байты, участвующие в CALL на NOP


Разбиваем команду на отдельные байты (выделив ее и нажав C, или Clear Code Bytes), затем в бинарном представлении просто впечатываем 90 пять раз. Готово!
Ghidra


Такую операцию повторяем для всех вызвов OneSlotItemContainer$$Clear() из нашей функции Forge() (На самом деле это нужно делать не для всех вызовов, потому что в коде есть один вызов this.main.Clear(); Но мне было слишком лениво выискивать конкретное исключение в ассемблерной каше, поэтому я убрал все вызовы)


Убираем поломку предмета в процессе крафта


Изначально мы делали int num3 = -10; и благодарили разработчика за оставленный дебаг флаг в качестве подсказки. Теперь это не кажется такой простой задачей сложно понять, какая из ~60 локальных переменных, найденных Гидрой, является нужной. После 15 минут поиска мне наконец удалось понять, что зубодробительная строчка на скриншоте ниже является той самой проверкой дебаг флага и вычитанием из переменной


Ghidra


К сожалению моих знаний Ассемблера не хватило, чтобы понять как именно это работает (судя по Гидре этот процесс занимает 4 команды начиная с MOVZX и заканчивая на AND), поэтому деликатно изменить эту часть я не смог. Другого способа изменить эту переменную я тоже не нашел в силу своих ограниченных знаний, поэтому я изменил подход


Посмотрев еще раз в замечательный (после работы с Гидрой) код старой версии игры в dnSpy я увидел, что за накопление "нестабильности" отвечает метод AddInstability


public bool AddInstability(int addedInstability, int fractureTierModifier = 0, int affixTier = 0)    {        int num = this.RollFractureTier(fractureTierModifier, affixTier);        if (num > 0)        {            this.Fracture(num); // <----- Предмет ломается тут            return false;        }        this.instability = ((int)this.instability + addedInstability).clampToByte();        this.RebuildID();        return true;    }

Гидра радует нас относитлеьно простым кодом данной функции


Ghidra


По коду мы видим, что сначала происходит вызов CALL ItemData$$RollFractureTier, затем мы проверяем результат TEST EAX и прыгаем в нужную ветку


Ghidra


Нам нужно, чтобы мы всегда шли по ветке uVar3 < 1. Тут можно сделать разные исправления например (могу ошибаться) поменять JG(Jump short if greater) на JLE(Jump short if less or equal)


Я решил вопрос иначе просто сделаем проверяемый регистр равным нулю и тогда остальной код будет работать как надо. Меняем CALL на XOR EAX, EAX (самый просто способ обнулить регистр в Ассемблере), который занимает два байта и оставшиеся три байта заполняем NOP'ами


Ghidra


Готово! Сохраняем, заменяем существующий GameAssembly.dll (почему-то Гидра в процессе экспорта файла всегда добавляет расширение .bin и его нужно удалять) и чувствуем себя хакерами


Выводы


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


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


И, хотя нативные бинарники и могут представлять опасность для ваших глаз и мозга, повехрностных знаний о том, как работают программы на уровне, близком к железу, зачастую бывает достаточно в связке с современными open-source инструментами для небольших модификаций

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru