Гайд Продолжение. GCClient и разблокировка Dota Plus

Ревёрсер среднего звена
Пользователь
Статус
Оффлайн
Регистрация
24 Ноя 2022
Сообщения
303
Реакции[?]
108
Поинты[?]
57K
Итак, вы хотите стать на тёмную сторону Доты. Сделать не чит, но ченджер... Что ж, сегодня я обучу вас азам этого искусства

В доте мета-вещами занимается публичный интерфейс CGCClient(GC — game coordinator). Стим-инвентарь, дота плюс, информация о профиле, даже присоединение к игре — всем этим заведует он. В этом гайде процесс будет описан на примере дота плюса. Итак, к делу:

GCClient берём из client.dll через интерфейс DOTA_GC_CLIENT. Алгоритм обновления информации об объекте GC-системы включает в себя три шага: найти, изменить, уведомить. Пойдём по порядку:

В GC всё построено на SharedObject-ах, которыми представлены, например, те же предметы в инвентаре. Нам нужен объект дотаплюса, он называется CDOTAGameAccountPlus. Первым делом ищем по хрефу «CDOTAGameAccountPlus», и... бинго!

Screenshot_610.png

Функция явно имеет в себе код нахождения объекта с нуля, это то, что нужно. Запишем её себе как FindGameAccountPlus. Переходим из IDA в x64dbg и смотрим в ReClass первый из qword-ов. Это CGCClientSharedObjectCache, главный контейнер в GC-системе. Далее в IDA смотрим, что за sub_XXXXXX:

Screenshot_612.png

Наблюдаем шедевр ручной обфускации от Valve, но много задумываться над бит-шифтами не стоит. Счётчик idx берётся с 0x10, а поинтер с 0x18, там у SO-кэша CUtlVector<CGCClientSharedObjectTypeCache*>. В цикле проверяется оффсет тайп-кэша на 0x28, и если это значение равно typeID, то возвращает этот TypeCache. Значение берётся из enum'а EEconTypeID, другие значения из которого можно восстановить по хрефу "Create(X)", где X это имя econ-класса(например, CEconItem).
Пожалуйста, авторизуйтесь для просмотра ссылки.
.

В нашем случае typeID равен 2012(0x7dc), и становится понятно, почему цикл идёт с конца списка: именно там находится тайп-кэш с дотаплюсом. Возвращаемся в FindGameAccountPlus, дальше оно просто достаёт из CUtlVector’а у тайп-кэша на 0x10 последний элемент(в нашем случае вообще единственный).
Пожалуйста, авторизуйтесь для просмотра ссылки.
. На этом этапе у вас должно быть что-то вроде:
C++:
enum EEconTypeID // тут ещё есть полезные значения, вписанные dotakoder'ом
{
    k_EEconTypeItem = 1,
    k_CEconGameAccountClient = 7,
    k_CDOTAGameAccountClient = 0x7D2,
    k_CDOTAParty = 0x7D3,
    k_CDOTAGameHeroFavorites = 0x7D7,
    k_ECDOTAMapLocationState = 0x7D8,
    k_ECDOTAPlayerChallenge = 0x7DA,
    k_CDOTAGameAccountPlus = 0x7DC
};

class CGCClientSharedObjectTypeCache : public VClass {
public
    GETTER(EEconTypeID, GetEconTypeID, 0x28);
};

class CGCClientSharedObjectCache : public VClass {
public:
    GETTER(CUtlVector<CGCClientSharedObjectTypeCache*>, GetTypeCacheList, 0x10);
};
Вас может потянуть на взятие сигнатуры этой функции, чтобы достать указатель на CGCClientSharedObjectCache, но этого делать не нужно, к концу гайда у вас всё будет.

Переходим к следующему шагу алгоритма: изменение данных. Некоторые SharedObjectы, на самом деле, являются лишь определённого рода обёртками над протобафами. Заметили, что после витейбла CDOTAGameAccountPlus сразу идёт CSODOTAGameAccountPlus? Это оно и есть. Получить его можно либо оффсетом, либо, как истинный джентльмен, вызвав vfunc 9 под названием GetPObject(который получает его по оффсету). Вполне логично будет предположить, что если в доте есть протобаф, то он определён в отслеживаемых прото-файлах. Находится CSOGameAccountPlus в
Пожалуйста, авторизуйтесь для просмотра ссылки.
.
Также в функции, вызывающей FindGameAccountPlus можно найти консольную команду dota_game_account_plus_debug, которая выводит значения полей протобафа.

