---
# System prepended metadata

title: L3AKCTF All Mobile challenges - notes

---

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