Help! Intercepting and Modifying Lua Scripts in Unity IL2CPP Game (BLEACH: Soul Resonance) - XLua Issues

lola1337

Platinian
Game: BLEACH: Soul Resonance (Unity + IL2CPP + XLua)

Platform: Android (x86_64 emulator)

Tools: Frida, Il2CppDumper, ZygiskFrida/frida-server

Goal:Intercept and modify Lua scripts that control combat mechanics (attack speed, damage, abilities).

Problem:All combat logic is implemented in Lua, and C# functions are not called directly during combat.

What We Tried (Didn't Work):
  1. Hooking C# functions via IL2CPP offsets:
  • EntityLogicComp$$SetRateSpeed (RVA: 0x2660104)
  • BaseEntity$$SetRateSpeed (RVA: 0x2582408)
  • get_AtkSpeed (RVA: 0x265F464)
  • Result: Functions are not called during combat
  1. Hooking XLua wrappers:
  • XLua.CSObjectWrap.BaseEntityWrap$$_m_SetRateSpeed
  • XLua.CSObjectWrap.EntityLogicCompWrap$$_m_SetRateSpeed
  • Result: Wrappers are not called
  1. Intercepting Lua via XLua functions:
  • XLua.LuaEnv$$DoString (Address: 40901428, RVA: 0x26F0C34)
  • XLua.LuaEnv$$LoadString (Address: 40902012, RVA: 0x26F0E9C)
  • Result: Hooking DoString/LoadString causes the game to freeze on a black screen after loading
Current Approach:Using Frida Server on emulator:
  • Installed frida-server-17.5.1-android-x86_64
  • Connection via MCP server
  • Hook on DoString at address base + 0x26F0C34
  • Problem: DoString is not called during combat (hook is installed, but no calls detected)
Questions:
  1. Why is DoString not being called? Could Lua be loaded through a different mechanism?
  2. How to safely intercept Lua without freezing the game?
  3. Are there alternative ways to modify Lua in XLua (e.g., via Lua tables in memory)?
  4. Could it be that combat logic runs in already-loaded Lua functions rather than through DoString?
Technical Details:
  • Architecture: Unity 2021.x + IL2CPP + XLua
  • Process: com.bleach.apj (PID: 3928)
  • Module: libil2cpp.so (base: 0x763867709000)
  • DoString offset: 0x26F0C34 (from Il2CppDumper)
  • Frida version: 17.5.1
What We Need: Advice on intercepting/modifying Lua in XLua without freezing the game, or alternative approaches to modifying combat logic.
 
Weird for me did you use "const char*" for buff right?

In my case the file i dump from game write extra 32 byte on header every file and i still finding a way to fix it because all file is corrupted and missing 32 byte at the end of file

Example:
68 7D 91 20 4D 78 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 56 22 00 00 00 00 00 00

It's should start with
1B 4C 4A


Btw i'm still new to LUA lucky my game didn't need to write customs LuaVM
 
Nevermind just found a temporary fix not sure this is happened because Bluestack or not so i need to test on other devices to confirm issue by inject it during runtime
 
Weird for me did you use "const char*" for buff right?

In my case the file i dump from game write extra 32 byte on header every file and i still finding a way to fix it because all file is corrupted and missing 32 byte at the end of file

Example:
68 7D 91 20 4D 78 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 56 22 00 00 00 00 00 00

It's should start with
1B 4C 4A


Btw i'm still new to LUA lucky my game didn't need to write customs LuaVM


const char buff is fine, the issue isn't there.

What's happening:
- You're dumping a wrapper with a 32-byte game header, not a raw Lua chunk
- "buff" points to the start of the file with the header; the actual Lua chunk starts at "buff + 0x20"
- If "size" includes the header and you read "size" bytes from the start, you lose the tail

How to fix:

1. Better: Hook "luaL_loadbufferx" directly - there "buff" already points to the clean Lua chunk without the header

2. Or: If hooking a wrapper, read:
```js
const luaChunkStart = this.buf.add(0x20); // skip header
const luaChunkSize = this.size - 0x20; // subtract header
const data = Memory.readByteArray(luaChunkStart, luaChunkSize);
```

Check: Find "1B 4C 4A" (or "1B 4C 75 61") in your dump - it should be at offset 0x20. If so, the header is stable and you just need to skip it
 
const char buff is fine, the issue isn't there.

What's happening:
- You're dumping a wrapper with a 32-byte game header, not a raw Lua chunk
- "buff" points to the start of the file with the header; the actual Lua chunk starts at "buff + 0x20"
- If "size" includes the header and you read "size" bytes from the start, you lose the tail

How to fix:

1. Better: Hook "luaL_loadbufferx" directly - there "buff" already points to the clean Lua chunk without the header

2. Or: If hooking a wrapper, read:
```js
const luaChunkStart = this.buf.add(0x20); // skip header
const luaChunkSize = this.size - 0x20; // subtract header
const data = Memory.readByteArray(luaChunkStart, luaChunkSize);
```

Check: Find "1B 4C 4A" (or "1B 4C 75 61") in your dump - it should be at offset 0x20. If so, the header is stable and you just need to skip it

Yeah, i hook into luaL_loadbuffer direct and the buff return extra 32 byte to me. Btw i can skip it by + 0x32 and i can dump the real file with 1B 4C 4A

Everything went well with decompiling but the problem is the game freeze when i'm trying to return LUA back to game (even the one that not modified yet)

I'm pretty sure it's return to extra 32 byte not the real one that why game can't load lua... Btw how can i skip that extra 32 byte when return LUA?
 
Yeah, i hook into luaL_loadbuffer direct and the buff return extra 32 byte to me. Btw i can skip it by + 0x32 and i can dump the real file with 1B 4C 4A

Everything went well with decompiling but the problem is the game freeze when i'm trying to return LUA back to game (even the one that not modified yet)

I'm pretty sure it's return to extra 32 byte not the real one that why game can't load lua... Btw how can i skip that extra 32 byte when return LUA?
The game passes a buffer with a 32-byte header before the Lua chunk to luaL_loadbufferx. If you return modified Lua without the header, the game won't parse it because it expects the same format. Don't change buff and size in the arguments - the game already passed correct values for its format. Modify only the Lua part of the buffer in place: read from buff + 0x20, patch it, write it back to the same address. Don't touch the header.If it still freezes, check: whether the Lua chunk length changes (if so, you need to recalculate the size in the header), whether the Proto structure is preserved (if you're patching bytecode), and whether alignment or data integrity after the header breaks.The game reads the buffer with the header itself. Your task is to modify only the Lua part without changing the format the game expects. If you need to return a new buffer, assemble it as [original header][modified Lua] with the correct size.
 
