Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- The goal of this experiment is to give Aya a new skill (we'll use Maintenance) and a new spell (we'll use Last Judgement). The purpose of this pastebin is to document the process used to achieve the goal, so that others may replicate it with the necessary tools. It is not a guide to patching your own game, and will include several technical explanations to explain how things are being changed. x86 assembly knowledge is extremely important if you plan on following along, at least the basics of how the assembly language works. If you wish to follow along and repeat this experiment, you will need Cheat Engine and Ghidra, with a fully analyzed version of Labyrinth 2's ENGLISH PATCHED exe. The process of analyzing the exe will take several hours, but is vital to analyzing the code in the game.
- 1. Figuring out the address where skills are loaded in memory
- -------------------------------------------------------------
- Open the game and hook Cheat Engine to it, load any save file where Aya has some skill points to spare. Reset her so that all her skills reset to level 0/1. Search for variables with current value equal to 1, then level up her Wind God's Fan skill. Repeat search for value equal to 2, and keep going until you isolate the variable keeping track of that skill's level. If there are multiple matches, reset her again and check which address dropped its value to 1, there should be only one result. Right click the address and "Find out what accesses this address", then load a save file. There should be 4 hits, all of them close in memory. Choose the one with the "mov [ecx+edx],eax" instruction and "Show disassembler". Right above it there should be a call instruction, note down the instruction's hex (should be E8 xxxxF3FF), type that in Ghidra's Instruction Pattern search and find the only instance of the code containing that call.
- Don't bother waiting for Ghidra to decompile this function, it's a huge one and we only really care about the assembly here. Note that some instructions above the exe loads the value 0x249 into memory, which corresponds to the spell ID for Wind God's Fan. If we alter this to any other valid spell ID, the game will properly alter Aya's spell into that new spell. Using enemy spells or invalid IDs crashes the game. If we scroll further up, we see similar instructions loading values 0xf5, 0x3e, 0x86, 0x100 and 0x12f. If we search on Ghidra's string references for Aya's skill names, such as Extra Steps, we can follow the reference trail to a function that loads Extra Steps and its description into memory, with a CMP instruction that checks for 0xf5 - leading us to believe this is Extra Steps' skill ID. Repeating this process for the other skills confirms this.
- If we scroll down instead, we see a bunch of code that eventually leads to loadings of 0x24a, 0x24b and 0x24c in memory, Aya's other spells. Even further down, we see a CMP of a specific address in memory to 1, and below that very similar code loading values 0x197, 0x198 and 0x182 in memory. A search for Aya's awakening skills reveals these to be her awakening skills' IDs, so everything below that CMP is only loaded in when Aya is awakened. If we switch that comparison to 0, we make it so her awakening skills are always available. Switching it to 2, 3, ... will make it so you need 2, 3, ... of her awakening items to unlock the skills.
- 2. Understanding how the game is loading skills and spells into memory
- ----------------------------------------------------------------------
- Let's break down the code that is loading the skill Tengu's Wind into memory. We know that the code must start somewhere before 0x86 is loaded, and end somewhere after it is loaded. If we look for a pattern among how skills are loaded, we can isolate each skill's code ro look like this:
- 00d1f0ec b8 08 00 MOV EAX,0x8
- 00 00
- 00d1f0f1 6b c8 16 IMUL ECX,EAX,0x16
- 00d1f0f4 8b 55 f8 MOV EDX,dword ptr [EBP - 0x8]
- 00d1f0f7 8d 84 0a LEA EAX,[EDX + ECX*0x1 + 0x4fc]
- fc 04 00 00
- 00d1f0fe b9 04 00 MOV ECX,0x4
- 00 00
- 00d1f103 6b d1 00 IMUL EDX,ECX,0x0
- 00d1f106 c7 04 10 MOV dword ptr [EAX + EDX*0x1],0x86
- 86 00 00 00
- Note that the only differences between skills are the values in the IMUL and final MOV instructions. The value in IMUL seems to increment as we load more spells, indicating that maybe it is the skill slot corresponding to that skill. This seems to match up to the order the skills show up in, starting at 0x14. Note that the final awakening skill loads in slot 0x1b, and the first spell loads in slot 0x1e, so there should be 2 empty slots to fill with skills. We can also assume that the slot 0x22 is empty, since that's where the fifth spell would be loaded. The value in the MOV instruction is simply the skill ID.
- The code is very simple to understand, it is taking 8 times the skill slot, adding it to some value loaded from memory, and using that as an offset to load the skill ID. Interesting to note that the offset is given only for EAX, since EDX is guaranteed to be 0x00 as a result of an IMUL with 0x00 as an operand. The catch here for optimization is that this entire offset calculation is being repeated for each skill, when it is very clear that addresses will be near adjacent, with a fixed offset given by the slot number.
- But before that, we should also analyze the code that takes care of loading the spell ID in memory. Taking Wind God's Fan as an example, and using a similar pattern technique like we did for skills, we can isolate the code as:
- 00d1f14f b8 08 00 MOV EAX,0x8
- 00 00
- 00d1f154 6b c8 1e IMUL ECX,EAX,0x1e
- 00d1f157 8b 55 f8 MOV EDX,dword ptr [EBP - 0x8]
- 00d1f15a 8d 84 0a LEA EAX,[EDX + ECX*0x1 + 0x4fc]
- fc 04 00 00
- 00d1f161 b9 04 00 MOV ECX,0x4
- 00 00
- 00d1f166 6b d1 00 IMUL EDX,ECX,0x0
- 00d1f169 c7 04 10 MOV dword ptr [EAX + EDX*0x1],0x249
- 49 02 00 00
- 00d1f170 6a 01 PUSH 0x1
- 00d1f172 b8 08 00 MOV EAX,0x8
- 00 00
- 00d1f177 6b c8 1e IMUL ECX,EAX,0x1e
- 00d1f17a 8b 55 f8 MOV EDX,dword ptr [EBP - 0x8]
- 00d1f17d 8d 84 0a LEA EAX,[EDX + ECX*0x1 + 0x4fc]
- fc 04 00 00
- 00d1f184 b9 04 00 MOV ECX,0x4
- 00 00
- 00d1f189 c1 e1 00 SHL ECX,0x0
- 00d1f18c 8b 14 08 MOV EDX,dword ptr [EAX + ECX*0x1]
- 00d1f18f 52 PUSH EDX
- 00d1f190 8b 45 f8 MOV EAX,dword ptr [EBP - 0x8]
- 00d1f193 8b 08 MOV ECX,dword ptr [EAX]
- 00d1f195 8b 09 MOV ECX,dword ptr [ECX]
- 00d1f197 e8 d1 2e CALL thunk_FUN_01613b50
- f3 ff
- 00d1f19c ba 08 00 MOV EDX,0x8
- 00 00
- 00d1f1a1 6b ca 1e IMUL ECX,EDX,0x1e
- 00d1f1a4 8b 55 f8 MOV EDX,dword ptr [EBP - 0x8]
- 00d1f1a7 8d 8c 0a LEA ECX,[EDX + ECX*0x1 + 0x4fc]
- fc 04 00 00
- 00d1f1ae ba 04 00 MOV EDX,0x4
- 00 00
- 00d1f1b3 c1 e2 00 SHL EDX,0x0
- 00d1f1b6 89 04 11 MOV dword ptr [ECX + EDX*0x1],EAX
- The code behaves in exactly the same way as the skill loading, but with some extra stuff at the end. After loading 0x249, it performs a function call to do something, then loads another value into some address. Since we dont know what the function does, taking a guess at what is being pushed into the stack is beyond us. However, do note the game is calculating the address we have on EAX again, the code up to the SHL instruction is identical to the one used to load 0x249 in memory. That SHL instruction btw is completely useless since it has 0x00 as its second operand.
- After the function call, it computes ONCE AGAIN the address offset it computed just a few instructions above, but this time on EDX instead of EAX. It does this so that the value in EAX (which likely comes from the function) is stored in the address pointed to by ECX+EDX. Needless to say, this entire mess can be shortened greatly. But we're getting ahead of ourselves.
- 3. Optimizing the code that loads skills
- ----------------------------------------
- The code that loads a single skill can't really be optimized any further, it needs to figure out where it must load the ID in memory. However, if we are chaining a bunch of these, there is no need to compute a new offset from scratch, we can use the result from the previous load, since we know the step the addresses take is equal to 8 bytes. We know this because 0x8 is loaded in EAX for the IMUL instruction. So instead of computing all of that multiple times, we can just do:
- MOV EAX,0x8
- IMUL ECX,EAX,0x16
- MOV EDX,dword ptr [EBP - 0x8]
- LEA EAX,[EDX + ECX*0x1 + 0x4fc]
- MOV ECX,0x4
- IMUL EDX,ECX,0x0
- MOV dword ptr [EAX + EDX*0x1],0x12f
- ADD eax,0x8
- MOV dword ptr [EAX + EDX*0x1],0x100
- ADD eax,0x8
- MOV dword ptr [EAX + EDX*0x1],0x86
- ADD eax,0x8
- MOV dword ptr [EAX + EDX*0x1],0x3e
- ADD eax,0x8
- MOV dword ptr [EAX + EDX*0x1],0xf5
- To load all 5 of Aya's skills in memory. Needless to say, this uses up way less bytes than the previous mess, and because we cannot go and change every single jump and call in the rest of the program, we should pad the now useless bytes with NOPs (opcode 90). This will ensure any and all relative jumps will still work, preserving the file size.
- 4. Using the free space to load a new skill slot
- ------------------------------------------------
- Now that we have a sequence of NOPs between loading the last skill and the first spell, we can use these new bytes to do whatever we want! Simply replace the NOPs as needed with the code you want to run. In this example, we'll be loading the skill Maintenance in slot 0x1c, which we assumed was an empty slot above. Maintenance's skill ID is 0x101, and since we're moving 4 slots at once when chaining our loads, we should add 4*0x8 = 0x20 to EAX:
- MOV EAX,0x8
- IMUL ECX,EAX,0x16
- MOV EDX,dword ptr [EBP - 0x8]
- LEA EAX,[EDX + ECX*0x1 + 0x4fc]
- MOV ECX,0x4
- IMUL EDX,ECX,0x0
- MOV dword ptr [EAX + EDX*0x1],0x12f
- ADD eax,0x8
- MOV dword ptr [EAX + EDX*0x1],0x100
- ADD eax,0x8
- MOV dword ptr [EAX + EDX*0x1],0x86
- ADD eax,0x8
- MOV dword ptr [EAX + EDX*0x1],0x3e
- ADD eax,0x8
- MOV dword ptr [EAX + EDX*0x1],0xf5
- ADD eax,0x20
- MOV dword ptr [EAX + EDX*0x1],0x101
- If you want to apply these changes in a hex editor, look for the sequence E8 F7 F0 F3 FF, which is the function call just before the first instruction in the algorithm we're changing. Change the bytes from there on to look like this, so it matches the code above:
- E8 F7 F0 F3 FF CALL function offset
- B8 08 00 00 00 MOV EAX,0x8
- 6B C8 14 IMUL ECX,EAX,0x14
- 8B 55 F8 MOV EDX,DWORD PTR [EBP-0x8]
- 8D 84 0A FC 04 00 00 LEA EAX,[EDX+ECX*1+0x4FC]
- B9 04 00 00 00 MOV ECX,0x4
- 6B D1 00 IMUL EDX,ECX,0x0
- C7 04 10 2F 01 00 00 MOV DWORD PTR [EAX+EDX*1],0x12F
- 05 08 00 00 00 ADD EAX,0x8
- C7 04 10 00 01 00 00 MOV DWORD PTR [EAX+EDX*1],0x100
- 05 08 00 00 00 ADD EAX,0x8
- C7 04 10 86 00 00 00 MOV DWORD PTR [EAX+EDX*1],0x86
- 05 08 00 00 00 ADD EAX,0x8
- C7 04 10 3E 00 00 00 MOV DWORD PTR [EAX+EDX*1],0x3E
- 05 08 00 00 00 ADD EAX,0x8
- C7 04 10 F5 00 00 00 MOV DWORD PTR [EAX+EDX*1],0xF5
- 05 20 00 00 00 ADD EAX,0x20
- C7 04 10 01 01 00 00 MOV DWORD PTR [EAX+EDX*1],0x101
- 5. Optimizing the code that loads spells
- ----------------------------------------
- The code that loads a single spell can be optimized, as we saw previously. There's no need to compute the address 3 times in a row, so we can shorten the code to be like shown below. Pushing EAX into the stack before the function call, then popping it to ECX saves the third offset calculation. We can also remove the useless SHL instructions.
- MOV EAX,0x8
- IMUL ECX,EAX,0x1e
- MOV EDX,dword ptr [EBP - 0x8]
- LEA EAX,[EDX + ECX*0x1 + 0x4fc]
- MOV ECX,0x4
- IMUL EDX,ECX,0x0
- MOV dword ptr [EAX + EDX*0x1],0x249
- PUSH EAX
- PUSH 0x1
- MOV ECX,0x4
- MOV EDX,dword ptr [EAX + ECX*0x1]
- PUSH EDX
- MOV EAX,dword ptr [EBP - 0x8]
- MOV ECX,dword ptr [EAX]
- MOV ECX,dword ptr [ECX]
- CALL thunk_FUN_01613b50
- POP ECX
- MOV EDX,0x4
- MOV dword ptr [ECX + EDX*0x1],EAX
- However, we can further optimize this code if we chain it from a previous skill load. Since EAX will already have some offset calculated, we can just add to it and go straight to loading the skill ID, just like we did for chaining skill loads. If we wanted to load Wind God's Fan right after we added Maintenance, we could simply add the following to our previous skill load code, remembering we must go 2 slots over the one we placed Maintenance in:
- ADD eax,0x10
- MOV dword ptr [EAX + EDX*0x1],0x249
- PUSH EAX
- PUSH 0x1
- MOV ECX,0x4
- MOV EDX,dword ptr [EAX + ECX*0x1]
- PUSH EDX
- MOV EAX,dword ptr [EBP - 0x8]
- MOV ECX,dword ptr [EAX]
- MOV ECX,dword ptr [ECX]
- CALL thunk_FUN_01613b50
- POP ECX
- MOV EDX,0x4
- MOV dword ptr [ECX + EDX*0x1],EAX
- The one thing to worry about here is fixing the CALL instruction. Because we changed how many bytes there are before it, the offset is now invalid, and leaving it as is will crash the game. You need to compute the new offset manually, based on how many bytes were taken care of. The formula to use for this is as follows:
- [Old base address] + [Old CALL offset] = [New base address] + [New CALL offset]
- 6. Using the free space to load a new spell slot
- ------------------------------------------------
- We have enough space left over in our NOPs from step 4 to include a new spell in there without much effort. For this example we'll just leave the code past the NOPs as is, loading Aya's spells normally with the original algorithm that computes the offsets multiple times. We'll add Last Judgement (spell ID 0x2d5) to Aya's 5th spell slot (number 0x22), and we'll leech off the load we did for Maintenance. The final code will look like this:
- MOV EAX,0x8
- IMUL ECX,EAX,0x16
- MOV EDX,dword ptr [EBP - 0x8]
- LEA EAX,[EDX + ECX*0x1 + 0x4fc]
- MOV ECX,0x4
- IMUL EDX,ECX,0x0
- MOV dword ptr [EAX + EDX*0x1],0x12f
- ADD eax,0x8
- MOV dword ptr [EAX + EDX*0x1],0x100
- ADD eax,0x8
- MOV dword ptr [EAX + EDX*0x1],0x86
- ADD eax,0x8
- MOV dword ptr [EAX + EDX*0x1],0x3e
- ADD eax,0x8
- MOV dword ptr [EAX + EDX*0x1],0xf5
- ADD eax,0x20
- MOV dword ptr [EAX + EDX*0x1],0x101
- ADD eax,0x30
- MOV dword ptr [EAX + EDX*0x1],0x249
- PUSH EAX
- PUSH 0x1
- MOV ECX,0x4
- MOV EDX,dword ptr [EAX + ECX*0x1]
- PUSH EDX
- MOV EAX,dword ptr [EBP - 0x8]
- MOV ECX,dword ptr [EAX]
- MOV ECX,dword ptr [ECX]
- CALL thunk_FUN_01613b50
- POP ECX
- MOV EDX,0x4
- MOV dword ptr [ECX + EDX*0x1],EAX
- If you want to apply these changes in a hex editor, look for the sequence E8 F7 F0 F3 FF, just like before, which is the function call just before the first instruction in the algorithm we're changing. Change the bytes from there on to look like this, so it matches the code above, with the already fixed CALL offset for the spell being loaded:
- E8 F7 F0 F3 FF CALL function offset
- B8 08 00 00 00 MOV EAX,0x8
- 6B C8 14 IMUL ECX,EAX,0x14
- 8B 55 F8 MOV EDX,DWORD PTR [EBP-0x8]
- 8D 84 0A FC 04 00 00 LEA EAX,[EDX+ECX*1+0x4FC]
- B9 04 00 00 00 MOV ECX,0x4
- 6B D1 00 IMUL EDX,ECX,0x0
- C7 04 10 2F 01 00 00 MOV DWORD PTR [EAX+EDX*1],0x12F
- 05 08 00 00 00 ADD EAX,0x8
- C7 04 10 00 01 00 00 MOV DWORD PTR [EAX+EDX*1],0x100
- 05 08 00 00 00 ADD EAX,0x8
- C7 04 10 86 00 00 00 MOV DWORD PTR [EAX+EDX*1],0x86
- 05 08 00 00 00 ADD EAX,0x8
- C7 04 10 3E 00 00 00 MOV DWORD PTR [EAX+EDX*1],0x3E
- 05 08 00 00 00 ADD EAX,0x8
- C7 04 10 F5 00 00 00 MOV DWORD PTR [EAX+EDX*1],0xF5
- 05 20 00 00 00 ADD EAX,0x20
- C7 04 10 01 01 00 00 MOV DWORD PTR [EAX+EDX*1],0x101
- 05 30 00 00 00 ADD EAX,0x30
- C7 04 10 D5 02 00 00 MOV DWORD PTR [EAX+EDX*1],0x2D5
- 50 PUSH EAX
- 6A 01 PUSH 0x1
- B9 04 00 00 00 MOV ECX,0x4
- 8B 14 08 MOV EDX,DWORD PTR [EAX+ECX*1]
- 52 PUSH EDX
- 8B 45 F8 MOV EAX,DWORD PTR [EBP-0x8]
- 8B 08 MOV ECX,DWORD PTR [EAX]
- 8B 09 MOV ECX,DWORD PTR [ECX]
- E8 42 2F F3 FF CALL 0xFFF32FC8
- 59 POP ECX
- BA 04 00 00 00 MOV EDX,0x4
- 89 04 11 MOV DWORD PTR [ECX+EDX*1],EAX
- The final result will be an Aya that knows Maintenance and Last Judgement, as well as every other skill and spell she has. This procedure can be performed for every character, and with enough NOPs it should be possible for everyone to have 10 skills and 5 spells, assuming there's enough room to optimize everything. There should be if you fully optimize the entire algorithm, I stuck to a small portion of it.
Add Comment
Please, Sign In to add comment