-
Notifications
You must be signed in to change notification settings - Fork 193
Modifying Game Code
Scanning and changing values is usually the first thing you learn, but it has limits. Sometimes the value moves too much, pointer paths are ugly or freezing the value just isn't the right fix.
That's when you start looking at the code that reads or writes the value.
If you're not comfortable with normal scans, changing values and using the address table yet, don't start here.
A game eventually becomes machine code that the CPU runs. The disassembly view shows those machine code bytes as assembly instructions.
Instead of changing the ammo value directly, you can change the instruction that subtracts ammo. Instead of freezing health, you can change the instruction that writes damage. This is usually more stable than chasing one temporary address forever.
The usual workflow is:
- Find the value normally
- Use
Find out what writes to this addressorFind out what accesses this address - Trigger the game action, like shooting or taking damage
- Jump to the instruction that touched the value
- Patch the instruction in the smallest way that does the job
- Test it
- Restore it if it breaks something
You don't need to master assembly before touching this stuff, but you do need to read a few common instructions. mov, add, sub, cmp, test, jmp and conditional jumps like je / jne show up constantly.
Useful references:
Open Memory Viewer and look at the disassembly view.
Useful shortcuts:
-
F2: pause/break the process and refresh the current instruction location -
F5: toggle a breakpoint on the selected instruction -
F7: step one instruction -
F8: step over a call
Right click an instruction and the two simplest code editing options are:
Replace instruction with NOPsEdit instruction
For finding the instruction in the first place, start from a value in the address table and use the right click options that find what writes or accesses that address. That usually gets you to the correct code much faster than browsing random disassembly.
The disassembly view also draws nearby control-flow references as arrows. Jumps use solid lines and calls use dashed lines. Hover over an arrow to see its source and target, or click it to move between them.
Nearby direct references appear automatically, running Tools -> Dissect Code adds incoming references found during code dissection. This is useful for seeing which branches enter or leave the code you are about to patch.
NOP means no operation. On x86, the single-byte NOP instruction is 0x90 and it does nothing.
When you use Replace instruction with NOPs, PINCE reads the selected instruction length and replaces that many bytes with 0x90.
So if the original instruction is 3 bytes long, PINCE writes:
90 90 90
This is useful when the instruction itself is the thing you want to remove. For example:
- The instruction subtracts ammo
- The instruction writes fall damage
- The instruction decreases durability
- The instruction branches into a failure path
It's not always the right fix. If the instruction also does something important for other objects, NOPing it can break more than expected.
AssaultCube is a simple native Linux game people often use for testing this kind of thing.
One possible workflow:
- Find your current clip ammo with a normal scan
- Add it to the address table
- Right click it and choose
Find out what writes to this address - Shoot once
- Look for the instruction that fired when ammo decreased
- Double click the instruction address to jump to it in the disassembly view
- Close or stop the watchpoint window if you don't need it anymore
- Right click the instruction and choose
Replace instruction with NOPs
On one version, the ammo decrease instruction looked like this:
add dword ptr [rax], -1That means: take the address in rax and add -1 to the 4 bytes integer stored there.
If you replace that instruction with NOPs, shooting no longer subtracts ammo from that address.
PINCE stores the original bytes the first time you patch or NOP an address. To undo it, open View -> Restore Instructions in Memory Viewer.
That window shows:
- The modified address
- The original bytes
- The disassembled form of those original bytes
Right click an entry and choose Restore this instruction. PINCE writes the saved original bytes back.
Small caveat: PINCE saves the original bytes only the first time that address is modified. If you patch the same address multiple times, restore goes back to the original bytes from before the first patch, not the previous patch.
Use Edit instruction when you want to replace an instruction instead of deleting it.
The dialog has two linked fields:
- Editing the assembly assembles it with Keystone and updates the bytes
- Editing the bytes disassembles them with Capstone and updates the assembly
You can enter multiple instructions separated by ;.
For example, changing:
add dword ptr [rax], -1to:
add dword ptr [rax], -2would make ammo decrease by 2 instead of 1.
Changing it to:
add dword ptr [rax], 2would make ammo go up by 2 when the instruction runs.
Assembly instructions are not all the same size. One instruction might be 2 bytes, another might be 7 bytes.
If your replacement is longer than the original, it can overwrite the next instruction and break the program.
PINCE handles shorter replacements for you. If the original instruction was 3 bytes and your replacement is 2 bytes, PINCE pads the leftover byte with 0x90.
Example:
test eax, eaxis 2 bytes:
85 c0
If it replaces a 3-byte instruction, PINCE writes:
85 c0 90
Longer replacements are different. PINCE shows this warning:
New instruction is X bytes long but old instruction is only Y bytes long
This will cause an overflow, proceed?
If you press OK, the extra bytes overwrite whatever instructions come next. Sometimes advanced users do this on purpose, but if you're not sure, cancel.
When the replacement code is too large to fit, the normal trick is a code cave.
The idea is:
- Copy enough original bytes from the target location to cover a jump
- Allocate or find executable memory somewhere else
- Write your custom code there
- Optionally run the original bytes you overwrote
- Jump back after the overwritten area
- Replace the original location with a jump to your cave
This lets you run more code than would fit in the original instruction slot.
The annoying part is that copied instructions are not always safe to move. rip-relative instructions, call, jmp and conditional jumps can break when relocated unless you adjust them correctly.
For this reason, use Libpince Engine once you get past quick GUI edits. It has helpers like patch(), nop(), restore(), alloc() and assemble(), plus templates for:
AOB scan + NOPAOB scan + patchCode injection
The GUI is good for quick testing. Libpince Engine is better when you want a repeatable cheat entry with [ENABLE] and [DISABLE] sections.