Bypass In-app purchases of react native iOS app
Written on March 23, 2020
Today we will reverse engineering a react native iOS app and bypass in-app purchase to use locked features. Below is an example of purchase screen whenever tap on premium content.
Figure 1: Sample In-app purchases locked contents
This post is for educational purposes only. How you use this information is your responsibility. I will not be held accountable for any illegal activities, so please use it at your discretion and contact the app’s author if you find issues. We will inspect an app has In-app purchases feature, name REDACTED. The figures during the post just for demonstrations, might not relevant to REDACTED app.
Below tools are used during this post:
After installing and launching REDACTED app on the device, it shows me the main screen which some navigation items on the tab-bar menu. Select one of the items, let say Tools lead me to a new screen which allows me to select tools app supports. Select either tool will navigate to a new screen that allows us to see that tool for a second followed by a purchase screen.
The only option we have is to subscribe and do In-app purchases, otherwise, click X button to cancel purchase will pop us back to Tools screen again. We nearly can use that tool but it’s blocked in front of our eyes. However, this seems to be client-side validation, if we can find out where is logic to show purchase screen and disable that, we can get rid of it and use the tool for free. Let’s do it ^_^
First of all, let’s start with UI. As usual, let trace from the text we can see on the Purchase screen, something like “other great features with Pro access”. So let do some static analysis by extracting the app .ipa file and looking for that screen. With the help of Frida iOS Dump or CrackerXI, we can easily pull out .ipa file of REDACTED app on a jailbroken device, unzip .ipa and navigate to Payload/REDACTED.app folder. All of the app resources and binary files are inside this folder. Normally app strings are stored in some below places:
- Localization files (.lproj/.strings)
- Property List *.plist
- Hard-coded in Mach-O file (binary file)
- json/text files…
This file size is around 5MB, it’s huge and looks like the whole application logic is inside this single file, but scare not!! Beautifier comes to rescue ^_^
main.jsbundle content and paste on Beautifier site we will get formatted one. It won’t decrypt the file to original source code but it formats syntax for readability, which we can use to reverse the app logic.
Let’s open original main.jsbundle and paste formatted content into any text editor, in my case I’m using Sublime and search for the text “other great features with Pro access”
Now searching for something like “purchase”, we will see that we can find some results, one of them likes below is worth to pay attention and dive deep.
Figure 5: Purchase search leads us to onPurchaseFinish callback
Do you see what I see?
hasProAccount rings the bell? By searching “purchase” we found a new flag
hasProAccount which is a boolean type and might be used to decide if the user has a free account or pro account. Let’s note down this variable and searching all of its references to see how it’s used.
To make our life easier, I found this JS Nice tools helpful. It’s statistical rename, type inference and deobfuscation, perfectly fit for our case so let give it a try
Figure 6: JS Nice comes to rescue
The deobfuscated result is awesome. We can see there is a bunch of if-else logic and it’s much easier to read and understand the function now.
This method will be triggered when we tap on any features in Tools tabs (CHROMATIC_TUNER_KEY, METRONOME_KEY, BRAIN_TUNER_KEY, CHORD_LIBRARY_KEY). It will do nothing if
true. But if
false, tap on any above features will do 2 things:
- Log an event with the name
BannerUpgradeView. The name is self-explained this would be the purchase view we see in Figure 1
- Show some kinds of marketing screen (which probably the purchase screen)
false and tab in features that do not belong to the above list, it will trigger that event as normal.
So from here we can make sure
hasProAccount will be the key to unlock In-app purchases. Let’s search
hasProAccount = to find out where this value is set, the results are only 3 places.
Figure 7: Search where hasProAccount is set
Patch the app
Now the job is trivial by replacing
hasProAccount = true for all 3 places:
g.hasProAccount = s=>
g.hasProAccount = true
g.hasProAccount = u=>
g.hasProAccount = true
g.hasProAccount = !1=>
g.hasProAccount = true
Then copy modified main.jsbundle to overwrite the one in the jailbroken device by below command:
scp main.jsbundle root@iphone_ip:/private/var/containers/Bundle/Application/REDACTED_UUID/REDACTED.app/main.jsbundle
Relaunch the app, BOOM!!! No more in-app purchases screen, even ads is gone too, so one stone two birds yeah!!!
- The client-side should show a purchase screen first instead of a content screen followed by. This would hide the feeling that premium content already been on the client but blocked by another purchase screen, try to get rid of purchase screen means can use the app.
- React native is fast to develop applications for cross-platforms, but as we can see client-side logic is in plain text and with some steps required we can reverse and unlock client-side logic.
- File Integrity Checks can be used to mitigate main.jsbundle modification