Инженер-программист
0,0
рейтинг
19 октября 2015 в 13:23

Разработка → Steam Protocol v2

Steam Logo

Завершающая статья цикла, самая интересная и самая объемная:


В статье будут рассмотрены протоколы обмена данными клиента Steam с различными серверами:


В очередной раз напомню, что рассматриваемые протоколы устарели и в настоящее время не используются (за исключением GDS и Config — для совместимости).

Все алгоритмы представлены в моем репозитории.

Весь протокол базируется на Socket'ах, поэтому для его разбора практически везде хватало WireShark'а. Всё описание протокола будет рассматриваться со стороны клиента (на Delphi). Местами будут приводится участки кода серверов на C++.

Почти у всех запросов имеется общий алгоритм
function TSteamNetwork.ConectToServer(Addr: TSockAddr; const QUERY; QSize: uint32; Command: pByte; CSize: uint32; var ReplySize: uint32; IsConfigServer: boolean = false): pByte;
var
  Accept: boolean;
  DestIP: uint32;
  Sock: CSocket;
begin
  result:=nil;
  Sock:=CSocket.Create(SOCKET_IP);
  if not Sock.Connect(Addr) then
    Exit;
  {if (Sock=nil) or (not Sock.Connect(Addr)) then
    Exit;  }
  Sock.SetTimeOut(3000);
  if not Sock.Send(QUERY, QSize) then
    Exit;
  if not Sock.recv(Accept, 1) then
    Exit;
  if IsConfigServer then
    if not Sock.recv(DestIP, 4) then
      Exit;
  if not Accept then
    Exit;
  CSize:=htonl(CSize);
  if not Sock.send(CSize, 4) then
    Exit;
  CSize:=htonl(CSize);
  if not Sock.send(Command^, CSize) then
    Exit;
  Sock.OnLoadingProc:=OnLoadingProc;
  result:=Sock.RecvFromLen(ReplySize);
  Sock.Free;
end;


Данный протокол используется при запросах ко всем списочным серверам (GD, Config, ContentList). В параметре QUERY передается указатель на массив байт, представляющих тип запроса, а в QSize — размер этого массива. В параметре Command передается указатель на массив байт с самой командой, а в CSize — размер этого массива. Переменная ReplySize содержит размер запрашиваемого ответа и после вызова будет равна фактически принятому объему данных. Приведенный выше код можно представить следующим псевдокодом:

Установить соединение
Отправить тип запроса
Принять подтверждение установления связи
Принять от сервера свой внешний IP-адрес, если это запрос к Config Server'у
Если связь не подтверждена, выход
Отправляем размер буфера запроса
Отправляем запрос
Принимаем размер ответа
Принимаем ответ

В некоторых случаях используется RSA-подпись, полученная по следующему алгоритму:

Подпись блока данных для Steam
char *RSASign(RSA *key, char *Mess, UINT32 size, UINT32 sign_size)
{
	char *sign = new char[sign_size];
	memset(sign, 0, sign_size);
	sign[0] = '\x00';
	sign[1] = '\x01';
	memset(&sign[2], 0xff, sign_size-38);
	memcpy(&sign[sign_size-36], "\x00\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14", 0x10);

	void *hash = HashSHA1(Mess, size);
	memcpy((void*)&sign[sign_size-20], hash, 20);
	delete hash;

	RSA_public_encrypt(sign_size, (UCHAR*)sign, (UCHAR*)sign, key, RSA_NO_PADDING);

	return sign;
}

char *RSASignMessage(RSA *key, char *Mess, UINT32 size)
{
	return RSASign(key, Mess, size, 128);
}

char *RSASignMessage1024(RSA *key, char *Mess, UINT32 size)
{
	return RSASign(key, Mess, size, 256);
}



Использовавшиеся ранее ключи для подписи

