Jump to content
  • 0

Changing health value from memory at runtime using offset address


chateau
 Share

Question

Hi there,

I am trying to manipulate memory value (related to health) at runtime based on an identified offset address. I can currently access the memory address that holds the health value by manually searching the exact health value of double type, but this address changes when I close and open the application. I would eventually like to persistently modify the health value after closing and opening the application, without having to manually search for the address.

I am unsure what to do next in order to modify the health value using libil2cpp offsets. These are the following steps I've done so far:

  1. Used a rooted 64-bit Android emulator (Nox Player) with arm64-v8a instruction set.
  2. Retrieved the latest .apk file directly from the Android emulator's root /data directory using "adb pull".
  3. Dumped the "libil2cpp.so" file from the game's .apk using il2cppdumper
  4. Used the dumped output from above to identify interesting function names "public double get_hp()" that contains offset addresses such as "Offset: 0x18D0258".
  5. On GameGuardian I've used "goto", then selected "Xa" and chose "libil2cpp.so".
  6. On GameGuardian I've used "Offset calculator" where the base address is from above and the offset is "18D0258"  from step 4.
  7. I am presented with a couple of interesting ARM64 instructions:  
    LDR D0, [X0, #0x28];
    RET;

     

My understanding is that X0 (general-purpose register, like a variable) holds a memory address and 0x28 offset is added to the X0 register and the result of X0 + 0x28 becomes the accessed memory address, where the value inside it is loaded into the D0 register.

Knowing that this is related to public double get_hp() what minimal changes do I need to make so that I can return a specific value to change the health or make it so that the health does not change at all?

Thanks!

Edited by chateau
Link to comment
Share on other sites

13 answers to this question

Recommended Posts

  • 0
1 hour ago, chateau said:

Hi there,

I am trying to manipulate memory value (related to health) at runtime based on an identified offset address. I can currently access the memory address that holds the health value by manually searching the exact health value of double type, but this address changes when I close and open the application. I would eventually like to persistently modify the health value after closing and opening the application, without having to manually search for the address.

I am unsure what to do next in order to modify the health value using libil2cpp offsets. These are the following steps I've done so far:

  1. Used a rooted 64-bit Android emulator (Nox Player) with arm64-v8a instruction set.
  2. Retrieved the latest .apk file directly from the Android emulator's root /data directory using "adb pull".
  3. Dumped the "libil2cpp.so" file from the game's .apk using il2cppdumper
  4. Used the dumped output from above to identify interesting function names "public double get_hp()" that contains offset addresses such as "Offset: 0x18D0258".
  5. On GameGuardian I've used "goto", then selected "Xa" and chose "libil2cpp.so".
  6. On GameGuardian I've used "Offset calculator" where the base address is from above and the offset is "18D0258"  from step 4.
  7. I am presented with a couple of interesting ARM64 instructions:  
    LDR D0, [X0, #0x28];
    RET;

     

My understanding is that X0 (general-purpose register, like a variable) holds a memory address and 0x28 offset is added to the X0 register and the result of X0 + 0x28 becomes the accessed memory address, where the value inside it is loaded into the D0 register.

Knowing that this is related to public double get_hp() what minimal changes do I need to make so that I can return a specific value to change the health or make it so that the health does not change at all?

Thanks!

LDR will load the register X0 + 0x28 into D0 adding [] will make the value X0 + offset a pointer, D0 will hold the value which this pointer hold *no edits happen*
editing get_hp method arm code isn't good in most cases because usally this method related to enemy aswell (creating a god mod will result a god mod for npc aswell ) 
what access the methods is the class field (the caller (field). X0 will hold the caller address in the original arm code) look into the fields if there is an indice to player even a string
searching the class name instead of the method and do some logic in a function to trigger the edit of the player hp only(by matching the player indice). the edit should be just the value no arm patching
if the method get_hp in a class specific for player than you can edit the arm code with no problem

Link to comment
Share on other sites

  • 0
20 minutes ago, XEKEX said:

editing get_hp method arm code isn't good in most cases because usally this method related to enemy aswell

In the il2cpp dump there is actually two get_hp() method, one under EnemyState class and the other PlayerState class. I've used the get_hp() offset that is under PlayerState class.

 

31 minutes ago, XEKEX said:

look into the fields if there is an indice to player even a string searching the class name instead of the method and do some logic in a function to trigger the edit of the player hp only(by matching the player indice). 

Can you elaborate? What do you mean by "do some logic in a function to trigger the edit"? There is only 1 player and 1 enemy.

Link to comment
Share on other sites

  • 0

Hi @chateau, the instruction will Load values from [X0, #0x28] into D0 register:

  • - LDR or LoaD Register will load a value from memory into the Register.
  • - D0 here is indeed Double Floating Point Register
  • - Meanwhile, X0 is a register where the Address is exist with 0x28 as Offset or a Displacement. (X0: 0000) + (Offset: 0x28) = 0028, for example.

[ Patches ]
You can try to force it with sending custom Values. For high precision you can read ARM Patching

# Basic: Double 1
fmov    d0, #1.00000000

# Low Precision : Double 12.5
fmov    d0, #12.50000000

# Mid Precision: Double 101
mov w0, #0x40590000  // Double 101 in Hex form
fmov d0, w0  // Push Double in w0 to d0

[ Scenario ]
HP are really depends on Mechanism the game use. Here is some variation:

  • 1) In some RPG games, the Character has it's own HP Scope which then get_HP will catch changes in-game and save it on either Save files or Configs. get_HP on this type of game will not affect whole entity that you can apply above patches.
  • 2) It's common, especially in Online Games where get_HP are basically handles all Entity list. All Entity will have HP values derived from get_HP. Altho it affects All Entity, in P2P Games, modifying get_HP directly won't affect other players or NPC where the Server are hosted by another Player. Minecraft Realm for Example (Tho it is now patched on BDS)

[ Conclusion ]

  • - Player Health are always, either tricky to find or impossible. As it depends on How the game really implements it.
  • - Changing get_HP is generally a bad practice. You should instead lookup for an Entity List and adjust it's attribute accordingly. I forgot the games name, but it has the pattern of Entity naming like player-0-[random_identifier] for player or bot-1-[random_identifier] for bot, also kinda the same in Minecraft: minecraft:player-0-[UUID] for Player Identifier. The point is: you should lookup to 'identifier' instead of get_HP
  • - Also See: General Games Implementation
Edited by MC189
words
Link to comment
Share on other sites

  • 0

Hey @MC189, thanks for sharing your insights and making it clear to understand.

1 hour ago, MC189 said:

For high precision you can read ARM Patching

I'm skimming through the ARM docs, so we use FMOV because we're dealing with double-precision floating-point and D0 is used to store a double-precision value?

1 hour ago, MC189 said:

[ Schenario ]
HP are really depends on Mechanism the game use. Here is some variation:

  • 1) In some RPG games, the Character has it's own HP Scope which then get_HP will catch changes in-game and save it on either Save files or Configs. get_HP on this type of game will not affect whole entity that you can apply above patches.
  • 2) It's common, especially in Online Games where get_HP are basically handles all Entity list. All Entity will have HP values derived from get_HP. Altho it affects All Entity, in P2P Games, modifying get_HP directly won't affect other players or NPC where the Server are hosted by another Player. Minecraft Realm for Example (Tho it is now patched on BDS)

