Sharp Edges GAME SECURITY NOTES

What Still Works After an Attacker Attaches to Your Process


A developer adds anti-debugging and integrity checks to a game, ships it, and later has no clear way to tell how much those checks are actually buying. Another developer watches a build get hooked and dumped in an afternoon despite having similar measures in place. Both end up asking the same question: do these checks work?

That question is too broad to be useful. A client-side check is not a switch that is either effective or useless. Once attacker-controlled code can operate in the same process, the more useful question is what the check can still observe from that position, and how much confidence a developer should place in the result.

An in-process check can still be useful after the process is instrumented. Its value depends on the operations it can observe, the effort required to get around it, and what decisions are made from the signal it produces.

PairIP makes a useful worked example because there is enough independent research and tool documentation to describe some of those boundaries without relying on a protection vendor's effectiveness claims.

What "Attaching to a Process" Actually Means Here

"Attach" covers several very different situations, so it helps to be precise. A traditional debugger may control a process from outside it. A runtime instrumentation framework such as Frida can also load attacker-controlled instrumentation into the target and intercept or replace functions while the application is running.

For a game developer, the important part is the trust boundary rather than the exact tool. Once attacker-controlled logic can inspect or change live process state, checks that run in that same process no longer sit in a separate trusted environment.

The frida-il2cpp-bridge project gives a concrete IL2CPP example. Its README describes a Frida module that can dump, trace, intercept, and replace IL2CPP methods at runtime without requiring the on-disk global-metadata.dat file. A previous Sharp Edges article looked at the static side of that boundary. Encrypting metadata can make file-based analysis harder, but a runtime-capable tool works from the application's live IL2CPP state instead.

That distinction matters. Static protection changes what an attacker can learn from files before the game runs. Runtime instrumentation works after those files have already been turned into executable code and live data structures. With enough access, an attacker may be able to inspect memory, intercept calls, alter return values, or redirect execution without rebuilding the APK.

None of that tells us how common a given technique is against a particular game. It tells us why an integrity check running inside the process has to be evaluated under a less comfortable assumption: the thing doing the checking may itself be reachable.

Why In-Process Checks Can Be Interfered With

Consider a simple integrity check that returns a Boolean result. Somewhere in the game, code calls that check and then decides what to do with the answer. If an attacker can instrument that call path, the check may be patched, skipped, or made to return a different value.

The same applies to many local security controls. Root checks, anti-debugging logic, file-integrity checks, and anti-hooking code all execute somewhere. Their implementation can be hidden, distributed, virtualized, or made expensive to understand, but they still have to execute on the device.

This is often where the discussion jumps straight to "client-side checks are bypassable" and stops. That loses an important engineering distinction. Reachable does not mean blind. A check may be defeatable by a capable attacker and still catch a large amount of lower-effort modification, generic tooling, or code changes that do not account for the protection.

For a developer, that is a more useful way to frame the control: what does it notice before an attacker has done the extra work needed to make it stop noticing?

What a VM-Based Protection Changes

Independent researchers have documented a Google Play protection commonly referred to as PairIP, associated with the native library libpairipcore.so. Public Google documentation does not appear to describe PairIP's technical architecture, so the mechanism here is based on independent analysis rather than an official platform specification.

Byteria's March 3, 2025 analysis describes portions of Java code being separated from the application's normal flow, transformed into bytecode for a custom VM, and executed through native code in libpairipcore.so. The analysis also describes runtime-created structures and anti-tampering techniques that make straightforward static analysis harder.

That changes the cost of understanding the check. Instead of reading ordinary application bytecode or following a conventional native function, an analyst may first have to recover how the VM represents and executes protected logic. The check has not moved to a trusted machine somewhere else; it has become more expensive to understand inside the same application.

That distinction is easy to miss because both outcomes feel like "stronger protection." One raises reverse-engineering cost. The other would change who controls the environment in which the verdict is produced. PairIP, as documented by independent research, primarily illustrates the first.

The Observable Surface Is Narrower Than "Frida Detected"

friTap's documentation provides a useful view from the tooling side. friTap is a TLS key-extraction and traffic-analysis tool built on Frida. Its --pairip-safe documentation says the normal Android hooking footprint can trip PairIP's integrity checks and cause a SIGSEGV. The safer mode therefore disables broad operations such as loader hooks, Java/ART hooks, several scans, and other inline-hooking behaviour that would modify protected code paths.

That is more informative than saying simply that "PairIP detects Frida." The documented problem is the footprint created by particular kinds of instrumentation. The existence of a Frida session and the modification of protected executable code are not the same event.

