7

Об игрушках unity и android

Делать было нечего, дело было до +30 за окном и появился интерес вернуться к задачке. которая у меня долго пылилась на полке: а именно получить способ забраться во внутренности запущееного нативного приложения на андроид-эмуляторе, который использует libhoudini в качестве транслятора ARM. Для несведующих поясню: есть устройство андроид - оно в основе своей использует процессор ARM архитектуры. есть приложения, которые написаны так, что в конечном счете представляют собой машинный код по эту конкретную архитектуру. при желании что-то сделать с этим приложением необходим какой-то способ управлять этим машинным кодом. чем мы и займемся.

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

мы знаем, что эмуляторы( memu, bluestacks, nox, genymotion, etc...) запускаются на платформах с базовой архтектурой x86_64, но позволяют исполнять код для архитектуры arm. это достигается за счет исползования intel овской поделки под названием lib(rary)houdini - некий транслятор, встраиваемый в конкретный эмулятор его разработчиками и позволяющий собственно транслировать arm код в x86( как это делается - не знаю, да и код этого транслятора вроде как нигде не опубликован).

мы так же имеем базовые понятия о том, что такое apk, pm, adb, (ba)sh

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

дополнительно к этому у нас есть: текстовый редактор + javascript/python/c и некоторое ПО для отладки и внедрения в приложения. в частности - frida

ну чтож. приступим.

для начала устанавливаем наше приложение себе на эмулятор (я взял genymotion) и запускаем его. оно работает. чтож... теперь нам стоит "найти" его на эмуляторе и скачать к себе на локальную машину для дальнейшего анализа ( adb pull нам в помощь).

конкретно эта игрушка, которую разбирал я шла в split- apk. соответственно выкачиваем все apk файлы и ищем в них что-то относящееся к unity ( а конкретно нас интересуют libunity.so libil2cpp.so global-metadata.dat)

коротко, в двух словах зачем все это нужно:

unity - мета-фреймворк для разработки приложений с последюущим их релизом под различные платфформы. основной инструмент там - c4+ соответственно для "кроссплатформенности" был создан некий транслятор, а именно cSharp->IL(intermediate langauge)->platform. вот этот последний переход осуществляется посредством специальной виртуальной машины(ВМ), разной для каждой платформы и эта ВМ использует различные данные из global-metadata.dat в процессе трансляции IL и его последующего исполнения.

т.к. сам по себе c4+ вполне себе легко подает реверзу, то задача на первоначальном этапе сводится к вытаскиванию этого c4+ кода каким-то образом из IL с целью последующего изучения и выбора направлений дальнейшего анализа.

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

к сожалению( или к счастью) те данные, которые необходимы для работы этого ПО частично или полностью unity-разработчки определенным образом шифруют\обфусцируют. а именно файл global-metadata.dat и наша задача, в случае если il2cpp dumper не сможет корректно отработать будет заключаться в том, чтобы этот global-metadata.dat "восстановить".

на самом сайте il2cppdumper разобраны некие типовые случаи этого "восстановления"

в нашем же все свелось к небольшому анализу(найти место запуска IL virtual machine просмотреть до момента обращения к global-metadata.dat) и copy-paste листинга(анализировал libil2cpp с помощью IDA)

в итоге имеем следующий листинг, который нам любезно предоставила ida

fileHandle2 = (__int64)fileHandle;

fileBuffer = (const void *)utils::MemoryMappedFile::Map(fileHandle);

os::File::Close(fileHandle2, &error);

if ( (_DWORD)error )

{

utils::MemoryMappedFile::Unmap(fileBuffer);

LABEL_21:

fileBufferInMem = 0LL;

goto LABEL_22;

}

size_of_fileBuffer = get_size_of_fileBuffer((unsigned __int64)fileBuffer);

fileBufferInMem = malloc(size_of_fileBuffer);

memcpy(fileBufferInMem, fileBuffer, size_of_fileBuffer);

unXorArray = malloc(64u);

v15 = LoadMetadataFile_Val_1;

v16 = LoadMetadataFile_Val_2;

for ( i = 0LL; i != 64; ++i )

{

v16 = 18000 * (unsigned __int16)v16 + HIWORD(v16);

unXorArray[i] = v16;

v15 = 36969 * (unsigned __int16)v15 + HIWORD(v15);

}

LoadMetadataFile_Val_1 = v15;

LoadMetadataFile_Val_2 = v16;

if ( size_of_fileBuffer >= 1 )

