Hacking the PokemonGo iOS App with 3 classes

Our mod version of PokemonGo app.

In my earlier post on “How to perform iOS Code Injection on .ipa files”, I have shown via a proof of concept how a developer can inject a single log message into an iOS .ipa file without modifying the original source codes. I have chosen the PokemonGo app as the target to demonstrate code injection because of its recent popularity and interests in the development community.

This post is inspired by the works of Will Cobb who is the author of the PokeGo++ mod. Download instructions for his mod app can be found at http://pokemongohacks.me/. The PokeGo++ mod has location spoofing features elegantly injected into the base PokemonGo game.

Screenshot taken from http://pokemongohacks.me/

We will attempt to create our own simple location spoofing mod for the game. It is possible to hack the original PokemonGo.ipa file straight from the iOS App Store and install onto a non-jailbroken phone (stay tune for a future post on this). For simplicity sake, I will showcase the code injection technique on a cracked .ipa file instead. Cracked .ipa are basically original ipa file with Digital Rights Management (DRM) removed. Here are the resources you need to build this mod app (or hack)

  1. Get a cracked but unmodified PokemonGo .ipa file. You can download the latest version at https://www.iphonecake.com/app_1094591345_.html
  2. Location Spoofing codes needed for code injection https://github.com/depoon/InjectibleLocationSpoofing
  3. Scripts to perform code injection https://github.com/depoon/iOSDylibInjectionDemo
  4. Cydia Impactor to resign and sideload/install app onto non-jailbroken devices. Cydia Impactor can be downloaded at http://www.cydiaimpactor.com/

As the code injection process and technique has been described in my earlier post https://medium.com/@kennethpoon/how-to-perform-ios-code-injection-on-ipa-files-1ba91d9438db, in this post we will only focus on the contents of the location spoofing codes.

Just 3 files???

That’s right. The complexity of hacking any app depends on what you want to achieve and your knowledge on iOS App Development. In our case, we only need to provide the minimum UI required to manage the faking of the device’s location. Now create a Cocoa Touch Dynamic Framework target in XCode and create the following classes linked to the framework target

  • PatchLoader (objective C)
  • LocationSwizzler (Swift)
  • PatchUIManager (Swift)
Xcode Project Navigation View showing the 3 classes we need.

Classes Descriptions

