Protocol Buffers на примере Dota 2

Protocol Buffers на примере Dota 2

DupeBusters

Сегодня в статье разберём, что такое protobuf'ы и как они устроены на примере Dota 2. 

Это не пособие по взлому игры! Любые эксперименты с запросами могут привести к потере предметов или блокировке аккаунта!

НАВИГАЦИЯ

Вопрос–ответ

Ссылки на ресурсы, используемые в этой статье

Наш чат - [ссылка]


Что такое Protocol Buffers простыми словами

Protocol Buffers (protobuf) — это способ упаковки и передачи структурированных данных между вами и сервером (GC)*.

*GC (Game Coordinator) — это сервер Valve, который отвечает за связь клиента с игровой инфраструктурой: подбор матчей, управление инвентарём, статистикой и другими игровыми данными.

Protobuf не передаёт “читаемый текст” — он сжимает данные в бинарный формат, чтобы экономить место и ускорять обмен между клиентом и сервером, в отличие от текстовых форматов вроде JSON.

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

Вот я захожу в игру — и сразу игровой клиент начинает отправлять в сторону сервера Valve десятки разных protobuf-запросов.

Исходящие пакеты ClientToGC (out), а ответы от сервера - ClientFromGC (in).

Начинается всё с самого простого:
клиент говорит серверу «Привет!» — отправляет сообщение ClientToGCHello (k_EMsgGCClientHello), а сервер в ответ шлёт «Добро пожаловать!» — ClientFromGCWelcome (k_EMsgGCClientWelcome).

Мило, правда?

После этого клиент как бы говорит:

«Свет мой зеркальце скажи, да всю правду доложи: я ль сегодня в топе, или снова в лоу-приоре?»

И сервер отвечает целой пачкой сообщений: присылает твой ранг, MMR, статистику, инвентарь, информацию о гильдии, друзьях, список серверов, на которые можно подключиться и т.д.

Вот примеры таких сообщений, даже по названию можно понять, за что они отвечают:

  • ClientToGCGetProfile / ClientFromGCProfileData
  • ClientToGCGetPlayerStats / ClientFromGCPlayerStats
  • ClientToGCGetInventory / ClientFromGCInventory

И всё это происходит буквально за несколько секунд после запуска игры — клиент обменивается десятками protobuf-пакетов туда-сюда. Если бы эти данные передавались не в бинарном виде, а как текст, например в формате JSON, как это делают обычные веб-запросы, серверы Valve просто бы задыхались от нагрузки, и каждый клиент превратился бы в мини DDoS-машину.

Поэтому и были придуманы protobuf'ы, которые преобразуют каждый запрос в бинарный формат, тем самым минимизируя нагрузку на сервер. Изображу схематично для понимания:

🖥️ Игровой клиент (Dota2/CS2/TF2)            ☁️ Game Coordinator (Valve)
───────────────────────────────────       ─────────────────────────────
[0] Устанавливает соединение с GC 
          ↓ ↓ ↓
[1] Создаёт запрос (ClientToGCHello)
{ 
client_version: 1534 
steam_id: 76561198000000000 
language: "russian"
...
}
          ↓ ↓ ↓
[2] Упаковывает в protobuf
(превращает в бинарный формат)
[0A 0C 08 9F D2 A5 8E E4 95 ...]
          ↓ ↓ ↓
[3] Отправка запроса           → → →      [4] Получает бинарный пакет
                                                        ↓ ↓ ↓

                                     [5] Распаковывает по схеме .proto:
                                   message ClientToGCHello {
                                    optional uint64 steam_id = 1;
                                    optional string language = 2;
                                    optional uint32 client_version = 3;
                                    }
                                                        ↓ ↓ ↓

[7] Получает ответ от сервера   ← ← ←         [6] Отправляет ответ 
                               (ClientFromGCWelcome в бинарном формате)

             ↓ ↓ ↓ 

[8] Распаковывает protobuf 
показывает: Welcome! (Соединение с GC установлено ✅)