For developers, this is a useful distinction because it shows what a code-integrity check is actually reasoning about. If the protected code changes in a way the check covers, that change may be observable. If an operation does not alter the thing being measured, the same check has less to say about it.

The boundary is visible in a frida-il2cpp-bridge discussion. A developer reported an IL2CPP game crashing after Frida was used to instrument the process. One respondent linked the behaviour to libpairipcore.so and then made a much broader claim: that Frida was effectively unusable with recent Unity games because they were being forced onto this VM protection.

The evidence in the thread does not support a conclusion that broad. It describes one application and a particular failure mode. friTap's later documentation is more specific about the operations that can trigger the integrity response. Compressing all of that into "Frida no longer works" throws away the useful part of the observation.

The same discussion also contains a report from a user whose application ran normally on a physical phone but crashed in the Android Studio emulator even when installed from Google Play and with nothing attached or hooked. It is a single community report, not a rate or a general property of PairIP. Still, it is a useful reminder that unusual environments and malicious modification are not always identical from a detector's point of view.

Detection and Response Share the Same Boundary

Detecting a change is only half of the design. The protection also has to decide what to do after a check fires.

For a purely local control, that response code is still executing on the client. A game may close, disable a feature, record a local result, or take some other action, but the response path is not automatically more trusted than the check that produced it.

friTap's documentation is useful here as well. It describes PairIP-protected applications terminating with SIGSEGV when certain inline hooks are present, and its --pairip-safe mode is explicitly designed to avoid the hooking footprint that causes those crashes. The implementation details visible from public tooling do not establish that the response is impossible to interfere with; they show that detection and response have been kept close to the protected runtime.

That is a sensible design pressure. Every extra function call, hookable interface, or handoff inside the same process is another place an attacker may try to intervene. Reducing those opportunities can increase the work required to suppress a response, even though it does not create a new trust boundary.

This is where "harder to bypass" and "trusted verdict" need to stay separate. They are both valuable properties, but they describe different things.

Which Judgements Have to Happen Elsewhere

An in-process check can produce a useful security signal. The harder question is how much consequence should depend on that signal alone.

Suppose the game detects code modification. Logging the event is one decision. Asking the player to restart is another. Rejecting a leaderboard score, refusing a purchase, or changing the state of an account carries a different cost if the signal is wrong or incomplete.

An earlier Sharp Edges article separated client protection, platform attestation, telemetry, and server-side validation because they answer different questions. That separation matters here. A local integrity check can report what it observed inside the client. A backend can combine that signal with information that the client cannot rewrite on its own.

Google's Play Integrity documentation makes the external-verdict model explicit. The API returns integrity information about the app and device, and the application's server consumes the verified verdict and decides how to handle the associated request. The backend, not the protected game process, owns the final policy decision.

PairIP and Play Integrity are therefore not stronger and weaker versions of the same mechanism. PairIP, as described by independent research, is runtime protection operating inside the application. Play Integrity is a platform-backed API that produces integrity verdicts for a backend to evaluate. One can raise the cost of tampering with local code; the other can provide a backend with evidence whose verification does not depend solely on that local process.

Neither makes the other unnecessary.

Where This Leaves a Developer Decision

For an in-process security check, "can it be bypassed?" is rarely the most useful first question. With enough time and capability, local controls may be analyzed and interfered with. That does not make them worthless, and it does not make every signal they produce trustworthy enough for every decision.

A better review starts with three concrete questions:

1. What operation does the check actually observe? File changes, loaded-code modification, a debugger state, a hook footprint, an environment signal, or something else?

2. How much work does an attacker have to do before that observation stops being reliable? A generic tool failing immediately is a different result from a protection surviving targeted analysis indefinitely.

3. What are you going to do with the signal? Local friction and telemetry have a different risk profile from rejecting economic state or taking irreversible account action.

PairIP is useful as an example because the public evidence exposes those layers. VM-based execution raises the cost of understanding protected logic. Integrity checks can react to specific instrumentation footprints. Tooling can sometimes reduce or avoid those footprints without proving that the protection as a whole has been defeated. Community reports then show how quickly a narrow observation can become an overgeneralized verdict.

The useful conclusion is not that in-process checks work or do not work. It is that their value has a shape. They observe some operations better than others, they impose different costs on different attackers, and the confidence of the resulting signal determines what should safely depend on it.

That is a much more actionable answer for a game developer than either "client protection blocks tampering" or "anything in the client can be bypassed." Both statements hide the engineering decision that actually has to be made.

References

← Back to list