protos/pogo0.431.0

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

A 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.
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 S2 cells · level 15 · around 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

From APK to .proto

  1. UnwrapThe .apkm is a zip holding base.apk (109 MB) and split_config.arm64_v8a.apk (98 MB). The second one carries the native libraries.

  2. Recognise the enginelib/arm64-v8a/libil2cpp.so (≈ 250 MB) and global-metadata.dat (37 MB) mean Unity with IL2CPP: the game’s C# was translated to C++ and compiled for ARM64. The metadata is unencrypted (magic FAB11BAF, version 39) and describes 212 assemblies.

  3. Find the protosThe holo-protos.dll assembly holds 7,145 types generated by protoc for C#, on top of a copy of the runtime called Niantic.Protobuf. Another 14 assemblies carry Niantic platform protos: social, authentication, telemetry, maps, Adventure Sync and Wayfarer.

  4. See what is missingGenerated code usually includes each file’s descriptor and constants such as CpFieldNumber = 3. There is none of that here, and get_Descriptor() returns null. What is left are the names, the C# types and the field offsets. The numbers only exist in the code that reads the messages.

  5. Recover the numbersI emulated each message’s InternalMergeFrom() and watched which field each tag ends up in. The process is described further down.

  6. VerifyI also emulated InternalWriteTo(), which goes the other way, and confirmed 10,430 of 10,454 fields. The 15 .proto files compile with protoc.

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.

TypeNameWhat followsTypes in the .proto
0VARINTa varint of 1 to 10 bytesint32 int64 uint32 uint64 sint32 sint64 bool enum
1I648 bytes, little-endianfixed64 sfixed64 double
2LENa varint length, then the bytesstring bytes, messages, packed repeated, map
5I324 bytes, little-endianfixed32 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:

Encode as

7-bit groups, least significant first

10101100
ac
00000110
06

2 bytes

continuation bitvalue bits

The tag is a varint too. Both ways:

tag = 36 << 3 | 2 = 290 (0x122)
bytes: a2 02
tag 290 → field 36, wire type 2 (LEN)
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.

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_id and a oneof data with 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_ becomes cp_multiplier. Digits stay attached (move1).
  • The generated C# strips the prefix from enum values. I rebuilt them as ENUM_NAME_VALUE so 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, move1 and move2 are int32 in 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.