Прекрасно, теперь мы можем включить дотаплюс следующим образом:
C++:
class CProtobufSharedObjectBase : public VClass {
public:
    google::protobuf::Message* GetPObject() {
        return CallVFunc<9, google::protobuf::Message*>();
    }
};

class CGCClientSharedObjectTypeCache : public VClass {
public:
    CProtobufSharedObjectBase* GetProtobufSO() {
        return *Member< CProtobufSharedObjectBase**>(0x10);
    }
    GETTER(uint32_t, GetEconTypeID, 0x28);
};

...

auto objCache = ???; // wait for it, wait for it...
for (auto& typeCache : objCache->GetTypeCacheList()) {
    if (typeCache->GetEconTypeID() != EEconTypeID::k_CDOTAGameAccountPlus)
        continue;

    auto message = (CDOTAGameAccountPlus*)typeCache->GetProtobufSO()->GetPObject();

    message->set_plus_flags(0);
    message->set_plus_status(1);
}
Но само по себе это ничего не изменит. Доту нужно оповестить об изменении, и тут мы переходим к последней стадии алгоритма. У GCClient есть массив листенеров, через которые оповещается GC-система. За обновления SO отвечает метод DispatchSOUpdated, можете посмотреть хрефы в дилибах. Я не буду вас мучать: «Unable to create object of type %d\n», с меньшим оффсетом от функции, пятый с конца call.

Screenshot_614.png

Видим тут простейшую итерацию по CUtlVector<ISharedObjectListener*> на 0x270 с вызовом виртуальной функции по индексу 1 с теми же параметрами(называется, как ни странно, SOUpdated). Параметры нам неизвестны, но и это не беда. Тут нам понадобятся
Пожалуйста, авторизуйтесь для просмотра ссылки.
(ведь econ-система в вальв-играх однородная):

SOUpdated( const CSteamID & steamIDOwner, const GCSDK::CSharedObject *pObject, GCSDK::ESOCacheEvent eEvent )

Тут нам нужны два из трёх:
  • CSteamID — хранится на 0x28 у SOCache(vfunc 2 GetOwner, не менялась аж с 2018), возьмите не по значению, а по указателю(т. к. в сурсе ссылка)
  • ESOCacheEvent — полностью пастим этот enum себе, нас интересует eSOCacheEvent_Incremental.
Кажется, что мы что-то забыли… да, сам SOCache где взять-то? На самом деле, в самих листенерах на 0xA0 лежит на него указатель(я беру через инвентарь). Кстати да, в их массиве на индексе 0 CGCClientSystem, а на 1 CDOTAPlayerInventory.

Теперь наша задача — просто ребилднуть цикл из DispatchSOUpdated, и готово. Оповестится, на самом, только один из листенеров(инвентарь реагирует только на предметы). Full code:
C++:
using SOID_t = uint64_t;

enum EEconTypeID
{
    k_EEconTypeItem = 1,
    k_CEconGameAccountClient = 7,
    k_CDOTAGameAccountClient = 0x7D2,
    k_CDOTAParty = 0x7D3,
    k_CDOTAGameHeroFavorites = 0x7D7,
    k_ECDOTAMapLocationState = 0x7D8,
    k_ECDOTAPlayerChallenge = 0x7DA,
    k_CDOTAGameAccountPlus = 0x7DC
};

enum ESOCacheEvent
{
    eSOCacheEvent_None = 0,
    eSOCacheEvent_Subscribed = 1,
    eSOCacheEvent_Unsubscribed = 2,
    eSOCacheEvent_Resubscribed = 3,
    eSOCacheEvent_Incremental = 4,
    eSOCacheEvent_ListenerAdded = 5,
    eSOCacheEvent_ListenerRemoved = 6,
};

class CGCClientSharedObjectCache : public VClass {
public:
    GETTER(CUtlVector<CGCClientSharedObjectTypeCache*>, GetTypeCacheList, 0x10);
    SOID_t* GetOwner() {
        return MemberInline<SOID_t>(0x28);
    }
};

class ISharedObjectListener : public VClass {
public:
    void DispatchUpdate(SOID_t* soid, void* sharedObj, ESOCacheEvent ev) {
        CallVFunc<1>(soid, sharedObj, ev);
    }
    GETTER(CGCClientSharedObjectCache*, GetSOCache, 0xA0);
};

class CProtobufSharedObjectBase : public VClass {
public:
    google::protobuf::Message* GetPObject() {
        return CallVFunc<9, google::protobuf::Message*>();
    }
};

class CGCClientSharedObjectTypeCache : public VClass {
public:
    CProtobufSharedObjectBase* GetProtobufSO() {
        return *Member< CProtobufSharedObjectBase**>(0x10);
    }
    GETTER(uint32_t, GetEconTypeID, 0x28);
};

