# Intro In this post we will do a rapid-fire gauntlet of the first three stages of the [OWASP MAS Crackmes](https://mas.owasp.org/crackmes/) using [Fermion](https://github.com/FuzzySecurity/Fermion) and [Binary Ninja](https://binary.ninja/). I am mostly just documenting my learnings as a reference for the Android platform, tools of the trade and `Java Instrumentation` with `Frida`. Take a seat, have a coffee and chill πŸ›‹οΈβ˜•β„οΈ! # Setup There are some emulator choices but I decided to use [Android Studio](https://developer.android.com/studio). Overall it seems like the best option and you get an integrated development environment also if you want to write some apps at a later stage. ![[MAS-1.png]] This using `QEMU` under the hood and I want to note that you actually can't launch an `Arm` VM for some reason. It's not really my area of expertise but I guess `Arm` emulation is too expensive computationally. Bottom line you still need a physical phone with an unlocked bootloader for `Arm64` testing. Once you have your VM going you will need the Android SDK to have `ADB`, you can find more details [here](https://developer.android.com/tools/adb). On Windows the path is in `%appdata%\Local\Android\Sdk\platform-tools`. Some commands you may need, in no particular order, include: ``` # ADB adb root adb shell adb push XXXXXXX /data/local/tmp/XXXXXXX adb install YYYYYYYYYY.apk # Frida adb push frida-server /data/local/tmp/ cd /data/local/tmp/ ./frida-server & ``` Of course you need a [Frida](https://frida.re/) client as well, I recommend that you use [Fermion](https://github.com/FuzzySecurity/Fermion) for practical reasons. Just remember that you need to match the `Frida version` of the client/server and also chose the correct server to match the `architecture of the phone` (like `x86` or `Arm64`). You also need some tooling to inspect and analyse APK's, here I'm using [JADX](https://github.com/skylot/jadx) but you have some other options as well including more advanced tools like [JEB Android](https://www.pnfsoftware.com/jeb/#features-matrix). Finally you need a decompiler, you can use [Ghidra](https://ghidra-sre.org/) for free if needed. Time to get to work! ![[MAS-2.gif]] # Some kind of p0wn.. ## Level 1 Ok let's have a quick look at the app first to have an idea of what is going on. ![[MAS-3.png]] We need some kind of credential here and there is also a detection for `emulation` of the device or `root access` on the device. Let's have a look at the app in `JADX`. We can quickly identify where this check comes from (`root` / `debug`). ![[MAS-4.png]] The `debuggable` check is like this: ```java package sg.vantagepoint.a; import android.content.Context; public class b { public static boolean a(Context context) { return (context.getApplicationContext().getApplicationInfo().flags & 2) != 0; } } ``` It looks like it is checking an `ApplicationInfo` flag. Based on the [integer](https://developer.android.com/reference/android/content/pm/ApplicationInfo#FLAG_DEBUGGABLE) it is looking if `FLAG_DEBUGGABLE` is set. I have no idea if we will trigger this, we'll find out! We know, however, that we *are triggering* the `root` check: ```java package sg.vantagepoint.a; import android.os.Build; import java.io.File; public class c { public static boolean a() { for (String str : System.getenv("PATH").split(":")) { if (new File(str, "su").exists()) { return true; } } return false; } public static boolean b() { String str = Build.TAGS; return str != null && str.contains("test-keys"); } public static boolean c() { for (String str : new String[]{"/system/app/Superuser.apk", "/system/xbin/daemonsu", "/system/etc/init.d/99SuperSUDaemon", "/system/bin/.ext/.su", "/system/etc/.has_su_daemon", "/system/etc/.installed_su_daemon", "/dev/com.koushikdutta.superuser.daemon/"}) { if (new File(str).exists()) { return true; } } return false; } } ``` Cool cool.. - `a()` checks if the `su` binary is present in any of the system paths - `b()` checks the build tags of the operating system, `test-keys` would indicate the environment is signed using the default keys provided with the Android Open Source Project (AOSP) - `c()` looks for some files the app doesn't like We can replace these functions with `Frida` like so and make them return `false`. ```js Java.perform(function() { var jbCheck = Java.use("sg.vantagepoint.a.c"); jbCheck.a.implementation = function(x) { send("[!] jb -> su path check") return false; }; jbCheck.b.implementation = function(x) { send("[!] jb -> signing key check") return false; }; jbCheck.c.implementation = function(x) { send("[!] jb -> we don't like these paths check") return false; }; }); ``` For efficiency and to `pre-empt the calls` we will want to have `Fermion` launch the app not attach to it. We can get the name we have to specify from the `Manifest`. ![[MAS-5.png]] It looks good and our instrumentation does not trigger the `debuggable` check. ![[MAS-6.png]] Good, now lets look for the password. The app takes our input and passes it to some function that returns a `Boolean`. ![[MAS-7.png]] If we look at this function we can see a few things: - The app is passing two byte arrays to another function (`sg.vantagepoint.a.a.a`) - The app compares the result of that call with our string This tertiary function must generate whatever the valid password is for the comparison. ![[MAS-8.png]] We don't even care really what the tertiary function does (it's doing some crypto) what we want to do rather is hook the inner function and capture the result. ```js Java.perform(function() { var passCheck = Java.use("sg.vantagepoint.a.a"); passCheck.a.implementation = function(x, y) { this.retval = this.a(x, y); send("[?] Looting credential.."); send(" |_ password is ==> " + bts(this.retval)); return this.retval; }; }); ``` Note that I am printing the result using a helper. The function returns a byte array and so I am converting it back to a string using `String.fromCharCode`, omitted here. This is only part of the solution though because this requires us to first enter bogus text into the field to trigger our hook. We are lazy, we don't want to do that so we use some more `Frida` to automatically `call sg.vantagepoint.uncrackable1.a.a -> hook sg.vantagepoint.a.a.a`. ```js function getPass(pass) { Java.perform(function(){ send("[>] Trigger password decryption.."); let a = Java.use("sg.vantagepoint.uncrackable1.a"); a.a(pass); }); } getPass("jumanji"); ``` ![[MAS-9.png]] ## Level 2 `level 2` has many of the same checks, which we won't cover here. Looking at the main activity, we see something interesting, the application loads a native module. ![[MAS-10.png]] Remember to select the module that matches the architecture of the phone, in our case we are emulating on `x86`. I'm not very familiar with how these apps are packed but it may be useful to look at different architectures for the same library to see if one is more easily decompiled than the other (`just speculating`). APK's are archives so we can extract the library easily and load it in a decompiler. But what are we looking for? We can see that `m` is created from `new CodeCheck()` and then our input is passed using `m.a(String)`. ![[MAS-11.png]] Tracking this reference, we see that `m.a(String)` is calling our native module, `bar(Byte[])`. ![[MAS-12.png]] Now, if we look in the decompiler (`Binary Ninja`) we can see that it spoils our fun a little bit. ![[MAS-13.png]] I want to note that `Binary Ninja` is doing the heavy lifting for us, putting the string back together. I checked in `Ghidra` and found what I presume the intended effect was. Still it would not be very hard to identify this string and convert it manually. ![[MAS-14.png]] Our old jailbreak hooks still work (with an `adjusted class path`). ![[MAS-15.png]] Interestingly, the native module also contains an `anti-debugging` function, which I've cleaned up below. The function creates a child process that then tries to debug its parent. Presumably if the parent is being debugged already this would fail. But we don't trigger this detection. ![[MAS-16.png]] Obviously, we could easily hook the check if we were affected by it. Something we may well have to do in `level 3`. ## Level 3 Leaving the password aside for now, superficially `level 3` looks similar to `level 2`. The `same jailbreak checks` exist, again using a `different class path`. Also there is the same native call to `bar`, presumably to compute & compare the password. However, looking at the app there are also other native calls defined. This seems suspect. ![[MAS-17.png]] In fact, if we launch the app using `Fermion`, with the jailbreak checks patched, the app immediately crashes. Interesting. In the library we see this same trick where a child process is created which then tries to `ptrace` its parent. We know this doesn't affect us. If we explore a bit more we find the following functions. ![[MAS-18.png]] You can see that this will loop, the `sub` creates a `tamperDetection` thread that will `fopen` and then `while -> true` keeps going. Some notes here: - The `fopen` call opens the memory map of the current process - The result of `fopen` cannot fail or we end up in `goodbye` which crashes the app - If the memory map contains the string `frida` or `xposed` we also end up in `goodbye` - Finally, there appears to be logging using `__android_log_print` but I didn't verify this My first idea was to simply instrument the native module (`libfoo.so`) but I had some issues with this because the module is not loaded when the app starts, it has a delayed load. I tried a few things here, like: - `Module.ensureInitialized -> resolve -> hook` - `setTimeout -> callback -> resolve -> hook` However, I kept having issues and I could not get it to work properly. I found a very ugly workaround but I don't want to document this bogus solution here! Eventually I took a different approach. If we look at this function again, we can see that a number of `C API's` are doing the real work here (`fopen` / `fgets` / `strstr`). I did some light profiling of `fgets` to understand how it was used by the application and I found that the native module only uses it in one place with a read size of `0x200 bytes`. Using this information I then wrote a hook to subvert the buffer rather than the function in the application module. ```js // Resolve fgets let pFgets = Module.findExportByName("libc.so", "fgets"); // Hook fgets Interceptor.attach(pFgets, { onEnter: function (args) { this.buffer = args[0]; // Destination this.len = args[1]; // Buffer size }, onLeave (retval) { // Only consider usage from the native application module if (parseInt(this.len) == 0x200) { // Blob the buffer // |_ end with null so strstr is deterministic let fill = new Array(0x1FE).fill(0x41).concat([0x00, 0x00]); let pBuff = new NativePointer(this.buffer); pBuff.writeByteArray(fill); } } }); ``` Now when we instrument the app and patch out the `java jailbreak checks` it launches normally. ![[MAS-19.png]] Excellent, finally we can divert our attention to recovering the password. Looking back at `JADX`, we can see an `XOR` key is passed to the native library. ```java private static final String xorkey = "pizzapizzapizzapizzapizz"; ... public void onCreate(Bundle bundle) { verifyLibs(); init(xorkey.getBytes()); ... ``` If we look in the native module, we can see this. ![[MAS-20.png]] The input bytes are not manipulated at all so if we wanted this key in `JavaScript` we could simply do the following: ```js // APK XOR key let bXor = new Uint8Array("pizzapizzapizzapizzapizz".split('').map(c => c.charCodeAt(0))); ``` But what are we XOR'ing? If we look at the `bar` native function, things become more clear. ![[MAS-21.png]] There is a function (I annotated as `derivedToken`) which builds a buffer and that buffer is XOR'd with the the APK key. At each index, the result is compared with what the user provided, again XOR'd with the APK key. It's a bit confusing maybe, but what we want to do is: - Get the derived bytes from `derivedToken` - `XOR` the bytes with the `APK XOR key` This should give us the password. Remember that above we hooked `fgets` to bypass security checks. At the time our hook triggers, the application module will be loaded so we don't have delayed load issues when we hit the hooked code. Because of that, we can trigger the rest of our logic from there. ```js // Resolve native pointers //================= let pFgets = Module.findExportByName("libc.so", "fgets"); // Globals //================= let pBaseAddress = null; let pDerive = null; // Native hooks //================= Interceptor.attach(pFgets, { onEnter: function (args) { this.buffer = args[0]; // Destination this.len = args[1]; // Buffer size }, onLeave (retval) { // Only consider usage from the native application module if (parseInt(this.len) == 0x200) { // Blob the buffer // |_ end with null so strstr is deterministic let fill = new Array(0x1FE).fill(0x41).concat([0x00, 0x00]); let pBuff = new NativePointer(this.buffer); pBuff.writeByteArray(fill); // Call our p0wn function libraryLoaded(); } } }); // p0wn //================= function libraryLoaded() { if (!pBaseAddress) { // Resolve pBaseAddress = Module.findBaseAddress("libfoo.so"); pDerive = pBaseAddress.add(0xfa0); // Native function let fMakeDerive = new NativeFunction( pDerive, "void", [ "pointer" ] ); // Make secret let pAlloc = Memory.alloc(0x19); fMakeDerive(pAlloc); send("[?] Derived application credential:"); send(hexdump(pAlloc, {length:0x19})); } } ``` We simply create a native function for `derivedToken` and read the bytes from the result buffer. You can see some output below. ``` [?] Attempting process start.. [+] Injecting => PID: 21402, Name: owasp.mstg.uncrackable3 [+] Process start success [?] Derived application credential: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF f32af780 1d 08 11 13 0f 17 49 15 0d 00 03 19 5a 1d 13 15 ......I.....Z... f32af790 08 0e 5a 00 17 08 13 14 00 ..Z...... [!] jb -> su path check [!] jb -> signing key check [!] jb -> we don't like these paths check ``` All that remains is to `XOR` the derived password with our `APK XOR key`. ```js function libraryLoaded() { if (!pBaseAddress) { // Resolve pBaseAddress = Module.findBaseAddress("libfoo.so"); pDerive = pBaseAddress.add(0xfa0); // Native function let fMakeDerive = new NativeFunction( pDerive, "void", [ "pointer" ] ); // Make secret let pAlloc = Memory.alloc(0x19); fMakeDerive(pAlloc); send("[?] Derived application credential:"); send(hexdump(pAlloc, {length:0x19})); // Get the APK XOR key let bXor = new Uint8Array("pizzapizzapizzapizzapizz".split('').map(c => c.charCodeAt(0))); // Xor the derived credential for (let i = 0; i < bXor.length; i++) { let originalByte = pAlloc.add(i).readU8(); let xorByte = originalByte ^ bXor[i]; pAlloc.add(i).writeU8(xorByte); } // Get the password send("[>] Decoded credential:"); send(hexdump(pAlloc, {length:0x19})); } } ``` ![[MAS-22.png]] # Conclusion That was pretty good, I learned quite a bit. I should note that `level 4` is separate from the other levels, the APK is called `r2pay` (there are two versions) and it was released at `r2Con` in 2020 (I think). I didn't have enough time to complete that as well but I will be back for another post πŸ™‡β€β™‚οΈ! ``` The path to erudition is strewn with the runes of yore, demanding decipherment by those bold enough to question the firmament itself. Seek ye the company of forgotten tomes; for oft, 'tis in the gloom that truth lies hidden, heavy with import. ```