Я понимаю, что на первый взгляд всё это может показаться чем-то сложным — особенно если ты далёк от программирования. Но камон, бро, ты же дотер 😎 Разбираться в механиках, которые намудрили Valve, у тебя в крови — тут всё по той же логике:
У тебя есть действия, которые ты можешь совершать в игре, и список всех этих действий хранится в специальных файлах .proto — это такие чертежи, где расписано, какие вообще команды существуют, что именно ты можешь “сказать” серверу (GC), какие данные для этого нужны и какой возможен ответ.

У каждого protobuf-сообщения есть свой уникальный номер msgid (message ID) — по нему клиент и сервер определяют, какой тип запроса или ответа был отправлен.

Например, ты можешь отправить серверу ClientHello (4006) — сказать «Привет!», а в ответ получить ClientWelcome (4004) — «Добро пожаловать!». Попросишь свой профиль GetProfileCard (7534) — получишь ProfileCardResponse(7535).
Схема простая — запрос (out) и ответ (in), как обмен репликами в диалоге. 💬

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


Для чего используют protobuf'ы

На практике множество сервисов используют protobuf'ы для получения данных. К примеру, DotaBuff может использовать их для сбора информации о матчах и статистике игроков. Аналогично работают сайты со статистикой в других играх Valve, таких как CS2. Например:
Protobuf-запрос GetProfileCard (7534) чётко показывает активность игрока:

id аккаунтов скрыты

Аккаунт 1 — ни разу не заходил в Dota 2 (карточка пустая)
Аккаунт 2
— заходил в игру, но не имел активности (нулевая статистика)
Аккаунт 3
— активный игрок с активным Dota Plus

Также это используют разработчики читов для анализа аккаунтов, которые скрывают свою статистику — protobuf'ы показывают реальное состояние профиля, игнорируя настройки приватности.

Отправка подарков 🎁

Одно из практических применений protobuf — это автоматическая отправка подарков. Например, магазины коллекционных наборов используют WrapAndDeliverGift (2565), чтобы доставить подарок игроку без необходимости заходить в игру. Разберём, как это работает:

🧩 Из чего состоит protobuf-сообщение

В protobuf есть два основных типа данных:

  • message — это как форма, в которой описано, какие данные передаются. Она может использоваться и для запроса (когда ты заполняешь поля и отправляешь), и для ответа (когда сервер присылает заполненную форму обратно). Например, для “отправить подарок” указываются: что дарим, кому дарим и текст сообщения.
  • enum — это список возможных ответов. Например: результат операции может быть «успешно», «отказано» или «ошибка».

Как отправляется подарок

Запрос: k_EMsgClientToGCWrapAndDeliverGift (2565)

Вы заполняете форму / "бланк" и отправляете его серверу:

message CMsgClientToGCWrapAndDeliverGift {
 optional uint64 item_id = 1;  // asset_id предмета, который вы хотите подарить
 optional uint32 give_to_account_id = 2; // account_id получателя подарка
 optional string gift_message = 3;    // текст сообщения, прикладываемого к подарку
}

Сервер возвращает статус операции (пример):

  • k_EGCMsgResponseOK — подарок успешно отправлен
  • k_EGCMsgResponseDenied — отказано (например, аккаунт имеет ограничение)
  • k_EGCMsgResponseTimeout — сервер не ответил вовремя и т.д.

Также сервер сообщает, сколько подарков ещё можно отправить и через сколько лимит сбросится. Внутри ответа есть дополнительный enum — EGCMsgInitiateTradeResponse(enum)

Он указывает, чем закончилась операция: успехом, отказом или ошибкой.

k_EGCMsgInitiateTradeResponse_Accepted = 0  // принято  
k_EGCMsgInitiateTradeResponse_Declined = 1  // отклонено  
k_EGCMsgInitiateTradeResponse_VAC_Banned = 2 // отправитель забанен
...

Это упрощённый пример! В самом сообщении больше полей и вариантов ответа. Если сложно разобраться в структуре запроса или ответа — просто отправьте protobuf в ChatGPT, он хорошо их знает и объяснит каждое поле простыми словами.


