Table of contents
Just another Smali patch / Root bypass / Frida Interceptor bypass / JDP Debugging / Secret disclosure / Ptrace Write-up.
Android Crackmes are a list of intentionally vulnerable Android applications.
APKs link. Github link. OWASP page.
You may check my DIVA article to setup an Android Lab Pentesting environment.
UnCrackable L1
A secret string is hidden somewhere in this app. Find a way to extract it.
Let’s download and install the app in our Android VM:
jamarir@kali:~$ curl -Lo UnCrackable-Level1.apk https://github.com/OWASP/owasp-mastg/raw/master/Crackmes/Android/Level_01/UnCrackable-Level1.apk
jamarir@kali:~$ adb install UnCrackable-Level1.apk
Performing Streamed Install
Success
Ro0t3d ? What’re you talkin’ about ?!
Opening the app with a rooted device shows the following alert:
In JADX, we see this popup is generated in the MainActivity
class:
If any of the c.a()
, c.b()
or c.c()
functions returns true
, then the application exits.
The first objective is to bypass this root detection, to prevent the application from exiting on a rooted device. This patch can be done either statically (edit the APK), or dynamically (edit the runtime’s logic). But first, let’s analyze why our device is flagged as rooted.
PATH Check Plz !
The first check is done by c.a()
:
This function checks if the file su
(SuperUser) exists in one of the PATH’s directories. su
is the binary that allows a program to elevate its privileges impersonating the root user:
In our Android VM, this file is located in /system/xbin/su
:
The Android’s PATH can be retrieved via the env
command:
jamarir@kali:~$ adb shell 'env |grep ^PATH'
PATH=/sbin:/system/sbin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin
Among these paths, the /system/xbin
directory will flag our device, because su
is present within it.
BUILD Check Plz !
The second method checks if the device’s build tag is test-keys
:
The android.os.Build
class contains the device’s build information, extracted from the system properties. Here’s an extract of the VM’s build:
jamarir@kali:~$ adb shell getprop |grep 'ro.build.'
[ro.build.characteristics]: [tablet]
[ro.build.date]: [Wed Mar 25 11:28:56 CST 2020]
[ro.build.date.utc]: [1585106936]
[ro.build.description]: [android_x86_64-userdebug 9 PI eng.lh.20200325.112926 test-keys]
[ro.build.display.id]: [android_x86_64-userdebug 9 PI eng.lh.20200325.112926 test-keys]
[ro.build.fingerprint]: [Android-x86/android_x86_64/x86_64:9/PI/lh03251128:userdebug/test-keys]
[ro.build.flavor]: [android_x86_64-userdebug]
[ro.build.host]: [server2]
[ro.build.id]: [PI]
[ro.build.product]: [x86_64]
[ro.build.tags]: [test-keys]
[ro.build.type]: [userdebug]
[ro.build.user]: [lh]
[ro.build.version.all_codenames]: [REL]
[ro.build.version.base_os]: []
[ro.build.version.codename]: [REL]
[ro.build.version.incremental]: [eng.lh.20200325.112926]
[ro.build.version.min_supported_target_sdk]: [17]
[ro.build.version.preview_sdk]: [0]
[ro.build.version.release]: [9]
[ro.build.version.sdk]: [28]
[ro.build.version.security_patch]: [2018-08-05]
The official tags are listed in this build numbers documentation (e.g. android-15.0.0_r5
). Our VM’s build tag is not one of these legitimate build tags; but test-keys
, which is flagged.
SU ARTIFACTS Check Plz !
The last check is done by c.c()
:
If one of the above paths exist in the filesystem, then our device is flagged. The only file flagged is .has_su_daemon
:
I couldn’t find the exact purpose of that file.
Apktool & Static smali patch
We can use Apktool to patch the APK locally. Let’s first disassemble the APK:
jamarir@kali:~$ apktool d UnCrackable-Level1.apk
The application’s logic is stored in smali files:
jamarir@kali:~$ tree UnCrackable-Level1/smali/
UnCrackable-Level1/smali/
└── sg
└── vantagepoint
├── a
│ ├── a.smali
│ ├── b.smali
│ └── c.smali
└── uncrackable1
├── MainActivity$1.smali
├── MainActivity$2.smali
├── MainActivity.smali
└── a.smali
These smali files are disassembled Android DEX Java files:
jamarir@kali:~$ file MainActivity.smali
MainActivity.smali: disassembled Android DEX Java class (smali/baksmali), ASCII text
This payatu blog explains the smali files’ structure. Basically, these files contain Dalvik opcodes (assembly instructions) (see Android documentation) that are run by a given class.
We don’t need to thoroughly understand smali code. But, to get a generic smali idea, let’s consider the following onCreate()
method:
Whose associated disassembled DEX code is:
This smali code is pretty human-readable and self-explanatory:
l.39:
.locals
sets the number of local variables used in a function. Only 1 local variable (v0
) is used throughout the function, either set to“Root detected!”
or“App is debuggable!”
.l.41-51:
invoke-static
runs a static function (with no parameter set in{}
), andmove-result v0
stores the result intov0
. Here, ifc.a()
(l.41-43), orc.b()
(l.45-47), isNotEqualZero
(i.e. not false), then the code jumps tocond_0
on line 53, which shows theRoot detected!
popup. Finally, ifa.c()
isEqualZero
(i.e. false), then we jump tocond_1
which goes to the nextb.a(getApplicationContext())
verification.l.47-70: Same logic as above to detect if the app is debuggable.
l.72: Returns
void
.
Therefore, to patch that smali method, we could for example delete the lines from 39 to 67 to only returns void
:
jamarir@kali:~$ apktool b UnCrackable-Level1
[...]
UnCrackable-Level1/smali/sg/vantagepoint/uncrackable1/MainActivity.smali[38,0] A .registers or .locals directive must be present for a non-abstract/non-final method
Could not smali file: sg/vantagepoint/uncrackable1/MainActivity.smali
Without forgetting to set at least .locals
or .registers
instruction:
jamarir@kali:~$ apktool b UnCrackable-Level1
jamarir@kali:~$ zipalign -f -v 4 UnCrackable-Level1/dist/UnCrackable-Level1.apk UnCrackable-Level1-patch.apk
jamarir@kali:~$ keytool -genkeypair -v -keystore dummy.keystore -alias dummy -keyalg RSA -keysize 2048 -validity 10000
jamarir@kali:~$ apksigner sign --ks dummy.keystore --ks-key-alias dummy UnCrackable-Level1-patch.apk
Note that we didn’t delete the lines from 68 to 70, i.e.
super.onCreate()
. That way, the application can be launched properly. Indeed, as the Android documentation states,onCreate()
is called when the activity is starting. This method (or its superclass one, if overriden) MUST be called from the main app’s thread. Otherwise, the application crashes:As a side note, the Smali Dalvik opcodes could also be shown directly in JADX:
Frida & Dynamic patch
Performing a runtime patch doesn’t require to alter the APK’s integrity, as we’ll patch the process running the application. In other words, we’ll alter the application’s process in-memory, which won’t affect the disk.
The System.exit()
function is called if a rooted device is detected:
An easy bypass would be to patch that function using the Frida Interceptor:
jamarir@kali:~$ cat patch.js
Java.use("java.lang.System").exit.implementation = function (var0) {
console.log("[+] Ain't exit !");
return;
}
jamarir@kali:~$ frida -U -f owasp.mstg.uncrackable1 -l patch.js
[...]
Spawned `owasp.mstg.uncrackable1`. Resuming main thread!
[VirtualBox::owasp.mstg.uncrackable1 ]-> [+] Ain't exit !
Here, Java.use(<className>)
gives us a JavaScript wrapper for a class, so that we may patch one of its function’s implementation (e.g. exit
).
We could also have patched the MainActivity.a()
function to do nothing:
jamarir@kali:~$ cat patch.js
let MainActivity = Java.use("sg.vantagepoint.uncrackable1.MainActivity");
MainActivity["a"].implementation = function (str) {
console.log(`MainActivity.a is called: str=${str}`);
//this["a"](str);
};
However, running this patch throws the following error:
jamarir@kali:~$ frida -U -f owasp.mstg.uncrackable1 -l patch.js
[...]
Error: java.lang.ClassNotFoundException: Didn't find class "sg.vantagepoint.uncrackable1.MainActivity" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /system/vendor/lib64, /system/lib64, /system/vendor/lib64]]
at <anonymous> (frida/node_modules/frida-java-bridge/lib/env.js:124)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/env.js:115)
at apply (native)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/env.js:97)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/class-factory.js:488)
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:949)
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:954)
at _make (frida/node_modules/frida-java-bridge/lib/class-factory.js:165)
at use (frida/node_modules/frida-java-bridge/lib/class-factory.js:62)
at use (frida/node_modules/frida-java-bridge/index.js:258)
at <eval> (/tmp/a/patch.js:1)
That’s because frida doesn’t know what this uncommon MainActivity.a()
function is referring to. For frida to know the application’s classes’ functions, we need to attach it to the application’s thread using Java.perform()
:
jamarir@kali:~$ cat patch.js
Java.perform(() => {
let MainActivity = Java.use("sg.vantagepoint.uncrackable1.MainActivity");
MainActivity["a"].implementation = function (str) {
console.log(`MainActivity.a is called: str=${str}`);
//this["a"](str);
};
})
jamarir@kali:~$ frida -U -f owasp.mstg.uncrackable1 -l patch.js
[...]
[VirtualBox::owasp.mstg.uncrackable1 ]-> MainActivity.a is called: str=Root detected!
Java.perform()
wasn’t required to patchSystem.exit()
, because this is a callback from the inherently known Java classes.Note that the patch could be shrinked into:
Java.perform(() => { Java.use("sg.vantagepoint.uncrackable1.MainActivity").a.implementation = (str) => {} })
Secret ? What’re you talkin’ about ?!
In the application, a secret string is asked. An invalid secret returns the following error:
The corresponding code is:
If a.a()
is true, then we have the success message. We could patch that function to always return true, but that wouldn’t be funny, as it won’t disclose the real password:
jamarir@kali:~$ cat patch.js
Java.perform(() => {
Java.use("sg.vantagepoint.uncrackable1.MainActivity").a.implementation = (str) => {}
Java.use("sg.vantagepoint.uncrackable1.a")["a"].implementation = function (str) {
console.log(`a.a("${str}") returns ${this["a"](str)}`);
return true;
};
})
jamarir@kali:~$ frida -U -f owasp.mstg.uncrackable1 -l patch.js
[...]
[VirtualBox::owasp.mstg.uncrackable1 ]-> a.a("P@ssw0rd123!") returns false
Note that the implementation’s patch MUST be formatted as
function() {}
. Indeed, using the arrow function() => {}
syntax throws an error:
jamarir@kali:~$ cat patch.js Java.perform(() => { Java.use("sg.vantagepoint.uncrackable1.MainActivity").a.implementation = (str) => {} Java.use("sg.vantagepoint.uncrackable1.a")["a"].implementation = (str) => { console.log(`a.a("${str}") returns:`); console.log(this["a"](str)); return true; }; })
jamarir@kali:~$ frida -U -f owasp.mstg.uncrackable1 -l patch.js [...] [VirtualBox::owasp.mstg.uncrackable1 ]-> a.a("P@ssw0rd123!") returns: Error: expected an unsigned integer TypeError: not a function at <anonymous> (/tmp/a/patch.js:6) at apply (native) at ne (frida/node_modules/frida-java-bridge/lib/class-factory.js:677) at <anonymous> (frida/node_modules/frida-java-bridge/lib/class-factory.js:655)
That’s because arrow functions do not instantiate their own
this
reference. In such a case,this
would refer back to the parent object. Therefore, we wouldn’t be able to call the function while patching it.
Frida log for password disclosure
Instead, let’s log the secret with frida. The secret is calculated by the sg.vantagepoint.a.a.a()
function. Thus we may simply log its output in frida:
jamarir@kali:~$ cat patch.js
Java.perform(() => {
Java.use("sg.vantagepoint.uncrackable1.MainActivity").a.implementation = (str) => {}
Java.use("sg.vantagepoint.a.a")["a"].implementation = function (bArr, bArr2) {
let result = this["a"](bArr, bArr2);
console.log(`sg.vantagepoint.a.a(${bArr}, ${bArr2}) returns ${result}`);
return result;
};
})
jamarir@kali:~$ frida -U -f owasp.mstg.uncrackable1 -l patch.js
[...]
[VirtualBox::owasp.mstg.uncrackable1 ]-> sg.vantagepoint.a.a(-115,18,118,-124,-53,-61,124,23,97,109,-128,108,-11,4,115,-52, -27,66,98,21,-53,91,-102,6,-61,-96,-75,-26,-92,-67,118,-102,73,-24,-16,116,-8,46,-1,29,-107,-85,124,23,20,118,24,-25) returns 73,32,119,97,110,116,32,116,111,32,98,101,108,105,101,118,101
The secret is encoded in ASCII as 73,[...],101
(e.g. 73
is I
). Then, we can use the fromCharCode()
JS function to decode it:
jamarir@kali:~$ cat patch.js
Java.perform(() => {
Java.use("sg.vantagepoint.uncrackable1.MainActivity").a.implementation = (str) => {}
Java.use("sg.vantagepoint.a.a")["a"].implementation = function (bArr, bArr2) {
let result = this["a"](bArr, bArr2);
console.log(`sg.vantagepoint.a.a(${bArr}, ${bArr2}) returns ${result}`);
for (var i = 0; i < result.length; i++) {
console.log(String.fromCharCode(result[i]));
}
return result;
};
})
jamarir@kali:~$ frida -U -f owasp.mstg.uncrackable1 -l patch.js
[...]
[VirtualBox::owasp.mstg.uncrackable1 ]-> sg.vantagepoint.a.a(-115,18,118,-124,-53,-61,124,23,97,109,-128,108,-11,4,115,-52, -27,66,98,21,-53,91,-102,6,-61,-96,-75,-26,-92,-67,118,-102,73,-24,-16,116,-8,46,-1,29,-107,-85,124,23,20,118,24,-25) returns 73,32,119,97,110,116,32,116,111,32,98,101,108,105,101,118,101
I
w
a
n
t
t
o
b
e
l
i
e
v
e
JDWP for password disclosure
As the OWASP MASTG on debugging states, using a debugger allows to step through Java code, set breakpoints on Java methods, inspect and modify input, local and instance variables. Therefore, if we set a breakpoint in the function using the secret as an input variable, we may disclose its value.
First, we’ll set the android:debuggable
attribute in the MANIFEST:
jamarir@kali:~$ apktool d UnCrackable-Level1.apk
jamarir@kali:~$ vim UnCrackable-Level1/AndroidManifest.xml
[...]
<application [...] android:debuggable="true">
[...]
Then, we rebuild, sign and reinstall the app:
jamarir@kali:~$ apktool b UnCrackable-Level1.apk
jamarir@kali:~$ zipalign -f -v 4 UnCrackable-Level1/dist/UnCrackable-Level1.apk UnCrackable-Level1.apk
jamarir@kali:~$ apksigner sign --ks dummy.keystore --ks-key-alias dummy UnCrackable-Level1.apk
jamarir@kali:~$ adb uninstall owasp.mstg.uncrackable1
jamarir@kali:~$ adb install UnCrackable-Level1.apk
We may confirm the app is debuggable if the app’s PID is listed in the adb jdwp
‘s output:
jamarir@kali:~$ adb shell 'ps |grep owasp'
u0_a106 8142 1090 3336036 126400 0 0 S owasp.mstg.uncrackable1
jamarir@kali:~$ adb jdwp
[...]
8142
Then, we can set the app as debuggable and bypass the app being flagged with Frida:
jamarir@kali:~$ adb shell am set-debug-app -w --persistent owasp.mstg.uncrackable1
jamarir@kali:~$ cat patch.js
Java.perform(() => {
Java.use("sg.vantagepoint.uncrackable1.MainActivity").a.implementation = (str) => {}
})
jamarir@kali:~$ frida -U -f owasp.mstg.uncrackable1 -l patch.js
The application waits for a debugger to attach. Thus, we’ll debug the app from our kali machine; the steps are:
Get the app’s PID:
jamarir@kali:~$ adb shell ps |grep 'PID\|owasp' USER PID PPID VSZ RSS WCHAN ADDR S NAME u0_a106 8406 1090 4005684 95192 0 0 S owasp.mstg.uncrackable1
Use JDWP to forward local commands to the remote app through ADB:
jamarir@kali:~$ adb forward tcp:4444 jdwp:8406 jamarir@kali:~$ adb forward --list 192.168.56.110:5555 tcp:4444 jdwp:8406
Suspend the thread for analysis:
jamarir@kali:~$ { echo "suspend"; cat; } |jdb -attach localhost:4444 Set uncaught java.lang.Throwable Set deferred uncaught java.lang.Throwable Initializing jdb ... > All threads suspended. >
Use JDP commands to debug the application. For example, we may get the
sg.vantagepoint.uncrackable1.a
methods:> classes [...] sg.vantagepoint.a.b sg.vantagepoint.a.c sg.vantagepoint.uncrackable1.MainActivity sg.vantagepoint.uncrackable1.MainActivity$1 sg.vantagepoint.uncrackable1.MainActivity$2 sg.vantagepoint.uncrackable1.a [...] > methods sg.vantagepoint.uncrackable1.a ** methods list ** sg.vantagepoint.uncrackable1.a a(java.lang.String) sg.vantagepoint.uncrackable1.a b(java.lang.String) [...]
Here, we want to debug the
java.String.equals()
function. Indeed, this method gets the secret as an argument, which can be disclosed with thelocals
JDP command.So we set a breakpoint to this function, and resume the application’s thread:
> stop in java.lang.String.equals Set breakpoint java.lang.String.equals > clear Breakpoints set: breakpoint java.lang.String.equals > resume All threads resumed.
Once that function is hit (after entering a random password), the thread stops its execution at the method breakpoint. Therefore, we may use the
locals
andresume
/cont
(ornext
/step
) commands to print input/local variables and advance execution respectively. Becauseequals()
is used numerous times by the application, we must alternatively executelocals
andresume
until we disclose the secret:main[1] locals Method arguments: Local variables: anObject = "autofill" main[1] resume All threads resumed. > Breakpoint hit: "thread=main", java.lang.String.equals(), line=997 bci=0 [...] main[1] resume Breakpoint hit: All threads resumed. main[1] "thread=main", java.lang.String.equals(), line=997 bci=0 main[1] locals Method arguments: Local variables: anObject = "I want to believe"
Finally, the following commands clear the local JDWP forwards and the app’s debuggable state:
jamarir@kali:~$ adb forward --remove-all
jamarir@kali:~$ adb shell am clear-debug-app
UnCrackable L2
Ro0t3d ? What’re you talkin’ about ?!
This app holds a secret inside. May include traces of native code.
Let’s download and install the app in our Android VM:
jamarir@kali:~$ curl -Lo UnCrackable-Level2.apk https://github.com/OWASP/owasp-mastg/raw/master/Crackmes/Android/Level_02/UnCrackable-Level2.apk
jamarir@kali:~$ adb install UnCrackable-Level2.apk
Performing Streamed Install
Success
Again, the app flags rooted devices and stops at startup:
Similarly as UnCrackable L1, 3 functions are used to check if the device is rooted: b.a()
, b.b()
and b.c()
:
These 3 functions are exactly the same as UnCrackable L1. So, because the application isn’t debuggable (default behavior), we may patch these 3 functions using frida:
jamarir@kali:~$ cat patch.js
Java.perform(() => {
Java.use("sg.vantagepoint.a.b").a.implementation = () => {return false}
Java.use("sg.vantagepoint.a.b").b.implementation = () => {return false}
Java.use("sg.vantagepoint.a.b").c.implementation = () => {return false}
})
jamarir@kali:~$ frida -U -f owasp.mstg.uncrackable2 -l patch.js
Secret ? What’re you talkin’ about ?!
Again, a secret is asked, which we have no knowledge of:
The verification function is CodeCheck.a()
:
You may check my DIVA article for more details about the native libraries.
This function simply returns the result of the native function bar()
from the libfoo.so
library, which takes our string as an input:
I’ve edited the decompiled Ghidra code to make the code more readable.
From line 18 to 22, we see that 4 variables are set one after the other, each with a size of 4 bytes, and then one variable of 7 bytes is set. We may assume that these variables pushed onto the stack are one big string of length 23 (4+4+4+4+7
). Therefore, let’s retype uStack_38
to char[24]
(I added one character for the ending NULL byte):
The flag is Thanks for all the fish
(you’re welcome BTW).
UnCrackable L3
The crackme from hell! A secret string is hidden somewhere in this app. Find a way to extract it.
Let’s download and install the app in our Android VM:
jamarir@kali:~$ curl -Lo UnCrackable-Level3.apk https://github.com/OWASP/owasp-mastg/raw/master/Crackmes/Android/Level_03/UnCrackable-Level3.apk
jamarir@kali:~$ adb install UnCrackable-Level3.apk
Performing Streamed Install
Success
P4tch3d ? What’re you talkin’ about ?!
Ro0t3d ? No.
The class detecting if the application is rooted is RootDetection
:
We can disable the comments in JADX editing the
File > Preferences
.
This class contains the exact same functions as the 2 previous challenges. Rooted blablabla... We’ll patch System.exit()
blablabla…
jamarir@kali:~$ cat patch.js
Java.use("java.lang.System").exit.implementation = (var0) => {}
jamarir@kali:~$ frida -U -f owasp.mstg.uncrackable3 -l patch.js
[...]
[VirtualBox::owasp.mstg.uncrackable3 ]-> Process crashed: Trace/BPT trap
***
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Android-x86/android_x86_64/x86_64:9/PI/lh03251128:userdebug/test-keys'
Revision: '0'
ABI: 'x86_64'
pid: 18410, tid: 18436, name: tg.uncrackable3 >>> owasp.mstg.uncrackable3 <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
rax 0000000000000000 rbx 0000747be6fb7280 rcx 0000747c8983f827 rdx 0000000000000006
r8 0000000000000031 r9 0000000000000031 r10 0000747be6fb7230 r11 0000000000000202
r12 0000747be70edb62 r13 0000747be70edb68 r14 0000747be70edb50 r15 0000747be70edb60
rdi 00000000000047ea rsi 0000000000004804
rbp 0000747c09214018 rsp 0000747be6fb7268 rip 0000747c8983f827
backtrace:
#00 pc 000000000007f827 /system/lib64/libc.so (offset 0x7f000) (tgkill+7)
#01 pc 000000000000374a /data/app/owasp.mstg.uncrackable3-OhVcoUnmZtmx3E7UbTKRug==/lib/x86_64/libfoo.so (goodbye()+10)
#02 pc 000000000000389a /data/app/owasp.mstg.uncrackable3-OhVcoUnmZtmx3E7UbTKRug==/lib/x86_64/libfoo.so
#03 pc 0000000000092bfb /system/lib64/libc.so (offset 0x7f000) (__pthread_start(void*)+27)
#04 pc 000000000002af0d /system/lib64/libc.so (offset 0x2a000) (__start_thread+61)
***
[VirtualBox::owasp.mstg.uncrackable3 ]->
Thank you for using Frida!
Fr1d4’3d ? No.
As the above frida backtrace shows, the last function called in the native library is libfoo.so (goodbye()+10)
:
We can disable the comments (and print NULL for null pointers) in Ghidra editing the
Edit > Tool options
.
This function rose a SIGTERM signal and terminated the program. This goodbye()
function is called by FUN_001037()
, itself called by _INIT_0()
:
Basically, _INIT_0()
creates a new FUN_001037c0()
process thread to perform some checks in the background:
The check is pretty simple to grasp, and done every 500ms (l.27). If, in /proc/self/maps
(l.29), there’s the substring frida
or xposed
(l.34), then goodbye()
is called (l.43).
The
maps
file contains the currently mapped memory regions and their access permissions.jamarir@kali:~$ man proc_pid_maps
Here’s an example of the UnCrackable L2’s
maps
after applying thefrida
patch:
jamarir@kali:~$ cat patch.js Java.perform(() => { Java.use("sg.vantagepoint.a.b").a.implementation = () => {return false} Java.use("sg.vantagepoint.a.b").b.implementation = () => {return false} Java.use("sg.vantagepoint.a.b").c.implementation = () => {return false} })
jamarir@kali:~$ frida -U -f owasp.mstg.uncrackable2 -l patch.js jamarir@kali:~$ adb shell 'ps |grep uncrackable2$' u0_a97 4035 1108 3402520 167436 0 0 S owasp.mstg.uncrackable2 jamarir@kali:~$ adb shell 'su -c "cat /proc/4035/maps"' |grep -vP ".*? \d+\s+$" |grep -vP '.*? \d+\s+\[.*$' [...] 717d25eb6000-717d25ed6000 r--s 00000000 00:10 6232 /dev/__properties__/u:object_r:hwservicemanager_prop:s0 717d25f36000-717d25f39000 r-xp 00000000 08:01 328372 /data/app/owasp.mstg.uncrackable2-YporSoqFPLF96kZP3sxtag==/lib/x86_64/libfoo.so 717d25f39000-717d25f3a000 r--p 00002000 08:01 328372 /data/app/owasp.mstg.uncrackable2-YporSoqFPLF96kZP3sxtag==/lib/x86_64/libfoo.so 717d25f3a000-717d25f3b000 rw-p 00003000 08:01 328372 /data/app/owasp.mstg.uncrackable2-YporSoqFPLF96kZP3sxtag==/lib/x86_64/libfoo.so 717d26054000-717d26059000 r--p 00000000 08:01 328384 /data/app/owasp.mstg.uncrackable2-YporSoqFPLF96kZP3sxtag==/oat/x86_64/base.odex 717d26059000-717d2605a000 r-xp 00005000 08:01 328384 /data/app/owasp.mstg.uncrackable2-YporSoqFPLF96kZP3sxtag==/oat/x86_64/base.odex 717d2605a000-717d2611c000 r--s 00000000 08:01 328385 /data/app/owasp.mstg.uncrackable2-YporSoqFPLF96kZP3sxtag==/oat/x86_64/base.vdex 717d2611c000-717d2611d000 r--p 00006000 08:01 328384 /data/app/owasp.mstg.uncrackable2-YporSoqFPLF96kZP3sxtag==/oat/x86_64/base.odex 717d2611d000-717d2611e000 rw-p 00007000 08:01 328384 /data/app/owasp.mstg.uncrackable2-YporSoqFPLF96kZP3sxtag==/oat/x86_64/base.odex 717d2612a000-717d26163000 r--s 00099000 08:01 328368 /data/app/owasp.mstg.uncrackable2-YporSoqFPLF96kZP3sxtag==/base.apk 717d2645d000-717d2655b000 r--p 00000000 00:10 6342 /dev/binder 717d26d4d000-717d27968000 r--p 00000000 00:05 42256 /memfd:frida-agent-64.so (deleted) 717d27969000-717d286f1000 r-xp 00c1b000 00:05 42256 /memfd:frida-agent-64.so (deleted) 717d286f1000-717d287b6000 r--p 019a2000 00:05 42256 /memfd:frida-agent-64.so (deleted) 717d287b7000-717d287d2000 rw-p 01a67000 00:05 42256 /memfd:frida-agent-64.so (deleted) 717d3830b000-717d392d9000 r-xp 00000000 07:01 7471 /system/vendor/lib64/egl/libGLESv1_CM_swiftshader.so [...]
As we can see, a frida agent named
frida-agent-64.so
was loaded in memory.
One way to bypass this memory check is to patch strstr()
so that it returns false
, even if frida
or xposed
are loaded in memory.
This requires to patch a libc
function, thus we’ll use the Frida Interceptor to hook a libc
function:
jamarir@kali:~$ cat patch.js
Java.perform(() => {
Java.use("java.lang.System").exit.implementation = (var0) => {}
Interceptor.attach(Module.findExportByName('libc.so', "strstr"),{
onEnter: function(args){
//console.log(`strstr("${args[0].readCString()}", "${args[1].readCString()}"`);
this.flagged = false;
if(args[1].readCString().indexOf("frida") != -1 || args[1].readCString().indexOf("xposed") != -1){
this.flagged = true;
}
},
onLeave: function(retval){
if (this.flagged) {
retval.replace(ptr("0x0"));
}
}
});
})
This patch is inspired by a vumail’s frida codeshare.
We first bypass the root detection patching the exit()
function. Then, we hook the strstr()
function loaded in memory (in libc
) to return (onLeave
) 0 (false
) if the second argument of strstr()
(onEnter
) contains either frida
or xposed
.
The address returned by
Module.findExportByName()
is the function’s one declared in the input (i.e.strstr
here). The first argument is set tolibc.so
, but we may set it tonull
to perform a global but costly search.In the current context, the
strstr
’s address is0x76206bb80dd0
:
jamarir@kali:~$ cat patch.js Java.perform(() => { Java.use("java.lang.System").exit.implementation = (var0) => {} console.log(Module.findExportByName(null, "strstr")); Interceptor.attach(Module.findExportByName('libc.so', "strstr"),{ [...]
jamarir@kali:~$ frida -U -f owasp.mstg.uncrackable3 -l patch.js [...] [VirtualBox::owasp.mstg.uncrackable3 ]-> 0x76206bb80dd0
Which is contained in
libc
's maps range of the application’s process (76206bb80000-76206bb81000
):
jamarir@kali:~$ adb shell 'su -c "cat /proc/4665/maps"' |grep 76206bb80 76206bb39000-76206bb80000 r-xp 00036000 07:01 8204 /system/lib64/libc.so 76206bb80000-76206bb81000 rwxp 0007d000 07:01 8204 /system/lib64/libc.so
Secret ? What’re you talkin’ about ?!
Now that we bypassed the root and frida detections, a secret is asked (similarly to the 2 above challenges):
init
First, it must be noted that a xorkey
is set to 🍕🍕🍕🍕pizz
, used in the init()
native function:
We may assume that DAT_xorkey
contains the xorkey, as its length is 0×18 == 24
:
verify & bar
The function verifying the code is check_code()
:
Or, more precisely, the native function bar()
:
Decompiled into:
Our input
is compared with DAT_00107040
(which I named DAT_xorkey
above) and the secret
variable (l.22-24). If there’s at least one mismatch, the code jumps to RETURN_FALSE
.
The secret
variable is likely to be set by the FUN_001012c0()
function. Indeed, this function’s signature takes param_1
as an input:
param_1
(or secret
) is an array of 16 undefined elements, setup at the end of that 1521-lines-of-code function:
Therefore:
The bytes 0 to 3 of
param_1
is set to the0×1311081d
.The bytes 4 to 7 of
param_1
is set to the0×1549170f
.The bytes 8 to 11 of
param_1
is set to the0×1903000d
.The bytes 12 to 15 of
param_1
is set to the0×15131d5a
.The bytes 16 to 23 of
param_1
is set to the0x14130817005a0e08
. We started from 16 becauseparam_1
is a pointer to a 16-length array, which implies thatparam[1]
is referencing the second 16-bytes slot (from byte 16 to 31).
Each memory slot (of size 32 bits = 8 bytes) is stored using the little-endian endianness.
Therefore, the secret is actually stored in the memory stack as 0x1d081113
+ 0x0f174915
+ 0x0d000319
+ 0x5a1d1315
+ 0x080e5a00
+ 0x17081314
.
Going back into the bar()
function, the checks are:
Basically, if one of our input
's character is different from the XOR operation between xorkey
and secret
, then the check fails.
It can be noted that the xorkey
(🍕🍕🍕🍕pizz
) is the same length as the secret
, i.e. 24, which is one of the XOR cipher conditions.
Thus, XOR’ing the two variables gives the flag :]
A small ptrace()
ride (out-of-scope)
In the bar()
function, if inc
isn’t equal to 2 (l.15), then the code verification isn’t done.
This variable is set to 2 by init()
(l.11), only if the FUN_00103910()
didn’t call exit(0)
(l.24):
First, a new child process is created using fork()
(l.13), which returns the child’s PID for the parent process, and 0 for the child process. Thus:
In a parent process context (
_Var1!=0
),pthread_create()
creates a new child process runningFUN_00103910()
, whose PID is stored inlocal_20
(l.33).In a child process context (
_Var1==0
), we attach the parent (tracee) to the child (tracee) usingptrace()
with thePTRACE_ATTACH
operation.
Once attached, the tracer sends a SIGSTOP
to the tracee. While stopped, the tracer may inspect / modify / debug the tracee’s thread.
If ptrace()
is successful (l.16), then lVar2==0
and we enter the if() {}
statement (l.17).
Then, waitpid()
is called numerous times (l.18,20,27) to wait until the tracer is noticed that the tracee’s state changed. For instance, after the PTRACE_ATTACH
operation, the tracer implicitely sents a SIGSTOP
signal to the tracee. Then, the tracer uses waitpid()
(l.18) to suspend its execution until the tracee actually stopped.
In the current context, the highlighted “child” and “children” actually refer to the tracee, which is the parent process.
When the tracer (child) has been noticed that the tracee (parent) actually stopped, it calls the PTRACE_CONT
to resume the debugging process. If there’s no error, _Var1
stores the tre tracee’s PID (l.20).
Finally, FUN_00103910()
simply loops for the tracer to keep debugging the tracee with ptrace(PTRACE_CONT)
. However, if an exit status is returned by waitpid()
(i.e. the tracee/parent changed its status to exit
), the tracer exits as well (l.24).
Also, one of the ptrace()
's errors states that a process already being traced cannot be traced twice.
As a consequence, the ptrace()
’s result is -1
, and we can’t enter the if() {}
statement (l.17).