Apps are easy to repackage.
Someone pulls your app (adb pull /path/YOURAPP.apk) and changes its behavior: the camera is injected, the device looks clean, ads are stripped or paywalls are removed. That is repackaging.
Why do we care? ~ Your server-side decisions are based on device-based signals. If every isRooted, isHooked, isFrench (๐จ) check returns “false” you cannot tell the difference between good traffic and someone harming your app.
This is particularly dangerous for Fintech, Crypto, Web3 and gambling apps that have sensitive code to protect.
Why is it overlooked? ~ Because security teams are academically trained to tackle the most powerful attacks, not the most scalable and profitable. Unlike complex Frida runtime manipulation, a repackaged app can be passed down the line to non-technical operators and to other devices.
And while your red team plays capture-the-flag on your paycheck trying to patch exotic exploits, a carder in his basement with a $20 ChatGPT subscription is three prompts away from fooling your KYC verifications.
Google certifies code it never once looked at
On Android, Google Play Integrity is supposed to catch this through PLAY_RECOGNIZED. We traced the whole check on a live device and the process is straightforward:
- The app calls the Play Integrity API.
com.google.android.finsky.integrityservice.IntegrityServicetakes the request and calls DroidGuard.com.google.android.gms.droidguard.DroidGuardServicecollects the package name, version code, install source, device id, SIM value and the request nonce into a hardware-backed attestation.- Google’s servers decide the verdict and sign it server-side.
- For
PLAY_RECOGNIZEDGoogle compares just two things: your app’s package name and its signing certificate. - If the certificate does not match, you get
UNRECOGNIZED_VERSION.
If both match, you getPLAY_RECOGNIZED.
(“the app and certificate match the versions distributed by Google Play.” https://developer.android.com/google/play/integrity/verdicts)
Google only checks the certificate on record, fixed the moment the app was installed, regardless of runtime state.
We proved it on a rooted Android 16 phone (Google Play Services 26.34.67) by repackaging gr.nikolasspyr.integritycheck: changed the code, re-signed it with our own key. Same package name, same version, different signing key:
genuine build signer SHA-256 17952b5c... CN=Android, O=Google Inc.
repackaged build signer SHA-256 2839c45a... CN=resignAndroid will not let you update the installed genuine app with the repackaged one. It checks the signer and refuses:
INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package signatures do not match newer version; ignoring!โ This is the whole defense Play Integrity offers. โ
With root we left the genuine install record untouched (so the system still had the genuine certificate on file) and overwrote the actual code file on disk with our repackaged build:
# with root: swap the code, leave the install record alone
su -c "cat repackaged.apk > /data/app/~~.../gr.nikolasspyr.integritycheck-.../base.apk"Then we asked Play Integrity for a verdict. For code that was ours, not Google’s, it returned:
{
"appIntegrity": {
"appRecognitionVerdict": "PLAY_RECOGNIZED",
"packageName": "gr.nikolasspyr.integritycheck",
"certificateSha256Digest": ["17952b5c..."],
"versionCode": "22"
},
"accountDetails": { "appLicensingVerdict": "LICENSED" },
"environmentDetails": { "playProtectVerdict": "NO_ISSUES" }
}The attacker’s code was running, and every check still reported genuine.
We also made sure the changed code actually ran, and was not a leftover compiled copy. We deleted the phone’s pre-built copy of the app, so Android had to load our modified code from the swapped file, and a small marker we added to that code wrote to the log from the very same app process that then received the PLAY_RECOGNIZED verdict.
The code on that phone was the attacker’s, signed 2839c45a.... Google reported the genuine certificate, 17952b5c...: PLAY_RECOGNIZED, LICENSED, Play Protect NO_ISSUES. The verdict reads the certificate cached at install; it never re-hashes the running code. A repackaged app carries a spotless, genuine verdict.
For a high-value app, three holes make it worse
If the app lives in fintech, crypto, web3, social, or gambling, the verdict leaves three openings:
- The Play Integrity check is proven falsifiable. A genuinely-signed app, hooked at runtime, runs changed behavior while the certificate on record stays the same. The verdict still says
PLAY_RECOGNIZED. - Off Google’s reach, you get no usable verdict. On a de-Googled device (one running without Google services, such as many China-market phones or a custom version of Android called a custom ROM), the Play Integrity check cannot run at all, so there is no verdict to act on. That forces a lose-lose choice on the defender: block everyone and lose real traffic, or allow everyone and let the attacker in.
- You get rate-limits and latency. Quotas and latency force the defender to check only on sensitive actions, leaving gaps the attacker can abuse.
On-device anti-repackaging alone has never held: a 2022 study bypassed every repackage-proofing scheme it tested with a single attack toolkit, and the attacker’s tools have only sharpened since. That is why the check that counts has to live off the device.
If Play Integrity is unreliable, how do I defend my app?
You can protect your app, and we do that by making it so hard that it becomes economically unviable for them, so the offenders will damage your competitors instead.
But how? Repackaging alone without runtime hook detection is like locking your door but leaving the windows open. There aren’t many tools online that can do both for free, but one of them is Intakto. Intakto reads the code at runtime: it re-hashes the app’s installed APK files and checks them against the certificate Android has on file, covering the blind-spot that Play Integrity doesn’t responsibly defend. We ran the same file swap against Intakto on a Pixel 3a (Android 12), and it flagged the swapped file. It does not catch a copy re-signed with a new key and installed fresh: that is the case where Play Integrity answers UNRECOGNIZED_VERSION. The signer gets read two ways, through a raw native call and through the Android API, so a hook that lies to one is caught by the other.
Indie developers and smaller apps don’t need any high-ticket SDKs and are well served by a free tool already online, the Intakto free tier. Paid anti-fraud SDKs are suggested only to high-value targets (web3, crypto, Fintech, …) where reducing fraud without slowing the app or destroying the conversion rate is one of the best and most measurable investments that can be made.