Background
Since last summer, my main hobby project has been developing my reverse engineering skills by tearing apart Pokémon GO and seeing what I could learn. I’ve played this game since it was released in 2016 (when I was 11), and it’s been a fairly major part of my life since then.
Last July I started by setting up a fairly basic PoGo map, as I described in an earlier blog post. This didn’t require any real technical skill on my part, because it was mostly setting up and running software that other people had already built. I wasn’t satisfied with this, and after I got my map running, I began exploring the game myself, and I found lots of interesting stuff.
The two main things I’ve found that I think are interesting enough to talk about are some silly things with routes, and diving into the PvP combat system.
Routes
Towards the end of July 2023, routes were released as a feature in PoGo. They involve a start and end POI, and you need to walk a path between them, and when you reach the end you are rewarded with items.
Shortly after routes were made available, it became apparent that they dropped some fairly valuable rewards, namely Rare Candy XL and Elite Fast TMs. I became curious as to what the actual reward rates for these items were because if they were high enough, routes could become a viable way to collect enough Rare Candy XL to power up legendaries for Master League, among other things.
My initial testing setup was fairly crude. I set up a spare phone with an alt account and some code that tapped the screen at the right places and times to start a route, then sent a command to the phone’s GPS to jump to the end of the route, then tap again to collect the rewards. I used the account’s journal afterward to check the item rewards and constructed a chart like this.
Each number represents a “bundle” of items. Each bundle contains a varied number of actual items, depending on what that item is. For example, Razz Berries come in groups of 3, and Rare Candy comes in bundles of 2.
This system could complete a single route about every 15 seconds and was very prone to crashing. If something went wrong with the screen inputs, it had no way to really recover. Not satisfied with this, I began to decompile the game and tear through its code to see if there was a way I could do this faster.
I used Il2CppDumper to extract a C# dump of the game’s Unity Il2Cpp header files, and from there I took a closer look at what everything did.
I’m not going to go into too much technical detail as to how I did anything in particular in this article, as much of what I discovered is stuff I don’t want bad-faith actors to take advantage of, but I can give a brief overview. Anything shared in this blog post should be more than enough for anyone with adequate skill to replicate without much issue.
In short, I discovered the part of the game’s code responsible for sending and receiving the raw remote procedure calls to the game’s servers. I was then able to write my own code which I injected into the running PoGo process that hooked into said components and allowed me to hijack the server communication process to send and receive my own RPCs without using the game’s GUI (graphical user interface) whatsoever.
In order to complete a single route, 3 RPCs are required, all of which can be sent in a single “bundle” of requests. PoGo uses Protobufs in its RPC layer to encode requests and responses. This is fairly common for applications such as this one. When converted to a human-readable format like JSON, the required requests are as follows.
First, one needs to make a call to METHOD_START_ROUTE
that looks like this. The route_id
field is a string used to uniquely identify the route, and the entry_fort_id
is the unique ID of the fort (pokestop or gym) that the player starts the route from (keeping in mind that a route can be started from either side, in most cases).
1
2
3
4
{
"route_id": "9c29890509094a528c13dbaa7da159ae.20",
"entry_fort_id": "6b94a6a4ba484368a25ea6c405ba467e.11"
}
This call is sufficient to start a route. If this request is sent while the game is running and visible, it will result in the route path becoming visible on the screen, and the little route icon showing up in the bottom right corner. This is the RPC sent when you press the “start route” button in the game normally. Here’s an example of this call being made programatically.
The next call that needs to be made is a little bit more interesting. In order to update your progress along the route (what makes the path fill in blue when you’re following a route in-game) you need to make a call to UPDATE_BREADCRUMB_HISTORY
. The JSON-encoded version of this request looks like this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"breadcrumb_history": [
{
"timestamp_ms": 1716164155,
"latitude_deg": 43.084235,
"longitude_deg": -77.676978,
"app_is_foregrounded": true,
"altitude_m": 0.41577601432800293
},
{
"timestamp_ms": 1716164160,
"latitude_deg": 43.084354,
"longitude_deg": -77.669166,
"app_is_foregrounded": true,
"altitude_m": 2.110531806945801
}
],
"session_context": "route_travel:9c29890509094a528c13dbaa7da159ae.20"
}
Unlike how it may initially look, this request is not used to report where you are in the game. Sending this request will not result in your avatar changing position on the map whatsoever. PoGo handles location through an entirely separate system from its RPC requests. Instead, what this does is report to the server where you were, and at what times. As long as a route is fairly straight, you can get away with only recording a position at the start and end of the route to reach the minimum required distance for route completion.
Weirdly, PoGo seems to have nearly no validation on this. You can submit breadcrumb history for a route while your avatar is on the other side of the globe. This makes it possible to complete a route from anywhere without technically spoofing, because the game still thinks you’re in your real location, and GPS values are never altered.
Something else interesting I found is that these breadcrumb histories are added to buddy distances, and they don’t seem to respect the 40km/day max that Adventure Sync and walking your buddy normally does. You can send many breadcrumb history calls without actually completing a route, making it possible to farm a stupid amount of buddy candy, again without the game even thinking your avatar is moving.
In order to finish a route, you need to make one final request to METHOD_PROGRESS_ROUTE
that looks like this.
1
2
3
{
"acquire_reward": true
}
Upon sending this, the server will either send back a response that includes reward information, or a response that the route completion wasn’t successful.
I quickly learned that there seemed to be a ~5 second rate limit on completing a route. However, after playing around with it a little further, I discovered that this rate limit is per-route rather than overall, meaning that you can split over many routes to effectively avoid the limit.
With this knowledge, I was able to build a bot that send the RPCs to complete routes basically as fast as possible and logged its results. I set up a Discord webhook to post statistics and information as it completed routes that looked like this.
Using this system, I was able to total hundreds of thousands of route completions, which let me collect the following data about item drop rates.
Razz Berry | Revive | Hyper Potion | Ultra Ball | Fast TM | Golden Razz | Rare Candy | Rare XL | Elite Fast | |
---|---|---|---|---|---|---|---|---|---|
Raw Item Count | 91896 | 73797 | 54870 | 36825 | 30506 | 2995 | 6182 | 609 | 141 |
Bundles | 30632 | 24599 | 18290 | 12275 | 30506 | 2995 | 3091 | 609 | 141 |
Raw Bundle Rate | 0.2487615521 | 0.1997677403 | 0.1485325407 | 0.09968490637 | 0.2477383099 | 0.02432230506 | 0.02510191817 | 0.004945670711 | 0.001145056766 |
Assumed Bundle Rate | 24.8% | 20% | 15% | 9.8% | 24.8% | 2.48% | 2.5% | 0.5% | 0.12% |
Notably, at a certain point last year, the item rates seemed to change a little bit. Elite Fast TMs used to have a 1.4% drop rate, but now only seem to make up 0.12% of reward bundles. The data in this table is aggregated from after this change was made.
I should note that sometime within the last few months or so, Niantic thankfully added better rate limits to routes, such that your account gets locked if you complete more than about 300 routes in a day.
This foray into the RPC layer of PoGo made me interested in what else might be possible, which led to the next part of this project.
PvP Vulnerabilities
It’s no secret that the PvP (Combat) system in PoGo is a buggy mess, but I became curious just how poorly done it is.
My first attempt to peek into PvP involved reading through the C# classes again, building out a UML diagram for them, and writing an agent to read memory and follow pointers to extract the current state of the CombatDirector object. The class relationships for the Combat system look like this.
This did work, but not very well, as this relied on discovering the obfuscated names of classes and fields which change with every update. Instead, I realized that if the game was silly enough to send information about the entire combat state all the time, there may be a much easier way to peek into what I wasn’t supposed to.
With the skills learned from my exploration of routes, I was able to build a program that, instead of sending and receiving RPC traffic, simply snoops on the traffic the game sends normally. If anyone reading this manages to make their own application of a similar nature, there is a public tool for parsing the RPC data from the game (which I’m sure anyone interested can google), which I only discovered after having done it myself 😭. Creating the MiTM tool to capture the RPC data is left as an exercise for the reader.
After analyzing the RPC traffic during PvP battles (friendly against an alt), I was able to discover that yes, in fact, a protobuf representing the entire state of the game was sent between the client and the server every single time the client performs an action. The vulnerability here is the ability to look ahead at what the opponent has hidden during a match, before that information is supposed to be revealed. If the combat system was programmed properly, there are multiple ways in which this issue could be avoided, but it seems the lazy route was taken, and here we are.
The following information is clear as day in the network traffic during a PvP combat battle.
- Both players’ full teams of Pokémon, including
- Species
- Form
- IVs
- Moveset
- Weight
- Height
- Nickname
- Type of pokéball captured with
- Energy
- Combat Power
- Attack Stage
- Defense Stage
- Both players’ entire public profiles, which include
- Trainer name
- Team
- Level
- Elo
- Rank
- Avatar Data
- XP amount
- Battles Won
- KM walked
- Pokemon caught
- Information about each player’s combat state, such as
- Which pokémon is active
- Which pokémon are in reserve
- Which pokémon are fainted
- The Unix epoch timestamp at which switch clock becomes available
- Number of shields left
- Current action, which can be one of
ATTACK
SPECIAL_ATTACK
SPECIAL_ATTACK_2
FAINT
CHANGE_POKEMON
QUICK_SWAP_POKEMON
- And a couple other unimportant ones
It should be noted that initially, I thought that information about which charged attack a player was throwing wasn’t visible to the client until after the shielding window was up, and I falsely reported this to a couple of content creator contacts. That was incorrect, and this information is in fact just as visible as everything else.
This is, obviously, a problem. The client is being trusted far more than it should be, which goes against a key security principle in client-server applications.
Back in September of 2023 I attempted to report this to Niantic via a few content creator connections I had and didn’t receive a response. In December 2023, I reported this again via Niantic’s official vulnerability disclosure form, after being told to via content creator proxy by a Niantic employee. I censored the name of the content creator in question for privacy.
I wrote up effectively what is in this article, along with some proof-of-concept code, and submitted it on December 15th, 2023. Shortly after doing so, I was told that Niantic received my report and was looking into it.
The form requested a 90-day period for Niantic to resolve the issue before I shared anything about it publicly, and potentially longer if Niantic requested it. They did not reply to me, and that 90-day window expired on the 15th of March, which was over 2 months ago.
It seems to be the case that Niantic is aware of this issue and actively choosing to do nothing about it, and I hope that by making the public aware it may pressure Niantic to reconsider. As I stated in my bug report, I have several suggestions for how to resolve this, if they care to hear it.
I feel I should also mention that I developed and researched this entirely out of my own curiosity, and not out of a desire to cheat at the game, nor to let others cheat at the game. I am not releasing the source code of my exploit, nor am I elaborating much on how to gather the information that I am describing. This being said, I know for a fact that I am not the only one who knows how to do this, and it’s being abused to a small degree. I don’t think this vulnerability has any significant impact on the GBL leaderboards, because the overlap of people who know how to do this and people who care about PvP is quite small, but it is there. I personally only used my proof-of-concept exploit in battles against friends who knew what I was doing, and against my own alts.
Here’s a video example of my proof-of-concept exploit.
I borrowed UI components from PvPoke, and modified them to suit my needs. I built this interface in a single afternoon, the code for it is pretty messy, but I think it illustrates my point fairly well. In the video, I censored the trainer names of the accounts being used for privacy. The screen capture of the game was recorded at the same time as my interface, but was overlayed using OBS (it’s not actually there on the web app page), simply there as a reference for what’s happening in the battle.
Future Projects
Summer break now having started, I’m looking into what other projects I can do in this same vein. I think I’m probably going to look into reverse engineering the Pokémon GO Plus accessory and maybe a few other things, and I’m sure I’ll write another blog post when I figure it out.