Перенаправление функций в native-библиотеках на Android

В данной статье я немного расскажу о том, как c помощью фреймворка AndHook можно перенаправлять вызовы функций в native-библиотеках. Можно перехватывать вызовы как публичных (экспортируемых функций), так и непубличные, напрямую по их адресу. Подробнее о перенаправлении можно почитать тут, и на странице фреймворка.

В качестве примера будет рассмотрен случай с внедрением своей библиотеки. Однако данный фреймворк также позволяет работать и без пересбора приложения с помощью xposed.

В данной статья я буду использовать Visual Studio и BatchApkTool для Windows. Вместо Visual Studio можно использовать Android Studio, или вообще компилировать через gcc или clang (вариант для продвинутых). Вместо BatchApkTool можно использовать apktool. Подробно по поводу работы с BatchApkTool я останавливаться не буду, т.к. разобраться с работой в программе не трудно.

Для начала нужно разобрать АПК с помощью BatchApkTool или apktool.

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

Настройка Visual Studio для работы с NDK

В программе Visual Studio Installer должны быть установлены следущие компоненты:


Остальные по желанию.

Cоздание нового проекта в Visual Studio

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

Нужно выбрать шаблон "Динамическая общая библиотека (Android)", и затем ввести название.

Выглядит примерно так

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

и открыть свойства. В свойствах проекта выбираем Конфигурация: Все конфигурации, Платформа: <Ваша целевоя платформа>, Целевой уровень API можно выбрать 14 для ARM и X86, а для ARM64 и X64 уже нужен 21 уровень.
Также по желанию можно поменять компилятор.
Затем во вкладке Компановщик->Ввод в поле дополнительные зависимости нужно указать путь к скачанной нами библиотке и библиотеке, функции которой мы хотим перехватывать:

Пример


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

Теперь нужно добавить заголовочный файл. Нужно перейти в папку с решением.

Как это сделать

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

Или так

Затем нужно подключит заголовочный файл, добавив в начало нашего .cpp файла строку

#include "AndHook.h"

Создадим фунцию JNI_OnLoad, в котором мы будем регистрировать наши хуки

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env = NULL;
    jint result = -1;
    LOGD("JNI_OnLoad start!");

    if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
        LOGE("JNI_OnLoad! GetEnv failed");
        return -1;
    }

    RegisterHooks();

    result = JNI_VERSION_1_4;
    LOGD("JNI_OnLoad! finished!");
    return result;
}

Непосредственно сама функция RegisterHooks:

// Для определения адреса удобно использовать директиву #define
#ifdef __arm__
#define Test_Offset 0x1234
#elif __aarch64__
#define Test_Offset 0x1235
#elif __i386__
#define Test_Offset 0x1236
#elif __x86_64__
#define Test_Offset 0x1237
#endif
void (*Old_Test)();
void Test()
{
    LOGD("Test!");
    // Do something
    Old_Test();
}
void (*Old_Test2)();
void Test2()
{
    LOGD("Test2!");
    Old_Test2(); // Здесь мы вызываем оригинальную функцию Test2
}
void RegisterHooks() {
    const void* libil2cpp = AKGetImageByName("libil2cpp.so"); // Тут указываем имя целевой библиотеки.
    if (libil2cpp == NULL) {
        LOGW("AKGetImageByName return null!");
        return;
    }

    // Перехват непубличной функции по ее адресу
    // Для реверс-инженеров адрес можно получить в IDA/Ghidra
    // В случае с il2cpp-игрой адрес можно получить программой https://github.com/Perfare/Il2CppDumper
    AKHookFunction(AKFindAnonymity(libil2cpp, Test_Offset), reinterpret_cast<void*>(&Test), reinterpret_cast<void**>(&Old_Test));
    // Здесь мы ищем функцию по адресу Test_Offset в файле libil2cpp и заменяем ее на нашу функцию Test, объявленную выше. Старая функция будет доступна по указателю Old_Test.

    // Перехват публичной функции
    AKHookFunction(AKFindSymbol(libil2cpp, "Test2"), reinterpret_cast<void*>(&Test2), reinterpret_cast<void**>(&Old_Test2));

    // Также мы можем вызывать непубличные функции не перехватывая их:
    void (*Test3)();
    Test3 = reinterpret_cast<void (*)()>(AKFindAnonymity(libil2cpp, 0x12345));
    Test3();

    // Еще мы можим патчить оригинальную библиотеку
    const uint8_t data[] = { 0x00, 0xF0, 0x20, 0xE3 }; // Инструкция NOP
    AKPatchMemory(reinterpret_cast<const void*>(AKFindAnonymity(libil2cpp, 0x123456)), reinterpret_cast<const void*>(&data), 4);
    // Здесь мы патчим инструкцию по адресу 0x123456

    AKCloseImage(libil2cpp);
}

Также для патчинга под несколько архитектур удобно использовать #define

Пример

#ifdef __arm__
#define Patch_Offset 0x1234
#define Patch_Data { 0x00, 0xF0, 0x20, 0xE3 }
#elif __aarch64__
#define Patch_Offset 0x1234
#define Patch_Data { 0x1F, 0x20, 0x03, 0xD5 }
#endif

const uint8_t data[] = Patch_Data;
const size_t len = sizeof(data) / sizeof(uint8_t);
AKPatchMemory(reinterpret_cast<const void*>(AKFindAnonymity(libil2cpp, Patch_Offset)), reinterpret_cast<const void*>(&data), len);

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

Выбрать целевую платформу

Собрать проект

Если платформ несколько повторить пункты 1-2.

Теперь необходимо скопировать нашу библиотеку и библиотеку AndHook в папку lib нашего приложения для каждой из архитектур. (Скомпилированнаяя библиотека будет в папке с решением).

Теперь для того чтобы наша библиотека загружалась нужно отредактировать приложение. Для этого нужно найти MainActivity нашего приложения. В начало метода onCreate в MainActivity добавим следующие строки:

const-string v0, "SharedObject1"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

Где вместо SharedObject1 имя вашей библиотеки без lib и .so
Для il2cpp-игр вместо MainActivity добавляем в метод onCreate в файле smali/com/unity3d/player/UnityPlayerActivity.smali.

Далее рекомпилируем приложение.

Полезные ссылки:
https://github.com/asLody/AndHook/
http://armconverter.com/
http://armconverter.com/hextoarm/

Специально для сайта ITWORLD.UZ. Новость взята с сайта Хабр