Thanks for making that clear, the game I'm currently working with is mostly client-sided where (I think) there is no network communication during a battle where I receive damage that changes my health.

1 hour ago, MC189 said:

[ Conclusion ]

  • - Player Health are always, either tricky to find or impossible. As it depends on How the game really implements it.

I totally agree, there is like 50~ health related methods and fields when I search through the il2cpp dump using dnSpy and a lot of duplicate names. Which I'm guessing that means doing a lot of trial and error.

1 hour ago, MC189 said:

You should instead lookup for an Entity List and adjust it's attribute accordingly.

I'm not sure if this is relevant in my case. Can you elaborate on this?

1 hour ago, MC189 said:
# Mid Precision: Double 101
mov w0, #0x40590000  // Double 101 in Hex form
fmov d0, w0  // Push Double in w0 to d0

I think 40590000 hex corresponds to 100 double. How can I write two lines of instruction? Do I just overwrite the instruction below it, which is a RET?
On godbolt, when I change the return value to 32 or above "double t() { return 32.0; }" It outputs multiple lines of instructions:

.LCPI0_0:
  .xword 0x4040000000000000 // double 32
t(): // @t()
  adrp x8, .LCPI0_0
  ldr d0, [x8, :lo12:.LCPI0_0]
  ret

How am I suppose to apply this in GameGuardian?