{

bufidx = 0LL;

uXidx = 1;

do

{

uXidxOffset = uXidx + 62;

if ( uXidx - 1 >= 0 )

uXidxOffset = uXidx - 1;

notDone = size_of_fileBuffer <= uXidx;

*((_BYTE *)fileBufferInMem + bufidx) ^= unXorArray[uXidx - 1 - (uXidxOffset & 0xFFFFFFC0)];

bufidx = uXidx++;

}

while ( !notDone );

}

LABEL_22:

if ( (v24 & 1) != 0 )

operator delete(v26);

if ( (v27 & 1) != 0 )

operator delete(v29);

return fileBufferInMem

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

мапится файл в память(fileHandle, который указывает на global-metdata.dat)

магия

возвращается fileBufferInMem

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

xor данных по ключу из генерируемого массива. крайне удобно тем, что a == xor(xor(a,b),b)

т.е. у нас на руках сразу есть ключ и закодированные данные. немного C и

#include <stdio.h>

#include <stdlib.h>

#include <stdint.h>

#include <string.h>

#include <stdlib.h>

#include <stdint.h>

typedef uint64_t QWORD; // DWORD = unsigned 64 bit value

typedef uint32_t DWORD; // DWORD = unsigned 32 bit value

typedef uint16_t WORD; // WORD = unsigned 16 bit value

typedef uint8_t BYTE; // BYTE = unsigned 8 bit value

#define LOWORD(l) ((WORD)(l))

#define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0xFFFF))

#define LOBYTE(w) ((BYTE)(w))

#define HIBYTE(w) ((BYTE)(((WORD)(w) >> 8) & 0xFF))

BYTE* readContent(char* filename, size_t* size_t_ptr) {

FILE *f = fopen(filename, "rb");

fseek(f, 0, SEEK_END);

*size_t_ptr= ftell(f);

fseek(f, 0, SEEK_SET); /* same as rewind(f); */

BYTE *string = malloc(*size_t_ptr);

fread(string, *size_t_ptr, 1, f);

fclose(f);

return string;

}

void storeContent(char* filename, BYTE* buffer,size_t bufferSize){

FILE *f = fopen(filename, "wb");

fwrite(buffer,bufferSize,1,f);

fclose(f);

}

BYTE* initXor() {

BYTE* buf=(BYTE*)malloc(64);

DWORD v16=0x2A;

for(int i=0;i!=64;i++) {

v16=18000*((WORD)v16)+HIWORD(v16);

buf[i]=LOBYTE(v16);

//printf("%02X,", buf[i]&0xFF);

}

return buf;

}

void unXorBuffer(QWORD size_of_fileBuffer,BYTE* fileBufferInMem) {

if (size_of_fileBuffer<1){

return;

}

BYTE* unXorArray=initXor();

QWORD bufidx = 0LL;

QWORD uXidx = 1;

BYTE notDone;

do

{

DWORD uXidxOffset = uXidx + 62;

if ( uXidx - 1 >= 0 )

uXidxOffset = uXidx - 1;

notDone = size_of_fileBuffer <= uXidx;

fileBufferInMem[bufidx] ^= unXorArray[uXidx - 1 - (uXidxOffset & 0xFFFFFFC0)];

bufidx = uXidx++;

}

while ( !notDone );

free(unXorArray);

}

int main(int argc, char *argv[]) {

BYTE* uXor=initXor();

size_t gmSize;

BYTE* gmBuffer=readContent("./global-metadata.dat",&gmSize);

printf("%d",gmSize);

unXorBuffer(gmSize,gmBuffer);

storeContent("./global-metadata.dat.dec",gmBuffer,gmSize);

return 0;

}

на выходе получим "правильный" global-metdata.dat

далее можно воспользовваться il2cppdumper и восстановить структуру( не содержимое!!) приложения, т.е. классы их поля и методы.

и тут скорее всего (а на это яно намекали имена функций в IDA в libil2cpp) мы ничего понять сходу не сможем. т.к все эти классы\поля\методы\ обфусцированы, т.е. нет возможности по названиям понять кто за что отвечает:

в таком случае наиболее очевидным и простым является анализ самого приложения с точки зрения "а что там вообще проиcходит. и куда мы можем сваять заплатку". в процессе анализа выявились 3 направления - данные игры( т.е всевозможные цены. шансы. награды и тп.) сохраниения - дада. в игре есть облачное сохранение. и ГПСЧ(random generator - вот он как раз и отвечает в конечном счете за всякие шансы\награды и тп)

разглядывая то, что "забыли\не смогли" закрыть разработчики своим обфускатором видно:

  1. какой-то античит(значит нас "пасут" где?)

  2. используется json( полистав листинг классов, аннотации на методах\полях - игра вдоль и поперек использует json для маршаллинга. учтем.)

  3. какой-то savegamefree - значит можно приицепиться к save & load фукциям и посмотреьт что сохраняется и как ( сразу скажу, что необходимости в этом нет, т.к у нас есть п2)