Инструментарий SteamKit2: NetHook2 и NetHook Analyzer

Окей, ты совершил действие в игре — но как его повторить? Как найти именно тот protobuf, который отвечает за это действие? Для этого нам понадобится NetHook2 (DLL) из экосистемы SteamKit2 — но сначала коротко о самой библиотеке:

SteamKit2 — это .NET-библиотека для работы с сетью Steam: она умеет "общаться" с GC, обрабатывать сообщения Steam, работать с сессиями, инвентарём и т. д. Среди контрибьюторов и людей, чьи проекты опираются на эту экосистему, есть знакомые имена: xPaw (создатель steamDB), JustArchi (Archi Steam Farm), DoctorMcKay (создатель node-steam) и ещё куча крутых ребят — так что инструментам доверять можно: это базовая платформа, на которой строятся сотни утилит и ботов.

NetHook2 - [ссылка]

NetHook2 — инструмент/библиотека, цель которого — перехват и логирование сетевых protobuf-сообщений в процессе игры или в Steam-клиенте. Проще: он умеет «подслушивать» обмен между клиентом и Game Coordinator, извлекать msgid, структуру сообщений и их поля и сохранять это в удобном виде для анализа.

Пример инжекта NetHook2.dll

По ссылке на NetHook2 есть пошаговая инструкция по сборке — следуйте ей, чтобы получить готовую NetHook2.dll и собрать NetHook Analyzer (это два разных инструмента).

⚠️ Если при сборке появятся ошибки или какие-то шаги будут непонятны, вставьте текст инструкции или логи сборки в ChatGPT — он поможет разобрать проблемный этап и подскажет, как исправить ошибки.

После успешной компиляции поместите свежесобранную NetHook2.dll в папку Steam согласно инструкции на GitHub.
Команды для запуска/остановки работы DLL (инжекта/эжекта):

rundll32 "NetHook2.dll",Inject
rundll32 "NetHook2.dll",Eject

⚠️ Важно: эти действия вмешиваются в работу клиента! Используйте только тестовые аккаунты!

NetHook2 Dump Analyzer

NetHook Analyzer — инструмент для просмотра, фильтрации и анализа логов, которые генерирует NetHook2. Если NetHook2 собирает данные, то Analyzer переводит их в понятный вид — здесь вы видите, что и когда отправляет клиент, и что в ответ возвращает GC.

Пример открытия сундука в клиенте

Разберём, как пользоваться NetHook Analyzer на примере открытия сундука в игре. Мы видим два запроса, которые отловил NetHook:

Исходящий (out): k_EMsgClientToGCUnlockCrate (2574)

message CMsgClientToGCUnlockCrate {
 optional uint64 crate_item_id = 1;
 optional uint64 key_item_id = 2;
}

Запрос UnlockCrate (Открыть сундук) может содержать всего два параметра:
• crate_item_id — ID сундука
• key_item_id — ID ключа

Так как у нас сундук, который не открывается ключом, клиент отправляет запрос только с crate_item_id. В случае успеха получаем ответ:

Входящий (in): k_EMsgClientToGCUnlockCrateResponse (2575)

message CMsgClientToGCUnlockCrateResponse {
 message Item {
  optional uint64 item_id = 1;
  optional uint32 def_index = 2;
 }
}

Ответ содержит данные о новом предмете:
• item_id — новый уникальный идентификатор конкретного предмета в вашем инвентаре (его asset_id)

• def_index —  внутренний тип предмета, тот самый ID, по которому игра понимает, что это за предмет.

Терминология

При работе с protobuf'ами встречаются специфические термины, которые чаще всего непонятны новичку. Коротко по самым непонятным:
• def_index(item definition) — внутренний ID типа предмета. Каждый предмет в игре описан один раз в конфиге items_game.txt. Пример: def_index 21840

def_index 21840

