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
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.Do APK ao .proto
DesembrulharO
.apkmé um zip combase.apk(109 MB) esplit_config.arm64_v8a.apk(98 MB). O segundo traz as bibliotecas nativas.Reconhecer o motor
lib/arm64-v8a/libil2cpp.so(≈ 250 MB) eglobal-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 (magicFAB11BAF, versão 39) e descreve 212 assemblies.Encontrar os protosO assembly
holo-protos.dlltem 7 145 tipos gerados peloprotocpara C#, sobre uma cópia do runtime chamadaNiantic.Protobuf. Há mais 14 assemblies com protos da plataforma Niantic: social, autenticação, telemetria, mapas, Adventure Sync e Wayfarer.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, eget_Descriptor()devolvenull. Ficam os nomes, os tipos C# e os offsets dos campos. Os números só existem no código que lê as mensagens.Recuperar os númerosEmulei o
InternalMergeFrom()de cada mensagem e observei em que campo cada tag é guardada. O processo está descrito mais abaixo.VerificarEmulei também o
InternalWriteTo(), que faz o caminho inverso, e confirmei 10 430 de 10 454 campos. Os 15 ficheiros.protocompilam noprotoc.
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.
| Tipo | Nome | O que vem a seguir | Tipos no .proto |
|---|---|---|---|
| 0 | VARINT | varint de 1 a 10 bytes | int32 int64 uint32 uint64 sint32 sint64 bool enum |
| 1 | I64 | 8 bytes, little-endian | fixed64 sfixed64 double |
| 2 | LEN | varint com o comprimento, depois os bytes | string bytes, mensagens, repeated compactados, map |
| 5 | I32 | 4 bytes, little-endian | fixed32 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:
Grupos de 7 bits, do menos significativo para o mais significativo
2 bytes
A tag também é um varint. Daqui para lá e de lá para cá:
bytes: a2 02
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.
| ID | Método | Pedido | Resposta |
|---|---|---|---|
| 2 | GET_PLAYER | GetPlayerProto | GetPlayerOutProto |
| 4 | GET_HOLOHOLO_INVENTORY | GetHoloholoInventoryProto | GetHoloholoInventoryOutProto |
| 101 | FORT_SEARCH | FortSearchProto | FortSearchOutProto |
| 102 | ENCOUNTER | EncounterProto | EncounterOutProto |
| 103 | CATCH_POKEMON | CatchPokemonProto | CatchPokemonOutProto |
| 106 | GET_MAP_OBJECTS | GetMapObjectsProto | GetMapObjectsOutProto |
| 125 | EVOLVE_POKEMON | EvolvePokemonProto | EvolvePokemonOutProto |
| 1001 | UPDATE_COMBAT | UpdateCombatProto | UpdateCombatOutProto |
| 3095 | ROTATING_SPAWN_ENCOUNTER | RotatingSpawnEncounterProto | RotatingSpawnEncounterOutProto |
| Ver os 444 métodos no explorador | |||
Algumas mensagens funcionam como índices do jogo inteiro.
- GameMasterClientTemplateProto é uma entrada do Game Master, a configuração descarregada no arranque. Tem um
template_ide umoneof datacom 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 acp_multiplier. Os dígitos ficam colados (move1). - O C# gerado tira o prefixo aos valores dos enums. Reconstruí-os como
NOME_DO_ENUM_VALORpara 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,move1emove2sãoint32no 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.