Anti-Cheat

Why we moved Unity anti-cheat checks into native C++

A check is only as strong as the place it runs

Most Unity anti-cheat code starts life the same way: a C# method that looks at something and decides whether the game should keep running. It is the natural place to start. It is also the first place an attacker looks.

This article explains why we moved the important parts of that logic out of managed C# and into a native C++ layer when we built OZeroSecurity, what that changes in practice, and — just as important — what it does not change.

What an attacker does with a managed check

Whatever the check does, attacking it usually takes three steps: read it, patch it, reuse the patch.

Read. A Mono build ships IL that a .NET decompiler turns back into readable C#, method names included. An IL2CPP build compiles that code to native machine code, but it still ships global-metadata.dat, which is enough to restore class and method names and their locations in the binary. We covered that path in Is IL2CPP Really Secure?. Either way, a method called IsTampered or CheckSpeed is easy to find.

Patch. A check almost always ends in a branch:

if (Integrity.IsTampered())
{
    Application.Quit();
}

Make IsTampered return false and the check is gone. In a Mono build that is a small IL edit. In an IL2CPP build it is a few bytes at an offset the metadata already pointed to.

Reuse. The patch is written once and works on every copy of that build. That is how a modded APK spreads: one person does the work, everyone else downloads the result.

None of this needs rare skills. The tools are public, and tutorials exist for most of them.

The problem with values hidden in the managed heap

The second weak spot is protected values. A common technique is to keep a gameplay value XOR-masked in a C# field and unmask it on read.

The catch is where everything lives. The key, the masked value and the real value all sit in the managed heap, often side by side. A memory dump taken at the right moment shows how they relate, and a memory scanner that watches the value change across a few reads can usually find the pair. Each unmask also tends to allocate, which adds garbage collection pressure in hot gameplay code.

What moving into native C++ changes

We did not try to make C# harder to read. We moved the parts that matter out of it.

Secure values live in the native layer. Types such as OZeroSV_Int and OZeroSV_Float keep the value in native memory. The C# side holds a token, not the number, so a plain scan for the value does not resolve it. Reads and writes are designed to add no managed heap allocation.

Key checks run in native code. Build integrity and signature validation, speed and time checks, memory tamper checks and injection or hooking signals are evaluated in the native layer. The managed code configures them and receives the results; the detection logic itself is not sitting in C#, where a decompiler would show it.

Native code is a harder target. Native code can be disassembled as well, but it carries no metadata file that names every function. Finding, understanding and patching a check there takes much more time than editing a managed method, and that time is the cost we want to raise.

It works locally. None of this needs a server, an account or a license key. The checks run inside the game on the player's device.

What it does not do

A native layer raises the cost of an attack. It does not make a game unbreakable, and we would rather say that plainly.

  • Native code can still be analyzed. A determined attacker with time can reverse native code too. The goal is to make each attempt slower and more expensive, not to promise it cannot happen.
  • Mono builds expose more. The native layer works with Mono, but the rest of your game is still easy to read. Use IL2CPP for release builds where Unity supports it.
  • WebGL has no native layer. In the browser only limited protection applies.
  • The client is never the authority. For online games, currency, purchases and rankings must be checked on a server you control. Client-side protection makes cheating harder; server-side validation decides what counts. (OZero Pro adds server-side verification and telemetry for teams that need that part.)

A practical checklist

  1. Build releases with IL2CPP and managed code stripping.
  2. Keep integrity data signed: generate the developer secret and turn on manifest signing (Auto Setup can apply both).
  3. Store currency, scores and other values worth editing in secure value types, not plain fields.
  4. Keep anything tied to money or rankings authoritative on a server.
  5. Before release, try the tamper paths yourself — repackage a build, edit a save file — and confirm the response you configured is the one you get.

Moving checks into native code is not a single trick. It changes where the attacker has to work, and it makes each step there slower.

OZeroSecurity keeps key checks and secure values in a native C++ layer and works offline, with no server, account or license key.

Related reading