I'll start with a few minor corrections and clarifications, just to make sure we're using the right terms and exact definitions.
I used Cheat engine to find the address of the variable i
in A::doThing
First of all, i
is not a variable of method A::doThing
. It is a member of the A
class.
Then I used the option to find what writes to that address
Doing this gives you the address of method A::doThing
is located. Specifically, the address at which A::i
is modified within that method.
from there I found what appeared to be an offset, did a pointer scan
I would assume you found the offset of A
at which i
is located, but I'm not 100% I understand what you mean by "did a pointer scan". I assume you subtracted the offset of i
from the address found at the first stage ("I used Cheat engine to find the address of the variable i
") to get the address of the object itself, then searched for pointers to that address.
Would "example.exe"+0001A2EC be considered the pointer to the instance of A class?
To be more accurate, example.exe+0x001A2EC
would be an address of a variable of type A *
(or pointer to an A
instance), and it's value will be the value returned by new A();
, i.e. an instance of class A
, as you expect. Reading it's value will point you to where that instance of A
is currently in memory.
However several assumptions you have may change between releases or even for different compiled versions of the same code base.
The most obvious one in your example code, is the fact you're using the exetuable's image base to access the main thread's stack. This is likely to break depending on how/where the stack is allocated for your process, and is likely to be unreliable even if executed several times in succession, depending on OS and configuration.
Even if you somehow address that, there are multiple other potential complications and slight changes that could make your life harder. To name a few:
- Even if you were using the process's stack offset instead of the image base, the offset in memory where the
A
pointer variable, a
, is located is likely to change. even just between different executions of an identical binary. For example, in your case a
is stored on the stack, and if the function defining a
could be called in several different flows, the stack depth at which the stack frame of the function creating a
may differ. if a function a_creator
may be called in either one of two flows main/a_creator
and main/game_load/a_creator
depending on circumstances (e.g. if you're loading a save), the offset at which a_craetor
's stack frame will start will be different.
- The offset at which
i
is located in the A
class's internal structure, may be changed because new members were added in the code. Although normally maintain the order members are defined at, optimizations may shift them around for better alignment. Different padding configuration settings may also change the location a member is actually at.
- Different optimization configurations may even completely eliminate the variable
a
, only storing it in a non-volatile register. This is very likely in the example implementation you provided, for example.
- A new version of the program can easily create move variables and make code changes so locations will be completely off.
- Inheritance and casting of
a
could lead you to different VTables than the ones you were looking for, as well as other complications.
I hope this did not discourage you from pursuing your goal, but encouraged you to gain a better understanding of the internals and intricate world of dynamic patching.
As a side note I'll include a warning about anti-cheat detection tools in a lot of games could make these endeavors quite expensive (account bans, etc).