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 подписчик

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

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

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

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

0
Автор поста оценил этот комментарий
Привет, а есть телеграм/какая то связь? Если есть телеграм, напиши пожалуйста мне RickEnd, нужна помощь с глобал метадатой на пк
раскрыть ветку (1)
1
Автор поста оценил этот комментарий

Привет. Нет. Их обфусцируют. Ищи в il2cpp код обфускации. Вытягивай его нативно( в асм) и Ctrl c, v его ассемблерной вставкой в с(чаще всего так проще).

И далее собираешься под нативную платформу.

Второй вариант ставить брейкпоинт(найдя, где заканчивается деобфускация / декрипт метадаты) и вытягивать его из памяти.

Если метаданные в рантайме по факту запросов подтягиваются - проще забить.

Примерно так.

показать ответы
0
Автор поста оценил этот комментарий
Интересно пообщаться, на самом деле нет правильного подхода, есть правильный результат, мне чистая метадата нужна для написания модификаций, поскольку с помощью её можно использовать il2cpp инспектор
раскрыть ветку (1)
0
Автор поста оценил этот комментарий

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

Вообщем тут вопрос скорее в времязатратах и целесообразности. Если это бесплатно будет - я бы не стал с этим заморачиваться.

0
Автор поста оценил этот комментарий
От дебаггера приложение падает, по поводу header, я не совсем понял по каким принципам он строится, я при помощи cheat engine читал память запущенного приложения, есть одна мысль, найти в функции длину метадаты, смещение на начало записи итогового файлбуффера, и начиная со смещения считать количество байт равное длине метадаты в файл
раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Падает - забирайте крешдамп:)

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

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

Ваша мысль как вытащить из памяти - правильная. Так и стоит сделать, если проблемы с дешифрации/деобфускацией имеются. Забирайте файл из рантайма.

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

и его переписал(когвнокод в посте и есть Ctrl c, v+стандартные макросы на hiword loword. Я тоже не сишник)

Если падает от дебагера - похоже, на всякий античит/антидебаг

Полагаю, что метадату в таком случае и подписать могли. и менять ее нельзя будет.

показать ответы
0
Автор поста оценил этот комментарий
Я глянул, да, это тоже видел, с Идой не подружился нормально, попробовал гидру, вроде удобнее, функции отличаются от стандартных прилично, разбираюсь runtime::init точно нашёл, предполагаю что в метадата кэш тоже уже был ну и метадата инит тоже предполагаю что нашёл, пробую разбираться. Дизассемблированием не занимался раньше, наверное не самую простую задачку взял, но она прикладная и это мотивацию дает
раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Да. Это чисто прикладная утилитарная задача. В дизасм листинге не надо досконально разбираться Достаточно понять в общих чертах что там вообще происходит. И найти нужные функции. Как вариант еще вспомнил - можно под gdb запуститься и воткнуть брейки в интересующие вас точки. И там смотреть уже по ситуации. Мб дамп памяти сделать и оттуда уже целиком метадату выдрать(найти ее по хидеру, он стандартный у нее вроде как. Ток размер итоговый нужен будет скорее всего).

Если нашли рантайм инит - остальное там 100% гдето рядом и лежит. Обычно такие штуки не обфусцируют и они по сигнатурам находятся быстро

показать ответы
1
Автор поста оценил этот комментарий
Спасибо, добрался до иды, с си в целом дружу, но пока тяжко идёт с поисками функции которая извлекает данные из обфусцированного файла. По поводу памяти в рантайме, понял что там не полный набор, как ты и написал всё подтягивается по факту запроса
раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Посмотри исходники виртуальной машины il2cpp в каком порядке она инициализируется и в какой функции идет чтение global-metadata.

Ее и ищи( по сигнатуре или по названию или по xref например).

Если в ней что-то лишнее, то это лишнее и будет скорее танец с бубном вокруг этого файла.

показать ответы
1
DELETED
Автор поста оценил этот комментарий

Вот ради такого контента я все еще сюда хожу. Респект и подписка!

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

спасибо.

показать ответы

Темы

Политика

Теги

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

Сообщества

18+

Теги

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

Сообщества

Игры

Теги

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

Сообщества

Юмор

Теги

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

Сообщества

Отношения

Теги

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

Сообщества

Здоровье

Теги

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

Сообщества

Путешествия

Теги

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

Сообщества

Спорт

Теги

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

Сообщества

Хобби

Теги

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

Сообщества

Сервис

Теги

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

Сообщества

Природа

Теги

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

Сообщества

Бизнес

Теги

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

Сообщества

Транспорт

Теги

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

Сообщества

Общение

Теги

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

Сообщества

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

Теги

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

Сообщества

Наука

Теги

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

Сообщества

IT

Теги

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

Сообщества

Животные

Теги

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

Сообщества

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

Теги

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

Сообщества

Экономика

Теги

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

Сообщества

Кулинария

Теги

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

Сообщества

История

Теги

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

Сообщества