Durante los años de jugar un MMORPG móvil, he adquirido cierta experiencia en ingeniería inversa, que me gustaría compartir en una serie de artículos. Temas de muestra:
- Analizar el formato del mensaje entre el servidor y el cliente.
- Escribir una aplicación de escucha para ver el tráfico del juego de una manera conveniente.
- Intercepción de tráfico y su modificación utilizando un servidor proxy no HTTP.
- Los primeros pasos para su propio servidor ("pirateado").
En este artículo,
analizaré el análisis del formato de mensaje entre el servidor y el cliente . Interesado, pido gato.
Herramientas requeridas
Para poder repetir los pasos que se describen a continuación, necesitará:
- PC (lo hice en Windows 7/10, pero MacOS también podría funcionar si los siguientes elementos están disponibles allí);
- Wireshark para el análisis de paquetes;
- 010 Editor para analizar paquetes por plantilla (opcional, pero le permite describir rápida y fácilmente el formato del mensaje);
- el dispositivo móvil en sí con el juego.
Además, es muy deseable tener a mano datos legibles del juego, como una lista de objetos, criaturas, etc. con sus identificadores. Esto simplifica enormemente la búsqueda de puntos clave en los paquetes y, a veces, le permite filtrar el mensaje deseado en un flujo constante de datos.
Analizando formato de mensaje entre servidor y cliente
Para comenzar, necesitamos ver el tráfico del dispositivo móvil. Es bastante simple hacer esto (aunque tomé esta decisión obvia durante mucho tiempo): en nuestra PC creamos un punto de acceso Wi-Fi, nos conectamos desde un dispositivo móvil, seleccionamos la interfaz deseada en Wireshark y tenemos todo el tráfico móvil ante nuestros ojos.
Después de ingresar al juego y esperar un tiempo para que se detengan las solicitudes que no están relacionadas con el servidor del juego, puede observar la siguiente imagen:
En esta etapa, ya podemos usar los filtros de Wireshark para ver solo los paquetes entre el juego y el servidor, así como solo con la carga útil:
tcp && tcp.payload && tcp.port == 44325
Si está parado en un lugar tranquilo, alejado de otros jugadores y del NPC, y no hace nada, puede ver mensajes que se repiten constantemente desde el servidor y el cliente (tamaño 76 y 84 bytes, respectivamente). En mi caso, la cantidad mínima de paquetes diferentes se envió en la pantalla de selección de personajes.
La frecuencia de la solicitud del cliente es muy similar al ping. Tomemos algunos mensajes para verificación (3 grupos, arriba es una solicitud de un cliente, debajo está la respuesta de un servidor):
Lo primero que llama la atención es la identidad de los paquetes. Los 8 bytes adicionales en la respuesta cuando se convierten al sistema decimal son muy similares a la marca de tiempo en segundos:
5CD008F8 16 = 1557137656 10
(del primer par).
Verificamos el reloj , sí, lo es. Los 4 bytes anteriores coinciden con los últimos 4 bytes en la solicitud. Al traducir, obtenemos:
A4BB 16 = 42171 10
, que también es muy similar al tiempo, pero en milisegundos. Coincide aproximadamente con el tiempo transcurrido desde el lanzamiento del juego, y lo más probable es que lo sea.
Queda por considerar los primeros 6 bytes de la solicitud y la respuesta. Es fácil notar la dependencia del valor de los primeros cuatro bytes del mensaje (llamemos a este parámetro
L
) en el tamaño del mensaje: la respuesta del servidor es más de 8 bytes, el valor de
L
también aumentó en 8, sin embargo, el tamaño del paquete es más de 6 bytes del valor de
L
en ambos casos. También puede observar que los dos bytes después de
L
retienen su valor tanto en las solicitudes del cliente como del servidor, y dado que su valor difiere en uno, podemos decir con confianza que este es el código de mensaje
C
(los códigos de mensaje asociados probablemente se determinarán secuencialmente). La estructura general es lo suficientemente clara como para escribir una plantilla mínima para 010Editor:
- primeros 4 bytes -
L
- tamaño de la carga útil del mensaje; - siguientes 2 bytes -
C
- código de mensaje; - carga útil en sí misma.
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">; };
Por lo tanto, el formato del mensaje de ping del cliente: envía el tiempo de ping local; formato de respuesta del servidor: envíe la misma hora y hora de envío de la respuesta en segundos. No parece difícil, ¿verdad?
Intentemos hacer un ejemplo más complicado. De pie en un lugar tranquilo y escondiendo los paquetes de ping, puedes encontrar mensajes teletransportados y crear objetos (manualidades). Comencemos con el primero. Al poseer los datos del juego, sabía qué valor del punto de teletransporte buscar. Para las pruebas, utilicé puntos con los valores
0x2B
,
0x67
,
0x6B
y
0x1AF
. Compare con los valores en los mensajes:
0x2B
,
0x67
,
0x6B
y
0x3AF
:
El desastre Dos problemas son visibles:
- los valores no son de 4 bytes, sino de diferentes tamaños;
- no todos los valores coinciden con los datos de los archivos, y en este caso la diferencia es 128.
Además, al comparar con el formato ping, puede notar alguna diferencia:
- incomprensible
0x08
antes del valor esperado; - Un valor de 4 bytes, 4 menos que
L
(llamémoslo D
Este campo no aparece en todos los mensajes, lo cual es un poco extraño, pero donde está, se conserva la dependencia L - 4 = D
Por un lado, para mensajes con no se requiere una estructura simple (como ping), pero por otro lado, parece inútil).
Creo que algunos de ustedes ya podrían haber adivinado la razón de la falta de coincidencia de los valores esperados, pero continuaré. Veamos qué está pasando en la nave:
Los valores esperados de 14183 y 14285 tampoco corresponden a los 28391 y 28621 reales, pero la diferencia aquí ya es mucho mayor que 128. Después de muchas pruebas (incluso con otros tipos de mensajes) resultó que cuanto mayor es el número esperado, mayor es la diferencia entre el valor en el paquete. Lo extraño fue que los valores de hasta 128 permanecieron solos. Entendido, ¿qué pasa? La situación obvia es para aquellos que ya se han encontrado con esto y, sin saberlo, tuve que desmontar este "código" durante dos días (al final, el análisis de los valores en forma binaria ayudó en el "pirateo"). El comportamiento descrito anteriormente se llama
Cantidad de longitud variable : una representación de un número que utiliza un número indefinido de bytes, donde el octavo bit de un byte (bit de continuación) determina la presencia del siguiente byte. De la descripción, es obvio que leer VLQ solo es posible en orden Little-Endian. Casualmente, todos los valores en los paquetes están en ese orden.
Ahora que sabemos cómo obtener el valor inicial, podemos escribir una plantilla para el 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); };
Y la función de convertir una matriz de bytes a un valor entero:
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);
Pero volvamos a la creación del tema. Nuevamente aparece
D
y nuevamente
0x08
frente al valor cambiante. Los dos últimos bytes del mensaje
0x10 0x01
son sospechosamente similares al número de elementos de creación, donde
0x10
tiene un papel similar a
0x08
pero aún incomprensible. Pero ahora puedes escribir una plantilla 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">; };
Que se vería así:
Y aún así, estos fueron ejemplos simples. Será más difícil analizar el evento del movimiento del personaje. ¿Qué información esperamos ver? Como mínimo, las coordenadas del personaje donde está mirando, la velocidad y el estado (de pie, corriendo, saltando, etc.). Como no hay líneas visibles en el mensaje, lo más probable es que el estado se describa a través de
enum
. Al enumerar las opciones, al compararlas simultáneamente con los datos de los archivos del juego, así como a través de muchas pruebas, puede encontrar tres vectores XYZ utilizando esta plantilla engorrosa:
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:
Los tres verdes resultaron ser las coordenadas de la ubicación, los tres amarillos, muy probablemente, muestran dónde está mirando el personaje y el vector de su velocidad, y el último solo es el estado del personaje. Puede observar bytes constantes (marcadores) entre los valores de coordenadas (
0x0D
antes del valor
X
,
0x015
antes de
Y
y
0x1D
antes de
Z
) y antes del estado (
0x30
), que son sospechosamente similares en significado a
0x08
y
0x10
. Habiendo analizado muchos marcadores de otros eventos, resultó que determina el tipo de valor que le sigue (los primeros tres bits) y el significado semántico, es decir. en el ejemplo anterior, si intercambias los vectores mientras mantienes sus marcadores (
0x120F
delante de las coordenadas, etc.), el juego (en teoría) normalmente debería analizar el mensaje. Según esta información, puede agregar un par de tipos nuevos:
struct Packed { VLQ marker <bgcolor=0xFFBB00>;
Ahora nuestra plantilla de mensaje de movimiento se ha reducido 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>; };
Otro tipo que podemos necesitar en el próximo artículo son las líneas precedidas por el valor
Packed
de su tamaño:
struct PackedString { Packed length; char str[length.v._]; };
Ahora, conociendo el formato de mensaje de muestra, puede escribir su aplicación de escucha para la conveniencia de filtrar y analizar mensajes, pero este es el tema para el próximo artículo.
Upd: gracias
aml por la sugerencia de que la estructura del mensaje descrita anteriormente es
Protocol Buffer , y también
Tatikoma por un enlace a un
artículo relacionado útil.