protos/pogo0.431.0

Engenharia reversa · com.nianticlabs.pokemongo 0.431.0 · arm64-v8a

Protos do Pokémon GO

Tudo o que o jogo troca com os servidores é protobuf. O APK guarda os nomes das mensagens e dos campos, mas os números dos campos só existiam no código ARM64 compilado. Aqui estão os dois juntos.

3 145 mensagens · 1 090 enums · 12 955 campos · 444 métodos RPC

Um pedido GET_MAP_OBJECTS (método 106) com 72 bytes: cinco células S2 à volta da Torre Eiffel e a posição do jogador. Cada linha é um campo. Primeiro vem a tag, depois o comprimento quando o tipo o exige, depois o valor. Exemplo codificado com os protos reconstruídos.
0a 2d 80 80 80 80 9c fc 9b f3 47 80 80 80 80 84 fc 9b f3 47 80 80 80 80 94 fc 9b f3 47 80 80 80 80 a4 fc 9b f3 47 80 80 80 80 e4 83 9c f3 47
cell_id = 1repeated uint64 · [47e66fe1c, 47e66fe04, 47e66fe14, 47e66fe24, 47e6701e4] · 5 células S2 · nível 15 · à volta de 48.85847, 2.29549
12 05 00 00 00 00 00
since_time_ms = 2repeated int64 · [0, 0, 0, 0, 0]
19 76 71 1b 0d e0 6d 48 40
player_lat = 3double · 48.8584
21 42 60 e5 d0 22 5b 02 40
player_lng = 4double · 2.2945

Do APK ao .proto

  1. DesembrulharO .apkm é um zip com base.apk (109 MB) e split_config.arm64_v8a.apk (98 MB). O segundo traz as bibliotecas nativas.

  2. Reconhecer o motorlib/arm64-v8a/libil2cpp.so (≈ 250 MB) e global-metadata.dat (37 MB) indicam Unity com IL2CPP: o C# do jogo foi traduzido para C++ e compilado para ARM64. A metadata está em claro (magic FAB11BAF, versão 39) e descreve 212 assemblies.

  3. Encontrar os protosO assembly holo-protos.dll tem 7 145 tipos gerados pelo protoc para C#, sobre uma cópia do runtime chamada Niantic.Protobuf. Há mais 14 assemblies com protos da plataforma Niantic: social, autenticação, telemetria, mapas, Adventure Sync e Wayfarer.

  4. Ver o que faltaO código gerado costuma incluir o descritor de cada ficheiro e constantes como CpFieldNumber = 3. Aqui não há nada disso, e get_Descriptor() devolve null. Ficam os nomes, os tipos C# e os offsets dos campos. Os números só existem no código que lê as mensagens.

  5. Recuperar os númerosEmulei o InternalMergeFrom() de cada mensagem e observei em que campo cada tag é guardada. O processo está descrito mais abaixo.

  6. VerificarEmulei também o InternalWriteTo(), que faz o caminho inverso, e confirmei 10 430 de 10 454 campos. Os 15 ficheiros .proto compilam no protoc.

Como se lê uma mensagem

Uma mensagem protobuf é uma sequência de pares chave-valor sem nomes. Cada par começa com uma tag codificada em varint, onde tag = (número do campo << 3) | wire type. O wire type diz como ler o valor que vem a seguir.

TipoNomeO que vem a seguirTipos no .proto
0VARINTvarint de 1 a 10 bytesint32 int64 uint32 uint64 sint32 sint64 bool enum
1I648 bytes, little-endianfixed64 sfixed64 double
2LENvarint com o comprimento, depois os bytesstring bytes, mensagens, repeated compactados, map
5I324 bytes, little-endianfixed32 sfixed32 float

Num varint, cada byte leva 7 bits de valor e o bit mais alto indica se há mais bytes. O CP 812 do exemplo do Pikachu fica ac 06, porque 0x2c + (0x06 << 7) = 44 + 768 = 812. Os tipos sint32 e sint64 passam primeiro por zig-zag (0→0, −1→1, 1→2, −2→3) para que os negativos pequenos ocupem pouco. Experimenta com outros números:

Codificar como

Grupos de 7 bits, do menos significativo para o mais significativo

10101100
ac
00000110
06

2 bytes

bit de continuaçãobits do valor

A tag também é um varint. Daqui para lá e de lá para cá:

tag = 36 << 3 | 2 = 290 (0x122)
bytes: a2 02
tag 290 → campo 36, wire type 2 (LEN)
No PokemonProto, o campo 36 é pokemon_display (PokemonDisplayProto).