Before I read your post I actually tried to modify other health related methods, like get_currentHP(). I've applied arbitrary instructions (where it mostly failed to recognize ARM64 opcode, but a suggestion was made by GameGuardian), after applying the suggestion (shown below), my health was constantly 0, although I was never able to die:

FMOV     D0, #0x3FC0000000
Edited by chateau
Link to comment
Share on other sites

  • 0
2 hours ago, chateau said:
.LCPI0_0:
  .xword 0x4040000000000000 // double 32
t(): // @t()
  adrp x8, .LCPI0_0
  ldr d0, [x8, :lo12:.LCPI0_0]
  ret

godbolt will assume that the code you write is an actuel program it store the double value in memory and then call ldr on it , in arm patching it's different ,
FMOV it's an FPU mov instruction , 
we use movk and movz , lsl to modify X0 to the desired hex value then send the X0 to the fpu register.
 

-- Load the lower 16 bits of the value into X0
MOVZ X0, #0x0000

-- Load the next 16 bits of the value into X0, left-shifted by 16 bits
MOVK X0, #0x4000, LSL #16

-- Load the next 16 bits of the value into X0, left-shifted by 32 bits
MOVK X0, #0x0000, LSL #32

-- Load the upper 16 bits of the value into X0, left-shifted by 48 bits
MOVK X0, #0x0000, LSL #48
-- X0 now hold : 0x4040000000000000
-- Copy the value in X0 to D0
FMOV D0, X0
RET

here is a better example of using lsl and movk :
 

--X0 = 0x1234567891234567
-- Load the lower 16 bits of the value into X0 (0x0000000000004567)
MOVZ X0, #0x4567

-- Load the next 16 bits of the value into X0, left-shifted by 16 bits (0x0000000091234567)
MOVK X0, #0x9123, LSL #16

-- Load the next 16 bits of the value into X0, left-shifted by 32 bits (0x0000567891234567)
MOVK X0, #0x5678, LSL #32

-- Load the upper 16 bits of the value into X0, left-shifted by 48 bits (0x1234567891234567)
MOVK X0, #0x1234, LSL #48

 

 

Edited by XEKEX
Link to comment
Share on other sites

  • 0
7 hours ago, XEKEX said:

LDR will load the register X0 + 0x28 into D0 adding [] will make the value X0 + offset a pointer, D0 will hold the value which this pointer hold *no edits happen*
editing get_hp method arm code isn't good in most cases because usally this method related to enemy aswell (creating a god mod will result a god mod for npc aswell ) 
what access the methods is the class field (the caller (field). X0 will hold the caller address in the original arm code) look into the fields if there is an indice to player even a string
searching the class name instead of the method and do some logic in a function to trigger the edit of the player hp only(by matching the player indice). the edit should be just the value no arm patching
if the method get_hp in a class specific for player than you can edit the arm code with no problem

For  healt enemy asswell must use unlink function , i dont know gameguardian can doit or not

2 hours ago, XEKEX said:

godbolt will assume that the code you write is an actuel program it store the double value in memory and then call ldr on it , in arm patching it's different ,
FMOV it's an FPU mov instruction , 
we use movk and movz , lsl to modify X0 to the desired hex value then send the X0 to the fpu register.
 

-- Load the lower 16 bits of the value into X0
MOVZ X0, #0x0000

-- Load the next 16 bits of the value into X0, left-shifted by 16 bits
MOVK X0, #0x4000, LSL #16

-- Load the next 16 bits of the value into X0, left-shifted by 32 bits
MOVK X0, #0x0000, LSL #32

-- Load the upper 16 bits of the value into X0, left-shifted by 48 bits
MOVK X0, #0x0000, LSL #48
-- X0 now hold : 0x4040000000000000
-- Copy the value in X0 to D0
FMOV D0, X0
RET

here is a better example of using lsl and movk :
 

--X0 = 0x1234567891234567
-- Load the lower 16 bits of the value into X0 (0x0000000000004567)
MOVZ X0, #0x4567

-- Load the next 16 bits of the value into X0, left-shifted by 16 bits (0x0000000091234567)
MOVK X0, #0x9123, LSL #16

