Reverse engineering · com.nianticlabs.pokemongo 0.431.0 · arm64-v8a
Pokémon GO protos
Everything the game exchanges with its servers is protobuf. The APK keeps the names of the messages and their fields, but the field numbers only existed in the compiled ARM64 code. Here are both, together.
3,145 messages · 1,090 enums · 12,955 fields · 444 RPC methods
GET_MAP_OBJECTS request (method 106) in 72 bytes: five S2 cells around the Eiffel Tower and the player’s position. Each row is a field: first the tag, then the length when the type needs one, then the value. Encoded with the reconstructed protos.From APK to .proto
UnwrapThe
.apkmis a zip holdingbase.apk(109 MB) andsplit_config.arm64_v8a.apk(98 MB). The second one carries the native libraries.Recognise the engine
lib/arm64-v8a/libil2cpp.so(≈ 250 MB) andglobal-metadata.dat(37 MB) mean Unity with IL2CPP: the game’s C# was translated to C++ and compiled for ARM64. The metadata is unencrypted (magicFAB11BAF, version 39) and describes 212 assemblies.Find the protosThe
holo-protos.dllassembly holds 7,145 types generated byprotocfor C#, on top of a copy of the runtime calledNiantic.Protobuf. Another 14 assemblies carry Niantic platform protos: social, authentication, telemetry, maps, Adventure Sync and Wayfarer.See what is missingGenerated code usually includes each file’s descriptor and constants such as
CpFieldNumber = 3. There is none of that here, andget_Descriptor()returnsnull. What is left are the names, the C# types and the field offsets. The numbers only exist in the code that reads the messages.Recover the numbersI emulated each message’s
InternalMergeFrom()and watched which field each tag ends up in. The process is described further down.VerifyI also emulated
InternalWriteTo(), which goes the other way, and confirmed 10,430 of 10,454 fields. The 15.protofiles compile withprotoc.
Reading a message
A protobuf message is a sequence of key-value pairs with no names. Each pair starts with a tag encoded as a varint, where tag = (field number << 3) | wire type. The wire type says how to read the value that follows.
| Type | Name | What follows | Types in the .proto |
|---|---|---|---|
| 0 | VARINT | a varint of 1 to 10 bytes | int32 int64 uint32 uint64 sint32 sint64 bool enum |
| 1 | I64 | 8 bytes, little-endian | fixed64 sfixed64 double |
| 2 | LEN | a varint length, then the bytes | string bytes, messages, packed repeated, map |
| 5 | I32 | 4 bytes, little-endian | fixed32 sfixed32 float |
In a varint, each byte carries 7 bits of the value and the top bit says whether more bytes follow. The Pikachu example’s CP of 812 becomes ac 06, because 0x2c + (0x06 << 7) = 44 + 768 = 812. The sint32 and sint64 types go through zig-zag first (0→0, −1→1, 1→2, −2→3) so that small negative numbers stay small. Try other numbers:
7-bit groups, least significant first
2 bytes
The tag is a varint too. Both ways:
bytes: a2 02
In PokemonProto, field 36 is pokemon_display (PokemonDisplayProto).
Repeated numeric fields travel packed: a single LEN record with the values back to back, like the cell_id in the example above. A map is a repeated message with key = 1 and value = 2, and a oneof leaves no trace on the wire: it is just a set of fields of which the sender writes one.
How the client talks to the server
Every action in the game is an RPC identified by a value of the Method enum, which has 445 values. The request is the message named after the method with the Proto suffix, and the response has the OutProto suffix. By that naming convention, 306 methods have both halves in this version. The rest are old methods whose messages Unity stripped because nothing used them any more.
| ID | Method | Request | Response |
|---|---|---|---|
| 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 |
| See all 444 methods in the explorer | |||
Some messages work as indexes of the whole game.
- GameMasterClientTemplateProto is an entry of the Game Master, the configuration downloaded at startup. It has a
template_idand aoneof datawith 247 kinds of definition, from Pokémon and moves to items, battles and events. - HoloInventoryItemProto is an inventory entry, with 43 variants: Pokémon, item, Pokédex, trainer stats and more.
- HoloholoClientTelemetryOmniProto is the telemetry, with 192 event types, from app startup to frame rate.
The envelope that carries these messages (authentication, device integrity data, request signing) is built in native code, in libNianticLabsPlugin.so. That part was not analysed and is not here.
How I recovered the field numbers
For each message, protoc generates a method that reads the bytes off the wire. Compiled, it is a switch on the tag. This is the one for GetOutstandingWarningsResponseProto.WarningInfo, at address 0xaebbb18 in libil2cpp.so, cut down to one of its cases:
bl ParsingPrimitives::ParseTag ; w0 = tag read cmp w0, #0x10 ; field 2, wire type 0 b.ne other_case bl ParsingPrimitives::ParseRawVarint32 str w0, [x20, #0x14] ; this+0x14
// metadata: offset 0x14 = C# field "Source" message WarningInfo { PlatformWarningType type = 1; Source source = 2; int64 start_timestamp_ms = 3; int64 end_timestamp_ms = 4; repeated StatementOfReason reason_statements = 5; }
Tag 0x10 ends up stored at this+0x14. The metadata says the field at that offset is called Source, so source = 2.
Doing this by hand for 3,145 messages was not an option. I loaded libil2cpp.so into Unicorn, a CPU emulator, and ran each message’s method on a fake object. ParseTag() was replaced by a function that returns a tag of my choosing, and every write to the object is logged. The candidate tags come from the constants the method itself compares against. That was 13,557 tags in 3,197 classes, in under a minute.
Each field’s C# type comes from the binary, the wire type from the tag, and the read function that gets called settles the rest. To tell int32 from sint32, the fake read function always returns 6: if the object receives 3, the value went through zig-zag. Repeated fields and maps come from the static constructor, which builds the codecs with FieldCodec.ForXxx(tag). Oneofs come from the XxxOneofCase enums, whose values are the field numbers themselves.
To check, I ran InternalWriteTo() for every message with fully populated objects and logged the tags it writes. It confirmed 10,430 of 10,454 singular fields, with no extra tags. The 24 left unconfirmed are in 8 messages where that second emulation did not run to the end.
Limitations
- Field names are derived from the C# names, so
cpMultiplier_becomescp_multiplier. Digits stay attached (move1). - The generated C# strips the prefix from enum values. I rebuilt them as
ENUM_NAME_VALUEso they compile in proto3, and some originals will not have that prefix. - Only the messages the client uses exist. Unity’s stripping removed the rest.
- In this version,
PokemonProto.pokemon_id,move1andmove2areint32in C#. In other versions they were enums. The decoder still shows the Pokémon and move names next to the numbers. - The transport envelope and the integrity layer are left out.