// MainKeySign
#define MainKeySign_n "86724794f8a0fcb0c129b979e7af2e1e309303a7042503d835708873b1df8a9e307c228b9c0862f8f5dbe6f81579233db8a4fe6ba14551679ad72c01973b5ee4ecf8ca2c21524b125bb06cfa0047e2d202c2a70b7f71ad7d1c3665e557a7387bbc43fe52244e58d91a14c660a84b6ae6fdc857b3f595376a8e484cb6b90cc992f5c57cccb1a1197ee90814186b046968f872b84297dad46ed4119ae0f402803108ad95777615c827de8372487a22902cb288bcbad7bc4a842e03a33bd26e052386cbc088c3932bdd1ec4fee1f734fe5eeec55d51c91e1d9e5eae46cf7aac15b2654af8e6c9443b41e92568cce79c08ab6fa61601e4eed791f0436fdc296bb373"
#define MainKeySign_e "07e89acc87188755b1027452770a4e01c69f3c733c7aa5df8aac44430a768faef3cb11174569e7b44ab2951da6e90212b0822d1563d6e6abbdd06c0017f46efe684adeb74d4113798cec42a54b4f85d01e47af79259d4670c56c9c950527f443838b876e3e5ef62ae36aa241ebc83376ffde9bbf4aae6cabea407cfbb08848179e466bcb046b0a857d821c5888fcd95b2aae1b92aa64f3a6037295144aa45d0dbebce075023523bce4243ae194258026fc879656560c109ea9547a002db38b89caac90d75758e74c5616ed9816f3ed130ff6926a1597380b6fc98b5eeefc5104502d9bee9da296ca26b32d9094452ab1eb9cf970acabeecde6b1ffae57b56401"
#define MainKeySign_d "11"
// NetworkKey
#define NetworkKey_n "bf973e24beb372c12bea4494450afaee290987fedae8580057e4f15b93b46185b8daf2d952e24d6f9a23805819578693a846e0b8fcc43c23e1f2bf49e843aff4b8e9af6c5e2e7b9df44e29e3c1c93f166e25e42b8f9109be8ad03438845a3c1925504ecc090aabd49a0fc6783746ff4e9e090aa96f1c8009baf9162b66716059"
#define NetworkKey_e "11"
#define NetworkKey_d "4ee3ec697bb34d5e999cb2d3a3f5766210e5ce961de7334b6f7c6361f18682825b2cfa95b8b7894c124ada7ea105ec1eaeb3c5f1d17dfaa55d099a0f5fa366913b171af767fe67fb89f5393efdb69634f74cb41cb7b3501025c4e8fef1ff434307c7200f197b74044e93dbcf50dcc407cbf347b4b817383471cd1de7b5964a9d"


General Direcrory Server


Является корневым во всей инфраструктуре и только хранит адреса прочих серверов. Тип запроса — 0x02000000 (используется при вызове ConectToServer). Ответом является список IP-адресов серверов в соответствии с запросом:

  • 4 байта длина списка;
  • N элементов списка вида IP:port (4+2 байта).

Известные запросы и их команды:

  • Список конфигурационных серверов — \x00;
  • Список аутентификационных серверов — \x00\xC4\x1D\x1A\x00;
  • Список корневых контент-серверов — \x06;
  • Список CSER-серверов — \x14.

Что за CSER-сервера — я так и не понял, поэтому больше нигде не упоминается о них.

Config Server


Тип запроса — 0x03000000. Известные запросы:

  • CDR;
  • Версия клиента;
  • Сетевые ключи;
  • Неизвестный запрос.

CDR и версии отдаются в виде BLOB-файлов.

CDR

Команда — 0x02 для получения файла или 0x09 для проверки его обновления. В случае обновления после запроса следует 20 байт хэша SHA-1 для имеющегося файла (или 0x00, если его нет). Ответом является «сырой» файл с CDR, который просто сохраняется на диск и в дальнейшем используется клиентом.

Версия клиента

Команда — 0x01.

Формирование BLOB'а на стороне сервера
	case ACTION_GET_VERSIONS_BLOB:
		#ifdef LOG
		Log(Client->ServerName, "Client %s - Sending Versions Blob", ClientAddr);
		#endif

		blob = new CBLOBFile();
		rootNode = blob->RootNode();
		rootNode->AddString("\x00\x00\x00\x00", 4, "\x00\x00\x00\x00", 4);
		rootNode->AddData("\x01\x00\x00\x00", 4, (char*)&SteamVersion, 4);
		rootNode->AddData("\x02\x00\x00\x00", 4, (char*)&SteamUIVersion, 4);
		rootNode->AddString("\x03\x00\x00\x00", 4, "\x00\x00\x00\x00", 4);
		rootNode->AddString("\x04\x00\x00\x00", 4, "\x14\x00\x00\x00", 4);
		rootNode->AddString("\x05\x00\x00\x00", 4, "\x17\x00\x00\x00", 4);
		rootNode->AddString("\x06\x00\x00\x00", 4, "\x0e\x00\x00\x00", 4);
		rootNode->AddString("\x07\x00\x00\x00", 4, "boo\x00", 4);
		//rootNode->AddString("\x08\x00\x00\x00", 4, "\x5c\x01\x00\x00", 4);
		rootNode->AddString("\x09\x00\x00\x00", 4, "foo\x00", 4);
		rootNode->AddString("\x0a\x00\x00\x00", 4, "\x11\x00\x00\x00", 4);
		rootNode->AddString("\x0b\x00\x00\x00", 4, "bar\x00", 4);
		rootNode->AddString("\x0c\x00\x00\x00", 4, "\x12\x00\x00\x00", 4);
		rootNode->AddString("\x0d\x00\x00\x00", 4, "foo\x00", 4);
		rootNode->AddString("\x0e\x00\x00\x00", 4, "", 0);
		rootNode->AddString("\x0f\x00\x00\x00", 4, "\x50\x01\x00\x00", 4);
		ReplySize = blob->SaveToMem(&reply, false);
		delete blob;

		Socket->SendInt32(ReplySize, true);
		Socket->Send(reply, ReplySize);
		break;


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

Сетевые ключи

