A brief but somewhat fun introduction to compromising data integrity on an Android application. **
This article will give an overview of setting up a rooted Android emulator, installing a tool called Frida, creating Frida scripts to intercept code at runtime, and replacing data in an application.
The Pirate Ship: Rooted Emulator
Though possible to hack without, having a rooted device or emulator image is essential to using frida effectively.
Frida’s tools communicate with a server running on a device to enable hooking into code at runtime.
The modes of operation and installation on devices are:
Injected on the device filesystem (requiring rooted/jailbroken access)
Embedded a gadget inside the application (repacking)
Repackaging an application and embedding a frida gadget is possible but can be more error prone. Rooting and android emulator is easy and risk free.
It’s Magisk
Magisk is common tool used to provide root access in android devices.
Magisk has a github repository for installing on an emulator specifically. The instructions are straight forward (some minor manual steps), so in summary:
- backup your emulator
ramdisk.img
- use the repo to patch the running emulator
- move the resulting patched
ramdisk.img
back to the emulator location
You can check that the device has been rooted inside the magisk app or with a root check application.
Frida
Frida is a suite of tools that provides an API for dynamically instrumenting code in native applications.
Installing frida on the host system is as simple as pip install frida-tools
.
Installing frida server on our rooted device means obtaining a binary for the
platform architecture, copying and running.
Frida Loader is an android
application that will manage that for you. Grab the APK from the
latest release
and install with adb install
.
When you run the launcher it will give you an option to download, install and run the latest and greatest.
To check the server is functioning first find the device frida-ls-devices
should return something like:
Id Type Name
------------- ------ ---------------------
local local Local System
emulator-5554 usb Android Emulator 5554
socket remote Local Socket
To view a list of process on the device use frida-ps -D emulator-5554
or frida-ps -U
for the USB device.
Hooking Treasure: Targeting an App
Now that we have a rooted device with frida installed, we can install the target the app and make sure it’s available by checking the package name:
adb shell pm list packages | grep jokeapp
(package:com.github.ryjen.jokeapp)
Note that I am using a development debug build for clarity, in reality the app would be install from an app store and may require disassembling to find target code.
Hooking is a Frida scripting API to allow replacing code at runtime.
Our goal is to intercept and return spoofed data for:
- the network API for a random joke
- saving a joke to the database
- sharing a joke
As an analogy, I will call these use cases the 3 parrots. They will echo back a message we tell it (reminds me of the hypno-parrot emoji).
Walk the Plank: Write Scripts
Writing Frida hooks is easy when you have the source code and a debug build. In the real world, you must locate the obfuscated methods after disassembling them. The process typically looks as follows:
- Disassemble the application APK in a tool called JADX-GUI
- Scan the resources and code for strings related to the targets (service, network, API, client, remote, database, repository, etc.)
- Use Frida to trace execution of target code using regex (frida-trace -U -j ‘x4.a!*’ Jiver)
- Use the application at points of execution to verify and gather more information (like parameters)
- Once you have located the target code to hook or have the source code, you can start writing scripts to exploit potential vulnerabilities.
Network Parrot 🦜
The following frida script intercepts the network service and returns spoofed data:
const Types = {
Service: "com.github.ryjen.jokeapp.data.api.JokeService",
Response: "com.github.ryjen.jokeapp.data.model.JokeResponse",
Continuation: "kotlin.coroutines.Continuation",
};
Java.perform(function () {
var Service = Java.use(Types.Service);
var Response = Java.use(Types.Response);
// Overload a private kotlin suspend function to return a hacked network response
Service.getRandomJoke.overload(Types.Continuation).implementation = function (
continuation
) {
var actual = this.getRandomJoke(continuation);
// suspend functions pass a flag to indicate it will not return
if (actual.toString() == "COROUTINE_SUSPENDED") {
return actual;
}
// log the actual data
console.log(actual);
// return the spoofed data
return Response.$new("hackID", "This app is HACKED", 200);
};
});
Â
The basic premise of the code is:
- Define java types used
- Use the java instrumentation to perform the hooking
- Define the java classes in the application binary to use
- Overload the API method in the network service classes
- Log the actual value
- Return the spoofed Response
Kotlin coroutines internally pass an intrinsic parameter to signal the function
has suspended instead of returned. In frida we need to handle this by checking
for COROUTINE_SUSPENDED
before performing the overload.
Running frida -U --no-pause -f com.github.ryjen.jokeapp -l network.js
the result
is the application parrots back the response every network call.
The logging should look something like this in the frida console after 3 refreshes:
JokeResponse(id=fii3Tv4hFd, joke=Today, my son asked "Can I have a book mark?" and I burst into tears. 11 years old and he still doesn't know my name is Brian., status=200)
JokeResponse(id=IeiyIRSnbxc, joke=I applied to be a doorman but didn't get the job due to lack of experience. That surprised me, I thought it was an entry level position., status=200)
JokeResponse(id=kOfaUvP7Muc, joke=What did the Dorito farmer say to the other Dorito farmer? Cool Ranch!, status=200)
Database Parrot 🦜
When the user adds a favorite joke, we want to hook saving to the database to insert spoofed data. The following script accomplishes the task.
const Types = {
Joke: "com.github.ryjen.jokeapp.domain.model.Joke",
Database: "com.github.ryjen.jokeapp.data.repository.joke.JokeRepository",
Continuation: "kotlin.coroutines.Continuation",
};
Java.perform(function () {
var Database = Java.use(Types.Database);
var Joke = Java.use(Types.Joke);
// Overload a private kotlin suspend function to return a hacked network response
Database.addFavorite.overload(Types.Joke, Types.Continuation).implementation =
function (joke, continuation) {
if (continuation.toString() == "COROUTINE_SUSPENDED") {
this.addFavorite(joke, continuation);
return null;
}
var spoofed = Joke.$new("jokeID", "This joke is OWNED", null, true);
console.log(joke);
this.addFavorite(spoofed, continuation);
return null; // kotlin unit
};
});
Â
The difference here is replacing the method call with a spoofed parameter instead of returning one.
Running frida -U --no-pause -f com.github.ryjen.jokeapp -l network.js
and
saving a joke adds the spoofed joke to the database.
Again, our logging should log the original data for snooping.
Joke(id=7p41Lmbpjqc, content=What has three letters and starts with gas? A Car., created=null, isFavorite=true)
Joke(id=8p49pWvcxAd, content=Every night at 11:11, I make a wish that someone will come fix my broken clock., created=null, isFavorite=true)
Joke(id=EYo4TCAdUf, content=I tried to write a chemistry joke, but could never get a reaction., created=null, isFavorite=true)
Sharing Parrot 🦜
When the user shares a joke, we want to intercept and send a hacked response. Could be useful to pass private application data to an external resource on the device.
const Types = {
Share: "com.github.ryjen.jokeapp.ui.components.Share",
String: "java.lang.String",
Context: "android.content.Context",
};
Java.perform(function () {
var Share = Java.use(Types.Share);
// A message with a exposed data or an attack link
var spoofed = "Please visit hacked link https://example.com";
var shareText = Share.text.overload(Types.Context, Types.String);
shareText.implementation = function (context, content) {
console.log(content);
shareText.call(this, context, spoofed);
};
});
Â
Running frida -U --no-pause -f com.github.ryjen.jokeapp -l share.js
and sharing
a joke results in the spoofed content shared instead. Logging would show the
original joke content as expected.
EOL, Shutdown: Conclusion
The best way to avoid hacking by Frida is to 1) avoid rooted mobile devices and 2) ensure integrity of the application binary.
An application distributed via the play store a verification API from Google play store is available to add a security layer to user actions. Backend checks might be necessary depending on your architecture.
At the application level some best practices include:
- Adding detection of rooting/jailbreaking
- Verifying the integrity of signing signature
- Verifying the vendor package
- Verifying release builds are not debuggable
- Checking for Frida installs
- Harden network access points
Enterprise applications requiring extra security should consider a service like NowSecure provided by the creators of Frida. Other options include VeraCode, Fortify, or Checkmarx.
Dynamic analysis security services (AKA Penetration Testing) run ad-hoc per identified risk can keep you ahead of unknown risks. Static analysis security services in continuous integration keep developers aware of code vulnerabilities.
As vulnerabilities and mitigations are constantly evolving, an application must be pro-active in keeping up to date and knowing its risk factors.
In summary, there are options to prevent hackers from breaching security at the device level and the application level. A service specializing in detection can be practical and adaptable in a cybersecurity landscape.
** DISCLAIMER: This is partially setting up a penetration testing environment. Do not use on targets without authorization as you would be potentially breaking the law. Bugcrowd has a list of projects accepting testing within limits.