Os campos repeated de números viajam compactados: um só registo LEN com os valores seguidos, como o cell_id do exemplo acima. Um map é um repeated de mensagens com key = 1 e value = 2, e um oneof não deixa marca no fio: é só um conjunto de campos dos quais o emissor escreve um.

Como o cliente fala com o servidor

Cada ação do jogo é um RPC identificado por um valor do enum Method, que tem 445 valores. O pedido é a mensagem com o nome do método e o sufixo Proto, e a resposta tem o sufixo OutProto. Pela convenção de nomes, 306 métodos têm o par completo nesta versão. Os restantes são métodos antigos cujas mensagens o Unity removeu por já não serem usadas.

Algumas mensagens funcionam como índices do jogo inteiro.

  • GameMasterClientTemplateProto é uma entrada do Game Master, a configuração descarregada no arranque. Tem um template_id e um oneof data com 247 tipos de definição, de Pokémon e golpes a itens, combates e eventos.
  • HoloInventoryItemProto é uma entrada do inventário, com 43 variantes: Pokémon, item, Pokédex, estatísticas do treinador e outras.
  • HoloholoClientTelemetryOmniProto é a telemetria, com 192 tipos de evento, do arranque da app até à taxa de frames.

O envelope que transporta estas mensagens (autenticação, dados de integridade do dispositivo, assinatura do pedido) é montado em código nativo, em libNianticLabsPlugin.so. Essa parte não foi analisada e não está aqui.

Como recuperei os números dos campos

Para cada mensagem, o protoc gera um método que lê os bytes do fio. Compilado, é um switch sobre a tag. Este é o de GetOutstandingWarningsResponseProto.WarningInfo, no endereço 0xaebbb18 de libil2cpp.so, reduzido a um dos casos:

bl   ParsingPrimitives::ParseTag   ; w0 = tag lida
cmp  w0, #0x10                   ; campo 2, wire type 0
b.ne outro_caso
bl   ParsingPrimitives::ParseRawVarint32
str  w0, [x20, #0x14]             ; this+0x14
// metadata: offset 0x14 = campo C# «Source»
message WarningInfo {
  PlatformWarningType type = 1;
  Source source = 2;
  int64 start_timestamp_ms = 3;
  int64 end_timestamp_ms = 4;
  repeated StatementOfReason reason_statements = 5;
}

A tag 0x10 acaba guardada em this+0x14. A metadata diz que o campo nesse offset se chama Source, logo source = 2.

Fazer isto à mão para 3 145 mensagens não era viável. Carreguei a libil2cpp.so no Unicorn, um emulador de CPU, e corri o método de cada mensagem com um objeto falso. O ParseTag() foi substituído por uma função que devolve a tag que eu escolho, e todas as escritas no objeto ficam registadas. As tags candidatas saem das constantes que o próprio método compara. Foram 13 557 tags em 3 197 classes, em menos de um minuto.

O tipo C# de cada campo vem do binário, o wire type vem da tag e a função de leitura chamada desfaz o resto das dúvidas. Para separar int32 de sint32, a função falsa de leitura devolve sempre 6. Se o objeto recebe 3, o valor passou por zig-zag. Os repeated e os map vêm do construtor estático, que cria os codecs com FieldCodec.ForXxx(tag). Os oneof vêm dos enums XxxOneofCase, cujos valores são os próprios números dos campos.

Para verificar, corri o InternalWriteTo() de todas as mensagens com objetos totalmente preenchidos e registei as tags que escreve. Confirmou 10 430 de 10 454 campos singulares, sem nenhuma tag a mais. Os 24 por confirmar estão em 8 mensagens em que essa segunda emulação não chegou ao fim.

Limitações

  • Os nomes dos campos são derivados dos nomes C#, por isso cpMultiplier_ passa a cp_multiplier. Os dígitos ficam colados (move1).
  • O C# gerado tira o prefixo aos valores dos enums. Reconstruí-os como NOME_DO_ENUM_VALOR para compilarem em proto3, e alguns originais não terão esse prefixo.
  • Só existem as mensagens que o cliente usa. O stripping do Unity removeu as outras.
  • Nesta versão, PokemonProto.pokemon_id, move1 e move2 são int32 no C#. Noutras versões eram enums. O descodificador mostra na mesma o nome do Pokémon e do golpe ao lado do número.
  • O envelope de transporte e a camada de integridade ficaram de fora.