/mini-militia-mod

I have clubbed together all my mods created for the Mini Militia game

Primary LanguageShell

Introduction

Mini Militia

Mini Militia was a very popular multiplayer game until PubG and their own commercialization killed it, It's one of the first mobile games I liked. 2 years ago, I did some modifications in the game binary and unlocked some hacks. Now that I have more free time, I wanted to finish it with as many tricks as I could find. I am going to use version 4.0.42, since newer versions have removed the LAN mode(Seriously? It was the best feature). So since it's an old version these mods will only work on LAN.

Method

Let's start with understanding the apk file. The steps I show below are linux specific, but the general concept can be applied on Windows or MAC as well.

Unpacking

First we unpack the mini militia apk file using apktool, ofcourse you need to install java for this:

$ java -jar apktool.jar d mini-militia -o unpack
I: Using Apktool 2.5.0 on mini-militia.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/justaashu/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Baksmaling classes2.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

This will create a unpack/ directory, with the apk file extracted. Here you can find all the code, resources and configuration files for the game.

Important Files and Directories

Here are some important files and directories that you can poke around and find assets and configs to change.

AndroidManifest.xml

The android manifest file having app permissions, activities etc.

smali/

The smali machine code for Compiled Android Java Classes.

assets/presMix.mp3

The app's music track, have fun changing it with your own track.

assets/da2sound.ckb

The Cricket Audio Bank file that has all the game sounds.

fonts/

The fonts used in the app, you can modify them just to have fun.

sd/, hd/ and hdr/

These contain all the maps and textures configs. \*.tmx are xml files having map configuration for each map. You can modify these to fiddle with weapon spwans and map data. Multiple png files having guns, backgrounds etc.

and ... 🥁 🥁

lib/armeabi-v7a/libcocos2dcpp.so

The Shared Object file that contains all the machine code for Compiled C++ Classes. This is the code that handles all the gaming functions, as these will be too slow in Java.

Getting more information about the binary:

$ cd unpack/lib/armeabi-v7a
$ file libcocos2dcpp.so
libcocos2dcpp.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /system/bin/linker, stripped

We can see that it is a 32 bit ARM binary, since it is stripped, we cannot just get the information to debug the binary. We have to use a disassembler to read the machine code.

You can use any disassembler, IDA Pro would be the best for this, but if you wanna do it for free, and comfortable with terminal only applications, I recommend Radare2. It has a bit of learning curve, but once you are comfortable, its really efficient. For Windows you can use IDA Pro or any other disassembler available that supports ARM.

Exploiting the Binary

To exploit the binary, we have to open it in radare2:

$ radare2 -a arm -b 32 libcocos2dcpp.so
Cannot determine entrypoint, using 0x0030cb40.
WARNING: No calling convention defined for this file, analysis may be inaccurate.
[0x0030cb40]> 

Check the symbols in the binary, since it's going to be a lot of output, we only print symbols with specific pattern:

[0x00af92c0]> is | grep -i AppPurchase
966    0x0054e8c0 0x0054e8c0 GLOBAL FUNC   180       InAppPurchaseBridge::isProductPurchased(std::string)
3984   0x0054ed30 0x0054ed30 GLOBAL FUNC   284       InAppPurchaseBridge::getProductPrice(std::string)
4026   0x0054e7e4 0x0054e7e4 GLOBAL FUNC   220       InAppPurchaseBridge::purchaseProductInGame(std::string)
7107   0x0054e978 0x0054e978 GLOBAL FUNC   12        InAppPurchaseBridge::readyToSignIn()
7108   0x0054e984 0x0054e984 GLOBAL FUNC   148       InAppPurchaseBridge::readyToPurchase()
7109   0x0054ea18 0x0054ea18 GLOBAL FUNC   128       InAppPurchaseBridge::hasPendingTransactions()
7111   0x0054ea98 0x0054ea98 GLOBAL FUNC   164       InAppPurchaseBridge::canMakePurchases()
7112   0x0054ec08 0x0054ec08 GLOBAL FUNC   296       InAppPurchaseBridge::showPurchaseStatusAlert()
7113   0x0054e794 0x0054e794 GLOBAL FUNC   18        InAppPurchaseBridge::restore()
7115   0x0054e7a8 0x0054e7a8 GLOBAL FUNC   58        InAppPurchaseBridge::purchaseProduct(std::string)
9549   0x0054e974 0x0054e974 GLOBAL FUNC   2         InAppPurchaseBridge::clearAllPurchases()