-- Load the next 16 bits of the value into X0, left-shifted by 32 bits (0x0000567891234567)
MOVK X0, #0x5678, LSL #32

-- Load the upper 16 bits of the value into X0, left-shifted by 48 bits (0x1234567891234567)
MOVK X0, #0x1234, LSL #48

 

 

👍

Link to comment
Share on other sites

  • 0
6 hours ago, XEKEX said:
--X0 = 0x1234567891234567
-- Load the lower 16 bits of the value into X0 (0x0000000000004567)
MOVZ X0, #0x4567

-- Load the next 16 bits of the value into X0, left-shifted by 16 bits (0x0000000091234567)
MOVK X0, #0x9123, LSL #16

-- Load the next 16 bits of the value into X0, left-shifted by 32 bits (0x0000567891234567)
MOVK X0, #0x5678, LSL #32

-- Load the upper 16 bits of the value into X0, left-shifted by 48 bits (0x1234567891234567)
MOVK X0, #0x1234, LSL #48

How do I apply those multiple lines of instructions using GameGuardian?
Do I just overwrite existing instructions?

Link to comment
Share on other sites

  • 0

Wouldn't that crash the game as you're overwriting instructions you shouldn't?

For example, I know the offset address from below (1D4CD45)

public double currentHP
{
    get // Offset = "0x1D4CD45"
    {
        return default(double);
    }
    set
    {
    }
}

Using "offset calculator" I go to the address (base addr + offset addr).

Then, I see the following instructions and you're telling me I should just overwrite them all to the following instructions you gave me?