Команда — 0x04. Имеет несколько нестандартный протокол обмена данными — размер ответа от сервера имеет размер 2 байта вместо 4-х для остальных ответов. Ответ сервера содержит:

  • Заголовок — \x30\x81\x9d\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7 \x0d\x01\x01\x01\x05\x00\x03\x81\x8b\x00\x30\x81\x87\x02\x81\x81\x00;
  • Нормальную часть NetworkKey;
  • Данные \x02\x01\x11 (последний байт из которого, видимо, является экспоненциальной частью NetworkKey;
  • 256 байт RSA-подписи с использованием MainKeySign. Размер подписи не включается а размер пакета!

Неизвестный запрос

Команда 0x07. Ответ сервера содержит константные 9 байт — \x00\x01\x31\x2d\x00\x00\x00\x01\x2c.

Authentication Server


Самый интересный и сложный для вскрытия протокола сервер. Основной особенностью является использование очень нестандартной системы измерения времени — в наносекундах от РХ. Исходные данные:

  • Имя пользователя;
  • Пароль пользователя.

По имени пользователя составляется Хеш-функция Дженкинса:

Исходный код функции

procedure mix(var a, b, c: uint32); inline;
begin
  dec(a, b);  dec(a, c);  a:=a xor (c shr 13);
  dec(b, c);  dec(b, a);  b:=b xor (a shl 8);
  dec(c, a);  dec(c, b);  c:=c xor (b shr 13);
  dec(a, b);  dec(a, c);  a:=a xor (c shr 12);
  dec(b, c);  dec(b, a);  b:=b xor (a shl 16);
  dec(c, a);  dec(c, b);  c:=c xor (b shr 5);
  dec(a, b);  dec(a, c);  a:=a xor (c shr 3);
  dec(b, c);  dec(b, a);  b:=b xor (a shl 10);
  dec(c, a);  dec(c, b);  c:=c xor (b shr 15);
end;

function jenkinsLookupHash2(Data: pByte; Length: integer; InitVal: uint32): uint32;
var
  a, b, c, len: uint32;
begin
  len:=Length;
  a:=$9e3779b9;
  b:=a;
  c:=InitVal;

  while (len>=12) do
  begin
    inc(a, Data[0] + (Data[1] shl 8) + (Data[2]  shl 16) + (Data[3]  shl 24));
    inc(b, Data[4] + (Data[5] shl 8) + (Data[6]  shl 16) + (Data[7]  shl 24));
    inc(c, Data[8] + (Data[9] shl 8) + (Data[10] shl 16) + (Data[11] shl 24));
    mix(a, b, c);
    Data:=pByte(@Data[12]);
    dec(len, 12);
  end;

  inc(c, length);
  if len>=11 then
    inc(c, Data[10] shl 24);
  if len>=10 then
    inc(c, Data[9] shl 16);
  if len>=9 then
    inc(c, Data[8] shl  8);
  if len>=8 then
    inc(b, Data[7] shl 24);
  if len>=7 then
    inc(b, Data[6] shl 16);
  if len>=6 then
    inc(b, Data[5] shl  8);
  if len>=5 then
    inc(b, Data[4]);
  if len>=4 then
    inc(a, Data[3] shl 24);
  if len>=3 then
    inc(a, Data[2] shl 16);
  if len>=2 then
    inc(a, Data[1] shl  8);
  if len>=1 then
    inc(a, Data[0]);

  mix(a, b, c);
  result:=c;
end;


Общий алгоритм взаимодействия с сервером:

  • Отправляем запрос аутентификации — 5 байт \x00\x00\x00\x00\x04;
  • Отправляем локальный IP-адрес;
  • Отправляем хэш имени пользователя;
  • Принимаем флаг подтверждения соединения. Будет ложь, если пользователя с таким хэшем имени нет в БД сервера;
  • Принимаем внешний IP-адрес клиента;
  • Отправляем пакет с именем пользователя (состав будет рассмотрен далее);
  • Принимаем «соль» для шифрования (8 байт);
  • Формируем пакет аутентификации (рассмотрим далее);
  • Принимаем байт подтверждения аутентификации;
  • Принимаем 8 байт времени сервера (вспоминаем наносекунды с РХ!);
  • Принимаем 8 байт срока действия пакета.

Байт подтверждения аутентификации может принимать следующие состояния:

0x00 — вход успешно выполнен;
0x01 — аккаунт не существует;
0x02 — аккаунт не существует или неверный пароль;
0x03 — слишком большая разница во времени клиента и сервера;
0x04 — аккаунт заблокирован.

Если вход был выполнен, то от сервера принимается пакет с данными пользователя (рассмотрим далее). Пакет с именем пользователя состоит из следующих полей:

  • Размер пакета (4 байта);
  • 1 байт 0x02;
  • Длина имени (2 байта);
  • Имя пользователя;
  • Длина имени (2 байта);
  • Имя пользователя.

Подготовка данных для пакета аутентификации:

  1. Рассчитываем хэш для блока данных, представленного первыми 4-мя байтами «соли», паролем пользователя и последними 4-мя байтами «соли»;
  2. Рассчитываем хэш для блока данных из внешнего и локального IP-адресов клиента;
  3. Формируем блок данных из текущего времени (нс с РХ!!!), локального IP-адреса и 4-х байт \x04\x04\x04\x04;
  4. Первые 8 байт пакета с п.3 xor'им с данными из п.2;
  5. Шифруем блок данных из п.4 алгоритмом AES-CBC с использованием ключа (данные из п.1) и вектора инициализации (любые данные).

Состав пакета аутентификации:

  • Размер пакета — константа, 0x00000036;
  • Вектор инициализации (при формировании данных);
  • 4 байта константы — \x00\x0C\x00\x10. Судя по значениям — это 16-битные размеры данных (IV и зашифрованной части);
  • Зашифрованные данные.

Ответ сервера с данными пользователями имеет следующий формат:

  • Заголовок TTicket_SubHeader;
  • Заголовок TTicketHeader;
  • Вектор инициализации firstIV;
  • Блок данных размером TTicketHeader.SZ2;
  • Второй заголовок TTicketHeader(2);
  • Второй блок данных размером TTicketHeader(2).SZ2;
  • Заголовок TTicket_TestData;
  • Данные TicketSign;
  • Заголовок TTicket_BLOBHeader;
  • Сам BLOB размером TTicket_BLOBHeader.Len2-20-sizeof(TTicket_BLOBHeader);
  • Подпись пакета.

Рассмотрим используемые структуры и их поля.


TTicket_SubHeader = packed record
    nullData1: uint16;
    outerIV: array[0..15] of byte;
    nullData2: uint16;
    nullData3: uint16;
    EncrData: array[0..63] of byte;
    TicketLen: uint16;
  end;

Поле EncrData содержит данные, которые необходимо расшифровать алгоритмом AES-CBC с использованием ключа (п.1 из подготовки данных аутентификации) и вектора инициализации TTicket_SubHeader.outerIV. На выходе получим заголовок UserHeader типа TTicket_UserHeader.


TTicket_UserHeader = packed record
    InnerKey: array[0..15] of byte;
    Dummy1: uint16;
    SteamID: uint64;
    Servers: packed record
      IP1: uint32;
      Port1: uint16;
      IP2: uint32;
      Port2: uint16;
    end;
    CurrentTime: uint64;
    ExpiredTime: uint64;
    Dummy2: array[0..9] of byte;
  end;

Назначение всех полей понятно из названия, а непонятные мне и самому непонятны. В конце-концов, большинство этих данных названо чисто интуитивно, исходя из их дальнейшего применения. Поле InnerKey будет использовано далее.


TTicketHeader = record
    SZ1,
    SZ2: uint16;
  end;


TTicket_TestData = packed record
    len: uint16;  //always $1000
    SteamID: uint64;
    ExternalIP: uint32;
  end;


TTicket_BLOBHeader = packed record
    NodeHeader: uint16;
    Len2: uint32;
    ZerosSize: uint32;
    BLOBLen: uint32;
    InnerIV: array[0..15] of byte;
  end;

Как упоминалось ранее, после этого заголовка идет блок данных с зашифрованным BLOB-файлом. Зашифрован он алгоритмом AES-CBC с использованием ключа UserHeader.InnerKey и вектора инициализации TTicket_BLOBHeader.InnerIV.

Content Lists Server


Хранит списки контент-серверов для различных файлов. Тип запроса — 0x0200000000. Имеет 2 запроса. Отличающихся только вторым и третьим байтами команды:

  • Список серверов для архива — 0x0000;
  • Список серверов для служебных архивов — 0x0100.

Общий формат команд для этого сервера:

  • 1 байт — 0x00;
  • 2 байта уточнения команды (\x00\x00 или \x0100);
  • 4 байта — ID запрашиваемого архива;
  • 4 байта — версия запрашиваемого архива;
  • 2 байта — максимальное количество серверов в ответе;
  • 4 байта — регион;
  • 4 байта — 0xFF.

В ответе сервера содержится список следующих элементов:


TContentListEntry = packed record
    ID: uint32;     // нагрузка на сервер???
    ClientUpdateIP: uint32;
    ClientUpdatePort: uint16;
    ContentServerIP: uint32;
    ContentServerPort: uint16;
  end;

Поле ID менялось для одних и тех же серверов, из чего я сделал вывод, что это нагрузка на данный сервер. Дальше следует две пары IP:port, которые почти всегда совпадали. Почему 2 пары — без понятия.

Content Server


Самый сложный для взаимодействия сервер, хранящий непосредственно контент игр и файлы самого Steam'а. Имеет свой протокол и обрабатывает 2 запроса:

  • Загрузка служебного архива (файлы клиента);
  • Загрузка игрового архива.

Рассмотрим загрузку служебного архива:

  • Отправляем команду — \x03\x00\x00\x00;
  • Принимаем флаг установления соединения;
  • Принимаем запрашиваемый файл;
  • Принимаем RSA-подпись файла.

Запрос файла и подписи происходит по имени файла (для подписи он получается "<имя файла>_rsa_signature"):

  • 4 байта — размер пакета (длина имени файла + 16);
  • 4 байта — команда \x00\x00\x00\x00;
  • 4 байта — \x00\x00\x00\x00;
  • 4 байта — длина имени файла;
  • Само имя файла.

Ответом на каждый такой запрос является запрашиваемый файл.

Исходный код описанного алгоритма

function TSteamNetwork.Content_DownloadPackage(Name: AnsiString; FileName: string): ENetWorkResult;
var
  Accepted: boolean;
  Sock: CSocket;
  PacketSize, Request, MessSize: uint32;
  Data, Mess: pByte;
  str: TStream;
  Addr: TSockAddr;

  procedure ProcPackage(N, FN: AnsiString);
  begin
    PacketSize:=htonl(4+8+Length(N)+4);
    if not Sock.Send(PacketSize, 4) then
      Exit;
    if not Sock.Send(CS_PACKAGE_GET_FILE, 4) then
      Exit;
    Request:=0;
    if not Sock.Send(Request, 4) then
      Exit;
    Request:=htonl(Length(N));
    if not Sock.Send(Request, 4) then
      Exit;
    if not Sock.Send(N[1], Length(N)) then
      Exit;
    Request:=0;
    if not Sock.Send(Request, 4) then
      Exit;

    if not Sock.Recv(PacketSize, 4) then
      Exit;
    Data:=Sock.RecvFromLen(PacketSize);
  end;
begin
  result:=eConnectionError;

  Addr:=ContentList_GetContentServer();
  if Addr.sin_addr.S_addr=0 then
    Exit;

  Sock:=CSocket.Create(SOCKET_IP);
  Sock.SetTimeOut(3000);
  if (Sock=nil) or (not Sock.Connect(Addr)) then
    Exit;

  if not Sock.Send(CS_PACKAGE_QUERY, 4) then
    Exit;
  if not Sock.Recv(Accepted, 1) then
    Exit;
  if not Accepted then
  begin
    result:=eServerReset;
    Exit;
  end;

  ProcPackage(Name, Wide2Ansi(FileName));
  MessSize:=PacketSize;
  Mess:=Data;
  ProcPackage(Name+'_rsa_signature', '');

  Sock.Free;

  if RSACheckSign(NetWorkKeySign, Data, Mess, MessSize, 128) then
  begin
    str:=TStream.CreateWriteFileStream(FileName);
    str.Write(Mess^, MessSize);
    str.Free;
    result:=eOK;
  end
    else result:=eSignError;

  FreeMem(Mess, MessSize);
  FreeMem(Data, 128);
end;


Загрузка игрового архива значительно сложнее и проходит множество этапов:

  • Отправляем команду \x07\x00\x00\x00;
  • Принимаем флаг установления соединения;
  • Отправляем 5 байт команды получения баннера — \x00\x00\x00\x00\x00;
  • Получаем флаг подтверждения;
  • Принимаем длину строки;
  • Принимаем строку с ссылкой;
  • Отправляем команду открытия архива — \x09;
  • Отправляем 8 байт \x00;
  • Отправляем 4 байта ID запрашиваемого архива;
  • Отправляем 4 байта версии запрашиваемого архива;
  • Принимаем 4 байта ID подключения;
  • Принимаем 4 байта MessageID;
  • Принимаем флаг подтверждения;
  • Принимаем 4 байта CacheID;
  • Принимаем 4 байта ManifestCheck;
  • Отправляем команду получения манифеста — \x04;
  • Отправляем 4 байта CacheID;
  • Отправляем 4 байта MessageID;
  • Принимаем блока данных с манифестом (часть заголовков GCF/NCF-архива);
  • Отправляем команду получения контрольных сумм — \x06;
  • Отправляем 4 байта CacheID;
  • Отправляем 4 байта MessageID;
  • Принимаем блока данных с контрольными суммами (часть заголовков GCF/NCF-архива);
  • Отправляем команду получения файла — \x07;
  • Отправляем 4 байта CacheID;
  • Отправляем 4 байта MessageID;
  • Отправляем 4 байта — индекс файла в архиве (начинается с 0);
  • Отправляем 4 байта — номер первой требуемой части (размер 1 части равен 0x00002000 — в соотв. с описанным ранее размером сектора);
  • Отправляем 4 байта — количество запрашиваемых частей;
  • Принимаем 4 байта CacheID;
  • Принимаем 4 байта MessageID;
  • Принимаем флаг подтверждения;
  • Принимаем количество отправляемых пользователю частей;
  • Принимаем указанное количество частей (рассмотрим далее);
  • Отправляем команду закрытия файла — \x03;
  • Принимаем 4 байта CacheID;
  • Принимаем 4 байта MessageID;
  • Принимаем флаг подтверждения;
  • Закрываем соединение.

Прием 1 части файла:

  1. Принимаем 4 байта CacheID;
  2. Принимаем 4 байта MessageID;
  3. Принимаем 4 байта размера части;
  4. Принимаем 4 байта CacheID;
  5. Принимаем 4 байта MessageID;
  6. Принимаем 4 байта размера блока;
  7. Принимаем блок указанного размера;
  8. Переходим на п.4, пока размер принятых блоков меньше размера части.

Исходный код описанного алгоритма

function TSteamNetwork.Content_DownloadGCF(AppID, Version: uint32): ENetWorkResult;
var
  Accepted: boolean;
  Sock: CSocket;
  i: integer;
  ConnID, MessageID, MsgID, BlockSize, CacheID, ManifestCheck: uint32;
  ManifestSize, ChecksumSize, PS: uint32;
  Manifest, Checksum: pByte;
  UpdateList: puint32;
  str: TStream;
  GCF: TGCFFile;
  q: array[0..HL_GCF_CHECKSUM_LENGTH*2] of byte;
  Addr: TSockAddr;

  function RecvPacket(var Size: uint32): pByte;
  var
    Pos, recived: uint32;
  begin
    result:=nil;
    if not Sock.Recv(CacheID, 4) then
      Exit;
    if not Sock.Recv(MsgID, 4) then
      Exit;
    if not Sock.Recv(Accepted, 1) then
      Exit;
    if Accepted then
      Exit;
    if not Sock.Recv(Size, 4) then
      Exit;

    if not Sock.Recv(CacheID, 4) then
      Exit;
    if not Sock.Recv( MsgID, 4) then
      Exit;
    if not Sock.Recv(BlockSize, 4) then
      Exit;
    Pos:=0;
    Size:=htonl(Size);
    BlockSize:=htonl(BlockSize);
    GetMem(result, Size);
    repeat
      recived:=Sock.Recvi(pByte(result+Pos)^, BlockSize);
      inc(Pos, recived);
    until (Pos>=Size) or (recived=0);
  end;

  function GetBannerURL(): boolean;
  var
    URL: pAnsiChar;
    Len: uint16;
  begin
    result:=false;
    FillChar(Q[0], 9, 0);
    Q[0]:=CS_STORAGE_BANNER_URL;
    Sock.SendFromLen(5, @Q[0]);
    if not Sock.Recv(Accepted, 1) then
      Exit;
    URL:=pAnsiChar(Sock.RecvFromLenShort(Len));
    //URL:=pAnsiChar(URL+#0);
    Writeln('Banner URL: "'+URL+'"');
    FreeMem(URL, Len);
    result:=true;
  end;

  function Open(): boolean;
  begin
    result:=false;
    AppID:=htonl(AppID);
    Version:=htonl(Version);

    FillChar(Q[0], 17, 0);
    Q[0]:=CS_STORAGE_OPEN;
    Move(ConnID, Q[1], 4);
    Move(MessageID, Q[5], 4);
    Move(AppID, Q[9], 4);
    Move(Version, Q[13], 4);
    if not Sock.SendFromLen(17, @Q[0]) then
      Exit;
    if not Sock.Recv(ConnID, 4) then
      Exit;
    if not Sock.Recv(MsgID, 4) then
      Exit;
    if not Sock.Recv(Accepted, 1) then
      Exit;
    if Accepted then
      Exit;
    if not Sock.Recv(CacheID, 4) then
      Exit;
    if not Sock.Recv(ManifestCheck, 4) then
      Exit;

    AppID:=htonl(AppID);
    Version:=htonl(Version);
    result:=true;
  end;

  function OpenEx(): boolean;
  begin
    //result:=false;
    result:=true;
  end;

  function GetManifest(): boolean;
  begin
    result:=false;
    FillChar(Q[0], 9, 0);
    Q[0]:=CS_STORAGE_GET_MANIFEST;
    Move(CacheID, Q[1], 4);
    Move(MessageID, Q[5], 4);
    if not Sock.SendFromLen(9, @Q[0]) then
      Exit;

    Manifest:=RecvPacket(ManifestSize);
    result:=(Manifest<>nil);
    inc(MessageID);
  end;

  function GetChecksum(): boolean;
  begin
    result:=false;
    FillChar(Q[0], 9, 0);
    Q[0]:=CS_STORAGE_GET_CHECKSUM;
    Move(CacheID, Q[1], 4);
    Move(MessageID, Q[5], 4);
    if not Sock.SendFromLen(9, @Q[0]) then
      Exit;

    Checksum:=RecvPacket(ChecksumSize);
    result:=(Checksum<>nil);
    inc(MessageID);
  end;

  function GetListUpdateFiles(): boolean;
  var
    r: byte;
    Count: uint32;
  begin
    result:=false;
    FillChar(Q[0], 13, 0);
    Q[0]:=CS_STORAGE_GET_LIST_UPDATE_FILES;
    Move(CacheID, Q[1], 4);
    Move(MessageID, Q[5], 4);
    Move(#0#0#0#0, Q[9], 4);
    Sock.SendFromLen(13, @Q[0]);

    if not Sock.Recv(CacheID, 4) then
      Exit;
    if not Sock.Recv(MsgID, 4) then
      Exit;
    if not Sock.Recv(r, 1) then
      Exit;
    if not Sock.Recv(Count, 4) then
      Exit;
    if Count=0 then
      Exit;

    if not Sock.Recv(CacheID, 4) then
      Exit;
    if not Sock.Recv(MsgID, 4) then
      Exit;
    UpdateList:=puint32(Sock.RecvFromLen(PS));

    str:=TStream.CreateWriteFileStream('.\package\7.diff');
    str.Write(UpdateList^, PS);
    str.Free;
    result:=true;
    inc(MessageID);
  end;

  function RecvChunk(var Size: uint32): pByte; //inline;
  var
    len, recvd: uint32;
  begin
    result:=nil;
    Size:=0;
    if not Sock.Recv(CacheID, 4) then
      Exit;
    if not Sock.Recv(MsgID, 4) then
      Exit;
    if not Sock.Recv(Size, 4) then
       Exit;
    Size:=htonl(Size);
    len:=0;
    GetMem(result, Size);
    repeat
      if not Sock.Recv( CacheID, 4) then
        Exit;
      if not Sock.Recv(MsgID, 4) then
        Exit;
      if not Sock.Recv(BlockSize, 4) then
        Exit;
      BlockSize:=htonl(BlockSize);
      write(BlockSize);
      recvd:=0;
      repeat
        inc(recvd, Sock.Recvi(pByte(result+len)^, BlockSize));
      until recvd=BlockSize;
      if recvd=uint32(SOCKET_ERROR) then
        break;
      inc(len, recvd);
    until len>=Size;
    inc(MessageID);
  end;

  function GetFile(Idx: uint32): ENetWorkResult;
  var
    Start, Count, i: integer;
    FileIdx, IsCompressed, ChunkSize, UncSize: uint32;
    Chunk: pByte;
  begin
    result:=eConnectionError;
    str:=GCF.OpenFile(Idx, ACCES_WRITE);
    Start:=0;
    Count:=GCF.ItemSize[Idx].Size div HL_GCF_CHECKSUM_LENGTH;  // размер в блоках = HL_GCF_CHECKSUM_LENGTH
    if GCF.ItemSize[Idx].Size mod HL_GCF_CHECKSUM_LENGTH>0 then
      inc(Count);

    FileIdx:=htonl(GCF.CheckIdx(Idx));
    Start:=htonl(Start);
    Count:=htonl(Count);

    FillChar(Q[0], 22, 0);
    Q[0]:=CS_STORAGE_GET_FILE;
    Move(CacheID, Q[1], 4);
    Move(MessageID, Q[5], 4);
    Move(FileIdx, Q[9], 4);
    Move(Start, Q[13], 4);
    Move(Count, Q[17], 4);
    Q[21]:=$00;
    if not Sock.SendFromLen(22, @Q[0]) then
      Exit;

    if not Sock.Recv(CacheID, 4) then
      Exit;
    if not Sock.Recv(MsgID, 4) then
      Exit;
    if not Sock.Recv(Accepted, 1) then
      Exit;
    if Accepted then
      Exit;
    if not Sock.Recv(Count, 4) then
      Exit;
    if not Sock.Recv(IsCompressed, 4) then
      Exit;

    Count:=htonl(Count);
    IsCompressed:=htonl(IsCompressed);

    result:=eOK;
    for i:=0 to Count-1 do
    begin
      Chunk:=RecvChunk(ChunkSize);

      UncSize:=HL_GCF_CHECKSUM_LENGTH;
      if (IsCompressed=1) then
      begin
        // zipped
        if uncompress(@q[0], UncSize, Chunk, ChunkSize)<>0 then
        begin
          result:=eZLibError;
          break;
        end;
        str.Write(q[0], UncSize);
      end else if (IsCompressed=2) then
      begin
        writeln(HL_GCF_CHECKSUM_LENGTH-ChunkSize);
        str.Write(Chunk^, ChunkSize);
        FillChar(q[0], HL_GCF_CHECKSUM_LENGTH, 0);
        str.Write(q[0], HL_GCF_CHECKSUM_LENGTH-ChunkSize);
      end
        else str.Write(Chunk^, ChunkSize);
      FreeMem(Chunk, ChunkSize);
    {$IFDEF DEBUG_CS_SLEEP}
        sleep(300);
    {$ENDIF}
    end;

    if (IsCompressed=2) and (CDR<>nil) then
      begin
        // encrypted (and zipped?)
        //GCF.DecryptItem(Idx, CDR.AppRecord[AppID].DecryptKey(Version));
      end;

    str.Free;
  end;

  function Close(): boolean;
  begin
    result:=false;
    FillChar(Q[0], 9, 0);
    Q[0]:=CS_STORAGE_CLOSE;
    Move(CacheID, Q[1], 4);
    Move(MessageID, Q[5], 4);
    Sock.SendFromLen(9, @Q[0]);

    if not Sock.Recv(CacheID, 4) then
      Exit;
    if not Sock.Recv(MsgID, 4) then
      Exit;
    if not Sock.Recv(Accepted, 1) then
      Exit;
    result:=true;
  end;
begin
  result:=eConnectionError;

  Addr:=ContentList_GetContentServer(AppID, Version, REGION_Rest_World);
  if Addr.sin_addr.S_addr=0 then
    Exit;

  Sock:=CSocket.Create(SOCKET_IP);
  Sock.SetTimeOut(3000);
  if (Sock=nil) or (not Sock.Connect(Addr)) then
    Exit;

  if not Sock.Send(CS_STORAGE_QUERY, 4) then
    Exit;
  if not Sock.Recv(Accepted, 1) then
    Exit;
  if not Accepted then
  begin
    result:=eServerReset;
    Exit;
  end;

  ConnID:=0;
  MessageID:=0;

  writeln('Get banner URL');
  if not GetBannerURL() then
  begin
    Sock.Free;
    Exit;
  end;
  writeln('Open');
  if not Open() then
  begin
    Sock.Free;
    Exit;
  end;
  writeln('Get manifest');
  if not GetManifest() then
  begin
    Sock.Free;
    Exit;
  end;
  writeln('Get checksums');
  if not GetChecksum() then
  begin
    Sock.Free;
    Exit;
  end;
  //RSASignMessage(NetWorkKeySign, Checksum, ChecksumSize-128);
  {if not GetListUpdateFiles() then
  begin
    closesocket(Sock);
    Exit;
  end;}

  GCF:=TGCFFile.Create('.\storage\common\'+Int2Str(AppID));
  GCF.LoadFromMem(Manifest, Checksum, ManifestSize, ChecksumSize, false);
  GCF.SaveToFile('.\storage\'+Int2Str(AppID)+'.ncf');

  for i:=0 to GCF.ItemsCount-1 do
    if (GCF.IsFile(i)) and (GCF.GetCompletion(i)<1) then
    begin
      Writeln(GCF.ItemPath[i]);
      {$IFDEF DEBUG_CS_SLEEP}
      sleep(100);
      {$ENDIF}
      if GetFile(i)<>eOK then
        break;
    end;

  GCF.Free;

  Close();
  Sock.Free;

  str:=TStream.CreateWriteFileStream('.\storage\'+Int2Str(AppID)+'.manifest');
  str.Write(Manifest^, ManifestSize);
  str.Free;
  FreeMem(Manifest, ManifestSize);
  str:=TStream.CreateWriteFileStream('.\storage\'+Int2Str(AppID)+'.checksum');
  str.Write(Checksum^, ChecksumSize);
  str.Free;
  FreeMem(Checksum, ChecksumSize);

  result:=eOK;
end;


Заключение


Вот и подошел к концу цикл статей о устаревшей части Steam'а. Единственное, что до сих пор активно используется — VDF-архивы.

В следующей статье могу затронуть более актуальную информацию — SteamAPI (steam.dll) и SteamClienAPI (steamclient.dll). И если вторая будет рассматриваться со стороны получения информации о пользователе в рамках разрешенного, то первая будет рассматриваться со стороны простейшего эмулятора этого API. Решение о том, писать об этом или нет — принимать сообществу.
Писать ли статьи о SteamAPI и SteamClientAPI?

Проголосовало 250 человек. Воздержалось 54 человека.

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

Андрей @andreili
карма
45,2
рейтинг 0,0
Инженер-программист
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (10)

  • 0
    Спасибо. Теперь понятно, почему стим сегодня весь день лежал.
  • 0
    А зачем этот? Есть же SteamKit и для формирования пакетов используется ProtoBuf. Я как-то занимался генерацией js классов/объектов из описательных файлов ProtoBuf и что-то забросил(стало не нужно)
    • +1
      Вы внимательно читали? Еще в первой статье я упоминал, что ProtoBuf используется в новых версиях протокола (начиная с v3), а в данной статье описана v3, в которой данные идут в сыром виде.
      Данная статья опубликована просто для ознакомления с инфраструктурой серверов (она осталась примерно такой же, только протокол сменили).
      • +1
        в статье v2 (просто опечатка в ответе)
        • 0
          Да, сори, опечатался.
  • 0
    А откуда прив ключи выдрали? Или они были одни и те же и в клиенте и в сервере?
    • 0
      Ключики выдрали еще до меня — выловил их у тимы RevCrew. Откуда они их вытащили — я вообще без понятия, поскольку часть этих ключей для клиента недоступна никоим образом.
      • 0
        Ага. Или удалось сбрутить или стырить, ок .:)
        • 0
          Брут RSA в 512 и 1024 бит? оО Это же сколько брутить пришлось бы? :)
          Скорее просто утекли с сырцами HL2 в бородатые годы :)
          • 0
            В теории, RSA512 с шансом в 1% вскрывался за 2 месяца эллиптикой. К сожалению, без гарантий. То есть если не вскрылось за 2 месяца — дальше нет смысла, убьёшь годы но гарантий по прежнему нет.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.