Now we have the names and addresses of each method. We can see the method InAppPurchaseBridge::isProductPurchased(std::string) at address 0x0054e8c0. We can infer this method is responsible for pro pack purchase. Let's seek to that address and print first 100 instructions (I have cut the below output).

[0x00af92c0]> s 0x0054e8c0
[0x0054e8c0]> pd 100
            ;-- InAppPurchaseBridge::isProductPurchased(std::string):
            ;-- method.InAppPurchaseBridge.isProductPurchased_std::string:
            0x0054e8c0      10b5           push {r4, lr}               ; InAppPurchaseBridge::isProductPurchased(std::string)
            0x0054e8c2      86b0           sub sp, 0x18
            0x0054e8c4      0190           str r0, [sp, 4]
            0x0054e8c6      fbf7ddfb       bl method IapManager::sharedIapManager() ; method.IapManager.sharedIapManager
                                                                       ; IapManager::sharedIapManager()

.....

====< 0x0054e964      ffe7           b 0x54e966
     ```--> 0x0054e966      bdf332ec       blx sym.__cxa_end_cleanup
        `-> 0x0054e96a      1846           mov r0, r3
            0x0054e96c      06b0           add sp, 0x18
            0x0054e96e      10bd           pop {r4, pc}
            0x0054e970      8e44           add lr, r1
            0x0054e972      4400           lsls r4, r0, 1
            ;-- InAppPurchaseBridge::clearAllPurchases():
            ;-- method.InAppPurchaseBridge.clearAllPurchases:
            0x0054e974      7047           bx lr                       ; InAppPurchaseBridge::clearAllPurchases()

Towards the end we can see at 0x0054e96a method is setting value of r3 register in r0: mov r0, r3 with hex value 1846. Generally r0 holds the return value of the method, so we convert this to always have value 1. We have to modify this to another 16 bit instruction as we cannot change offsets of other methods. Now, we can check different ways to set register r0 to 1 using ARM instructions manual and see which has 16 bit instruction. You can get hex value of instructions here. We will use movs r0, 1 which has hex value 0120.

Now we can use any hex editor, to edit the 2 bytes at address 0x0054e96a from 1846 to 0120. I used the dd command to replace the 2 bytes with our own, like below:

$ printf '\x01\x20' | dd conv=notrunc of=libcocos2dcpp.so bs=1 seek=$((0x0054e96a))
2+0 records in
2+0 records out
2 bytes copied, 2.9397e-05 s, 68.0 kB/s

This will make the method always return true. And we will be able to unlock the pro pack in the game. If you wanna do more modifications go ahead to the Used Methods Section.

Rebuilding the APK

Now that we have modified our binary file and other config files and assets, we need to convert it back into the apk so we can enjoy our mods. For this just use apk tool:

$ cd ../../../
$ java -jar apktool.jar b unpack
I: Using Apktool 2.5.0
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether sources has changed...
I: Smaling smali_classes2 folder into classes2.dex...
I: Checking whether resources has changed...
I: Building resources...
I: Copying libs... (/lib)
I: Building apk file...
I: Copying unknown files/dir...
I: Built apk...

This will create the new modified apk file in unpack/dist directory. Now we just need to sign the apk with our own key, so that it can be installed in an Android phone. Obviously, since we are signing it with our own key, we won't recieve any updates on this (which is a good thing) and it can't be installed on top of pre installed mini militia app if you have the original installed, you will have 2 instances of the app in your phone.

First we will create a key to sign our apk, this is a one time process:

$ cd unpack/dist
$ keytool -genkey -v -keystore mini.keystore -alias minikey -keyalg RSA -keysize 2048 -validity 10000
Enter keystore password:  
Re-enter new password: 
What is your first and last name?
  [Unknown]:  
What is the name of your organizational unit?
  [Unknown]:  
What is the name of your organization?
  [Unknown]:  
What is the name of your City or Locality?
  [Unknown]:  
What is the name of your State or Province?
  [Unknown]:  
What is the two-letter country code for this unit?
  [Unknown]:  
Is CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown correct?
  [no]:  yes

Generating 2,048 bit RSA key pair and self-signed certificate (SHA256withRSA) with a validity of 10,000 days
	for: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown
[Storing mini.keystore]

This will create a mini.keystore file with a key of alias minikey.

Now we simply sign our apk with the keystore:

$ jarsigner mini-militia.apk -keystore mini.keystore minikey
Enter Passphrase for keystore: 
jar signed.

Warning: 
The signer's certificate is self-signed.