чтож. приступим.

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

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

для упрощения работы с frida и il2cpp я наткнулся в гугле на il2cpp-bridge - эдакая удобная надстройка над функционалом frida для более удобного вызова\анализа\перехвата интересующих меня функций (я нашел ее много позже и до этого полз через классический вариант. поиск RVA искомых функций в дампе c4+ файле, который получил из il2cppdumper и последующим их перехватом)

Атаковать будем ГСЧ - т.е. искуственно выдавать нужные нам шансы для какого-либо дейтсвиия. а именно "выпадение нужной нам вещи". для этого надо понимать. что ГСПЧ - это некая функция, которая имеет "начальную точку" и период(т.е. через какое-то большое количество обращений будут возвращаться те же самые значения от старта)

для этого находим все функции, где упомянется слово Random и "прицепимся" к ним. посмотрим какой именно ГПСЧ используется ( как минимум у нас их два - это System::Random и unity random ) входные и выходные значения в момент "выпадения вещи" (и продолжим уже исключительно с ним)

згрузим сохранение - повторим -загрузим-повторим.

наблюдеаем следующее:

каждый раз выпадает одна и та же вещь . следовательно где-то в сохранении (и даже можно найти где. но не нужно) сохраняется текущее значение для "начального значения" нашего ГПСЧ(initial seed, seed) и используется вызов всяких функций randomXXXX

по семантике понятно, что это "качество" вещи.

аттрибуты. значения аттибутов.

нас интересует только качество

следовательно схема такая:

перехватываем установку seed - устанавливаем свое значение. ловим нужную нам вещь

собственно вот рожденный в процессе ковыряния быдлокод, который приводит к требуемому результату

boilerplate взят тут

git remote -v

origin https://github.com/oleavr/frida-agent-example.git (fetch)

origin https://github.com/oleavr/frida-agent-example.git (push)

import {Buffer} from "buffer";

import "frida-il2cpp-bridge";

console.log("Rebuilded")

function awaitForCondition(callback: any) {

var i = setInterval(function () {

var addr = Module.findBaseAddress('libil2cpp.so');

console.log("Address found:", addr);

if (addr) {

clearInterval(i);

callback(+addr);

}

}, 0);

}

function _attach(base: any, klass: any, mtd: any, get_StackTrace: any) {

const va = mtd.virtualAddress

if (va == 0x0) {

//console.log("Attach fail", mtd.virtualAddress)

return

}

try {

Interceptor.attach(va, {

onEnter: function (args) {

console.log("onenter ", klass.fullName, `mtd:"${mtd}"`);

if (get_StackTrace != null) {

console.log(get_StackTrace.invoke());

}

//console.log("encode password", [>input<]pwd);

}

});

} catch (err) {

console.log(klass.fullname.tostring(), mtd, err)

console.log("error interception rva", mtd, va.sub(base))

}

}

const klassesOfInterests = [

// /*MainCharacterData*/"SH.491471447.sh_cxse1",

// /*some json related stuff*/"SH.491473776.sh_dbdo1",

// /*some json related stuff*/"SH.491473773.sh_dbdl1",

// /*some json related stuff cur character data? */"SH.491471465.sh_cxsw1",

// /*some json related stuff to files/sc.d? */"SH.491471742.sh_cydk1",

// /*diamond related data*/"SH.491471457.sh_cxso1",

// /*some logs to json handling?*///"SH.491475490.sh_ddri1",

// //"SH.491471477.sh_cxth1",

//"UnityEngine.Random",

// "System.Random",

// "RandomGemRewardVisualInfo",

// "PseudoRandom",

// "DefaultRandom",

"CodeStage",

// "SH.Feature.MS.CardProcess",

// "SH.Feature.RL.DiamondProcess"

]

const mtdsToSkip = [".ctor", "CurrentOwned", "get_IsRunning"]

/*

*

* enum of some events related to diamondProcess and game

* SH.491471427.sh_cxrk1

*

* ????

* SH.491471477.sh_cxth1

* assets related stuff?

* SH.491471546.sh_cxvx1

* */

