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

# 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

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

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

These preconditions below need to be reversed since it sets up the decryption key

These variables are set by a function at `0x68930`. This is the receiver checking that you send the correct values.

When seeing the strings `getAction`, `getStringExtra`, one could guess that these are intents. And indeed the app nicely shows that it has a BroadcastReceiver

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.

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.

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)