The game passes a buffer with a 32-byte header before the Lua chunk to luaL_loadbufferx. If you return modified Lua without the header, the game won't parse it because it expects the same format. Don't change buff and size in the arguments - the game already passed correct values for its format. Modify only the Lua part of the buffer in place: read from buff + 0x20, patch it, write it back to the same address. Don't touch the header.If it still freezes, check: whether the Lua chunk length changes (if so, you need to recalculate the size in the header), whether the Proto structure is preserved (if you're patching bytecode), and whether alignment or data integrity after the header breaks.The game reads the buffer with the header itself. Your task is to modify only the Lua part without changing the format the game expects. If you need to return a new buffer, assemble it as [original header][modified Lua] with the correct size.
The main point is that finding the damage function is not enough. You can't wrap it in a C function or connect it directly, because a gaming Lua VM requires certain calling conventions, stack layout, and closure structures. A transfer or interception at this level will result in a failure due to mismatched stack frames, corrupted Lua state, or broken closure chains.
The only reliable approach is to work at the fragment level: intercept luaL_loadbufferx or loadFunction, modify the Proto bytecode before uploading it to the VM, and correct certain instructions (for example, MULK) that affect the damage calculation. This way, you change the bytecode itself without interfering with execution at runtime, so the VM handles everything correctly, and there are no problems with stack corruption or closure.
 
The main point is that finding the damage function is not enough. You can't wrap it in a C function or connect it directly, because a gaming Lua VM requires certain calling conventions, stack layout, and closure structures. A transfer or interception at this level will result in a failure due to mismatched stack frames, corrupted Lua state, or broken closure chains.
The only reliable approach is to work at the fragment level: intercept luaL_loadbufferx or loadFunction, modify the Proto bytecode before uploading it to the VM, and correct certain instructions (for example, MULK) that affect the damage calculation. This way, you change the bytecode itself without interfering with execution at runtime, so the VM handles everything correctly, and there are no problems with stack corruption or closure.

I'm not quite understand well but i learn a lot from you and finally i can forward to next step after successful return my Modified Lua and cheat is work well also. It's will be useful if we can access function directly without modified the whole LUA but yeah if it's work then work 😂
 
I'm not quite understand well but i learn a lot from you and finally i can forward to next step after successful return my Modified Lua and cheat is work well also. It's will be useful if we can access function directly without modified the whole LUA but yeah if it's work then work 😂
I didn't understand it myself, but how did you implement the cheat then? I personally made a bytecode runtime patch in a chunk, and you?
 
I'm not quite understand well but i learn a lot from you and finally i can forward to next step after successful return my Modified Lua and cheat is work well also. It's will be useful if we can access function directly without modified the whole LUA but yeah if it's work then work 😂
Did you even reverse proto and the bytecode itself? I may be communicating incorrectly due to the fact that my native language is not English.
 
I'm not quite understand well but i learn a lot from you and finally i can forward to next step after successful return my Modified Lua and cheat is work well also. It's will be useful if we can access function directly without modified the whole LUA but yeah if it's work then work 😂
Or did you just decide to decompile the whole lua chunk as simply as possible, modified the text, and then returned the compiled chunk back?
 
The bad side i can't make Mod Menu to enable or disable function because the game load LUA only once time. Maybe it's possible if i can swith back original LUA by force call? But i don't know about these things yet. As for now i'm okay because i can hack it
 
The bad side i can't make Mod Menu to enable or disable function because the game load LUA only once time. Maybe it's possible if i can swith back original LUA by force call? But i don't know about these things yet. As for now i'm okay because i can hack it
What hacks did u make exactly? I saw someone just do autokill with a normal gg script
 
Back
Top Bottom