4.0 CheatDroid is rebuilt from scratch, and a 2.x license carries over. What changed
CheatDroid

Protecting Game Values in Memory from Scanners

Kick the Buddy checks its save file, and CheatDroid can’t get past that check. In memory, though, the gold was a plain 32-bit int. One scan for 100, a few punches at the buddy, a second scan for 106, three addresses left. I set all three to 999999, and the game saved the new balance to its checked file by itself.

How a scanner finds a value

It reads every writable region of the game’s process (heap, stack, anonymous mappings, the data segments of its libraries) and lists every address that holds the number on screen. Then it waits for the number to change and drops the addresses that didn’t follow. Two or three rounds usually leave a handful.

CheatDroid does this as root with process_vm_readv and process_vm_writev, and falls back to /proc/<pid>/mem. It never attaches as a debugger, so checking TracerPid in /proc/self/status finds nothing. Freezing a value means writing it back twice a second.

A value without a number on screen goes the same way. The scanner snapshots everything and narrows by direction: more, less, changed, the same. A health bar falls to that in a few rounds.

Never keep the real number

Store the value XORed with a random key, and pick a new key every time the value changes:

public struct ObscuredInt
{
    int key, hidden;

    public int Value
    {
        get => hidden ^ key;
        set
        {
            key = UnityEngine.Random.Range(int.MinValue, int.MaxValue);
            hidden = value ^ key;
        }
    }
}

A scan for 100 finds nothing, because 100 isn’t stored anywhere. The direction scan finds nothing either: with a fresh key on every write, the stored number jumps around at random when the coins go up by one. That also beats the XOR search some scanners have, which assumes the key stays put. And a write to one of these addresses decodes to nonsense.

Keep it in a field. A struct behind a property hands out copies, and coins.Value += 5 on a copy doesn’t even compile. And keep it to what matters: currency, health, ammo, score. The cost is a couple of XORs per access, which nobody will measure, but every obscured field is one more thing to get wrong in serialization.

Notice when someone writes

Keep a second, plain copy that the game never reads. A scanner finds that one, because it’s the only 100 there is. If it ever differs from the real value, someone wrote to your memory, and you know without a single root check.

ObscuredInt coins;
int decoy; // never read for gameplay

void SetCoins(int value) { coins.Value = value; decoy = value; }
bool Tampered => decoy != coins.Value;

What to do with Tampered is a product decision. I’d reset the value to the last saved one and report it to your server, and not show the player anything that tells them which address it was.

In Unity you don’t have to write any of this: Code Stage’s Anti-Cheat Toolkit on the Asset Store has obscured types with this kind of detection built in.

Floats

Health bars, fuel and timers are usually floats. Obscure the bits: BitConverter.SingleToInt32Bits on the way in, XOR, and back on the way out. XORing the float itself doesn’t compile, and multiplying by a constant doesn’t hide anything from a scan for “decreased”.

What it doesn’t stop

Someone who reads your code finds the XOR and the decoy. Someone who hooks your functions changes what AddCoins does. CheatDroid does neither, and neither does a memory scanner. If that’s the threat you face, the balance belongs on a server.

Traffic Racer is the other game in my list whose file held and whose memory didn’t. Its cash came out of one scan for 500 and one for 0 after buying an upgrade: the tutorial has the screenshots.