- Статус
- Оффлайн
- Регистрация
- 22 Авг 2022
- Сообщения
- 1,177
- Реакции
- 158
Оффтоп
2026 пиздато начинается. мне нравится. Гайд будет мелкий, я заебался всё это писать. Вторая часть хз когда, возможно через неделю.
Предисловие
Всем привет. Недавно мне улыбнулась удача написать чит на игру Rust. Игра собрана с помощью il2cpp и имеет рандомизацию имён в метадате il2cpp и шифрование некоторых вещей.
Как было раньше
Ранние версии игры (девблоги) не имели шифрования имён и читы там писать намного легче. Я сомневаюсь, что с того времени много что поменялось, поэтому давайте посмотрим как всё было устроено раньше.
Существует класс BaseNetworkable со статичным полем clientEntities:
Внутри класса BaseNetworkable.EntityRealm содержится список из сущностей:
И из него уже можно вытащить все networkable объекты (сущности)
Информация из дампа il2cpp
Для начала я сдамплю il2cpp метадату используя инструмент
В последней версии игры нету никакого статичного поля, которое содержало бы clientEntities
Однако BaseNetworkable.EntityRealm всё ещё существует в дампе:
И статически он находится в новом классе
С ходу бросаются пару мыслей:
- Почему некоторые типы, которые мы видели раньше (BaseNetworkable.EntityRealm, clientEntities) обёрнуты в какие-то классы?
- Какое конкретно поле с BaseNetworkable.EntityRealm выбрать из класса BaseNetworkable.%ca5d42befe66446a3fbc6568b3756c3773e80757?
Поиск и дешифрование BaseNetworkable экземпляра
Для того, чтобы найти чтение поля из BaseNetworkable.%ca5d42befe66446a3fbc6568b3756c3773e80757 (тот самый, что имеет несколько BaseNetworkable.EntityRealm) нужно посмотреть референсы к его TypeInfo
Для тех, кто не в курсе
В il2cpp для обращения к статическому полю необходимо найти указатель на TypeInfo класса, прочитать static_fields и от него обратиться к нужному статическому полю. Все необходимые структуры дампер создаёт в файле il2cpp.h:
Продолжаем. Адрес с typeinfo лежит в файле script.json:
Не забываем перевести число в шестнадцатеричную систему счисления.
Снова для тех, кто не в курсе
Дампер имеет встроенные скрипты, для определения названий функций и типов для IDA (и не только). В замечательной папке с дампом лежит файл ida_with_struct_py3.py:
Загружаем его из контекстного меню:
Ожидаем примерно минут 10, пока не увидим следующее окно:
Просто нажимаем "Ок" и получаем красивое отображение в IDA.
Подготавливаем иду к работе
Для начала меняем image base на 0, чтобы переходить сразу по RVA. Сделать это можно в контекстком меню иды -> Edit -> Segments -> Rebase program -> вводим 0 -> Ок. Затем открываем вкладку экспортов и вводим il2cpp_gchandle_get_target. После перехода вас встретит джамп на некий sub:
Переименновываем саб в j_il2cpp_gchandle_get_target
Теперь я буду иногда показывать по несколько скриншотов: со скриптом и без.
Переходим на адрес с typeinfo и видим следующее:
Берём любой попавшийся xref и декомпилируем:
В 100% случаев вы увидите похожую картину:
(со скриптом)
(без)
Выделил 0xB8, я показывал его ранее - это оффсет на static_fields
Открываем эту функцию
(со скриптом)
(без)
Сразу понимаем, что поле, которое нам надо использовать лежит по оффсету 0x38
Нас интересует данный промежуток - это расшифровка:
Ида может делать ошибки в определии типов, поэтому она добавляет свои уебанские макросы: HIDWORD, LODWORD и тд.
Поэтому выполняем следующие действия при попадании в функцию с дешифровкой:
1. Убираем типы с указателем в аргументах. В моём случае второй аргумент это указатель на __int64. Кликаем на него и жмём клавишу 'Y', чтобы убрать указатель (для тупых это звёздочка).
2. Если увидите
В итоге у вас должно получиться:
В свой код переносим локальные переменные (голубой цвет) и необходимую область (красный цвет). В результат функции помещаем результат со скрина (фиолетовый цвет).
Мой код на данном этапе выглядит так:
Здесь тоже необходимо применить парочку изменений:
1.Заменяем тип _BYTE на unsigned char, _WORD на unsigned short, _DWORD на unsigned long, _QWORD на unsigned long long
2.Используем свою функцию чтения памяти (вероятно у вас свой 1337 гипервизор или драйвер) в тех местах, где идёт разыменнование чего либо с аргументом. Говоря по дебильному: везде где есть выражение
Результат:
Называем функцию в стиле
Не обращайте внимание на красный il2cpp_gchandle_get_target - мы к нему ещё вернёмся.
По такому же принципу расшифровываем entity_list. Нам осталось понять как из %fd9c39723c3f962c0308a43e4106cb774701a27c (в прошлом ListDictionary) получить список сущностей и массив из них. ListDictionary это generic класс - то есть обобщённый класс с разными типами в <>. Поэтому у полей все оффсеты равны 0, а все методы имеют rva -1
Под методами дампер собрал нам GenericInstMethod - реализации функций для конкретных типов. Например: функция с rva 0x5930B70 применяется для типов с <object, object>, <object, float>, <uint, object>, а функция с rva 0x7F4FA10 применяется только для типа <__Il2CppFullySharedGenericType, __Il2CppFullySharedGenericType>
Напомню, у нас с вами тип <%1c24d316ef52282e54d325685a623c64a0124e28, BaseNetworkable>:
В классе ListDictionary мне хотелось бы посмотреть реализацию функции int %c11b6f5f8ca36f1b5661153e32660fc2f41ea536(). По её пустому количеству аргументов и возвращаемому типу мне кажется, что это функция count. Конкретного типа реализации для
Переходим по нужному RVA и наблюдаем следующее:
(со скриптом)
(без)
Со скриптом мы прекрасно понимаем, что эта функция действительно является количеством элементов в списке. Т.к. fields это просто вложенная структура, то мы складываем оффсеты.
fields оффсет - 0x10
_size оффсет - 0x8 (0x10 + 0x8 = 0x18 итог)
_items (массив) оффсет - 0x0 (0x10 + 0x0 = 0x10 итог)
Долгожданный il2cpp_gchandle_get_target
На этом долго останавливаться не буду, эта функция не менялась со времён эры динозавров за исключием одного оффсета:
Данный оффсет берём с иды:
На этом всё. Жду мнения
2026 пиздато начинается. мне нравится. Гайд будет мелкий, я заебался всё это писать. Вторая часть хз когда, возможно через неделю.
Предисловие
Всем привет. Недавно мне улыбнулась удача написать чит на игру Rust. Игра собрана с помощью il2cpp и имеет рандомизацию имён в метадате il2cpp и шифрование некоторых вещей.
Как было раньше
Ранние версии игры (девблоги) не имели шифрования имён и читы там писать намного легче. Я сомневаюсь, что с того времени много что поменялось, поэтому давайте посмотрим как всё было устроено раньше.
Существует класс BaseNetworkable со статичным полем clientEntities:
Внутри класса BaseNetworkable.EntityRealm содержится список из сущностей:
И из него уже можно вытащить все networkable объекты (сущности)
Информация из дампа il2cpp
Для начала я сдамплю il2cpp метадату используя инструмент
Пожалуйста, авторизуйтесь для просмотра ссылки.
. Как оказалось игра абсолютно не имеет никакого шифрования метадаты, как делают это другие игры по типу standoff 2 или genshin impact.В последней версии игры нету никакого статичного поля, которое содержало бы clientEntities
Однако BaseNetworkable.EntityRealm всё ещё существует в дампе:
И статически он находится в новом классе
С ходу бросаются пару мыслей:
- Почему некоторые типы, которые мы видели раньше (BaseNetworkable.EntityRealm, clientEntities) обёрнуты в какие-то классы?
- Какое конкретно поле с BaseNetworkable.EntityRealm выбрать из класса BaseNetworkable.%ca5d42befe66446a3fbc6568b3756c3773e80757?
Поиск и дешифрование BaseNetworkable экземпляра
Для того, чтобы найти чтение поля из BaseNetworkable.%ca5d42befe66446a3fbc6568b3756c3773e80757 (тот самый, что имеет несколько BaseNetworkable.EntityRealm) нужно посмотреть референсы к его TypeInfo
Для тех, кто не в курсе
В il2cpp для обращения к статическому полю необходимо найти указатель на TypeInfo класса, прочитать static_fields и от него обратиться к нужному статическому полю. Все необходимые структуры дампер создаёт в файле il2cpp.h:
Продолжаем. Адрес с typeinfo лежит в файле script.json:
Не забываем перевести число в шестнадцатеричную систему счисления.
Снова для тех, кто не в курсе
Дампер имеет встроенные скрипты, для определения названий функций и типов для IDA (и не только). В замечательной папке с дампом лежит файл ida_with_struct_py3.py:
Загружаем его из контекстного меню:
Ожидаем примерно минут 10, пока не увидим следующее окно:
Просто нажимаем "Ок" и получаем красивое отображение в IDA.
Подготавливаем иду к работе
Для начала меняем image base на 0, чтобы переходить сразу по RVA. Сделать это можно в контекстком меню иды -> Edit -> Segments -> Rebase program -> вводим 0 -> Ок. Затем открываем вкладку экспортов и вводим il2cpp_gchandle_get_target. После перехода вас встретит джамп на некий sub:
Переименновываем саб в j_il2cpp_gchandle_get_target
Теперь я буду иногда показывать по несколько скриншотов: со скриптом и без.
Переходим на адрес с typeinfo и видим следующее:
Берём любой попавшийся xref и декомпилируем:
В 100% случаев вы увидите похожую картину:
(со скриптом)
(без)
Выделил 0xB8, я показывал его ранее - это оффсет на static_fields
Открываем эту функцию
(со скриптом)
(без)
Сразу понимаем, что поле, которое нам надо использовать лежит по оффсету 0x38
Нас интересует данный промежуток - это расшифровка:
Ида может делать ошибки в определии типов, поэтому она добавляет свои уебанские макросы: HIDWORD, LODWORD и тд.
Поэтому выполняем следующие действия при попадании в функцию с дешифровкой:
1. Убираем типы с указателем в аргументах. В моём случае второй аргумент это указатель на __int64. Кликаем на него и жмём клавишу 'Y', чтобы убрать указатель (для тупых это звёздочка).
2. Если увидите
HIDWORD(переменная), то жмём клавишу 'Y' на переменной и меняем её тип на unsigned int.В итоге у вас должно получиться:
В свой код переносим локальные переменные (голубой цвет) и необходимую область (красный цвет). В результат функции помещаем результат со скрина (фиолетовый цвет).
Мой код на данном этапе выглядит так:
Здесь тоже необходимо применить парочку изменений:
1.Заменяем тип _BYTE на unsigned char, _WORD на unsigned short, _DWORD на unsigned long, _QWORD на unsigned long long
2.Используем свою функцию чтения памяти (вероятно у вас свой 1337 гипервизор или драйвер) в тех местах, где идёт разыменнование чего либо с аргументом. Говоря по дебильному: везде где есть выражение
*(тип*)(a1 + что-нибудь или вообще ничего) заменяем на своё чтение памятиРезультат:
Называем функцию в стиле
decrypt_client_entities_wrapper (мне вообще без разницы как вы её назовёте).Не обращайте внимание на красный il2cpp_gchandle_get_target - мы к нему ещё вернёмся.
По такому же принципу расшифровываем entity_list. Нам осталось понять как из %fd9c39723c3f962c0308a43e4106cb774701a27c (в прошлом ListDictionary) получить список сущностей и массив из них. ListDictionary это generic класс - то есть обобщённый класс с разными типами в <>. Поэтому у полей все оффсеты равны 0, а все методы имеют rva -1
Под методами дампер собрал нам GenericInstMethod - реализации функций для конкретных типов. Например: функция с rva 0x5930B70 применяется для типов с <object, object>, <object, float>, <uint, object>, а функция с rva 0x7F4FA10 применяется только для типа <__Il2CppFullySharedGenericType, __Il2CppFullySharedGenericType>
Напомню, у нас с вами тип <%1c24d316ef52282e54d325685a623c64a0124e28, BaseNetworkable>:
В классе ListDictionary мне хотелось бы посмотреть реализацию функции int %c11b6f5f8ca36f1b5661153e32660fc2f41ea536(). По её пустому количеству аргументов и возвращаемому типу мне кажется, что это функция count. Конкретного типа реализации для
<%1c24d316ef52282e54d325685a623c64a0124e28, BaseNetworkable> нету, но есть <%1c24d316ef52282e54d325685a623c64a0124e28, object> (ведь BaseNetworkable обычный объект по лору C#)Переходим по нужному RVA и наблюдаем следующее:
(со скриптом)
(без)
Со скриптом мы прекрасно понимаем, что эта функция действительно является количеством элементов в списке. Т.к. fields это просто вложенная структура, то мы складываем оффсеты.
fields оффсет - 0x10
_size оффсет - 0x8 (0x10 + 0x8 = 0x18 итог)
_items (массив) оффсет - 0x0 (0x10 + 0x0 = 0x10 итог)
Долгожданный il2cpp_gchandle_get_target
На этом долго останавливаться не буду, эта функция не менялась со времён эры динозавров за исключием одного оффсета:
Данный оффсет берём с иды:
На этом всё. Жду мнения
Последнее редактирование:
