I am trying to reverse engineer the asset encryption for "Arcane Knight : Idle RPG". I have made some progress but I am currently stuck. Any advice would be greatly appreciated.

Here is what I have found so far:

The game is built with Unity and uses IL2CPP.

I have successfully used Il2CppDumper to generate dummy DLLs from libil2cpp.so and global-metadata.dat.

Using Ghidra, I have located what I believe is the main asset loading function, LoadAsync, in the EM.AssetManagement.AssetBundleAssetLoader class at address 0x3ECF6F8.

I've traced the function calls and found a promising loop inside the function FUN_0381a354, which seems to process the data blocks. The actual decryption seems to be inside a function it calls, FUN_037d7b80.

I'm having trouble identifying the exact decryption algorithm (like XOR) and the key inside these functions

Below is a practical “check-list” I use when I’m stuck at exactly the same point (found the right function but can’t recognise the cipher/key).
Pick the items that match your tool chain and the time you want to invest.

────────────────────────────────────────

  1. Tidy-up the static view ──────────────────────────────────────── 1.1 Give the functions real names.
    • Rename FUN_0381a354 to something like handleBlockLoop() and FUN_037d7b80 to decryptBlock() in Ghidra.
    • Decompile both, then press “o” to switch to the decompiler’s “graphs” view – much easier to see loops and branches.

1.2 Identify the I/O interface.
• Where is the input buffer coming from? Look for memcpy/memmove or simple pointer arithmetic right before FUN_037d7b80.
• How large is one round? If the loop in 0381a354 increments the pointer by 0x10 or 0x08 you can already guess:
0x10 → AES / MD5 style blocks
0x08 → DES / TEA
any length → XOR or rolling XOR

1.3 Collect the constants.
• Highlight every literal in decryptBlock() – Ghidra shows them in a side list.
• Typical giveaways:
0x9e3779b9 / 0xc6ef3720 → TEA / XXTEA
0x6a09e667 / 0xbb67ae85 … → SHA-256/512 IVs
AES S-box → 256-byte table starting with 0x63 0x7c 0x77 0x7b …
Tiny numbers (1,2,3,4) only → XOR / Rotate / Add

──────────────────────────────────────── 2. Track data & code cross-refs ──────────────────────────────────────── 2.1 Strings as bait
search_strings filter:
“BundleKey”, “Encrypt”, “EM.”, “bytes”, “IV”, “pass”, “salt”
Any function that references the same strings as decryptBlock() usually contains the key/IV generation logic.

2.2 Who else calls FUN_037d7b80?
Ghidra –> References –> Incoming.
If you see a one-shot caller that just passes two arguments (buf,len) and returns, that thin wrapper is perfect for hooking.

──────────────────────────────────────── 3. Do a one-time dynamic dump ──────────────────────────────────────── (IL2CPP = native ARM code, so normal Android debuggers/Frida work)

3.1 Get the symbol address at runtime
• adb shell “cat /proc/<pid>/maps | grep libil2cpp.so” → base
• RuntimeAddress = base + 0x37d7b80 – imageBase you set in Ghidra.

3.2 Hook and dump Example Frida snippet:

javascript
const decrypt = Module.findBaseAddress("libil2cpp.so").add(0x37d7b80);
Interceptor.attach(decrypt, {
    onEnter(args) {
        this.buf   = args[0];
        this.len   = args[1].toInt32();
    },
    onLeave(ret) {
        const data = Memory.readByteArray(this.buf, this.len);
        send({event:"decrypted", size:this.len}, data);
    }
});

Run the game once, capture the plaintext bundle → now you have ground truth to compare with whatever algorithm you reverse statically.

──────────────────────────────────────── 4. Recognise common Unity bundle “tricks” ──────────────────────────────────────── • Simple XOR with a short key (4–32 bytes)
– You’ll see byte ^ key[i & mask] pattern.

• XOR preceded by LZ4/zip decompression
– Look for calls to LZ4_decompress_safe() (string “decompress_safe” often left in binary).

• AES-CTR / AES-CBC
– You’ll spot the 256-byte S-box table or direct calls to OpenSSL/BoringSSL functions if they didn’t strip symbols.

• XXTEA (popular in Asian mobile games)
– Two 32-bit loops, delta = 0x9e3779b9, rounds = 32-64.

──────────────────────────────────────── 5. Finding the key/IV ──────────────────────────────────────── 5.1 Hard-coded array
– Search for db XX XX XX XX XX where XX are the constants you saw being XORed with input.
– In IL2CPP builds those arrays often live in .rodata right below the function.

5.2 Generated from device/account
– Follow the call chain upward until you hit UnityEngine.Application… functions (device GUID, playerPrefs, etc.).
– Much easier to just dump the key at runtime with Frida’s Interceptor.returnValue.

5.3 External file / native-C# bridge
– Any call into android :: ContentResolver, JNI_OnLoad, or EM.FileManager could be reading an obb/asset for the key.

──────────────────────────────────────── 6. Validate quickly ──────────────────────────────────────── When you think you know the algorithm & key:

  1. Copy 32–64 bytes of raw encrypted data from the first bundle block (use adb pull).
  2. Re-implement the suspected algorithm in Python.
  3. Compare result with the memory dump from section 3. If they match – done!

──────────────────────────────────────── 7. If you’re still stuck ──────────────────────────────────────── • Post the decompiled pseudocode of FUN_037d7b80 (or share the constants you see).
Community eyes can often spot TEA/AES patterns instantly.

Good luck! Once you get a single bundle to decrypt, the rest of the game’s assets fall in minutes.

38ZnrsVs.txt

the decompiled code

Tool call: read_file with args: { "filepath": "/sandbox/38ZnrsVs.txt" } succeeded

I skim-med the pseudocode you uploaded (FUN_03ecef74). It is a gigantic “driver” that:

• Allocates/initialises a huge amount of game infrastructure (UI, scene graph, audio, network, etc.).
• Dispatches to many other helpers (names that start with 03f7…, 046e…, 06b1… and so on).
• Finishes by pushing several resources into job queues and returns.

In other words: it is not the inner decryptor you’re hunting. It is several layers above it – most likely the coroutine that orchestrates an entire bundle-loading phase. Reversing it won’t reveal the cipher itself; it will only keep you busy reading Unity plumbing.

How to get back on the right track

  1. Jump to 0x037d7b80 (your suspected decrypt function) in Ghidra and let the decompiler regenerate that function alone.
    • It should be tiny compared with the 1 000-line blob you just posted.
    • If it still looks huge, you are one level too high; step into the callee(s) that actually touch the data buffer.

  2. Visual cues to confirm you are in the decryptor:
    a. The function’s first argument is usually a void* buffer or uint8_t* data.
    b. Inside the body you should see a tight for / while loop with one of these patterns:
    – XOR (^, eor, xor, vpxor …)
    – Add/Sub (+ key[i&3])
    – Rotate (ROR, ROL)
    – Table look-ups (Sbox[ byte ])
    c. Very few other calls; maybe one to memcpy/memmove or to LZ4 if the file is compressed first.

  3. Look for constants • 0x9e3779b9 / 0xc6ef3720 → TEA / XXTEA
    • 256-byte table starting with 0x63 0x7c 0x77 … → AES S-box
    • Two or four hard-coded 32-bit words (e.g. 0xA3B1BAC6) → XTEA / Blowfish variants
    • Only 1,2,3,4 as constants → plain XOR with rolling key

  4. Use Ghidra’s “Search -> For Scalars” and “Search -> For Strings” • The real key is often an 8–32 byte static array in .rodata right next to the decrypt function.
    • Or it is fetched from PlayerPrefs / Android keystore; if so you’ll see calls to JNI or UnityEngine.PlayerPrefs before the decrypt loop.

  5. Dynamic confirmation (quick & easy) • Attach Frida to libil2cpp, set Interceptor.attach at 0x037d7b80.
    • Dump the input and output buffers for one call.
    • Compare with a Python proof-of-concept implementation of the candidate algorithm; iterate until they match.

Why the big function you posted is still useful

It tells you:

• Which resource-loading branch ends up calling the decryptor (follow the call chain: FUN_03ecef74 → … → FUN_0381a354 → FUN_037d7b80).
• Where the plaintext goes afterwards (good for breakpoints).
• Which global singletons are initialised right before the decrypt (good places to search for the key if it is generated at runtime).

Next steps

  1. Decompile only FUN_037d7b80 (or its immediate callee if it just forwards).
  2. Post that much smaller snippet (a few dozen lines) – the community can usually identify the cipher immediately.
  3. Once the algorithm is confirmed, dump a sample ciphertext/plaintext pair with Frida and re-implement the decryptor in Python to make sure you have the right key.

Let me know when you have the 0x037d7b80 pseudocode or the raw constants; I’ll help you recognise the exact algorithm and pull the key out.