[OWASP MASTG] Android - UnCrackable L1 & L2 & L3

[OWASP MASTG] Android - UnCrackable L1 & L2 & L3

·

19 min read

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 {}), and move-result v0 stores the result into v0. Here, if c.a() (l.41-43), or c.b() (l.45-47), is NotEqualZero (i.e. not false), then the code jumps to cond_0 on line 53, which shows the Root detected! popup. Finally, if a.c() is EqualZero (i.e. false), then we jump to cond_1 which goes to the next b.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 patch System.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 the locals 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 and resume/cont (or next/step) commands to print input/local variables and advance execution respectively. Because equals() is used numerous times by the application, we must alternatively execute locals and resume 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 the frida 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 to libc.so, but we may set it to null to perform a global but costly search.

In the current context, the strstr’s address is 0x76206bb80dd0:

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 the 0×1311081d.

  • The bytes 4 to 7 of param_1 is set to the 0×1549170f.

  • The bytes 8 to 11 of param_1 is set to the 0×1903000d.

  • The bytes 12 to 15 of param_1 is set to the 0×15131d5a.

  • The bytes 16 to 23 of param_1 is set to the 0x14130817005a0e08. We started from 16 because param_1 is a pointer to a 16-length array, which implies that param[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:

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).

Did you find this article valuable?

Support jamarir's blog by becoming a sponsor. Any amount is appreciated!