Под номером 21840 мы видим, что это предмет под названием Fluttering Breeze (аркана-набор на Windranger). Мы видим его тип — bundle (набор, который содержит в себе другие предметы), дату выпуска, к какому ивенту он относится, его редкость, связанные с ним игровые файлы, файлы локализации и т.д.
• item_id / asset_id— уникальный идентификатор конкретного экземпляра предмета в вашем/чужом инвентаре. Встречается в .proto как tool_id, crate_id, key_item_id или любой другой item_id. Пример: откройте свой инвентарь и перетащите предмет в строку браузера — в ссылке будет его уникальный asset_id.

asset_id предмета Fluttering Breeze (аркана-набора на Windranger)

По asset_id торговые сайты понимают, какой именно предмет из вашего инвентаря нужно выставить / забрать, даже если у вас есть несколько одинаковых предметов одного типа.
• event_id — идентификатор игрового события (ивента): Battle Pass, The International, сезонные активности. Посмотреть полный список можно [здесь].
• currency — код валюты, например 1 = USD, 5 = RUB. Используется для запросов, связанных с внутриигровыми покупками.

Отдельно стоит отметить файлик items_game.txt в котором чаще всего содержится просто вся необходимая нам информация.


Для тех, кто выбирает белые паруса вместо чёрных

Да, искать баги и лазейки в механиках Valve может казаться заманчивым занятием — вроде бы и интересно, и, может, где-то даже прибыльно. Но по факту почти все такие тесты и «находки» заканчиваются одинаково: либо теряешь тестовый предмет, либо сам аккаунт.

Реально заработать на этом почти невозможно. Правильный путь — оформить отчёт через HackerOne [ссылка], где Valve официально принимает такие обращения, подтверждённые баги фиксируют и выплачивают законное вознаграждение, поэтому ты получаешь компенсацию легально, без риска быть обвинённым в злоупотреблении и бана аккаунта.

сумма вознаграждений указанная на HackerOne

Обзор на недоработки и забавные костыли

1. Молотком по голове

Для извлечения рун из предметов Dota 2 есть два инструмента (tool):

  1. Artificer's Hammer - достает обычные руны, 1 заряд - 1 руна
  2. Master Artificer's Hammer - 2 действия: достает руны и удаляет предмет (если достать руну не вышло, все равно удалит предмет)

И самое забавное — Master Artificer’s Hammer на самом деле тот же самый Artificer’s Hammer. Оба выполняют одно и то же действие: разделяют гем и предмет. Разница лишь в том, что к Master Hammer добавили сверху функцию удаления предмета, но сам protobuf-запрос у них один и тот же:

запрос CMsgExtractGems (1086)

message CMsgExtractGems {
 optional uint64 tool_item_id = 1;  //asset_id молотка
 optional uint64 item_item_id = 2;  //asset_id предмета с гемом
 optional uint32 item_socket_id = 3 //номер сокета в предмете
}

Проверки на «извлекаемость» гема не было вовсе — сервер просто выполнял запрос. Зато при вставке гема (другой protobuf-запрос) проверка уже есть: вы не сможете вставить редкую руну в обычный сокет.

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

arcana-предмет из которого извлекли редкий гем без удаления самого предмета

Также вы не сможете извлечь руну, которая не существует как отдельный предмет (у неё нет собственного def_index). В таком случае сервер просто вернёт исходный предмет, не создавая ничего нового или просто отклонит запрос.

После фикса из предметов с редкими гемами (призматическими и эфириальными) стало невозможно извлечь любые руны — вместо того чтобы ограничить только редкие, Valve просто накатили общую заглушку. Спустя день ошибку исправили.

попытка достать автограф руну из арканы

2. Хотел как лучше — получилось как всегда: из редкого в хлам

Чуть больше года назад в игру добавили возможность упаковывать предметы обратно в набор. На практике это убило рынок рун: большинство наборов с рунами можно было перепаковать — и тем самым дюпнуть руны.

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

отключенная страница на торговой площадке у гема Explosive Burst

Да, их всё ещё можно купить через запрос или ордер, но страница предмета просто не прогрузится.