awaitForCondition(function (base: any) {

const il2cpp = ptr(base);

//bind(il2cpp, ObscuredRefsRVAs)

//bind(il2cpp, BayatGamesFns)

Il2Cpp.perform(() => {

const SystemString = Il2Cpp.corlib.class("System.String");

const single = Il2Cpp.corlib.class("System.Single");

const int32 = Il2Cpp.corlib.class("System.Int32");

const get_StackTrace = Il2Cpp.corlib.class("System.Environment").method("get_StackTrace");

const SystemBoolean = Il2Cpp.corlib.class("System.Boolean");

const SystemType = Il2Cpp.corlib.class("System.Type");

const cSharp = Il2Cpp.domain.assembly("Assembly-CSharp");

const cSharpFP = Il2Cpp.domain.assembly("Assembly-CSharp-firstpass");

/*

with SEED=1 we have right top legendary item

this is Range(x,y) return values

inside 1 Range 0,1 System.Single 0.0003153085708618164

inside 1 Range -0.20000000298023224,0 System.Single -0.11122268438339233

inside 1 Range -0.20000000298023224,0 System.Single -0.06576250493526459

inside 1 Range -0.20000000298023224,0 System.Single -0.03238866850733757

*/

let seedValue = 1;

global.setRandomSeed = function (value) {

seedValue = value;

console.log("seed", seedValue)

}

let rrValues = []

const setRandomRange = function (...values) {

rrValues = [];

const rrTmp = [];

values.forEach(v => {

//const sV = Il2Cpp.string(`${v}`);

//rTmp.push(single.tryMethod("Parse", 1).invoke(sV));

rrTmp.push(v)

})

rrValues = rrTmp.reverse().sort()

console.log(rrValues.reverse());

}

global.overloadValues = function (vMin, vMax) {

var x = [];

for (var i = vMin; i <= vMax; i++) {

x.push(i);

}

rrValues = x.reverse();

console.log(rrValues)

}

global.setRandomRange = setRandomRange;

global.fixRandom = function () {

cSharp.image.classes.filter(klass => klass.fullName.includes("SH.Feature.E.RandomEquipmentProcess"))

.forEach(k =>

k.methods.filter(m => m.name.includes("Generate"))

.forEach(m => {

if (m.parameterCount != 2) {

return

}

m.implementation = function (a, b) {

console.log('genrate', k.fullName, m)

console.log(a)

console.log(b)

return this.tryMethod("Generate").invoke(a, b);

}

})

)

/*

* mix SEED with UNITY int32 RANDOM.RANGE gives us neceesarry results

* setRandomSeed(1123120)

* setRandomSeed(1023121)

* overloadValues(20,23)

* */

Il2Cpp

.domain.assembly("UnityEngine.CoreModule")

.image.class("UnityEngine.Random")

.methods.forEach(mtd => {

console.log('inject in ', mtd)

mtd.implementation = function (...args) {

const rva = mtd.relativeVirtualAddress;

if (!!rva && rva.equals(ptr(0x12e8d0c))) {

const a = args[0]

const b = args[1]

//this is item id, but idk what value it need to be?

// if (a === 0 && b === 100) {

if (a === 0 && b === 100) {

const v = rrValues.pop()

if (v !== undefined) {

console.log('overload Range(0,100) with', mtd, a, b, v)

return 0 + v

}

}

const rv = mtd.invoke(a, b);

console.log('range(x,y)', a, b, rv)

// int32 Range(int32, int32)

return rv

}

if (rva.equals(ptr(0x12e8cc0))) {

//float Range

//console.log('floatRange');

return 0.0006;//mtd.invoke(seedValue !== undefined ? seedValue : 1);

}

if (rva.equals(ptr(0x12e8b64))) {

//InitState

console.log('initState');

return mtd.invoke(seedValue !== undefined ? seedValue : 1);

}

const rv = mtd.invoke(...args);

//console.log('invoked', mtd, mtd.relativeVirtualAddress, args, rv)

return rv;

}

})

}

});

})

запустили приложение на эмуляторе.

запустили frida с нашим скриптом. загрузили свою сохраненную игру.

пробуем без "своего" ГСПЧ:

включаем "свой":

вуаля.

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

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

на этом интерес к этой задаче интерес мой закончился, но этот пост - пусть остается. в том числе и как памятка мне

ЗЫ техники изменения значений переменных в памяти малоэффективны из-за используемого античита

Спасибо, что прочитал.

Больше постов читайте по тегу «Программирование». А если хотите изучить новую профессию, посмотрите актуальные курсы от проверенных школ с реальными отзывами на сайте Пикабу Курсы.

Лига программистов

2.2K постов11.9K подписчик

Правила сообщества

- Будьте взаимовежливы, аргументируйте критику

- Приветствуются любые посты по тематике программирования

- Если ваш пост содержит ссылки на внешние ресурсы - он должен быть самодостаточным. Вариации на тему "далее читайте в моей телеге" будут удаляться из сообщества

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества