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:
- Analise o formato da mensagem entre servidor e cliente.
- Escrevendo um aplicativo de escuta para visualizar o tráfego do jogo de maneira conveniente.
- Interceptação de tráfego e sua modificação usando um servidor proxy não HTTP.
- 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:
- valores não são 4 bytes, mas de tamanhos diferentes;
- 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);
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>;
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.