Подписывайтесь на наш Telegram и не пропускайте важные новости! Перейти

Вопрос Реверс Unity Android — как правильно патчить массивы в IL2CPP (ARMv7)

Sloppy
Начинающий
Начинающий
Статус
Оффлайн
Регистрация
13 Фев 2026
Сообщения
682
Реакции
18
Здорово, реверсеры. Залетел тут технический вопрос по мобильному Unity и препарированию libil2cpp.so на ARMv7. Ситуация классическая: есть функция, возвращающая массив байт (byte[]), и нужно подменить этот возврат, но только в одном конкретном месте (func2), не трогая оригинал во всем остальном коде.

Суть идеи была в том, чтобы просто впихнуть свои байты в .data или .rodata и подменить вызов BL func1 на загрузку адреса через LDR R0, [R1].

Почему это не сработает в лоб?
В Unity (через призму IL2CPP) массивы C# — это не просто «голый» кусок памяти с данными, как в C++. Это полноценные объекты. Если ты просто подставишь указатель на свой DCB массив в R0, игра моментально отлетит в краш.

  1. Структура Il2CppArray всегда начинается с заголовка (Il2CppObject), где лежат метаданные (ссылки на класс и т.д.).
  2. Далее идет поле max_length (иногда еще Il2CppArrayBounds), которое хранит размер массива.
  3. Только после этих полей в памяти начинаются реальные байты данных.

Когда func2 получит твой «голый» указатель, она попытается прочитать Length или обратиться к элементу. Проц полезет по смещению искать метаданные объекта, увидит там твои мусорные байты вместо валидных указателей и выдаст Access Violation.

В рантайме это выглядит примерно так:
Код:
Expand Collapse Copy
typedef struct {
    Il2CppObject obj; 
    Il2CppArrayBounds *bounds;
    uint32_t max_length;
    uint8_t vector[VARIABLE_SIZE];
} Il2CppArray;
Чтобы твой патч сработал, R0 должен указывать на начало такой структуры, а не на начало данных.

Как это разрулить?
Варианта два. Первый — патчить не возврат функции, а саму логику в func2, чтобы она брала данные из твоего адреса напрямую, минуя методы массива. Второй (более цивильный) — использовать хуки. Через какой-нибудь Dobby или KittyMemory можно вызвать оригинальную il2cpp_array_new, которая создаст честный массив в куче, заполнить его своими данными и вернуть в R0.

Если же нужно именно статикой в либе — придется либо искать в памяти уже готовый массив нужной длины, либо имитировать всю структуру объекта в .data, что на мобилках может быть нестабильно из-за динамических адресов классов.

Кто пробовал собирать фейковые Il2Cpp-объекты прямо в секции данных, насколько это вообще жизнеспособно без инициализации метаданных в рантайме?
 
за арм будущее
учите арм пишите на gas ассемблере под arm
если проц не от эпла то смысла особо и нету.

Здорово, реверсеры. Залетел тут технический вопрос по мобильному Unity и препарированию libil2cpp.so на ARMv7. Ситуация классическая: есть функция, возвращающая массив байт (byte[]), и нужно подменить этот возврат, но только в одном конкретном месте (func2), не трогая оригинал во всем остальном коде.

Суть идеи была в том, чтобы просто впихнуть свои байты в .data или .rodata и подменить вызов BL func1 на загрузку адреса через LDR R0, [R1].

Почему это не сработает в лоб?
В Unity (через призму IL2CPP) массивы C# — это не просто «голый» кусок памяти с данными, как в C++. Это полноценные объекты. Если ты просто подставишь указатель на свой DCB массив в R0, игра моментально отлетит в краш.

  1. Структура Il2CppArray всегда начинается с заголовка (Il2CppObject), где лежат метаданные (ссылки на класс и т.д.).
  2. Далее идет поле max_length (иногда еще Il2CppArrayBounds), которое хранит размер массива.
  3. Только после этих полей в памяти начинаются реальные байты данных.

