Understanding runtime-loaded DEX
DexClassLoader and InMemoryDexClassLoader defer the real code to startup, so a static decompiler shows only a stub.
PairIP · Google Play Automatic Protection
PairIP is the name reverse engineers use for the distribution-time protection Google Play applies to a submitted build, shown in the Play Console as Automatic Protection or "Protected with Play". It packs a native core, libpairipcore.so, plus a small Java stub, and at startup they check the environment before the real application runs. The guard raises the cost of debugging, hooking and repacking. It does not change how the app talks to its own backend, which is where the reconstruction work actually is.
You upload a build to Google Play. Play can replace what it distributes with a wrapped version of that build. This is Automatic Protection, also called "Protected with Play". The wrapping component, and the runtime guard it adds, are what reverse engineers refer to as PairIP. The guard ships inside the APK you download from Play; it is part of the app, not a policy the app asks about.
Because it is applied at distribution, you cannot opt a single file in or out and the guard is present on the delivered artifact. Because it is an environment guard, it changes what instrumenting the app looks like. It does not add endpoints, it does not replace the request logic, and it does not sit between the app and the server it calls.
Two files on disk and one manifest value are enough to confirm PairIP. The manifest points the application class at com.pairip.application.Application. The native library libpairipcore.so is present for each ABI the app ships. A verification package sits under com.pairip.licensecheck, which is the part that shows the "get this app from Play" message when the install source fails.
# 1. the manifest names the PairIP application stub $ grep -o 'android:name="[^"]*Application"' AndroidManifest.xml com.pairip.application.Application # 2. the native runtime guard ships once per ABI $ ls lib/arm64-v8a/ libpairipcore.so # 3. the distribution-time verification package $ apktool d app.apk -o out && find out/com/pairip -type f out/com/pairip/application/Application.smali out/com/pairip/licensecheck/...
Fig. 01 · PairIP announces itself through the application stub and the native core, both of which survive normal obfuscation.
The guard runs before the real application starts and asks the native core a set of environment questions. The exact set is decided per build, so verify what this APK actually implements rather than assuming a fixed list. Read the stub and the library, then confirm each signal against the code that uses it.
| Signal to look for | What it may indicate | How to confirm it |
|---|---|---|
| Signature comparison | The build checks the signing certificate. | Read the callers and the strings that name the certificate. |
| Dex or package hashing | Tampering with classes or resources is detected. | Find where the digest is computed and compared. |
| Debugger and tracer strings | An anti-debugging path exists. | Search for debugger and tracer markers, then trace the branch. |
| Hook and instrumentation markers | The guard knows common hook frameworks. | Search the strings and the GOT or PLT for rewritten entries. |
| Root and Magisk markers | A root or Magisk check may exist. | Search for su, Magisk and mount strings, then trace their use. |
| Emulator markers | An emulator check may exist. | Search build properties and hardware strings, then trace the branch. |
| Install-source handling | The source of the "get this app from Play" prompt. | Read the license-check path and what triggers the prompt. |
These are leads, not conclusions. A string match does not prove a check runs; read the code that uses it.
com.pairip.application.Application loads libpairipcore.so and calls it in onCreate. The native function returns a result, and the Java stub decides the next move: start the real application, start a degraded version, or stop and surface a message. This is a small, decidable gate.
static { System.loadLibrary("pairipcore"); } // Illustrative structure, not a capture from one build. The real method // names and arities differ per version, so read them from the decompiled // stub. This is only the shape to look for in the Application stub. private native int environmentCheck(); // integrity or signature private native boolean isDebuggerAttached(); // debugger or tracer check private native boolean hasRootMarkers(); // root, su or mount markers public void onCreate() { // run the checks, then decide how to start the app }
Fig. 02 · the Java layer is a thin decision point. The checks themselves live in native code inside libpairipcore.so.
The two are frequently conflated and they are not the same thing. PairIP (Automatic Protection) is applied to the build and shipped inside the APK; it runs locally and guards the environment the app starts in. The Play Integrity API is a runtime attestation the app calls to ask Google about the device and the app's integrity; it is a separate, network-queryable signal. A PairIP-guarded app may also use Play Integrity, and it may not. They do not share a check surface.
| PairIP / Automatic Protection | Play Integrity API |
|---|---|
| Applied at distribution, shipped in the APK | Called at runtime by the app |
| Local check at startup, in the native core | Remote attestation via a Google-provided verdict |
| Guards the environment the app starts in | Reports device and app integrity to the app's backend |
| Failure blocks or degrades startup | Verdict is read by the app and can gate a server call |
Static first. Confirm the markers, dump the exports and strings of libpairipcore.so, and list the stub's native methods to learn the check surface. That tells you what the app will test and where the gate is. The strings in the library and the method table of the stub are the fastest read.
# the stub's native methods tell you the call surface. # jadx reads the DEX directly to Java; apktool produces Smali, not .class files. $ jadx -d out app.apk $ sed -n '/class Application/,/^}/p' out/sources/com/pairip/application/Application.java # exported symbols in the native core $ nm -D lib/arm64-v8a/libpairipcore.so | head # strings the library references $ strings lib/arm64-v8a/libpairipcore.so | grep -iE 'tracerpid|magisk|frida|ptrace|su' | sort -u
Fig. 03 · the native exports and the stub's method table map out which checks the gate runs.
Then get the app running under the conditions it expects, so the gate passes, and observe the layer that matters. The reliable path is a non-rooted, non-debuggable, Play-installed environment. If you instrument, do it after the gate, and expect the guard to know the common instrumentation markers.
The endpoint list, the authentication flow, the request signing, the payload serialization: those belong to the app and survive once it is running. That is the output you are after.
The failures that read as "impenetrable" are almost always the startup gate checking the place you are standing, not the protocol you want.
| Symptom | What it usually is | Route |
|---|---|---|
| App shows "get this app from Play" | Install-source check failed | Install from the expected distribution, or satisfy the source check |
| App exits right after a toast | Signature or package-integrity check failed | Verify the build is untouched; satisfy the signature it expects |
| App runs but the API returns 403 | A request-generation issue, not the gate | Reproduce the signing, session and nonce the app builds |
| App diverges under a debugger | Anti-debugging path | Observe after the gate, in an unmodified environment |
Once the guard is satisfied, the protocol recovery proceeds like any other Android app. The extra work is in the analysis environment: getting the app to run where the gate passes, and observing the request layer afterward. The request logic itself is not a PairIP problem.
A PairIP guard changes the cost of getting inside to observe. It does not change what needs to be reconstructed once you are.
DexClassLoader and InMemoryDexClassLoader defer the real code to startup, so a static decompiler shows only a stub.
The login is the entry point, but the token is the state the app leaves behind and later proves.
The copy is evidence of what was sent; the missing problem is how the application generated it.
Relevant capability
For PairIP-guarded apps we recover the endpoints, the signing and the request flow, working through the runtime checks rather than around them. PairIP reverse engineering
Start a project
We rebuild the complete application flow and return the full API in Python, JavaScript/TypeScript, Postman and complete API documentation.