# L3AKCTF All Mobile challenges - notes # BrainCalc - jadx-gui to find weird python stuff - find relevant files in resources - extract the code using 7-zip - reverse the pyc: https://pylingual.io/ ![d362b899-d656-484b-9d1c-a2c1acc61955](https://hackmd.io/_uploads/ByBlXlzUel.png) # Apkocalypse / filestorage If you can't find MainActivity right away, look in `AndroidManifest.xml` The emulator I used is Genymotion, the devices are rooted by default. ## Root bypass First, bypass root detection to open the app. We will use Frida (dynamic instrumentation tool) for this. Install with pip: `pip install frida-tools` You need a frida-server running on the device Check your frida version and install the relevant `frida-server-17.2.11-android-x86_64.xz` https://github.com/frida/frida/releases/tag/17.2.11 ``` adb push frida-server /data/local/tmp adb shell chmod +x /data/local/tmp/frida-server adb shell /data/local/tmp/frida-server ``` Root detection in question: ```java if (RootDetection.isDeviceRooted(this)) { Toast.makeText(this, "Root access detected, some features may be disabled.", 0).show(); finish(); return; } ``` The bypass is trivial, just make `isDeviceRooted` always return false: ```javascript // hook.js Java.perform(function() { var Root = Java.use("ctf.l3akctf.filestorage.RootDetection"); Root.isDeviceRooted.overload('android.content.Context').implementation = function(){ console.log("root bypass!") return false } }); ``` `Java.perform()` has the ability to interact with Java classes/functions. In contrast to the `Interceptor` API which can hook native code as we will see. Btw, `jadx-gui` has this handy feature where you can right click and copy as frida snippet. You do have to put it in a `Java.perform()` though. There's a number of ways to start the script. ```c // -n: process name, find with frida-ps -U frida -U -l hook.js -n storeftw // -F: foreground app frida -U -l hook.js -F // -f: spawn an app - slight delay is needed before executing frida -U -l hook.js -f ctf.l3akctf.filestorage ``` ## Getting flag from native function ```java public class MainActivity extends AppCompatActivity { public native void storeftw(); static { System.loadLibrary("storeftw"); } ``` Once again this is extractable with 7-zip, `filestorage.apk\lib\x86_64\libstoreftw.so`. To reverse, use Binary Ninja of course We want to log the flag contents ![e91e12ea-ccbd-437a-ab8a-7c19f1d18939](https://hackmd.io/_uploads/Sk6ZXeGUee.png) This time we use Frida's Interceptor API which looks like this: ```javascript var base = Module.findBaseAddress("libstoreftw.so"); var relative_ptr = base.add(0x1337) var symbol_ptr = Module.findExportByName(null, "symbol") Interceptor.attach(ptr, { onEnter: function(args) { // ... }, onLeave: function(retval) { retval.replace(0) } }); ``` `0x62522` is the call to fwrite with the flag, so you can hook that. But first there is a function (at offset `0x627d0`) being called at `0x61f44` that checks for debuggers. We hook all these and read flag. ## Idea ![8f923d50-f432-463a-8101-f79f691c7eb2](https://hackmd.io/_uploads/ryUNXlf8ll.png) ## Solution ```js Java.perform(function() { var Root = Java.use("ctf.l3akctf.filestorage.RootDetection"); Root.isDeviceRooted.implementation = function(){ console.log("root bypass!") return false } }); setTimeout(() => { var base = Module.findBaseAddress("libstoreftw.so"); console.log("base: " + base); Interceptor.attach(Module.findExportByName(null, "ptrace"), { onLeave: function(retval) { console.log("[Debugger Check] Ptrace part skipped"); retval.replace(-1); } }); Interceptor.attach(base.add(0x6281d), { onEnter: function(args) { console.log("bypassing kill()"); args[1] = 0; } }) // read flag on fwrite() Interceptor.attach(base.add(0x62522), { onEnter: function(args) { console.log(args[0].readUtf8String()); } }) }, 1000) // frida -U -l .\hook.js -f ctf.l3akctf.filestorage ``` # Androbro Again native stuff. The root checks in MainActivity are not used. In the class `TheChecker`, a native functon `d()` is used as a flag checker. This time they are getting initialized in `JNI_OnLoad (0x69c30)` Some reversing is required to rename these functions ![8b679754-2796-4e91-b2dc-c75497181e96](https://hackmd.io/_uploads/HkfL7gfLle.png) These preconditions below need to be reversed since it sets up the decryption key ![3e529a89-173e-4b70-a6a7-6c2cf07b7c9f](https://hackmd.io/_uploads/r1hPQlz8xe.png) These variables are set by a function at `0x68930`. This is the receiver checking that you send the correct values. ![65494ade-9ca2-4b5f-a3ed-af57b74fa24c](https://hackmd.io/_uploads/H1ytQgMLel.png) When seeing the strings `getAction`, `getStringExtra`, one could guess that these are intents. And indeed the app nicely shows that it has a BroadcastReceiver ![85c9407e-263f-4a25-934d-9e4be189a041](https://hackmd.io/_uploads/BkJ9QeGLeg.png) Now you could use Frida Interceptor to log all the intent conditions (simple strcmp, see solution) and setup the key it wants. ``` adb shell am broadcast -a THE_TRIGER adb shell am broadcast -a THE_UNLOCKER --es key "6a209693a9acaf10dcd2e425bab62a5e48698b7fc3" ``` After you've done this you can finally reverse the flag checking process. ![b1d72a36-8b6d-4287-986b-07689e6f5c6a](https://hackmd.io/_uploads/Hy6qQlzIee.png) It decrypts an asset `E/M/O/H/G/CMVASFLW.EXE` and loads it as a class. I added one more hook to recover the class file ```js Interceptor.attach(base.add(0x69621), { onEnter: function(args) { // hexdump is a builtin frida function console.log(hexdump(args[1].readByteArray(0x1000))) } }) ``` Clean it up using cyberchef or something and get a .dex file and decompile using https://www.decompiler.com/. The flag is AES encrypted with the key and IV included. ## Solution ```js function hookNativeStuff() { var base = Module.findBaseAddress("libragnar.so"); console.log(base) Interceptor.attach(base.add(0x674b0), {onEnter: () => console.log("registerReceiver called!")}) Interceptor.attach(base.add(0x68930), {onEnter: () => console.log("receive called!")}) Interceptor.attach(base.add(0x689f8), { onEnter: function(args) { console.log(`strcmp1(${args[0].readUtf8String()}, ${args[1].readUtf8String()})`) } }) Interceptor.attach(base.add(0x69177), { onEnter: function(args) { console.log(`strcmp2(${args[0].readUtf8String()}, ${args[1].readUtf8String()})`) } }) Interceptor.attach(base.add(0x68a83), { onEnter: function(args) { console.log(`strcmp3(${args[0].readUtf8String()}, ${args[1].readUtf8String()})`) } }) Interceptor.attach(base.add(0x69621), { onEnter: function(args) { console.log(hexdump(args[1].readByteArray(0x1000))) } }) } function waitForModule(name, cb){ var handle = setInterval(function(){ var m = Process.findModuleByName(name); if (m){ clearInterval(handle); cb(m); } }, 100); } waitForModule("libragnar.so", hookNativeStuff) // send the following intents // adb shell am broadcast -a THE_TRIGER // adb shell am broadcast -a THE_UNLOCKER --es key "6a209693a9acaf10dcd2e425bab62a5e48698b7fc3" ``` # PricelessL3ak MainActivity is a decoy flag checker. The real one is triggered by intents. I spent quite a lot of time trying to make sense of it. `ctf.l3akctf.pricelessl3ak.h1832fla12` is the class accepting intents. To trigger the validation: ``` adb shell am start -n ctf.l3akctf.pricelessl3ak/.h1832fla12 -a BINGO adb shell am start -n ctf.l3akctf.pricelessl3ak/.h1832fla12 -a BANGO -f 0x20000000 --es f "theflag" ``` The `0x20000000` is because ChatGPT told me so. Tracing the function calls with the flag in it eventually you get to this beast of a function `X.f.a`. Sadly jadx-gui doesn't decompile it well but using Bytecode Viewer with the Fernflower decompiler works well. ![ad39e180-81f3-4a05-bd27-8e30d5708e2c](https://hackmd.io/_uploads/Sy13Xez8el.png) This is a Virtual Machine, a common concept in Reverse Engineering challenges. Basically it's some made up language that interprets given bytecode. Thus, the thing passed to this function (`data.enc` but processed) is a program for this interpreter. I needed to debug this bytecode while it was running to trace the instructions, but it was in the middle of a Java function. Luckily I (ChatGPT) was able to dump the instructions to logcat by directly modifying the smali file, adding a log function and calling it before the instruction `var7` was parsed. ## Patching the .smali Since the VM is `X.f.a`, we will edit the `smali/X/f.smali` file after unpacking it. ```shell apktool d -r .\pricelessl3ak.apk ``` Ask your favorite LLM to add some logging ```smali .method private static logInstruction(IIII)V .locals 2 # Format the message: "opcode,register,operand1,operand2" new-instance v0, Ljava/lang/StringBuilder; invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V invoke-virtual {v0, p0}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; const-string v1, "," invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v0, p2}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; invoke-virtual {v0, p3}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder; invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v0 const-string v1, "VM_LOG" invoke-static {v1, v0}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I return-void .end method ``` Line 82 should look like this: ```smali .line 82 check-cast v8, Lctf/l3akctf/pricelessl3ak/v27a8612b; iget v9, v8, Lctf/l3akctf/pricelessl3ak/v27a8612b;->a:I iget v10, v8, Lctf/l3akctf/pricelessl3ak/v27a8612b;->b:I iget v11, v8, Lctf/l3akctf/pricelessl3ak/v27a8612b;->c:I iget v12, v8, Lctf/l3akctf/pricelessl3ak/v27a8612b;->d:I invoke-static {v9, v10, v11, v12}, LX/f;->logInstruction(IIII)V ``` The easiest way to rebuild is probably uber-apk-signer which signs and aligns the apk. ``` apktool b pricelessl3ak java -jar uber-apk-signer.jar -a .\pricelessl3ak\dist\pricelessl3ak.apk adb install pricelessl3ak\dist\pricelessl3ak-aligned-debugSigned.apk ``` Trigger the flag checker using the intents, and you should see this in `adb logcat` ``` ... 07-15 16:19:18.782 3149 3202 I VM_LOG : 40,0,1,0 07-15 16:19:18.782 3149 3202 I VM_LOG : 65,0,0,2161 07-15 16:19:18.782 3149 3202 I VM_LOG : 48,14,0,0 07-15 16:19:18.782 3149 3202 I VM_LOG : 40,14,0,1 07-15 16:19:18.782 3149 3202 I VM_LOG : 65,0,0,2165 07-15 16:19:18.782 3149 3202 I VM_LOG : 113,0,0,20480 07-15 16:19:18.783 3149 3202 I VM_LOG : 64,0,0,2166 07-15 16:19:18.783 3149 3202 I VM_LOG : 112,0,0,0 ``` To verify, the last opcode should be 112, the halt instruction. ## The VM Lastly, you had to figure out what the correct input is by reversing the VM. ```python # disassemble.py opcodes = { 16: "ADD", 17: "SUB", 18: "MUL", 20: "MOD", 32: "AND", 33: "OR", 34: "XOR", 35: "NOT", 38: "ROL", 39: "ROR", 40: "CMP", 48: "LOAD", 49: "STORE", 50: "LOADSTR", 51: "MOVE", 64: "JMP", 65: "JZ", 66: "JNZ", 67: "JC", 68: "JA", 69: "JBE", 70: "JNC", 112:"HALT", } trace = open("trace.txt", "r").readlines() for inst in trace: opcode, reg, op1, op2 = [x.strip() for x in inst.split(",")] opcode = int(opcode) # print(opcode, reg, op1, op2) print(opcodes[opcode], "reg"+reg, op1, op2) ``` After this, it's not too hard to recreate the program in python and give it to chatgpt to solve. ## Solution ```python from z3 import * def rol(val, r_bits, max_bits=8): return RotateLeft(val, r_bits) def ror(val, r_bits, max_bits=8): return RotateRight(val, r_bits) # Constants INPUT_LEN = 30 xors1 = [0, 23, 46, 69, 92, 115, 138, 161, 184, 207, 230, 253, 20, 43, 66, 89, 112, 135, 158, 181, 204, 227, 250, 17, 40, 63, 86, 109, 132, 155] xors2 = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 0, 33, 68, 105, 144, 185, 228, 17, 64, 113, 164, 217, 16, 73] rols = [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6] expected = [ 216, 80, 35, 22, 129, 176, 231, 76, 154, 181, 75, 217, 42, 152, 88, 20, 234, 198, 144, 81, 32, 72, 67, 114, 40, 133, 75, 173, 165, 128 ] # each check we get different parts of the flag params_list = [ (0, 0, 0), (1, 3, 7), (2, 6, 14), (3, 9, 21), (4, 12, 28), (5, 15, 5), (6, 18, 12), (7, 21, 19), (8, 24, 26), (9, 27, 3), (10, 0, 10), (11, 3, 17), (12, 6, 24), (13, 9, 1), (14, 12, 8), (15, 15, 15), (16, 18, 22), (17, 21, 29), (18, 24, 6), (19, 27, 13), (20, 0, 20), (21, 3, 27), (22, 6, 4), (23, 9, 11), (24, 12, 18), (25, 27, 23) ] # Input symbols input_syms = [BitVec(f'in_{i}', 8) for i in range(INPUT_LEN)] # Initial solver s = Solver() # ASCII range for sym in input_syms: s.add(sym >= 0x20, sym <= 0x7e) # Fixed prefix s.add(input_syms[0] == ord("L")) s.add(input_syms[1] == ord("3")) s.add(input_syms[2] == ord("A")) s.add(input_syms[3] == ord("K")) s.add(input_syms[4] == ord("{")) # s.add(input_syms[10] == ord("l")) # s.add(input_syms[11] == ord("_")) # s.add(input_syms[14] == ord("c")) # s.add(input_syms[21] == ord("3")) # s.add(input_syms[23] == ord("t")) # s.add(input_syms[20] == ord("t")) # s.add(input_syms[28] == ord("!")) # At the start of the program this happens adjusted = [input_syms[i] - 32 for i in range(INPUT_LEN)] # then it transforms once again transformed = [] for i in range(INPUT_LEN): val = adjusted[i] val ^= xors1[i] val ^= xors2[i] val = rol(val, rols[i]) transformed.append(val) # and again for idx in range(len(params_list)): a = transformed[params_list[idx][0]] b = transformed[params_list[idx][1]] c = transformed[params_list[idx][2]] val1 = (b * b) & 0xFF val2 = (val1 + a) & 0xFF val3 = (a * c) & 0xFF xor_val = (val2 ^ val3) & 0xFF mod_val = (b + c) % 8 res = If(mod_val != 0, ror(xor_val, mod_val), xor_val) s.add(res == expected[idx]) # Enumerate all solutions found = 0 while s.check() == sat: m = s.model() result = [m[input_syms[i]].as_long() for i in range(INPUT_LEN)] print("Solution", found + 1, ":", "".join(chr(c) for c in result)) found += 1 # Add constraint to block the current model s.add(Or([input_syms[i] != result[i] for i in range(INPUT_LEN)])) if found == 0: print("No solutions found.") else: print(f"Total solutions found: {found}") # L3AK{P4rc3l_cycl3_1Nt3nt_VM!!} ``` (this was written on my phone while I was on the train)