Reverse engineering React Native and Hermes Byte code
For the first time, I really go away from my editorial direction, which is normally web development. This time, we’ll dive into tweak making and reverse engineering an iOS application, but not a native application, a React Native-based application, which can be quite harder in some situation…
Let’s put some context, I always liked iOS tweaking and jailbreak, however, I don’t know anything about Objective-C (language used by Apple before Swift and still used a lot today). I always wanted to learn to make a tweak, and it finally happens, last week, I make a fork of BHInsta, a tweak that modifies how Instagram works. I added features to make it less addictive (such as removing Reels, suggested posts in Explore, etc.).
One of the biggest barriers was, before you would need jailbreak to achieve something like this. Today it’s not required anymore, you can inject FLEX, a debugging program into the application and find classes and methods to hook and modify.
After doing this, I felt like I wanted to do something more personal, not a fork, something new, and I found something — I’m actually learning Korean, I use flashcards application to remember words and it’s quite useful. However, iOS being iOS, almost every flashcard apps have in-app payments or ads. With this new knowledge, I decided to make my own “premium bypass” for AnkiPro, one of the most popular flashcard app. Let’s start the technical aspect now!
🌍 1st attempt: Trafficking network requests
Theos is a build system mostly used to make iOS tweak. It basically creates a dylib
file (similar to a dll
on Windows) that contains replaced logic and behavior for our targeting application. We can, after that, inject the dylib
into the ipa
file using Sideloadly or Azule for example.
After decompressing the AnkiPro IPA, I quickly noticed the application was written in React Native, a main.jsbundle
file was there.
Because, I don’t know how React Native works, I first decided to make a Theos tweak and to intercept HTTP requests. I came up with something like this:
What this code does is basically replacing the didReceiveData
of RCTNetworkTask
, a sort of bridge for networking internally used by React Native (source here).
By inspecting the HTTP traffic with FLEX, I noticed the application needs a JWT and makes a call to get one, even if you are not logged in, you get a “guest JWT”. This JWT contains a premium
boolean property. I decided to tamper this JWT by setting premium
on true.
When I built and tested the app on my phone, I noticed two things:
- The application does not check the JWT signature. As I don’t have the secret key, I can’t generate a valid JWT. This is a good thing for us.
- I was a premium user, however, because the JWT is faked, every request was denied. Bugs everywhere 🐛
Because the application makes other requests than just logging in the user, this patching method was not working correctly. I gave up and decided to patch the JavaScript code directly…
📦 2nd attempt: Patching the JS bundle
React Native compiles the application into a bundle, called main.jsbundle
on iOS. Since React Native v0.60.2, Hermes, a JavaScript engine that basically makes apps faster and smaller, is an opt-in feature. However, it also compiles the application into an assembly-like code, which makes it harder to reverse engineer.
Hopefully, a disassembler/assembler tool exists: hbctool. Using a fork of this tool, updated to support Hermes v90 (used by AnkiApp), I could get three files:
metadata.json
: Contains information about the Hermes bundle and instructions on how to re-assemble itstring.json
: Contains every text and localization of the applicationinstructions.hasm
: Contains the entire application logic, in “Hermes Assembly Code”
After diving in the instructions file, I found two interesting things: A isUserPremium
variable and a useIsUserPremium
method.
Patching the variable
The following code has been truncated for readability. Here is my interpretation on how this works:
- After start-up or user login, this function “21159” gets called.
- This function loads in the register “0” an environment, which seems to be a JavaScript object. This object very likely contains user data.
- A bit later, the function assign an ID “31496” (non-local variable, “isUserPremium”) with a boolean value from this JS Object.
By simply removing the LoadFromEnvironment
and replacing it by LoadConstUInt8
, we can load a true
boolean every time.
Useful information: The list of OP codes instructions of Hermes Bytecode, link here.
Patching the method
I have to admit, I’m quite unsure how the following code works. The function seems very similar to a React Hook (with the well known use
prefix), however, it never loads or call the isUserPremium
variable (located at 31496 as we saw before). It seems to call a closure “21193”.
I ended up replacing the whole function logic with a true return statement. This could break the application because it is normally a closure, however, after rebuilding and testing it on my phone, everything works fine, and the premium was bypassed correctly!
The four premium functions: no learning limits, access to different learning algorithms, text-to-speech and customizable cards are now available.
This article is meant for an educational purpose, I do not plan sharing the patched .ipa
file. However, I released a PoC of an automatic patcher, link here.
One last thing, Objective-C and Bytecode is a new thing to me, I don’t claim this article is 100% truthful. Feel free to correct me in the comments!
Thank you for reading, bye! 👋