Bypassing In-App Protections Uncrackable 3 Android
Hello guys and welcome back to another walkthrough. This time we’ll be looking at how ton bypass. The Android application has implemented protection from the java layer and also inside the native library. I’ve already written two posts concerning how to bypass similar checks in an application hence this should be a walk in the park. The only pre-requise you’ll need is an understanding on frida and how it performs hooking. We’ll begun by hooking the protection in the Java layer and find out that still the application crashes and realize the native binary has an extra layer of protection. We hook into the native library and patch the code. Am sure there are other plenty of solutions out there but it was cool how i did it and figured why not make a walkthrough of it maybe it might help someone
The Android application i used can be downloaded using the below link
Try as much as possible to not look at the spoilers and take this as a learning opportunity.
After installing the application using adb and opening it we get the following error message
Time to do some reverse engineering. I opened the application using jadx-gui
Looking through the code to determine where the checks are being triggered fro the main activity we notice that it’s being triggered from an if statement code that calls a RootDetection class
My main aim was to begin by hooking into this 3 classes and see if i can be able to bypass this checks using frida. let’s being by creating the frida scripts. Below is a template i wrote which include popular frida articles which have helped me along the way
/**
* Articles
* https://node-security.com/posts/frida-for-ios/
* https://frida.re/docs/javascript-api/
* https://awakened1712.github.io/hacking/hacking-frida/
* https://github.com/interference-security/frida-scripts
* https://learnfrida.info/advanced_usage/
* https://8ksec.io/advanced-frida-mobile/
* https://github.com/frida/frida/issues/298
* https://github.com/frida/frida/issues/607
* https://deviltux.thedev.id/notes/ios-frida-scripting/
* https://github.com/frida/frida/issues/985 working
*/
const colors = {
colorize: (str, cc) => `\x1b${cc}${str}\x1b[0m`,
red: str => colors.colorize(str, '[31m'),
green: str => colors.colorize(str, '[32m'),
yellow: str => colors.colorize(str, '[33m'),
blue: str => colors.colorize(str, '[34m'),
cyan: str => colors.colorize(str, '[36m'),
white: str => colors.colorize(str, '[37m'),
};
Java.perform(function(){
try{
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
We are going to build our scripts from this specific template. Looking the the root detection class it has three functions checkRoot1, checkRoot2 and checkRoot3 which returns boolean values. If root is detected it returns true
There’s also an integrity check class that also returns true if the application is being debugged
Let’s start by writing our code
Below is the code i wrote
/**
* Articles
* https://node-security.com/posts/frida-for-ios/
* https://frida.re/docs/javascript-api/
* https://awakened1712.github.io/hacking/hacking-frida/
* https://github.com/interference-security/frida-scripts
* https://learnfrida.info/advanced_usage/
* https://8ksec.io/advanced-frida-mobile/
* https://github.com/frida/frida/issues/298
* https://github.com/frida/frida/issues/607
* https://deviltux.thedev.id/notes/ios-frida-scripting/
* https://github.com/frida/frida/issues/985 working
*/
const colors = {
colorize: (str, cc) => `\x1b${cc}${str}\x1b[0m`,
red: str => colors.colorize(str, '[31m'),
green: str => colors.colorize(str, '[32m'),
yellow: str => colors.colorize(str, '[33m'),
blue: str => colors.colorize(str, '[34m'),
cyan: str => colors.colorize(str, '[36m'),
white: str => colors.colorize(str, '[37m'),
};
Java.perform(function(){
try{
var rootdetect1 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect1["checkRoot1"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot1 called by the application"));
var ret = this.checkRoot1();
console.log(colors.green("\t[*] Return value for checkRoot1: "+ret));
return ret
}
var rootdetect2 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect2["checkRoot2"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot2 called by the application"));
var ret = this.checkRoot2();
console.log(colors.green("\t[*] Return value for checkRoot2: "+ret));
return ret
}
var rootdetect3 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect3["checkRoot3"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot3 called by the application"));
var ret = this.checkRoot3();
console.log(colors.green("\t[*] Return value for checkRoot3: "+ret));
return ret
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
Java.perform(function(){
try{
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
Before we can run the code we need to know the application id. This can be done using frida-ps and the below command
frida-ps -Uia | grep -i crack
Now that we have the package name we can run the application while injecting our script. The command i used was
frida -U -l scripts/hook.js -f owasp.mstg.uncrackable3
Looking at the script only checkroot1 is called and it returns true
Let’s change the return value to false. This requires us to update the script
/**
* Articles
* https://node-security.com/posts/frida-for-ios/
* https://frida.re/docs/javascript-api/
* https://awakened1712.github.io/hacking/hacking-frida/
* https://github.com/interference-security/frida-scripts
* https://learnfrida.info/advanced_usage/
* https://8ksec.io/advanced-frida-mobile/
* https://github.com/frida/frida/issues/298
* https://github.com/frida/frida/issues/607
* https://deviltux.thedev.id/notes/ios-frida-scripting/
* https://github.com/frida/frida/issues/985 working
*/
const colors = {
colorize: (str, cc) => `\x1b${cc}${str}\x1b[0m`,
red: str => colors.colorize(str, '[31m'),
green: str => colors.colorize(str, '[32m'),
yellow: str => colors.colorize(str, '[33m'),
blue: str => colors.colorize(str, '[34m'),
cyan: str => colors.colorize(str, '[36m'),
white: str => colors.colorize(str, '[37m'),
};
Java.perform(function(){
try{
var rootdetect1 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect1["checkRoot1"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot1 called by the application"));
var ret = this.checkRoot1();
console.log(colors.green("\t[*] Return value for checkRoot1: "+ret+"\n\t[+] Returning false"));
return false
}
var rootdetect2 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect2["checkRoot2"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot2 called by the application"));
var ret = this.checkRoot2();
console.log(colors.green("\t[*] Return value for checkRoot2: "+ret));
return ret
}
var rootdetect3 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect3["checkRoot3"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot3 called by the application"));
var ret = this.checkRoot3();
console.log(colors.green("\t[*] Return value for checkRoot3: "+ret));
return ret
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
Java.perform(function(){
try{
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
When we inject the script a second time all three root checks and all return false. But why is the application still erroring out ???
There was one last integrity check Let’s try hooking the method and see if it’ll be enough to bypass the check
Below is the revised code
/**
* Articles
* https://node-security.com/posts/frida-for-ios/
* https://frida.re/docs/javascript-api/
* https://awakened1712.github.io/hacking/hacking-frida/
* https://github.com/interference-security/frida-scripts
* https://learnfrida.info/advanced_usage/
* https://8ksec.io/advanced-frida-mobile/
* https://github.com/frida/frida/issues/298
* https://github.com/frida/frida/issues/607
* https://deviltux.thedev.id/notes/ios-frida-scripting/
* https://github.com/frida/frida/issues/985 working
*/
const colors = {
colorize: (str, cc) => `\x1b${cc}${str}\x1b[0m`,
red: str => colors.colorize(str, '[31m'),
green: str => colors.colorize(str, '[32m'),
yellow: str => colors.colorize(str, '[33m'),
blue: str => colors.colorize(str, '[34m'),
cyan: str => colors.colorize(str, '[36m'),
white: str => colors.colorize(str, '[37m'),
};
Java.perform(function(){
try{
var rootdetect1 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect1["checkRoot1"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot1 called by the application"));
var ret = this.checkRoot1();
console.log(colors.green("\t[*] Return value for checkRoot1: "+ret+"\n\t[+] Returning false"));
return false
}
var rootdetect2 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect2["checkRoot2"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot2 called by the application"));
var ret = this.checkRoot2();
console.log(colors.green("\t[*] Return value for checkRoot2: "+ret));
return ret
}
var rootdetect3 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect3["checkRoot3"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot3 called by the application"));
var ret = this.checkRoot3();
console.log(colors.green("\t[*] Return value for checkRoot3: "+ret));
return ret
}
var integritycheck = Java.use("sg.vantagepoint.util.IntegrityCheck");
integritycheck["isDebuggable"].overload('android.content.Context').implementation = function(context_0){
console.log(colors.cyan("\t[*] isDebuggable called by the application"));
var ret = this.isDebuggable(context_0);
console.log(colors.green("\t[*] Return value for isDebuggable: "+ret));
return ret
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
Java.perform(function(){
try{
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
Running the script again we get the following error
Though we’ve managed to patch all the in-app protection from the java layer still the application fails. But looking at the error message returned by frida we get some really useful information We get the stack trace and offset where the error occurred. And i was mostly interested in the libfoo.so shared library since it’s related to the Android application itself
I de-compiled the binary with apktool to get the shared library. The command i used was
java -jar apktool_2.10.0.jar d UnCrackable-Level3.apk
I loaded the shared library to binary ninja which by far has become the best disassembler i have used(My opinion.). But you can use any other disassembler like ghidra or hopper or ida.
Remember we had the offset where our application was erroring out. My first though was to just jump to that offset
Pressing g when in binary ninja brings up the jump to offset window
I just input the offset we got earlier and pressed enter
Let’s patch the memory address to include nopsleds this will ensure that when that code segment is hit nothing happens the application just keeps on executing
To hook into the library we first need to understand how it happens. linker64 from arm64 architecture is used to load the shared library to memory. We need to get the address of the symbol do_dlopen which is used to load the library in memory. The reason as to why we are doing this is because we are catering for a scenario where we try to hook the shared library before it’s loaded because this will cause frida to error out hence we need to wait to the exact moment when the shared library has fully loaded in memory and just before it’s functions are executed. To know that the library has fully loaded we hook the call_constructor method
Below is the code used to ensure that this preconditions are met
var do_dlopen_address = null;
var call_constructor_address = null
var linker64 = Process.findModuleByName("linker64").enumerateSymbols();
for(var i = 0;i < linker64.length;i++){
if((JSON.stringify(linker64[i])).indexOf("do_dlopen") >= 0){
console.log(colors.green("\t[+] Address of do_dlopen leaked: "+linker64[i].address));
do_dlopen_address = linker64[i].address
}else if((JSON.stringify(linker64[i])).indexOf("call_constructor") >= 0){
console.log(colors.green("\t[+] Address of call_constructor leaked: "+linker64[i].address));
call_constructor_address = linker64[i].address
}
}
var libfoo_loaded = 0
Java.perform(function(){
try{
Interceptor.attach(do_dlopen_address, {
onEnter: function(args){
if((args[0].readCString()).indexOf("libfoo.so") >= 0){
console.log(colors.yellow("\t[+] libfoo.so has begun loading in memory"));
Interceptor.attach(call_constructor_address, {
onEnter: function(args){
if(libfoo_loaded == 0){
console.log(colors.yellow("\t[+] Libfoo.so has finished loading in memory"));
libfoo_loaded = 1
}
}
})
}
},
onLeave: function(retval){
//do nothing
}
})
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
Ensure the abbove snippet is added to the pre-existing code to be
/**
* Articles
* https://node-security.com/posts/frida-for-ios/
* https://frida.re/docs/javascript-api/
* https://awakened1712.github.io/hacking/hacking-frida/
* https://github.com/interference-security/frida-scripts
* https://learnfrida.info/advanced_usage/
* https://8ksec.io/advanced-frida-mobile/
* https://github.com/frida/frida/issues/298
* https://github.com/frida/frida/issues/607
* https://deviltux.thedev.id/notes/ios-frida-scripting/
* https://github.com/frida/frida/issues/985 working
*/
const colors = {
colorize: (str, cc) => `\x1b${cc}${str}\x1b[0m`,
red: str => colors.colorize(str, '[31m'),
green: str => colors.colorize(str, '[32m'),
yellow: str => colors.colorize(str, '[33m'),
blue: str => colors.colorize(str, '[34m'),
cyan: str => colors.colorize(str, '[36m'),
white: str => colors.colorize(str, '[37m'),
};
Java.perform(function(){
try{
var rootdetect1 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect1["checkRoot1"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot1 called by the application"));
var ret = this.checkRoot1();
console.log(colors.green("\t[*] Return value for checkRoot1: "+ret+"\n\t[+] Returning false"));
return false
}
var rootdetect2 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect2["checkRoot2"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot2 called by the application"));
var ret = this.checkRoot2();
console.log(colors.green("\t[*] Return value for checkRoot2: "+ret));
return ret
}
var rootdetect3 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect3["checkRoot3"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot3 called by the application"));
var ret = this.checkRoot3();
console.log(colors.green("\t[*] Return value for checkRoot3: "+ret));
return ret
}
var integritycheck = Java.use("sg.vantagepoint.util.IntegrityCheck");
integritycheck["isDebuggable"].overload('android.content.Context').implementation = function(context_0){
console.log(colors.cyan("\t[*] isDebuggable called by the application"));
var ret = this.isDebuggable(context_0);
console.log(colors.green("\t[*] Return value for isDebuggable: "+ret));
return ret
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
var do_dlopen_address = null;
var call_constructor_address = null
var linker64 = Process.findModuleByName("linker64").enumerateSymbols();
for(var i = 0;i < linker64.length;i++){
if((JSON.stringify(linker64[i])).indexOf("do_dlopen") >= 0){
console.log(colors.green("\t[+] Address of do_dlopen leaked: "+linker64[i].address));
do_dlopen_address = linker64[i].address
}else if((JSON.stringify(linker64[i])).indexOf("call_constructor") >= 0){
console.log(colors.green("\t[+] Address of call_constructor leaked: "+linker64[i].address));
call_constructor_address = linker64[i].address
}
}
var libfoo_loaded = 0
Java.perform(function(){
try{
Interceptor.attach(do_dlopen_address, {
onEnter: function(args){
if((args[0].readCString()).indexOf("libfoo.so") >= 0){
console.log(colors.yellow("\t[+] libfoo.so has begun loading in memory"));
Interceptor.attach(call_constructor_address, {
onEnter: function(args){
if(libfoo_loaded == 0){
console.log(colors.yellow("\t[+] Libfoo.so has finished loading in memory"));
libfoo_loaded = 1
}
}
})
}
},
onLeave: function(retval){
//do nothing
}
})
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
Java.perform(function(){
try{
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
Running the script we get the exact moment the libfoo library finishes loading in memory.
We are now okay to patch the binary..
I wrote a function that patches the specific memory address specified during runtime below is the code snippet
function patchMemory(base_address, offset){
try{
var patch_address = base_address.add(offset);
Memory.patchCode(patch_address, Process.pageSize, code => {
const cw = new Arm64Writer(code, { pc: patch_address });
cw.putNop();
cw.putRet();
});
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}
The function requires two arguments which are the base address and the offset to jump to from the base address
To get the base address i used the following api from frida
Module.getBaseAddress("libfoo.so");
Remember we need to get the base address during runtime because the address will keep on changing due to aslr
To call the function we’ll do so as below
The full exploit chain will be
/**
* Articles
* https://node-security.com/posts/frida-for-ios/
* https://frida.re/docs/javascript-api/
* https://awakened1712.github.io/hacking/hacking-frida/
* https://github.com/interference-security/frida-scripts
* https://learnfrida.info/advanced_usage/
* https://8ksec.io/advanced-frida-mobile/
* https://github.com/frida/frida/issues/298
* https://github.com/frida/frida/issues/607
* https://deviltux.thedev.id/notes/ios-frida-scripting/
* https://github.com/frida/frida/issues/985 working
*/
const colors = {
colorize: (str, cc) => `\x1b${cc}${str}\x1b[0m`,
red: str => colors.colorize(str, '[31m'),
green: str => colors.colorize(str, '[32m'),
yellow: str => colors.colorize(str, '[33m'),
blue: str => colors.colorize(str, '[34m'),
cyan: str => colors.colorize(str, '[36m'),
white: str => colors.colorize(str, '[37m'),
};
Java.perform(function(){
try{
var rootdetect1 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect1["checkRoot1"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot1 called by the application"));
var ret = this.checkRoot1();
console.log(colors.green("\t[*] Return value for checkRoot1: "+ret+"\n\t[+] Returning false"));
return false
}
var rootdetect2 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect2["checkRoot2"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot2 called by the application"));
var ret = this.checkRoot2();
console.log(colors.green("\t[*] Return value for checkRoot2: "+ret));
return ret
}
var rootdetect3 = Java.use("sg.vantagepoint.util.RootDetection");
rootdetect3["checkRoot3"].overload().implementation = function(){
console.log(colors.cyan("\t[*] checkRoot3 called by the application"));
var ret = this.checkRoot3();
console.log(colors.green("\t[*] Return value for checkRoot3: "+ret));
return ret
}
var integritycheck = Java.use("sg.vantagepoint.util.IntegrityCheck");
integritycheck["isDebuggable"].overload('android.content.Context').implementation = function(context_0){
console.log(colors.cyan("\t[*] isDebuggable called by the application"));
var ret = this.isDebuggable(context_0);
console.log(colors.green("\t[*] Return value for isDebuggable: "+ret));
return ret
}
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
var do_dlopen_address = null;
var call_constructor_address = null
var linker64 = Process.findModuleByName("linker64").enumerateSymbols();
for(var i = 0;i < linker64.length;i++){
if((JSON.stringify(linker64[i])).indexOf("do_dlopen") >= 0){
console.log(colors.green("\t[+] Address of do_dlopen leaked: "+linker64[i].address));
do_dlopen_address = linker64[i].address
}else if((JSON.stringify(linker64[i])).indexOf("call_constructor") >= 0){
console.log(colors.green("\t[+] Address of call_constructor leaked: "+linker64[i].address));
call_constructor_address = linker64[i].address
}
}
var libfoo_loaded = 0
Java.perform(function(){
try{
Interceptor.attach(do_dlopen_address, {
onEnter: function(args){
if((args[0].readCString()).indexOf("libfoo.so") >= 0){
console.log(colors.yellow("\t[+] libfoo.so has begun loading in memory"));
Interceptor.attach(call_constructor_address, {
onEnter: function(args){
if(libfoo_loaded == 0){
console.log(colors.yellow("\t[+] Libfoo.so has finished loading in memory"));
var libfoo_base_address = Module.getBaseAddress("libfoo.so");
patchMemory(libfoo_base_address, "0x31ac");
libfoo_loaded = 1
}
}
})
}
},
onLeave: function(retval){
//do nothing
}
})
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
function patchMemory(base_address, offset){
try{
var patch_address = base_address.add(offset);
Memory.patchCode(patch_address, Process.pageSize, code => {
const cw = new Arm64Writer(code, { pc: patch_address });
cw.putNop();
cw.putRet();
});
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
}
Java.perform(function(){
try{
}
catch(e){
console.log(colors.red("\t[*] An error occured: "+e.stack))
}
});
Let’s save the exploit and run with frida again. ooking down below all the in-app protections have been bypassed fully
This shows how frida is definitely a world class tool to have in your arsenal as an android pentester. I might this similar attack using frida Stalker though am having a few bugs am still working on 🙂.
I hope you enjoyed the walkthough if so clap of me down below and follow me so that you don’t miss any upcoming walkthough