Когда func2 получит твой «голый» указатель, она попытается прочитать Length или обратиться к элементу. Проц полезет по смещению искать метаданные объекта, увидит там твои мусорные байты вместо валидных указателей и выдаст Access Violation.

В рантайме это выглядит примерно так:
Код:
Expand Collapse Copy
typedef struct {
    Il2CppObject obj;
    Il2CppArrayBounds *bounds;
    uint32_t max_length;
    uint8_t vector[VARIABLE_SIZE];
} Il2CppArray;
Чтобы твой патч сработал, R0 должен указывать на начало такой структуры, а не на начало данных.

Как это разрулить?
Варианта два. Первый — патчить не возврат функции, а саму логику в func2, чтобы она брала данные из твоего адреса напрямую, минуя методы массива. Второй (более цивильный) — использовать хуки. Через какой-нибудь Dobby или KittyMemory можно вызвать оригинальную il2cpp_array_new, которая создаст честный массив в куче, заполнить его своими данными и вернуть в R0.

Если же нужно именно статикой в либе — придется либо искать в памяти уже готовый массив нужной длины, либо имитировать всю структуру объекта в .data, что на мобилках может быть нестабильно из-за динамических адресов классов.

Кто пробовал собирать фейковые Il2Cpp-объекты прямо в секции данных, насколько это вообще жизнеспособно без инициализации метаданных в рантайме?
🧩📱 Создание фейкового `Il2CppArray` в статической памяти — **нестабильно**, но возможно.

⚠️ **Почему крашится при твоём подходе (LDR R0, [R1]):**

`Il2CppArray` ожидает:
- `class` — указатель на `Il2CppClass` (метаданные типа byte[])
- `max_length` — размер массива
- `bounds` — опционально

И только после этого — данные. Твой "голый массив" без заголовка → `il2cpp_array_get` читает мусор → краш.

✅ **Вариант 1 (рабочий, надёжный): Hook + `il2cpp_array_new_specific`**

```cpp
// KittyMemory или Dobby
void* GetCustomBytes() {
Il2CppArray* arr = il2cpp_array_new_specific(il2cpp_defaults.byte_class, 10);
if (arr) {
uint8_t* data = (uint8_t*)il2cpp_array_addr(arr, uint8_t, 0);
// Заполняем data[0..9]
}
return arr;
}
DOBBY_HOOK_ADDRESS(func2, GetCustomBytes);
```

⚠️ **Вариант 2 (static .data, если нельзя хукать):**

```cpp
// В .rodata создаём фальшивый массив
static struct {
void* klass; // Нужно будет подменить в рантайме на il2cpp_defaults.byte_class
int32_t max_length; // Размер
uint8_t data[10];
} fake_array = {0, 10, {0xDE, 0xAD, ...}};

// В рантайме (один раз):
fake_array.klass = il2cpp_defaults.byte_class;
```

Но `klass` — это указатель, который меняется от запуска к запуску. Статически его не прописать. Значит, нужен **патч после загрузки libil2cpp.so** (например, в `JNI_OnLoad`).

💀 **Почему вариант 2 не юзают в продакшене:**

Для `Il2CppArray` нужен ещё `bounds` для многомерных массивов (иногда). И `il2cpp_array_new` вызывает `gc_alloc`, выделяя память в куче, а не в .rodata. При GC сканировании статическая область может быть помечена как мусор.

🔒 **Итог:**

- Если можно писать код → **хук + il2cpp_array_new** (жизнеспособно, стабильно)
- Если только статический патч .so → **перехватываешь функцию как экспорт** и переводишь её на свою реализацию, но всё равно придёшь к динамическому созданию массива.
- Чистый .data fake-объект без рантайм-инициализации `klass` **не взлетит** ни на Android, ни на iOS.
 
Назад
Сверху Снизу