(which will give us 0x1234567891234567, but the value here is not important, I'm trying to understand the process).

LDR D0, [X0, #0x28]; 		-- Edit this instruction to: MOVZ X0, #0x4567
RET;  				-- Edit this instruction to: MOVK X0, #0x9123, LSL #16
STR X19, [SP, #-0x20]!; 	-- Edit this instruction to: MOVK X0, #0x5678, LSL #32
STP X29, X30, [SP, #0x10]; 	-- Edit this instruction to: MOVK X0, #0x1234, LSL #48

 

I can confirm that if I only edit the first line ^ from above to:

FMOV     D0, #0x3FC0000000

My health will decrease to 0, but I will have godmode (will not be able to die).

Edited by chateau
Link to comment
Share on other sites

  • 0

My understanding is that I have to use:

  1. branch instruction (jump instruction) to control the execution flow and jump to an unused section of memory.
  2. code cave (find unused section of memory) to inject additional instructions

I have a feeling that this is going to be related to "allocate memory page" on GameGuardian.

I've never used GameGuardian before this. I'm curious as to why this was never mentioned before.

Edited by chateau
Link to comment
Share on other sites

  • 0
3 hours ago, chateau said:

Wouldn't that crash the game as you're overwriting instructions you shouldn't?

For example, I know the offset address from below (1D4CD45)

public double currentHP
{
    get // Offset = "0x1D4CD45"
    {
        return default(double);
    }
    set
    {
    }
}

Using "offset calculator" I go to the address (base addr + offset addr).

Then, I see the following instructions and you're telling me I should just overwrite them all to the following instructions you gave me?

(which will give us 0x1234567891234567, but the value here is not important, I'm trying to understand the process).

LDR D0, [X0, #0x28]; 		-- Edit this instruction to: MOVZ X0, #0x4567
RET;  				-- Edit this instruction to: MOVK X0, #0x9123, LSL #16
STR X19, [SP, #-0x20]!; 	-- Edit this instruction to: MOVK X0, #0x5678, LSL #32
STP X29, X30, [SP, #0x10]; 	-- Edit this instruction to: MOVK X0, #0x1234, LSL #48

 

I can confirm that if I only edit the first line ^ from above to:

FMOV     D0, #0x3FC0000000

My health will decrease to 0, but I will have godmode (will not be able to die).

Using "offset calculator" I go to the address (base addr + offset addr). should give you "PUSH" instruction (-- this will indicate the start of the function ) 
-- "POP" instruction indicate the end of the function.
overwrite the original code won't crush the game if you do it right with the correct instructions.

 

--return 100 as double value instructions are : 
~A8 MOVZ X0, #0x5900 -- overwrite push instrunction ( dummy address : 0x0000 ) 
~A8 MOVK X0, #0x4000, LSL #16 -- the instruction below push ( dummy address : 0x0004 ) 
~A8 MOVK X0, #0x0000, LSL #32 -- etc ( dummy address : 0x0008 ) 
~A8 MOVK X0, #0x0000, LSL #48 -- ( dummy address : 0x000C ) 
~A8 FMOV D0, X0
~A8 RET
-- 6 (4 bytes address edits ) in total ( look dummy address )
-- the CPU will assume the function body is like after the edit : 
-- double currentHP () {
-- return 100
-- }

after you edit these 6 addresses the left over address until POP instructions became code cave you can inject a code in it without using gg.allocatePage() ( some game detect new alloc memory and it force close the game ) 
note : register X will hold 64 bit values , W will hold 32 bit values

Edited by XEKEX
Link to comment
Share on other sites

  • 0
Quote

Wouldn't that crash the game as you're overwriting instructions you shouldn't?
My understanding is that I have to use:
-branch instruction (jump instruction) to control the execution flow and jump to an unused section of memory.
-code cave (find unused section of memory) to inject additional instructions

Your terminology is correct, just use what you think is good or just try it.

  • - In general this shouldn't be a problem. Usually the game just doing a simple check by comparing current_HP with max_Health or some values limitation that will cause crash, restarting match or bans. Overwriting 1 Instruction with multiple is allowed but this could lead into a problem if your game also check for Function Sizes or Memory Pages.
  • - Now, you can also allocate Memory for your modified function and make the game access that. This gives you more advantage to revert the values to original or to avoid #1 detection. This also comes with some caveat: the game can also detect this if the accessed codes not in the same Memory Range the games allocated.
  • - If you're planning to overwrite the Instruction directly on the Lib Files, then it would surely make the game crash since the hash size doesn't match. Nowadays, games uses MD5 hashing to prevent this.

To be honest, it doesn't really matter. If the game has some kind of protection, then we should just 'Disable' it instead of tirelessly hiding cheats.

Quote

My health will decrease to 0, but I will have godmode (will not be able to die).

I assume you're only want Increased Health and not Godmode?

Link to comment
Share on other sites

  • 0
4 hours ago, XEKEX said:

should give you "PUSH" instruction (-- this will indicate the start of the function ) 
-- "POP" instruction indicate the end of the function.

Going to public double get_hp() offset address contains the following instructions: "LDR" then "RET", not "PUSH".

Do I have to look for another offset address? Also, I don't think PUSH is common for ARM64, but I may be wrong. I do see a lot of STP (store pair) and LDP (load pair) when scrolling through memory, but not PUSH instruction. I'm not sure how to navigate to the 'start of the function' like you've mentioned, it would be nice if you can tell me what I should look for when inspecting the dump that contains many offset addresses, which is something like:

// [Address(RVA = "0x1CC4FD4", Offset = "0x1CC4FD4", VA = "0x1CC4FD4")]
// [Attribute(Name = "IteratorStateMachineAttribute", RVA = "0xF3BC10", Offset = "0xF3BC10")]
// [Attribute(Name = "ObserverDelegateAttribute", RVA = "0xF3BC10", Offset = "0xF3BC10")]
public IEnumerator HPChanged(double newHP, long order)

// [Address(RVA = "0x1D4A118", Offset = "0x1D4A118", VA = "0x1D4A118")]
public void ApplyAllHPChange(double, [Optional] List<>, [Optional] EnemySkill, [Optional] BattleEnemy)

// [Token(Token = "0x5040822")]
// [Address(RVA = "0x1F57230", Offset = "0x1F57230", VA = "0x1F57230")]
public void SetHP(double newMaxHP, double newCurrentHP, bool immediate)
  
// [Token(Token = "0x504061F")]
// [Address(RVA = "0x1EBCBFC", Offset = "0x1EBCBFC", VA = "0x1EBCBFC")]
public void SetAllHP(double newHP)

 

5 hours ago, MC189 said:

If the game has some kind of protection, then we should just 'Disable' it instead of tirelessly hiding cheats.

I agree, when looking for offset addresses I found "CheatViolation" public enum which contains many enum constant values like Damage, EnemyHealthChange etc... I'll come to this later, most important part is that I understand how to patch memory at runtime.

5 hours ago, MC189 said:

I assume you're only want Increased Health and not Godmode?

I want to be able to have control over health, just to learn about memory patching. The process is more important than the outcome.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.