OWASP MSTG Crackme 3 writeup (Android)
The application
├── owasp
│ └── mstg
│ └── uncrackable3
│ └── R.java
└── sg
└── vantagepoint
├── uncrackable3
│ ├── BuildConfig.java
│ ├── CodeCheck.java
│ ├── MainActivity.java
│ └── R.java
└── util
├── IntegrityCheck.java
└── RootDetection.java
When we inspect the content of MainActivity.java
we can quickly see that things are getting spicier and the root detection is not the only control anymore, but integrity checks are added to prevent code tampering.
On line 101 we see
if (RootDetection.checkRoot1() || RootDetection.checkRoot2() || RootDetection.checkRoot3() || IntegrityCheck.isDebuggable(getApplicationContext()) || tampered != 0) {
showDialog("Rooting or tampering detected.");
}
tampered
is set to 31337 from the function verifyLibs
when the native libraries are hooked or manipulated.
06-15 08:57:46.680 2817 2841 V UnCrackable3: Tampering detected! Terminating...
--------- beginning of crash
and the app miserably crashes.
Native library
libfoo.so
private CodeCheck m;
static {
System.loadLibrary("foo");
}
that implements the bar()
function
private native boolean bar(byte[] bArr);
We investigate further whether the native code changed.
CodeCheck native function
bar
function we will:- rename the .apk in .zip
- extract the native module
lib/libfoo.so
- reverse it using Ghidra
Ghidra
Java_sg_vantagepoint_uncrackable2_CodeCheck_bar
being exported and including few small changes from the previous challenge.- the input string has 24 chars (line 27:
if (iVar2 == 0x18)
) - every character of the string inserted by the user is XORed with each character of the
xorkey
(private static final String xorkey = "pizzapizzapizzapizzapizz";
)
if (*(byte *)(iVar1 + uVar3) != (*(byte *)puVar4 ^ local_40[uVar3])) goto LAB_00013456;
LAB_00013456
is called and 0 is returned, making the check fail.Java_sg_vantagepoint_uncrackable3_MainActivity_init
that is the implementation of the init(byte[] Array)
used to initialize the XOR key(init(xorkey.getBytes());
)Frida
We can see that a goodbye()
function is defined in libfoo.so
and is making the app crash. Using Ghidra we note that there is a new functionality able to detect whether Frida
or Xposed
are used.
goodbye()
function. To bypass all the controls we are going two use two different techniques:- overload of the
fgets
function to avoid file detection - overload of
System.exit(int)
to stop the app to close when clicking OK
function rootAndTamperingDetectionBypass(){
console.log("[*] Start removing root and tampering detection");
var System = Java.use('java.lang.System');
System.exit.implementation = function(var0) {
console.log("Ciao bella, I'm sorry but today you are not exiting");
};
console.log("[*] fgets overloading to avoid Frida detection")
var fgetsPtr = Module.findExportByName("libc.so", "fgets");
var fgets = new NativeFunction(fgetsPtr, 'pointer', ['pointer', 'int', 'pointer']);
Interceptor.replace(fgetsPtr, new NativeCallback(function (buffer, size, fp) {
var retval = fgets(buffer, size, fp);
var bufstr = Memory.readUtf8String(buffer);
if (bufstr.indexOf("frida") > -1) {
Memory.writeUtf8String(buffer, "ByeByeFrida:\t0");
}
return retval;
}, 'pointer', ['pointer', 'int', 'pointer']));
console.log("[*] Root and tampering detection removed, you can safely click OK");
}
At this point we can run our application using the following command:
frida -U -l hook.js -f owasp.mstg.uncrackable3 --no-pause
where hook.js
will call the function rootAndTamperingDetectionBypass()
at execution time, preventing the app to crash.
Exploit
Java_sg_vantagepoint_uncrackable2_CodeCheck_bar
on line 24, uses another function that we call FUN_00010fa0
, that is used to load the secrets in memory, but unfortunately is not exported and therefore we cannot directly hook it.libfoo.so
library and the offset of the function.0xFA0
. Let’s see how we can hook it. These are the steps to extract the secret:- Calculate the base address of
libfoo.so
usingModule.findBaseAddress('libfoo.so')
Add the offset to the base address to get to the function using
base_address.add(0xFA0)
Get the buffer
onEnter
Get the first 24 bytes
onLeave
XOR each byte of the secret with each byte of the well known XOR key
libfoo.so
is loaded when calling the function
function extractSecret(){
setTimeout(function(){
var base_address = Module.findBaseAddress('libfoo.so')
var extract_secret_function = base_address.add(0xFA0)
Interceptor.attach(extract_secret_function,{
onEnter(args) {
console.log('Base address libfoo.so: ' + base_address)
console.log('Base address secret_function: ' + extract_secret_function)
console.log('Reading buffer args[0]')
this.buf = args[0]
console.log('Buffer reading completed')
},
onLeave(result) {
console.log('---------------------')
var numBytes = 24
var buff = Memory.readByteArray(this.buf,numBytes)
console.log('[*] Secret key hexdump')
console.log('---------------------')
console.log(hexdump(buff, { length: numBytes, ansi: true }));
var secret_key = new Uint8Array(buff)
var str = "";
for(var i = 0; i < secret_key.length; i++) {
str += (secret_key[i].toString(16) + " ");
}
console.log('---------------------')
console.log('[*] Secret key ')
console.log('---------------------')
console.log(secret_key)
console.log('---------------------')
console.log('[*] XOR key ')
console.log('---------------------')
var xor_key = 'pizzapizzapizzapizzapizz';
console.log(xor_key);
console.log('---------------------')
console.log('[*] Plaintext secret')
console.log('---------------------')
var secret = []
for (var i =0;i
When we run the full script hook.js
, using the command
frida -U -l hook.js -f owasp.mstg.uncrackable3 --no-pause
the secret will be printed out as soon as the button Verify
is clicked.
making owasp great again
is printed in the console and the challenge is completed