You may also have to zipalign the apk. Only do this if previous apk doesn't work. It was not required for me. You can get the zipalign tool from android sdk. If you have android studio you can sign the apk from android studio UI as well avoiding all these steps.

zipalign -p -f -v 4 infile.apk outfile.apk

Now just move the apk file in your mobile phone and install it! 🎉 🎊

Used Methods

Below are all the hacks I could figure out and the method used.

Unlimited Ammo

Find and change instruction subs r3, 1 to subs r3, 0 in triggerPull method for all weapons *::triggerPull(). This removes the code to subtract 1 bullet at trigger pull of each weapon.

Unlimited Health

The method SoldierHostController::getHP() is responsible for setting health of a player. Towards the end of the method, we can see that method is setting r0 register with r3's value mov r0, r3 at address 0x004d8ff6. Since r0 is generally used as a return value, we can change this to any value we want. At first I changed it to mov r0, 1, this gave me infinite health as health value was always inferred as 1 by the game. But on UI this 1 value only filled a very small amount of the health bar, which made me realize that it is probably a percent value. So let's set it to 100 movs r0, 0x64.

Before

[0x004d8ff6]> pd 10
            0x004d8ff6      1846           mov r0, r3
            0x004d8ff8      04b0           add sp, 0x10
            0x004d8ffa      10bd           pop {r4, pc}
            ;-- SoldierHostController::setHP(int):

After

[0x004d8ff6]> pd 10
            0x004d8ff6      6420           movs r0, 0x64
            0x004d8ff8      04b0           add sp, 0x10
            0x004d8ffa      10bd           pop {r4, pc}
            ;-- SoldierHostController::setHP(int):

Pro Pack

Change method InAppPurchaseBridge::isProductPurchased(std::string) to always return true by modifying instruction to movs r0, 1 at address 0x0054e96a. This sets the propack as purchased.

Unlimited Flight

Change method SoldierHostController::hasPower() to always return true by modifying instruction to movs r3, 1 at address 0x004d7f2a. This allows us to have unlimited flight.

No Reload

Change method Weapon::getReloadTime() to always return 0 by modifying instruction to movs r0, 0 at address 0x00518358. This sets reload time to zero for all weapons.

Rounds Per Fire

Modify the method Weapon::getRoundsPerFire() to shoot 4 bullets at once by modifying instruction to movs r0, 4 at address 0x00518666, we can set it to any number to get one shot kill.

Weild Dual Weapon

Modify the method Weapon::isDualWield() to always return true by modifying instruction movs r0, 1 at address 0x00518696 this allows us to dual weild any weapon but it breaks the UI part as weapon goes to secondary, to resolve this we enforce weapon dual weild as primary by modifying method Weapon::isDualWieldPrimaryOnly() to always return true by modifying instruction to movs r0, 1 at address 0x005186b6.

Fixed Spawn Weapon

Change the method WeaponFactory::createRandomStartWeapon() to always pass a hardcoded index to method cocos2d::CCArray::objectAtIndex(unsigned int) which gets the weapon for a given index at address 0x0051c454. For this we modify instructions to set r1 register with the hardcoded index for the weapon of our choice right before calling the objectAtIndex method, so we change instruction at 0x0051c452 to movs r1, 1.

Weapons and Indexes

Index Weapon Name
0 Magnum
1 Uzi
2 Desert Eagle

Purchase all items

Change the method ItemPurchase::isItemPurchased(std::string) to always return true by changing the instruction at 0x003d053e from movs r3, 0 to movs r3, 1. This will set all the items in the shop as purchased.

Other Interesting Methods

You can also check other interesting methods to explore and do share other hacks that you were able to find.

  • Weapon::getDamage()
  • Weapon::getMeleeDamage()
  • Weapon::isReloading()
  • Weapon::getZoomScale()
  • Weapon::changeZoomLevel()
  • Weapon::setAccuracyMod(float)
  • Weapon::setZoomMod(float)
  • WeaponFactory::isDualWeapon(ItemType)
  • WeaponFactory::createRandomSecondaryWeapon()
  • WeaponFactory::createRandomPrimaryWeapon()
  • SoldierHostController::setHP(int)
  • SoldierHostController::setMaxHP(int)
  • SoldierManager::getRespawnTime()
  • SoldierHostController::getBackupStarterWeapon()
  • SoldierHostController::getPrimaryStarterWeapon()
  • InAppPurchaseBridge::getProductPrice(std::string)
  • WeaponFactory::sharedWeaponFactory()