class CGCClient : public VClass {
public:
    GETTER(CUtlVector<ISharedObjectListener*>, GetSOListeners, 0x270);

    void DispatchSOUpdated(SOID_t* soid, void* sharedObj, ESOCacheEvent ev) {
        auto listeners = GetSOListeners();
        for (auto& listener : listeners)
            listener->DispatchUpdate(soid, sharedObj, ev);
    };
};

...

void UpdateDotaPlusStatus() {
    auto inventory = Interfaces::GCClient->GetSOListeners()[1];
    auto objCache = inventory->GetSOCache();
    for (auto& typeCache : objCache->GetTypeCacheList()) {
        if (typeCache->GetEconTypeID() != k_CDOTAGameAccountPlus)
            continue;

        auto message = (CDOTAGameAccountPlus*)typeCache->GetProtobufSO()->GetPObject();
        message->set_plus_flags(!Config::UnlockDotaPlus);
        message->set_plus_status(Config::UnlockDotaPlus);
        Interfaces::GCClient->DispatchSOUpdated(objCache->GetOwner(), typeCache->GetProtobufSO(), eSOCacheEvent_Incremental);
    }
}
Не забудьте, что вызывать апдейт нужно ТОЛЬКО в главном потоке игры! Это сделано для синхронизации. Вставьте куда-нибудь в хук RunFrame и будет вам счастье. Выставленный вами статус игра сохранит по выходу, поэтому рекомендую добавить проверку на уже включённый дота плюс, чтобы лишний раз плашку на весь экран не показывать.

Данный гайд был непосредственно проспонсирован Liberalist(https://yougame.biz/threads/242900/#post-2518552) и og(https://yougame.biz/threads/250228/post-2624629), спасибо нашим меценатам! Также не забывайте, что даже если я здесь что-то забыл, в
Пожалуйста, авторизуйтесь для просмотра ссылки.
всё есть.
 
Последнее редактирование:
Пользователь
Статус
Оффлайн
Регистрация
8 Апр 2022
Сообщения
673
Реакции[?]
106
Поинты[?]
69K
харош, жаль не могу лайки ставить
Все они типа uint32, прямо в таком порядке переносим их в .proto-файл и компилируем его(я показывал этот процесс в гайде на NetChannel, если вы не знаете, как):

Код:
message CDOTAGameAccountPlus {
required int32 account_id = 1;
required int32 original_start_date = 2;
required int32 plus_flags = 3;
required int32 plus_status = 4;
required int32 prepaid_time_start = 5;
required int32 prepaid_time_balance = 6;
required int32 next_payment_date = 7;
required int32 steam_agreement_id = 8;
}
это уже есть в протобафе кстати dota_gcmessages_common.pb.h
1680179719166.png
 
Последнее редактирование:
Ревёрсер среднего звена
Пользователь
Статус
Оффлайн
Регистрация
24 Ноя 2022
Сообщения
303
Реакции[?]
108
Поинты[?]
57K
Ревёрсер среднего звена
Пользователь
Статус
Оффлайн
Регистрация
24 Ноя 2022
Сообщения
303
Реакции[?]
108
Поинты[?]
57K
а что насчет шмоток? :roflanBuldiga:
Тут я сосу + лежу
Однажды мы и до этого дойдём, у меня прозрения только периодически случаются, и то, метод в гайде изначально расписан "меценатами", я только расширил кусок кода до полного объяснения структуры
 
Пользователь
Статус
Оффлайн
Регистрация
8 Апр 2022
Сообщения
673
Реакции[?]
106
Поинты[?]
69K
Blyat, и зачем я тогда это делал...
В любом случае, спасибо за вклад в гайд
ну логично впринцепе не может быть такого что протобафа нема а он юзается как то( что даже если команду добавили которая инфу показывает что там лежит)
а шмотки добавить в инвентарь то не так трудно(мне атодота помогэто сделать) а вот одеть вигре трудно
 
Ревёрсер среднего звена
Пользователь
Статус
Оффлайн
Регистрация
24 Ноя 2022
Сообщения
303
Реакции[?]
108
Поинты[?]
57K
Участник
Статус
Оффлайн
Регистрация
23 Май 2019
Сообщения
781
Реакции[?]
331
Поинты[?]
63K
ссылку добавь в гайд
Пожалуйста, авторизуйтесь для просмотра ссылки.
GetEconTypeID() это енум EEconTypeID, и вместо < юзай ==
EEconTypeID можно построить самому из хрефов в client.dll по типу "Create(_CLASS_NAME)"
1680190740276.png
1680190876712.png
 
Сверху Снизу