This mod unlocks all levels and equipment, and grants you unlimited money, gems, and enchantments. Android only. Verified end to end on Shadow Fight 2 2.46.0; nothing in the setup is tied to a particular game version.
I used to play Shadow Fight 2 a lot when I was younger and recently wanted to revisit the Titan fight without grinding through the whole game. None of the mods I found online worked on the latest version, so I tasked Claude Code with cracking the game. I gave it an Android VM and let it cook; after a lot of trial and error, it reverse-engineered the integrity checks from the obfuscated code and made me this CLI tool.
The full technical story is in WRITEUP.md. Essentially, editing the progress file (users.xml) isn't enough; the game discards any changes if the new hash doesn't match the saved hash (users.xml.hash). Claude figured out that the hash is computed using users.xml encoded a certain way, a fixed salt embedded in the APK, and a device-dependent unique ID. That last ID is tied to the app's signing certificate, so setup re-signs the game with the key in this repo and then reads the ID back off the phone. That takes three commands and no root.
- An Android device with Shadow Fight 2 installed, plus a USB cable
adbon yourPATH, from Android Platform Tools (brew install android-platform-toolson macOS)- Android SDK build-tools for
zipalignandapksigner, only needed once during setup (Android Studio's SDK Manager, orsdkmanager 'build-tools;36.0.0') - uv, which runs the scripts and fetches Python for them
- USB debugging turned on: Settings → About phone → tap Build number seven times, then Developer options → USB debugging
Plug the phone in and confirm that adb devices lists it as device. Turn USB debugging back off when you're done editing saves, because while it is on anything with USB access can read app data and install packages.
Three commands, once per device. Start by pulling your save, because the next step uninstalls the Play Store copy and uninstalling deletes the phone-side save:
uv run pull_users_xml.pyRe-sign and reinstall the game's own APKs. Their bytes are unchanged apart from the signature blocks:
uv run resign_install.pyThen read the device value into .env, using a small reader app signed with the same key:
uv run capture_device_value.py --write-envTo check the value, launch the game once so it writes a save, then:
uv run pull_users_xml.py && uv run capture_device_value.pyThat recomputes the hash for the pulled save and compares it with the one the game wrote. Match means you're set. Otherwise stop, because a wrong value only produces saves the game rejects.
Worth knowing:
- Use the committed
tools/sf2mod.keystore. The value follows the signing certificate, so a different key gives a different value. - Re-signing invalidates the hash on the save your Play Store copy wrote, so push the save you pulled back with
push_users_xml.pybefore launching the re-signed game if you want your progress. - On Android 7 and older,
ANDROID_IDis device-wide, socapture_device_value.pyjust reads it and no re-signing is needed. - A re-signed client is against the game's terms of service, and it won't have Play Store or in-app purchases. Don't do this on an account you care about.
Pull the current save files into .local/saves/:
uv run pull_users_xml.pyEdit .local/saves/users.xml manually. examples/users.xml is a maxed-out starting point you can use as a reference.
Then, push the edited save back:
uv run push_users_xml.pypush_users_xml.py computes users.xml.hash, snapshots the phone-side save under .local/backups/, force-stops the app, and pushes the XML/hash pair to the main save slot. It does the same for users_backup.xml when a local copy exists, and mirrors users.xml into the backup slot when it doesn't, so each XML keeps its own hash. Pass --mirror-main-to-backup to force that mirroring, or --no-pre-push-snapshot to skip the snapshot. Launch the app manually once the push finishes.
Backups land in .local/backups/<timestamp>/. Restore one with:
uv run push_users_xml.py --restore .local/backups/YYYYMMDD-HHMMSSffffffhash_users_xml.py writes users.xml.hash beside a local file, or prints it without writing:
uv run hash_users_xml.py .local/saves/users.xml
uv run hash_users_xml.py .local/saves/users.xml --print --no-writeresign_install.py: re-sign the game's own APKs withtools/sf2mod.keystoreand reinstall themcapture_device_value.py: read the device value off the phone and verify it against a pulled savepull_users_xml.py: pull current save files for editingpush_users_xml.py: hash and push a local editedusers.xmlhash_users_xml.py: standalone hash generatortools/: the keystore, the prebuilt reader APK, and its source (tools/deviceid-reader/build.shrebuilds it).env.example: template for the per-install device valueAGENTS.md: instructions for an agent walking you through the device-ID setup (CLAUDE.mdsymlinks to it)WRITEUP.md: full reverse-engineering writeupexamples/: a reference save file as a starting pointtests/: pytest suite covering the hash primitives. Run withuv sync --group dev && uv run pytest.