Como analisar o protocolo de rede MMORPG móvel

Ao longo dos anos jogando um MMORPG móvel, ganhei alguma experiência em engenharia reversa, que gostaria de compartilhar em uma série de artigos. Tópicos de exemplo:

  1. Analise o formato da mensagem entre servidor e cliente.
  2. Escrevendo um aplicativo de escuta para visualizar o tráfego do jogo de maneira conveniente.
  3. Interceptação de tráfego e sua modificação usando um servidor proxy não HTTP.
  4. Os primeiros passos para o seu próprio servidor ("pirateado").

Neste artigo, discutirei a análise do formato da mensagem entre o servidor e o cliente . Interessado, peço gato.

Ferramentas necessárias


Para poder repetir as etapas descritas abaixo, você precisará:

  • PC (fiz no Windows 7/10, mas o MacOS também pode funcionar se os itens abaixo estiverem disponíveis);
  • Wireshark para análise de pacotes;
  • 010Editor para analisar pacotes por modelo (opcional, mas permite descrever rápida e facilmente o formato da mensagem);
  • o próprio dispositivo móvel com o jogo.

Além disso, é muito desejável ter em mãos dados legíveis do jogo, como uma lista de objetos, criaturas etc. com seus identificadores. Isso simplifica bastante a pesquisa de pontos-chave nos pacotes e, às vezes, permite filtrar a mensagem desejada em um fluxo constante de dados.

Análise formato de mensagem entre servidor e cliente


Para começar, precisamos ver o tráfego do dispositivo móvel. É muito simples fazer isso (embora eu tenha tomado essa decisão óbvia por muito tempo): em nosso PC, criamos um ponto de acesso Wi-Fi, conectamos a ele a partir de um dispositivo móvel, selecionamos a interface desejada no Wireshark - e temos todo o tráfego móvel diante de nossos olhos.

Após entrar no jogo e aguardar algum tempo para que as solicitações não relacionadas ao servidor do jogo sejam interrompidas, é possível observar a seguinte imagem:


Nesta fase, já podemos usar os filtros Wireshark para ver apenas pacotes entre o jogo e o servidor, bem como apenas a carga útil:

tcp && tcp.payload && tcp.port == 44325 

Se você estiver em um local calmo, longe de outros jogadores e do NPC, e não fizer nada, poderá ver constantemente mensagens repetidas do servidor e do cliente (tamanho 76 e 84 bytes, respectivamente). No meu caso, o número mínimo de pacotes diferentes foi enviado na tela de seleção de personagens.


A frequência da solicitação do cliente é muito semelhante ao ping. Vamos receber algumas mensagens para verificação (três grupos, acima, é uma solicitação de um cliente, abaixo, é a resposta de um servidor):


A primeira coisa que chama sua atenção é a identidade dos pacotes. Os 8 bytes adicionais na resposta quando convertidos para o sistema decimal são muito semelhantes ao registro de data e hora em segundos: 5CD008F8 16 = 1557137656 10 (do primeiro par). Verificamos o relógio - sim, é. Os 4 bytes anteriores correspondem aos últimos 4 bytes na solicitação. Ao traduzir, obtemos: A4BB 16 = 42171 10 , que também é muito semelhante ao tempo, mas em milissegundos. Ele coincide aproximadamente com o tempo desde o lançamento do jogo, e provavelmente é.

Resta considerar os 6 primeiros bytes da solicitação e resposta. É fácil perceber a dependência do valor dos quatro primeiros bytes da mensagem (vamos chamar esse parâmetro L ) no tamanho da mensagem: a resposta do servidor é superior a 8 bytes, o valor de L também aumentou 8, no entanto, o tamanho do pacote é mais 6 bytes do valor de L nos dois casos. Você também pode observar que os dois bytes após L retêm seu valor nas solicitações do cliente e do servidor, e, como seu valor difere em um, podemos dizer com confiança que esse é o código de mensagem C (os códigos de mensagem associados provavelmente serão determinados sequencialmente). A estrutura geral é clara o suficiente para escrever um modelo mínimo para 010Editor:

  • primeiros 4 bytes - L - tamanho da carga útil da mensagem;
  • próximos 2 bytes - C - código da mensagem;
  • carga própria.

 struct Event { uint payload_length <bgcolor=0xFFFF00, name="Payload Length">; ushort event_code <bgcolor=0xFF9988, name="Event Code">; byte payload[payload_length] <name="Event Payload">; }; 

Portanto, o formato da mensagem de ping do cliente: envia a hora local do ping; formato de resposta do servidor: envie a mesma hora e hora do envio da resposta em segundos. Não parece difícil, certo?

Vamos tentar tornar um exemplo mais complicado. Parado em um local silencioso e escondendo os pacotes de ping, você pode encontrar mensagens de teleporte e criar itens (artesanato). Vamos começar com o primeiro. Possuindo os dados do jogo, eu sabia qual o valor do ponto de teleporte a procurar. Para testes, usei pontos com os valores 0x2B , 0x67 , 0x6B e 0x1AF . Compare com os valores nas mensagens: 0x2B , 0x67 , 0x6B e 0x3AF :


A bagunça. Dois problemas são visíveis:

  1. valores não são 4 bytes, mas de tamanhos diferentes;
  2. nem todos os valores correspondem aos dados dos arquivos e, nesse caso, a diferença é 128.

Além disso, ao comparar com o formato ping, você pode notar alguma diferença:

  • incompreensível 0x08 antes do valor esperado;
  • Um valor de 4 bytes, 4 menor que L (vamos chamá-lo de D Este campo não aparece em todas as mensagens, o que é um pouco estranho, mas onde está, a dependência L - 4 = D preservada. Por um lado, para mensagens com uma estrutura simples (como ping) não é necessária, mas por outro - parece inútil).

Acho que alguns de vocês já poderiam ter adivinhado o motivo da incompatibilidade dos valores esperados, mas continuarei. Vamos ver o que está acontecendo no ofício:


Os valores esperados de 14183 e 14285 também não correspondem aos reais 28391 e 28621, mas a diferença aqui já é muito maior que 128. Após muitos testes (inclusive com outros tipos de mensagens), verificou-se que quanto maior o número esperado, maior a diferença entre o valor no pacote. O que foi estranho foi que os valores de até 128 permaneceram sozinhos. Entendi, o que houve? A situação óbvia é para quem já encontrou isso e, sem saber, tive que desmontar esse "código" por dois dias (no final, a análise dos valores em formato binário ajudou no "hacking"). O comportamento descrito acima é chamado de Quantidade de comprimento variável - uma representação de um número que usa um número indefinido de bytes, em que o oitavo bit de um byte (o bit de continuação) determina a presença do próximo byte. A partir da descrição, é óbvio que a leitura do VLQ só é possível na ordem Little-Endian. Coincidentemente, todos os valores nos pacotes estão nessa ordem.

Agora que sabemos como obter o valor inicial, podemos escrever um modelo para o tipo:

 struct VLQ { local char size = 1; while(true) { byte obf_byte; if ((obf_byte & 0x80) == 0x80) { size++; } else { break; } } FSeek(FTell() - size); byte bytes[size]; local uint64 _ = FromVLQ(bytes, size); }; 

E a função de converter uma matriz de bytes em um valor inteiro:

 uint64 FromVLQ(byte bytes[], char size) { local uint64 source = 0; local int i = 0; local byte x; for (i = 0; i < size; i++) { x = bytes[i]; source |= (x & 0x7F) * Pow(2, i * 7); //   <<   , ..     ,  uint32,        uint64 if ((x & 0x80) != 0x80) { break; } } return source; }; 

Mas voltando à criação do sujeito. Novamente D aparece e novamente 0x08 na frente do valor alterado. Os últimos dois bytes da mensagem 0x10 0x01 são suspeitosamente semelhantes ao número de itens de criação, onde 0x10 tem uma função semelhante a 0x08 mas ainda incompreensível. Mas agora você pode escrever um modelo para este evento:

 struct CraftEvent { uint data_length <bgcolor=0x00FF00, name="Data Length">; byte marker1; VLQ craft_id <bgcolor=0x00FF00, name="Craft ID">; byte marker2; VLQ quantity <bgcolor=0x00FF00, name="Craft Quantity">; }; 

O que ficaria assim:


E ainda assim, esses eram exemplos simples. Será mais difícil analisar o evento do movimento do personagem. Que informações esperamos ver? No mínimo, as coordenadas do personagem para onde ele está olhando, velocidade e estado (em pé, correndo, pulando etc.). Como nenhuma linha é visível na mensagem, o estado é provavelmente descrito por meio de enum . Enumerando as opções, comparando-as simultaneamente com os dados dos arquivos do jogo, bem como através de muitos testes, você pode encontrar três vetores XYZ usando este modelo complicado:

 struct MoveEvent { uint data_length <bgcolor=0x00FF00, name="Data Length">; byte marker; VLQ move_time <bgcolor=0x00FFFF>; FSkip(2); byte marker; float position_x <bgcolor=0x00FF00>; byte marker; float position_y <bgcolor=0x00FF00>; byte marker; float position_z <bgcolor=0x00FF00>; FSkip(2); byte marker; float direction_x <bgcolor=0x00FFFF>; byte marker; float direction_y <bgcolor=0x00FFFF>; byte marker; float direction_z <bgcolor=0x00FFFF>; FSkip(2); byte marker; float speed_x <bgcolor=0x00FFFF>; byte marker; float speed_y <bgcolor=0x00FFFF>; byte marker; float speed_z <bgcolor=0x00FFFF>; byte marker; VLQ character_state <bgcolor=0x00FF00>; }; 

Resultado visual:


Os três verdes acabaram sendo as coordenadas do local, os três amarelos, provavelmente, mostram para onde o personagem está olhando e o vetor de sua velocidade, e o último single - o estado do personagem. Você pode observar bytes constantes (marcadores) entre os valores das coordenadas ( 0x0D antes do valor X , 0x015 antes de Y e 0x1D antes de Z ) e antes do estado ( 0x30 ), que são suspeitosamente semelhantes no significado de 0x08 e 0x10 . Tendo analisado muitos marcadores de outros eventos, descobriu-se que determina o tipo do valor a seguir (os três primeiros bits) e o significado semântico, ou seja, no exemplo acima, se você trocar os vetores enquanto mantém seus marcadores ( 0x120F na frente das coordenadas, etc.), o jogo (teoricamente) normalmente deve analisar a mensagem. Com base nessas informações, você pode adicionar alguns novos tipos:

 struct Packed { VLQ marker <bgcolor=0xFFBB00>; //    VLQ! local uint size = marker.size; //       ( , )          switch (marker._ & 0x7) { case 1: double v; size += 8; break; //     case 5: float v; size += 4; break; default: VLQ v; size += v.size; break; } }; struct PackedVector3 { Packed marker <name="Marker">; Packed x <name="X">; Packed y <name="Y">; Packed z <name="Z">; }; 

Agora, nosso modelo de mensagem de movimento foi reduzido significativamente:

 struct MoveEvent { uint data_length <bgcolor=0x00FF00, name="Data Length">; Packed move_time <bgcolor=0x00FFFF>; PackedVector3 position <bgcolor=0x00FF00>; PackedVector3 direction <bgcolor=0x00FF00>; PackedVector3 speed <bgcolor=0x00FF00>; Packed state <bgcolor=0x00FF00>; }; 

Outro tipo que podemos precisar no próximo artigo são as linhas precedidas pelo valor Packed de seu tamanho:

 struct PackedString { Packed length; char str[length.v._]; }; 

Agora, conhecendo o formato da mensagem de amostra, você pode gravar seu aplicativo de escuta para a conveniência de filtrar e analisar mensagens, mas este é o tópico do próximo artigo.

Upd: obrigado aml pela dica de que a estrutura de mensagens descrita acima é Protocol Buffer e também Tatikoma por um link para um artigo relacionado útil.

Source: https://habr.com/ru/post/pt451512/


All Articles