пример ордера на руну Explosive Burst

Интереснее всего то, как это “починили”: теперь, если в наборе есть руны, стили или ограничение на трейд — упаковать его просто нельзя. Но что будет, если вынуть руны из unusual-курьера и потом упаковать его в набор? Формально все условия соблюдены, и сервер это разрешит.

Результат: курьер потеряет качество unusual и станет обычным.

история инвентаря, как unusual курьер стал обычным

Понять, почему так происходит, можно по файлу items_game.txt:

набор Collection of Couriers Pack (def_index 20031)

Если открыть описание набора, то можно заметить, что у набора нет поля item_quality, например unusual или genuine, оно просто не прописано. При упаковке предмета GC берёт данные именно из этого шаблона, а раз качество в нём не указано, сервер сбрасывает его до стандартного значения. Поэтому после переупаковки unusual-курьер превращается в обычного, теряя качество unusual и все редкие сокеты.

3. Немного локализации никому не помешает

Оказывается, в Dota 2 можно переименовать предмет так, что игра начнёт применять к нему официальную локализацию.

Если в названии указать локализационный тег, начинающийся с символа #, клиент воспримет его как внутренний ключ и подставит перевод из своих файлов. Например, при переименовании предмета в #DOTA_Set_Fractal_Horns_of_Inner_Abysm он получит официальное имя Fractal Horns of Inner Abysm во всех языках. Самое забавное — это работает даже в интерфейсе Steam. Полный список таких тегов можно найти в файле items_game.txt

А при чём тут protobuf'ы, спросите вы? Всё просто. Через сообщение CMsgClientToGCNameItem (1006) можно переименовать не только обычные предметы, но и, например, наборы (bundle), что в обычном клиенте нельзя сделать.

message CMsgClientToGCNameItem {
    optional uint64 subject_item_id = 1; // asset_id предмета
    optional uint64 tool_item_id = 2;    // asset_id инструмента
    optional string name = 3;            // новое имя предмета
}

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

4. Размножение рун / гемов

Как писал @voidsploit суть дюпа (дублирования рун) была в том, что сервер не проверял, какие именно руны вы вставляете в слоты, достаточно было, чтобы соответствующий asset_id находился в вашем инвентаре. Один запрос позволял поместить несколько рун в предмет, из-за чего одну и ту же руну можно было поместить в несколько слотов и тем самым получить их дубликаты.

Запрос: k_EMsgGCAddItemToSocket (1088)

// Запрос на вставку рун в предмет
message CMsgAddItemToSocket {
    optional uint64 item_item_id = 1; 
    repeated .CMsgAddItemToSocketData gems_to_socket = 2; 
}

// Данные об одной конкретной руне
message CMsgAddItemToSocketData {
    optional uint64 gem_item_id = 1; 
    optional uint32 socket_index = 2 [default = 65535]; 
}

Разбор структуры

  • item_item_id — это asset_id самого предмета (например, арканы или курьера), куда вставляются руны
  • gems_to_socket — это список (специальный тип repeated), в котором можно передать сразу несколько рун
  • gem_item_id — asset_id конкретной руны
  • socket_index — номер слота в предмете, куда она вставляется

Так как вставить можно сразу несколько, разработчики вынесли данные о каждой руне (её ID и слот) в отдельную вложенную структуру CMsgAddItemToSocketData. Поле repeated позволяет передавать список таких структур, то есть вставлять несколько рун одним запросом. Valve не добавили проверку на уникальность рун внутри запроса, и сервер принимал дубли gem_item_id как валидные. 

5. Как разработчики читов абузили токены Crownfall

Совсем недавно был случай, когда разработчик одного популярного чита для Dota 2 обнаружил лазейку в ивенте Crownfall, которая позволяла получать награды сразу за все четыре акта, включая платные ветки. Разберем как это работало:

абуз токенов запросом OverworldTradeTokens (8970)

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

Запрос: k_EMsgClientToGCOverworldTradeTokens (8970)

