I Just Bypassed Your App’s Payment Process
Today, I present a not-so-pleasant scenario for mobile app developers- a world in which their hard work is compromised. Imagine waking up to find your app tampered with, your ideas exposed, crucial features bypassed, and a corrupted app version circulating in the wild. This is the nightmare of app tampering.
Before I proceed, here is a disclaimer: The example below is a highly simplified version of a real-world Android app that is easy to understand and convenient to use as an example.
Now let's switch roles. Imagine I'm the malicious actor about to show you the possible havoc I could wreak on your app.
What can I do as a malicious actor?
What can I, as a malicious actor, do? In short, I can reverse engineer apps, and I'm about to demonstrate how I can tamper with them. For the purpose of this demonstration, I've chosen an Android application. But don't underestimate my ability to tamper with an iOS app - it is very much within my capabilities.
Let’s begin with a definition:
Reverse engineering involves dissecting an application to gain insight into its inner workings. In this blog, my focus is on tampering with mobile apps, not the decompiling process (but I can tell you two of the widely used tools, Jadx and Frida, among others, which are very effective at decompiling a mobile app).
Reverse engineering typically commences with a thorough static analysis of the application, covering the following steps:
- Decompiling the mobile application
- Examining the application package
- Scrutinizing the application's data storage directory
Once the static analysis phase concludes, then the malicious actor will delve into observing the application's behavior through dynamic analysis methods, including:
- Tracking the application’s interaction flow
- Monitoring the application's logs
- Analyzing network communications
- Engaging in runtime tampering (my focus in this blog)
To begin, as a malicious actor, I have entered your Java code by decompiling it using JADX.
The app I am dissecting is, in fact, a fitness challenge app with exclusive in-app purchases. It has features like:
- User registration and profile creation
- Fitness challenges
- Activity tracking
- Workout logging
- Progress tracking
- Community engagement
- Notifications and reminders
- Integration with wearable devices
- Personalized recommendations
- Achievement and rewards
- Customer support and feedback
What caught my eye first? The first security blunder I stumbled upon — no obfuscation whatsoever. Can you believe it? Obfuscation is like the basic lock on your front door. Without it, malicious actors (like me) can walk right in and reverse engineer your business logic, hooking onto critical points. After a closer look, I noticed that most of your Java business logic files reside neatly under the package's directory. There begins my static analysis.
I then chanced upon something intriguing.
The following functions further interested me:
initiatePurchaseFlow:
This function is the starting point for buying something within the app; hence, it is implemented on the client side. This function is called when you click a button to purchase a 30-day fitness challenge. It sets up the details of what you're buying (like the name of the challenge and how much it costs) and then starts the process of actually buying it. If everything goes well, you'll see a confirmation screen where you can complete the purchase.
handlePurchase
serves as a central piece of logic to handle the result of in-app purchases and manage the app's behavior accordingly based on the purchase state. It tells you what's happening with your purchase, whether it's completed, pending, or if there's some confusion. And when your purchase is complete, it gives you access to the premium feature you bought.
Here's what the code looks like:
// Method to initiate in-app purchase flow
private void initiatePurchaseFlow(String skuId_30daysChallenge) {
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(SkuDetailsParams.newBuilder().setSku(skuId_30daysChallenge).setType(BillingClient.SkuType.INAPP).build())
.build();
BillingResult result = billingClient.launchBillingFlow(MainActivity.this, billingFlowParams);
if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// Purchase flow initiated successfully
}
}
// Method to handle purchase
private void handlePurchase(Purchase purchase) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
// Handle purchased item
unlock30dayChallengeFeature(purchase.getSku());
Toast.makeText(MainActivity.this, "Purchase successful", Toast.LENGTH_SHORT).show();
} else if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) {
// Handle pending purchase
Toast.makeText(MainActivity.this, "Purchase pending", Toast.LENGTH_SHORT).show();
} else if (purchase.getPurchaseState() == Purchase.PurchaseState.UNSPECIFIED_STATE) {
// Handle unspecified purchase state
Toast.makeText(MainActivity.this, "Purchase state unspecified", Toast.LENGTH_SHORT).show();
}
}
// Method to purchase premium feature (example)
private void purchasePremiumFeature() {
String skuId_30daysChallenge = "7749"; // SKU ID of the premium feature
initiatePurchaseFlow(skuId_30daysChallenge);
}}
}
Overall, the initiatePurchaseFlow
method is responsible for preparing the necessary parameters for the in-app purchase, initiating the purchase flow, and handling the result of the purchase initiation.
Static analysis is a success. It is time to dive into dynamic analysis to uncover more security loopholes. My next step is to use code injection tools to tamper with the app’s runtime.
I have successfully launched your mobile app with Frida.
The tampering fun
Returning to the decompiled code, I identified an attack target I could focus on to bypass the payment mechanism.
In Frida, I wrote a JavaScript to hook into the initiatePurchaseFlow()
method within the MainActivity
class to bypass the payment process.
Once hooked into the app, I will override the initiatePurchaseFlow
function with my custom implementation. In the custom implementation, I can simulate a successful purchase without actually going through the payment process.
Here's an example of JavaScript code that hooks onto the initiatePurchaseFlow
function and bypasses the purchase process:
Java.perform(function() {
var MainActivity = Java.use('com.example.myapp.MainActivity');
MainActivity.initiatePurchaseFlow.overload('java.lang.String').implementation = function(skuId_30daysChallenge) {
// Call the original initiatePurchaseFlow method
this.initiatePurchaseFlow(skuId_30daysChallenge);
// Bypass payment process
// For example, call the handlePurchase method directly with a purchased state
var Purchase = Java.use('com.android.GooglePay.api.Purchase');
var PurchaseState = Purchase.PurchaseState.PURCHASED.value;
var purchase = Purchase.$new();
purchase.setPurchaseState(PurchaseState);
this.handlePurchase(purchase);
};
});
- I use Frida to hook onto the
initiatePurchaseFlow
method of the MainActivity class. - Inside the hooked implementation, I call the original
initiatePurchaseFlow
method to initiate the purchase flow. - Then, I bypass the payment process. In this example, I directly call the
handlePurchase()
method with a PURCHASED state, effectively simulating a successful purchase without going through the actual payment flow.
The example code explains that tampering with the initiatePurchaseFlow
function can have several consequences for app publishers/organizations.
I can modify the app's behavior, potentially making in-app features available for free, which would otherwise have to be purchased.
If the payment handling logic is bypassed or manipulated, it could open up many security loopholes in the app. For example, an attacker might make use of the tampered function to make unauthorized in-app purchases without payment. For in-app purchases especially, payment processing/transactions occur on the client side (as in the example above) as opposed to server-side e-commerce.
Tampering with payment handling logic can result in financial losses for the app owner or users. For instance, if payments are not properly verified and processed, legitimate transactions may be lost, or fraudulent transactions may occur, leading to financial losses for the business.
It can result in legal and regulatory issues. Many jurisdictions have laws and regulations governing online payments, and non-compliance can lead to legal liabilities, fines, or even legal action against the app owner.
A compromised payment system can severely damage the reputation of the app and its developer. Users expect their financial transactions to be secure and reliable, and any breach of trust can lead to negative reviews, decreased user retention, and damage to the app's reputation. Potential partners, investors, or customers may be reluctant to engage with an app with a history of security breaches or payment processing issues, leading to a loss of business opportunities.
In this app, for example, I could purchase in-app fitness challenge features without actually paying for them, as the payment process was bypassed. I can scale my attack by rolling out a modified app package in the market where users can unlock premium in-app features for free without having to pay for it. That doesn’t sound good for your business, does it?
What could you have done to prevent this malicious attack?
Flipping the script here: now, I'm the developer, standing guard to prevent your dystopian dream from becoming a reality.
Demonstrating how easily an Android application can be reverse-engineered and tampered with using tools like Frida is a stark reminder of the security risks within the mobile app ecosystem. Techniques like code obfuscation, runtime code injection checks (implementing RASP) are crucial strategies to enhance my app's security posture. By using these protection mechanisms, I can significantly reduce the risk of unauthorized tampering and safeguard the integrity of my mobile application.
To start, I'll implement Runtime Application Self-Protection (RASP). RASP combined with threat monitoring, offers insights into the nature of attempted attacks and helps me understand security gaps to strengthen my app's security. With RASP, I can apply anti-debugging techniques to detect and thwart attackers' attempts to use debugging tools to examine my app's execution and manipulate its behavior. I'll also set up periodic checks for artifacts related to debuggers and monitor for abnormal debugging behavior.
RASP with threat monitoring will let me see occurrences of these checks being triggered. So, runtime monitoring allows me to detect and deter tampering by continuously analyzing and monitoring my app's runtime behavior.
By implementing a combination of these techniques, I can significantly enhance the security of my application and mitigate the risks of tampering and reverse engineering. Also, remember that automatically injected RASP checks are always obfuscated, too.
Reality check: The approaches and tactics I just laid out aren’t things you can easily whip up on your own. Reach out to our team of experts to discover how Guardsquare can help you achieve the best security strategy for your mobile app.