LocationSwizzler.swift
import Foundation
import CoreLocation
public class LocationSwizzler: NSObject {

static var currentLocation = CLLocationCoordinate2DMake(1.293760, 103.853709) //Raffles City

static var originalMethod: Method?
static var swizzleMethod: Method?

static var originalDelegate: CLLocationManagerDelegate?

static public func turnOnSwizzleForCoordinate(){

let m1 = class_getInstanceMethod(CLLocation.self, #selector(getter: CLLocation.coordinate))
   let m2 = class_getInstanceMethod(self, #selector(self.fakeCoordinate)) 

method_exchangeImplementations(m1, m2)
}

public func fakeCoordinate() -> CLLocationCoordinate2D {
return LocationSwizzler.currentLocation
}

}

LocationSwizzler class performs method swizzling on CLLocation’s getter coordinate method and the method will return a fake location instead of the device’s location. As the user’s location will be represented as a CLLocation object, objects that consume the coordinate information will receive the fake location. However, this hack affects all CLLocation objects and it may cause the app to have side effects.

import Foundation
open class PatchUIManager: NSObject {

var hijackingSubviews = [UIView]()

override init(){
super.init()
self.setupViewsForLoading()

}

fileprivate func setupViewsForLoading(){
guard let delegate = UIApplication.shared.delegate else{
return
}
guard let window = delegate.window else {
return
}
let label = UILabel(frame: CGRect(x: 0,y: 80, width: window!.frame.size.width, height: 50))
label.text = “← — UIWindow Hijacked!!! — →”
label.textAlignment = .center
label.backgroundColor = UIColor(red: 0, green: 1, blue: 1, alpha: 0.5)
label.tag = 29999

self.hijackingSubviews.append(label)

let buttonImage = self.imageFromColor(size: CGSize(width: 40,height: 40), color: UIColor(red: 1, green: 1, blue: 0, alpha: 0.5))

let nButton = UIButton(frame: CGRect(x: 40,y: 150, width: 40, height: 40))
nButton.setTitle(“N”, for: .normal)
nButton.setTitleColor(UIColor.black, for: .normal)
nButton.titleLabel!.textAlignment = .center
nButton.setImage(buttonImage, for: .normal)
nButton.addTarget(self, action: #selector(self.moveN), for: .touchUpInside)
nButton.tag = 29998


let wButton = UIButton(frame: CGRect(x: 0, y:190, width: 40, height: 40))
wButton.setTitle(“W”, for: .normal)
wButton.setTitleColor(UIColor.black, for: .normal)
wButton.titleLabel!.textAlignment = .center
wButton.setImage(buttonImage, for: .normal)
wButton.addTarget(self, action: #selector(self.moveW), for: .touchUpInside)
wButton.tag = 29997

let sButton = UIButton(frame: CGRect(x: 40, y: 230, width: 40, height: 40))
sButton.setTitle(“S”, for: .normal)
sButton.setTitleColor(UIColor.black, for: .normal)
sButton.titleLabel!.textAlignment = .center
sButton.setImage(buttonImage, for: .normal)
sButton.addTarget(self, action: #selector(self.moveS), for: .touchUpInside)
sButton.tag = 29996

let eButton = UIButton(frame: CGRect(x: 80, y: 190, width: 40, height: 40))
eButton.setTitle(“E”, for: .normal)
eButton.setTitleColor(UIColor.black, for: .normal)
eButton.titleLabel!.textAlignment = .center
eButton.setImage(buttonImage, for: .normal)
eButton.addTarget(self, action: #selector(self.moveE), for: .touchUpInside)
eButton.tag = 29995

self.hijackingSubviews.append(nButton)
self.hijackingSubviews.append(wButton)
self.hijackingSubviews.append(sButton)
self.hijackingSubviews.append(eButton)
}

@objc open func hijackAppWindow(){
self.displayDisplayLabel()
}

fileprivate func displayDisplayLabel(){
guard let delegate = UIApplication.shared.delegate else{
return
}
guard let window = delegate.window else {
return
}
for hijackingView in hijackingSubviews {
if let existingHijackView = window?.viewWithTag(hijackingView.tag) {
existingHijackView.removeFromSuperview()
}
window?.addSubview(hijackingView)
}
}

private func imageFromColor(size: CGSize, color: UIColor) -> UIImage{
UIGraphicsBeginImageContext(CGSize(width: size.width, height: size.height))

UIBezierPath.init(roundedRect: CGRect(x: 0, y: 0, width: size.width, height: size.height), cornerRadius: 3).addClip()

UIGraphicsGetCurrentContext()!.setFillColor(color.cgColor)
UIGraphicsGetCurrentContext()!.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height))
let colorImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return colorImage!;
}

}
extension PatchUIManager{

func moveN(){
var currentCoordinate = LocationSwizzler.currentLocation
currentCoordinate.latitude = currentCoordinate.latitude+0.0001
LocationSwizzler.currentLocation = currentCoordinate
}

func moveW(){
var currentCoordinate = LocationSwizzler.currentLocation
currentCoordinate.longitude = currentCoordinate.longitude-0.0001
LocationSwizzler.currentLocation = currentCoordinate
}

func moveS(){
var currentCoordinate = LocationSwizzler.currentLocation
currentCoordinate.latitude = currentCoordinate.latitude-0.0001
LocationSwizzler.currentLocation = currentCoordinate
}

func moveE(){
var currentCoordinate = LocationSwizzler.currentLocation
currentCoordinate.longitude = currentCoordinate.longitude+0.0001
LocationSwizzler.currentLocation = currentCoordinate
}
}

PatchUIManager helps to add UIViews on the UIApplication’s main app window. In this class, we will add a label with a message UIWindow Hijacked!!! , along with 4 buttons to allow us to walk our avatar anywhere in the game, ignoring the device’s actual location.

#import “PatchLoader.h”
#import “LocationSpoofing-Swift.h”
@implementation PatchLoader
static void __attribute__((constructor)) initialize(void){
NSLog(@”==== Code Injection in Action====”);

[LocationSwizzler turnOnSwizzleForCoordinate];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
static PatchUIManager *patchUIManager;
patchUIManager = [PatchUIManager new];
[patchUIManager hijackAppWindow];

});

}
@end

The PatchLoader is our entry point to perform code injection into the .ipa file. As this file is loaded into memory, the __attribute__((constructor)) initialize method will be called. The method will setup the CLLocation Swizzling via the LocationSwizzler class. The PatchLoader app will initialize and use PatchUIManager to add custom views onto the UIWindow. Since at this point the actual game is not launched, we will trigger the UI Hijack 2 seconds later (hopefully the UIWindow will be ready to be exploited

Go ahead and build the dynamic library in XCode and then perform the following processes (patching, resigning and installing) shown in the diagrams below. Since we have Swift classes in our dynamic library, do remember to add the standard Swift dylib libraries into the patching process.

Patching process using optool
.ipa resigning and sideloading using Cydia Impactor on non jailbroken devices

Once you have successfully performed all the above process, you should be able to see the custom UIViews added onto the app’s UIWindow. You will also notice that your avatar is not located where you physically are (the location is preset at Raffles City in Singapore). By tapping on the yellow control buttons, you will be able to walk your avatar anywhere you want. Disappointingly, our “walk anywhere” feature is not as responsive as the PokeGO++ version. Here is a video evidence of using our newly baked modified .ipa file and progressing further into the game by collecting pokeballs and catching a monster (and earning experience).

Our very own PokemonGo Mod

If you are interested to see the entire process of patching and sideloading, you can view my presentation during the iOS Dev Scout November meetup

iOS Dylib Injection Talk by Kenneth Poon

So that’s 3 files needed to perform a small hack on the PokemonGo game. I must admit, these codes are not properly written and I encourage volunteers to help improve them. I hope readers find this post insightful, knowing how vulnerable iOS Apps can be. Feel free to comment or buzz me at de_poon@hotmail.com.

[Added on 31 Jan 2017]

A kind soul, Vitor Venturin, shared that after patching the ipa (with code injection), the PokemonGo app crashes. The best way to debug any crash issue is to refer to the device console log.

Termination Description: DYLD, Library not loaded: @rpath/libswiftQuartzCore.dylib | Referenced from: /var/containers/Bundle/Application/0965B9B7-ECAB-4AD6-8733-2DEF8A8153DC/pokemongo.app/Dylibs/LocationSpoofing

Based on the screenshot above, an exception was thrown during app launch because there was a missing dylib dependency that was required from the original PokemonGo ipa itself. The solution for this is simple. As the missing dylib is a standard swift xcode library, you can simply copy the libswiftQuartzCore.dylib file from

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphoneos/libswiftQuartzCore.dylib

to the “Dylibs” folder that you use during the patching process. If the missing dependency is not available in Xcode, you may want to scan the main .ipa file itself.