message CMsgClientToGCOverworldTradeTokens {
    optional uint32 overworld_id = 1;   
    optional .CMsgOverworldTokenQuantity token_offer = 2;
    optional .CMsgOverworldTokenQuantity token_request = 3;
    optional uint32 recipe = 4;
    optional uint32 encounter_id = 5;
}

• overworld_id — ID акта Crownfall

• token_offer — токены, которые игрок отдаёт (что предлагает на обмен).

• token_request — токены, которые игрок хочет получить в результате обмена.

• recipe — ID "рецепта" обмена

• encounter_id —  ...честно, не помню, вроде ID конкретного узла внутри акта (лавка обмена токенов на карте), слишком много времени прошло с момента, как я разбирал этот запрос 😅

Самое забавное, что у Valve было почти полгода, чтобы забанить аккаунты, использовавшие этот абуз, но они так этого и не сделали. В итоге даже совершенно нулевые аккаунты Steam за пару рублей приносили по 5 долларов прибыли, фактически озолотив разработчиков чита.


И это лишь малая часть того, что можно найти. Protocol Buffers — не магия, а просто быстрый способ обмена данными между клиентом и GC. Зная, где искать (.proto, items_game.txt) и какие инструменты использовать (SteamKit2, NetHook2, NetHook Analyzer), можно разобраться в логике запросов и понять, где прячутся «неожиданные» механики. Но знать, как всё устроено — не значит заработать: большинство найденных лазеек бесполезны и чаще заканчиваются блокировкой аккаунта, чем реальной выгодой.


💬 Вопрос–ответ

❓ Откуда берутся файлы .proto в репозитории на GitHub SteamDatabase и файл items_game.txt?

.proto-файлы восстанавливают энтузиасты из сообществ SteamDatabase и SteamKit2 из бинарных данных игр Valve — в клиенте они хранятся только в скомпилированном виде, а «стимовские» схемы берут из официального Steamworks SDK для разработчиков.

Файл items_game.txt — это часть игровых ресурсов Dota 2, лежащий прямо в клиенте игры по пути "Dota 2 beta/game/dota/scripts/items" (внутри VPK-архива pak01_dir.vpk, который можно открыть через GCFScape или аналогичные инструменты).

❓ Если я отправлю одновременно два и более запросов на модификацию предмета, можно ли дюпнуть скин?

Нет. Сервер обрабатывает все запросы последовательно, даже если они отправлены почти одновременно. Все, кроме первого, получат ошибку выполнения.

❓ Почему в статье часто упоминается ChatGPT?

Потому что ChatGPT, в отличие от других «умных» чат-ботов, знает и разбирается в структуре protobuf’ов.

❓ А если установить два разных подключения к GC и отправить запросы с них?

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

❓ У китайцев есть свой клиент Dota 2 (Perfect World). Может, у них особые protobuf’ы для абуза скинов?

Особые protobuf’ы действительно есть — например, те, что связаны с ограничением времени игры для детей и подростков. Но всё, что касается предметов, экономики и ивентов, полностью идентично глобальной версии. Valve давно перестали делать отдельные ивенты или механики для Perfect World внутри клиента.


Ссылки на ресурсы, используемые в этой статье

SteamDatabase / Protobufs — .proto-файлы игр Valve и Steam

SteamKit2 — .NET-библиотека для взаимодействия со Steam и Game Coordinator

NetHook2 & NetHook Analyzer — инструменты для перехвата protobuf-сообщений и их анализа

HackerOne / Valve — официальная платформа для отчётов об уязвимостях и получения вознаграждений

items_game.txt — игровой конфиг Dota 2


Автор: @Noil7 Статья подготовлена для: @DupeBusters

Если остались вопросы — залетайте в [наш чат].

Если вы создаёте собственный контент на основе материалов из этой статьи — будь то видео, посты или гайды — пожалуйста, не забывайте указывать ссылку на источник и автора. Уважение к первоисточнику помогает сохранять качество и мотивирует делиться новой информацией!

Report Page