├── tests ├── __init__.py ├── test_apk │ ├── coper.apk │ ├── coper2.apk │ ├── moqhao.apk │ ├── sesdex.apk │ ├── subapp.apk │ ├── coper2_0.apk │ ├── coper2_1.apk │ ├── coper3_2.apk │ ├── crocodilus.apk │ ├── inflate.apk │ ├── inflate2.apk │ ├── kangapack.apk │ ├── pronlocker.apk │ ├── simpleaes.apk │ ├── simplexor.apk │ ├── dexprotector.apk │ ├── simple_xor2.apk │ ├── loader_rc4_key_0.apk │ ├── multiple_rc4_init.apk │ ├── default_dex_protector.apk │ ├── loader_rc4_multiple_stage.apk │ ├── loader_rc4_second_key_0.apk │ ├── multidex_without_header.apk │ ├── simple_first_100_byte.apk │ ├── simple_skip4_zlib_base64.apk │ ├── simple_xor_zlib_base64.apk │ ├── loader_rc4_static_key_in_key_class.apk │ ├── protect_key_chines_manifest_without_zlib.apk │ └── protect_key_chines_manifest_without_zlib2.apk └── test.py ├── src └── kavanoz │ ├── __init__.py │ ├── loader │ ├── __init__.py │ ├── androidnativeemu │ │ └── libc.so │ ├── appsealing.py │ ├── multidex_header.py │ ├── subapp.py │ ├── crocodilus.py │ ├── simple.py │ ├── simple_xor_zlib.py │ ├── moqhao.py │ ├── simple_xor.py │ ├── kangapack.py │ ├── sesdex.py │ ├── simply_xor2.py │ ├── pronlocker.py │ ├── old_rc4.py │ ├── simple_aes.py │ ├── rc4.py │ ├── coper.py │ └── multidex.py │ ├── plugin_loader.py │ ├── debug_utils.py │ ├── utils.py │ ├── core.py │ ├── smali_regexes.py │ └── unpack_plugin.py ├── assets ├── unpack.gif └── demo.tape ├── requirements.txt ├── pyproject.toml ├── LICENSE ├── .github └── workflows │ ├── python-version-test.yml │ └── python-publish.yml ├── .gitattributes ├── .gitignore └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/kavanoz/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/kavanoz/loader/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/unpack.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eybisi/kavanoz/HEAD/assets/unpack.gif -------------------------------------------------------------------------------- /src/kavanoz/loader/androidnativeemu/libc.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eybisi/kavanoz/HEAD/src/kavanoz/loader/androidnativeemu/libc.so -------------------------------------------------------------------------------- /tests/test_apk/coper.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5c04b0eedc8f20cbf53b78e418dab70e05cba6c5eddeaf270e832420d99e18cb 3 | size 558021 4 | -------------------------------------------------------------------------------- /tests/test_apk/coper2.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ff7f74a9a551ff303fe8f502859766a7909a87bf85b8820bda64c3fb0f04d0cb 3 | size 521319 4 | -------------------------------------------------------------------------------- /tests/test_apk/moqhao.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7f419e190f5767e4b6ad27644635434e81bb24a39a67916b637214ee75eb6957 3 | size 522392 4 | -------------------------------------------------------------------------------- /tests/test_apk/sesdex.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:33233ed7c1612a693f349c0d2ac41d5ecea882a7ecf57a25c8fb27994f16ae4c 3 | size 156475 4 | -------------------------------------------------------------------------------- /tests/test_apk/subapp.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5da1c0dd5d7e3b8e0e925b5c784aeccadaf9ad9671f68efd226bd5d7ee864fe6 3 | size 915522 4 | -------------------------------------------------------------------------------- /tests/test_apk/coper2_0.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:6cd0fbfb088a95b239e42d139e27354abeb08c6788b6083962943522a870cb98 3 | size 5187620 4 | -------------------------------------------------------------------------------- /tests/test_apk/coper2_1.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:117aa133d19ea84a4de87128f16384ae0477f3ee9dd3e43037e102d7039c79d9 3 | size 8904925 4 | -------------------------------------------------------------------------------- /tests/test_apk/coper3_2.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:83eea636c3f04ff1b46963680eb4bac7177e77bbc40b0d3426f5cf66a0c647ae 3 | size 8248744 4 | -------------------------------------------------------------------------------- /tests/test_apk/crocodilus.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c5e3edafdfda1ca0f0554802bbe32a8b09e8cc48161ed275b8fec6d74208171f 3 | size 2419299 4 | -------------------------------------------------------------------------------- /tests/test_apk/inflate.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:62ecd2052e4bd0287ecc472c02e0b30ecb0dd035a5fbd90feddd98cc0054aee4 3 | size 5292211 4 | -------------------------------------------------------------------------------- /tests/test_apk/inflate2.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bbfd4d328fd4f031bc03eebf8ea596086e0f1ff8a0fc45b18798ea0739e8bcfd 3 | size 3700745 4 | -------------------------------------------------------------------------------- /tests/test_apk/kangapack.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2c05efa757744cb01346fe6b39e9ef8ea2582d27481a441eb885c5c4dcd2b65b 3 | size 20029841 4 | -------------------------------------------------------------------------------- /tests/test_apk/pronlocker.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:06f5a293c02cf54d15b2a33ed53007c3afbb0811bc7d515c9251b4acf3113cdd 3 | size 1911399 4 | -------------------------------------------------------------------------------- /tests/test_apk/simpleaes.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:299feea51f737f85aeb5d81f692bc794cd25c3b9a4fa0c361c666c57910901a7 3 | size 3665144 4 | -------------------------------------------------------------------------------- /tests/test_apk/simplexor.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:350049a97527e91268ca794317649b0c8e4e7eaef53e3a7fa864a88326cb6364 3 | size 1891440 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | androguard>=4.0.1 2 | apkInspector>=1.2.1 3 | androidemu>=0.0.4 4 | arc4>=0.3.0 5 | halo>=0.0.31 6 | lief>=0.16.3 7 | loguru>=0.7.2 8 | pycryptodome>=3.19.1 9 | -------------------------------------------------------------------------------- /tests/test_apk/dexprotector.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:dd1806de5e695ca12b6646c0a7ddc6268400a7a7351c5052042fd3d98225995a 3 | size 3192399 4 | -------------------------------------------------------------------------------- /tests/test_apk/simple_xor2.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4c4e5a351a412d0d0d25f720b663c8dc529cecf7c64ad9d78076ab87cca71b7a 3 | size 2095238 4 | -------------------------------------------------------------------------------- /tests/test_apk/loader_rc4_key_0.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:57b85c592efc5f82dff42d14f445dfb412aa88f705403a912653235fecb4756f 3 | size 2038747 4 | -------------------------------------------------------------------------------- /tests/test_apk/multiple_rc4_init.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:6faf00a5652e38897c76cad572c6052bbcbc602c6793f91f4c0551758a3ef538 3 | size 402395 4 | -------------------------------------------------------------------------------- /tests/test_apk/default_dex_protector.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2e84fd3484fcde105c437f1c0366954f4bdee7c1d3b334e2daa366a6592e4432 3 | size 1127171 4 | -------------------------------------------------------------------------------- /tests/test_apk/loader_rc4_multiple_stage.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c7301f48e32b30da78112957ab3bbe731cda5c9fd4491deb814dc5133e687154 3 | size 1962499 4 | -------------------------------------------------------------------------------- /tests/test_apk/loader_rc4_second_key_0.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3d04f3862334ee0569b7c2b97cadeaccf65a0d9a9d6e7d5bd5084ca77c39bc27 3 | size 2068961 4 | -------------------------------------------------------------------------------- /tests/test_apk/multidex_without_header.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c3ad0c33c266771931054adf886710d77e1afe2b9ac4219e473b90162d1c04d6 3 | size 2835722 4 | -------------------------------------------------------------------------------- /tests/test_apk/simple_first_100_byte.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:b3a4127a7943bcd73de2a8b14c8f82fbc200da1eac424995728ff2da122ff9f3 3 | size 1206644 4 | -------------------------------------------------------------------------------- /tests/test_apk/simple_skip4_zlib_base64.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1111b60df01246063c6b56f9e300a4dc4790b58387c3612b796e453664f97cb0 3 | size 442789 4 | -------------------------------------------------------------------------------- /tests/test_apk/simple_xor_zlib_base64.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:294db272882a5a703392033862f51aa0d4c119f92800a7380fac87fd6ce747c6 3 | size 309742 4 | -------------------------------------------------------------------------------- /tests/test_apk/loader_rc4_static_key_in_key_class.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:dcb729cc87e6b43f81aa903c092596b7ef6bf10659dad0264017dc5737cfa361 3 | size 1172533 4 | -------------------------------------------------------------------------------- /tests/test_apk/protect_key_chines_manifest_without_zlib.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1da274d45d8f23c700daad253d588474b31aafc9c3a334d8a76e6dc18f194330 3 | size 2014205 4 | -------------------------------------------------------------------------------- /tests/test_apk/protect_key_chines_manifest_without_zlib2.apk: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:496e20eb04c7fb0fd64a1d3b519cb2515656e0486253f376176fdc996efebd16 3 | size 5826387 4 | -------------------------------------------------------------------------------- /assets/demo.tape: -------------------------------------------------------------------------------- 1 | Output unpack.gif 2 | Set Shell zsh 3 | Sleep 1s 4 | Type "kavanoz coper.apk" 5 | Enter 6 | Sleep 1s 7 | Type "kavanoz inflate.apk" 8 | Enter 9 | Sleep 4.5s 10 | Type "kavanoz loader_rc4_key_0.apk" 11 | Enter 12 | Sleep 2.5s 13 | Type "kavanoz multiple_rc4_init.apk" 14 | Enter 15 | Sleep 3s 16 | 17 | -------------------------------------------------------------------------------- /src/kavanoz/loader/appsealing.py: -------------------------------------------------------------------------------- 1 | from androguard.core.apk import APK 2 | from androguard.core.dex import DEX 3 | from kavanoz.unpack_plugin import Unpacker 4 | from kavanoz.utils import xor 5 | 6 | 7 | class LoaderAppsealing(Unpacker): 8 | def __init__(self, apk_obj: APK, dexes, output_dir): 9 | super().__init__( 10 | "loader.appsealing", "Appsealing unpacker", apk_obj, dexes, output_dir 11 | ) 12 | 13 | def lazy_check(self, apk_object, dvms): 14 | for f in self.apk_object.get_files(): 15 | if "assets/AppSealing" in f: 16 | return True 17 | 18 | def start_decrypt(self, native_lib: str = ""): 19 | self.logger.info("Not implemented yet") 20 | return 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "kavanoz" 7 | version = "0.0.6" 8 | keywords = ["android","malware","unpacking","packer"] 9 | license = {text="MIT License"} 10 | readme = "README.md" 11 | description = "Unpacking framework for common android malware" 12 | dynamic = ["dependencies"] 13 | authors = [ 14 | {email = "eybisii@gmail.com"}, 15 | {name = "Ahmet Bilal Can"} 16 | ] 17 | maintainers = [ 18 | {name = "Ahmet Bilal Can", email = "eybisii@gmail.com"} 19 | ] 20 | [tool.setuptools.dynamic] 21 | dependencies = {file = ['requirements.txt']} 22 | [project.scripts] 23 | kavanoz = "kavanoz.core:cli" 24 | [tool.setuptools.packages.find] 25 | # All the following settings are optional: 26 | where = ["src"] # ["."] by default 27 | include = ["*"] # ["*"] by default 28 | namespaces = true # true by default 29 | [tool.setuptools.package-data] 30 | kavanoz = ["loader/androidnativeemu/libc.so"] 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/python-version-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python multiple version test 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Git LFS Pull 28 | run: git lfs pull 29 | shell: bash 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 34 | pip install . 35 | - name: Test with unittest 36 | run: | 37 | python -m unittest 38 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.11' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /src/kavanoz/loader/multidex_header.py: -------------------------------------------------------------------------------- 1 | from androguard.core.apk import APK 2 | from androguard.core.dex import DEX 3 | from kavanoz.unpack_plugin import Unpacker 4 | import struct 5 | 6 | 7 | class LoaderMultidexHeader(Unpacker): 8 | def __init__(self, apk_obj: APK, dvms, output_dir): 9 | super().__init__( 10 | "loader.multidex.header", "Unpacker for multidex", apk_obj, dvms, output_dir 11 | ) 12 | 13 | def start_decrypt(self, native_lib: str = ""): 14 | self.logger.info("Starting to decrypt") 15 | self.decrypted_payload_path = None 16 | self.brute_assets() 17 | 18 | def brute_assets(self): 19 | self.logger.info("Starting brute-force") 20 | asset_list = self.apk_object.get_files() 21 | for filepath in asset_list: 22 | f = self.apk_object.get_file(filepath) 23 | if self.read_size_append_dex(f): 24 | self.logger.info("Decryption finished! unpacked.dex") 25 | return self.decrypted_payload_path 26 | return None 27 | 28 | def read_size_append_dex(self, file_data): 29 | # dex_header_size_off = 0x20 30 | if len(file_data) <= 0x20 - 3 + 4: 31 | return 32 | size = struct.unpack(" list[Unpacker]: 31 | module = importlib.import_module(module_name) 32 | module_dict = module.__dict__ 33 | 34 | check_in = None 35 | 36 | if "__all__" in module_dict: 37 | check_in = { 38 | key: module_dict[key] 39 | for key in module_dict["__all__"] 40 | if key in module_dict 41 | } 42 | else: 43 | check_in = { 44 | key: val for key, val in module_dict.items() if key not in BLACKLISTED_KEYS 45 | } 46 | 47 | valid_items = [ 48 | mod 49 | for inner_module_name in check_in 50 | if (mod := module_dict[inner_module_name]) 51 | and inner_module_name.startswith("Loader") 52 | and issubclass(mod, Unpacker) 53 | ] 54 | 55 | if not valid_items: 56 | del sys.modules[module_name] 57 | return None 58 | else: 59 | return valid_items 60 | 61 | 62 | def get_plugins(): 63 | for plugin in dicover_plugins(PLUGIN_DIRECTORY): 64 | yield import_plugin(plugin) 65 | -------------------------------------------------------------------------------- /src/kavanoz/loader/subapp.py: -------------------------------------------------------------------------------- 1 | from androguard.core.apk import APK 2 | from androguard.core.dex import DEX 3 | from kavanoz.unpack_plugin import Unpacker 4 | from kavanoz.utils import xor 5 | 6 | 7 | class LoaderSubapp(Unpacker): 8 | def __init__(self, apk_obj: APK, dvms, output_dir): 9 | super().__init__( 10 | "loader.subapp", 11 | "Unpacker for chinese packer1, Beingyi", 12 | apk_obj, 13 | dvms, 14 | output_dir, 15 | ) 16 | 17 | def start_decrypt(self, native_lib: str = ""): 18 | self.logger.info("Starting to decrypt") 19 | package_name = self.apk_object.get_package() 20 | self.decrypted_payload_path = None 21 | if package_name != None: 22 | self.brute_assets(package_name) 23 | 24 | def brute_assets(self, key: str): 25 | self.logger.info("Starting brute-force") 26 | asset_list = self.apk_object.get_files() 27 | for filepath in asset_list: 28 | f = self.apk_object.get_file(filepath) 29 | if self.solve_encryption(f, key): 30 | self.logger.info("Decryption finished! unpacked.dex") 31 | return self.decrypted_payload_path 32 | return None 33 | 34 | def solve_encryption(self, file_data, key): 35 | if len(key) < 3 or len(file_data) < 3: 36 | return False 37 | xored_h = file_data[0] ^ key[0].encode("utf-8")[0] 38 | xored_h2 = file_data[1] ^ key[1].encode("utf-8")[0] 39 | xored_h3 = file_data[2] ^ key[2].encode("utf-8")[0] 40 | if xored_h != ord("d") or xored_h2 != ord("e") or xored_h3 != ord("x"): 41 | return False 42 | xored_data = xor(file_data, key.encode("utf-8")) 43 | if self.check_and_write_file(xored_data): 44 | return True 45 | return False 46 | -------------------------------------------------------------------------------- /src/kavanoz/debug_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from unicorn.arm_const import * 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def hook_code(uc, address, size, user_data): 9 | instruction = uc.mem_read(address, size) 10 | instruction_str = "".join("{:02x} ".format(x) for x in instruction) 11 | 12 | logger.debug( 13 | "# Tracing instruction at 0x%x, instruction size = 0x%x, instruction = %s" 14 | % (address, size, instruction_str) 15 | ) 16 | 17 | if instruction == b"\x00\x00\x00\x00": 18 | logger.error("Uh oh, we messed up.") 19 | uc.emu_stop() 20 | 21 | 22 | def hook_block(uc, address, size, user_data): 23 | instruction = uc.mem_read(address, size) 24 | instruction_str = "".join("{:02x} ".format(x) for x in instruction) 25 | 26 | logger.debug( 27 | "# Block at 0x%x, instruction size = 0x%x, instruction = %s" 28 | % (address, size, instruction_str) 29 | ) 30 | 31 | 32 | def hook_unmapped(uc, access, address, length, value, context): 33 | pc = uc.reg_read(UC_ARM_REG_PC) 34 | 35 | logger.debug( 36 | "mem unmapped: pc: %x access: %x address: %x length: %x value: %x" 37 | % (pc, access, address, length, value) 38 | ) 39 | uc.emu_stop() 40 | return True 41 | 42 | 43 | def hook_mem_write(uc, access, address, size, value, user_data): 44 | pc = uc.reg_read(UC_ARM_REG_PC) 45 | logger.debug( 46 | ">>> Memory WRITE at 0x%x, data size = %u, data value = 0x%x, pc: %x" 47 | % (address, size, value, pc) 48 | ) 49 | 50 | 51 | def hook_mem_read(uc, access, address, size, value, user_data): 52 | pc = uc.reg_read(UC_ARM_REG_PC) 53 | data = uc.mem_read(address, size) 54 | logger.debug( 55 | ">>> Memory READ at 0x%x, data size = %u, pc: %x, data value = 0x%s" 56 | % (address, size, pc, data.hex()) 57 | ) 58 | 59 | 60 | def hook_interrupt(uc, intno, data): 61 | logger.debug(">>> Triggering interrupt %d" % intno) 62 | return 63 | -------------------------------------------------------------------------------- /src/kavanoz/loader/crocodilus.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | 3 | from androguard.core.apk import APK 4 | from kavanoz.unpack_plugin import Unpacker 5 | from kavanoz.utils import unpad_pkcs5 6 | 7 | 8 | class LoaderCrocodilus(Unpacker): 9 | """ 10 | Read asset files. Try to decrypt with AES; key [32:48] iv [48:64], size [64:72], data [72:] 11 | """ 12 | 13 | def __init__(self, apk_obj: APK, dvms, output_dir): 14 | super().__init__( 15 | "loader.Crocodilus", "Unpacker for crocodilus", apk_obj, dvms, output_dir 16 | ) 17 | 18 | def start_decrypt(self, native_lib: str = ""): 19 | self.logger.info("Starting to decrypt for Crocodilus") 20 | self.decrypted_payload_path = None 21 | self.brute_assets() 22 | 23 | def brute_assets(self): 24 | asset_list = self.apk_object.get_files() 25 | for filepath in asset_list: 26 | if "assets/" in filepath: 27 | f = self.apk_object.get_file(filepath) 28 | if self.solve_encryption(f): 29 | self.logger.info( 30 | f"Decryption finished! {self.decrypted_payload_path}, found it in {filepath}" 31 | ) 32 | 33 | def solve_encryption(self, file_data): 34 | if len(file_data) < 72: 35 | return 36 | aes_key = file_data[32:48] 37 | aes_iv = file_data[48:64] 38 | aes_size = file_data[64:72] 39 | aes_data = file_data[72:] 40 | if len(aes_data) % 16 != 0: 41 | return 42 | aes_size = int.from_bytes(aes_size, "big") 43 | cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv) 44 | dec_data = cipher.decrypt(aes_data) 45 | try: 46 | dec_data = unpad_pkcs5(dec_data) 47 | except ValueError: 48 | # self.logger.error("Unpadding failed") 49 | return 50 | if len(dec_data) != aes_size: 51 | return 52 | if self.check_and_write_file(dec_data): 53 | return True 54 | else: 55 | return False 56 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/test_apk/simple_xor_zlib_base64.apk filter=lfs diff=lfs merge=lfs -text 2 | tests/test_apk/subapp.apk filter=lfs diff=lfs merge=lfs -text 3 | tests/test_apk/coper2_1.apk filter=lfs diff=lfs merge=lfs -text 4 | tests/test_apk/coper3_2.apk filter=lfs diff=lfs merge=lfs -text 5 | tests/test_apk/dexprotector.apk filter=lfs diff=lfs merge=lfs -text 6 | tests/test_apk/simple_skip4_zlib_base64.apk filter=lfs diff=lfs merge=lfs -text 7 | tests/test_apk/inflate2.apk filter=lfs diff=lfs merge=lfs -text 8 | tests/test_apk/sesdex.apk filter=lfs diff=lfs merge=lfs -text 9 | tests/test_apk/pronlocker.apk filter=lfs diff=lfs merge=lfs -text 10 | tests/test_apk/coper2.apk filter=lfs diff=lfs merge=lfs -text 11 | tests/test_apk/default_dex_protector.apk filter=lfs diff=lfs merge=lfs -text 12 | tests/test_apk/inflate.apk filter=lfs diff=lfs merge=lfs -text 13 | tests/test_apk/loader_rc4_key_0.apk filter=lfs diff=lfs merge=lfs -text 14 | tests/test_apk/multiple_rc4_init.apk filter=lfs diff=lfs merge=lfs -text 15 | tests/test_apk/loader_rc4_second_key_0.apk filter=lfs diff=lfs merge=lfs -text 16 | tests/test_apk/multidex_without_header.apk filter=lfs diff=lfs merge=lfs -text 17 | tests/test_apk/simpleaes.apk filter=lfs diff=lfs merge=lfs -text 18 | tests/test_apk/simple_xor2.apk filter=lfs diff=lfs merge=lfs -text 19 | tests/test_apk/simplexor.apk filter=lfs diff=lfs merge=lfs -text 20 | tests/test_apk/coper.apk filter=lfs diff=lfs merge=lfs -text 21 | tests/test_apk/loader_rc4_multiple_stage.apk filter=lfs diff=lfs merge=lfs -text 22 | tests/test_apk/loader_rc4_static_key_in_key_class.apk filter=lfs diff=lfs merge=lfs -text 23 | tests/test_apk/moqhao.apk filter=lfs diff=lfs merge=lfs -text 24 | tests/test_apk/coper2_0.apk filter=lfs diff=lfs merge=lfs -text 25 | tests/test_apk/kangapack.apk filter=lfs diff=lfs merge=lfs -text 26 | tests/test_apk/protect_key_chines_manifest_without_zlib2.apk filter=lfs diff=lfs merge=lfs -text 27 | tests/test_apk/protect_key_chines_manifest_without_zlib.apk filter=lfs diff=lfs merge=lfs -text 28 | tests/test_apk/simple_first_100_byte.apk filter=lfs diff=lfs merge=lfs -text 29 | tests/test_apk/crocodilus.apk filter=lfs diff=lfs merge=lfs -text 30 | -------------------------------------------------------------------------------- /src/kavanoz/loader/simple.py: -------------------------------------------------------------------------------- 1 | from androguard.core.apk import APK 2 | from androguard.core.dex import DEX 3 | from kavanoz.unpack_plugin import Unpacker 4 | from kavanoz.utils import xor 5 | 6 | 7 | class LoaderSimple(Unpacker): 8 | def __init__(self, apk_obj: APK, dvms, output_dir): 9 | super().__init__( 10 | "loader.simple", "Simple methods to unpack", apk_obj, dvms, output_dir 11 | ) 12 | 13 | def start_decrypt(self, native_lib: str = ""): 14 | self.logger.info("Starting to decrypt") 15 | package_name = self.apk_object.get_package() 16 | self.decrypted_payload_path = None 17 | if package_name != None: 18 | if self.brute_assets(package_name): 19 | return 20 | 21 | def brute_assets(self, key: str): 22 | self.logger.info("Starting brute-force") 23 | asset_list = self.apk_object.get_files() 24 | for filepath in asset_list: 25 | f = self.apk_object.get_file(filepath) 26 | if self.try_one_byte_xor(f): 27 | return self.decrypted_payload_path 28 | return None 29 | 30 | def try_one_byte_xor(self, file_data): 31 | for k in range(1, 256): 32 | xored_data = xor(file_data[:16], k.to_bytes(1, "little")) 33 | if not self.check_header(xored_data): 34 | continue 35 | self.logger.info(f"Found single byte xor key : {k}") 36 | if self.check_obfuse(): 37 | xored_data = xor(file_data[:100], k.to_bytes(1, "little")) 38 | xored_data += file_data[100:] 39 | else: 40 | xored_data = xor(file_data, k.to_bytes(1, "little")) 41 | if self.check_and_write_file(xored_data): 42 | return True 43 | return False 44 | 45 | def check_obfuse(self) -> bool: 46 | # Check if obfuse.NPStringFog class exists 47 | # If so, xor only first 100 bytes of the file 48 | res = self.find_class_in_dvms("Lobfuse/NPStringFog;") 49 | if res: 50 | self.logger.info("Found obfuse.NPStringFog class") 51 | return True 52 | self.logger.info("obfuse.NPStringFog class not found") 53 | return False 54 | -------------------------------------------------------------------------------- /src/kavanoz/loader/simple_xor_zlib.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import re 3 | from kavanoz.unpack_plugin import Unpacker 4 | from kavanoz.utils import xor 5 | import zlib 6 | 7 | 8 | class LoaderSimpleXorZlib(Unpacker): 9 | decrypted_payload_path = None 10 | 11 | def __init__(self, apk_object, dvms, output_dir): 12 | super().__init__( 13 | "loader.simplexor", 14 | "Unpacker for multiple simple unpackers", 15 | apk_object, 16 | dvms, 17 | output_dir, 18 | ) 19 | 20 | def start_decrypt(self): 21 | self.logger.info("Starting to decrypt") 22 | self.decrypt_files() 23 | 24 | def decrypt_files(self): 25 | if self.decrypted_payload_path == None: 26 | out_file = "unpacked.dex" 27 | else: 28 | index = re.findall(r"\d+", self.decrypted_payload_path) 29 | if index: 30 | ii = int(index[0]) 31 | out_file = f"unpacked{ii+1}.dex" 32 | else: 33 | out_file = "unpacked1.dex" 34 | 35 | for filepath in self.apk_object.get_files(): 36 | if not filepath.startswith("assets"): 37 | continue 38 | fd = self.apk_object.get_file(filepath) 39 | if len(fd) < 8: 40 | return False 41 | if fd[4] == 0x78 and fd[5] == 0x9C: 42 | try: 43 | dec = zlib.decompress(fd[4:]) 44 | except Exception as e: 45 | self.logger.error(e) 46 | return False 47 | else: 48 | xor_k = fd[4] 49 | zlib_d = fd[5:] 50 | dec = xor(zlib_d, xor_k.to_bytes(1, "little")) 51 | if dec[:2] != b"\x78\x01": 52 | return 53 | try: 54 | dec = zlib.decompress(dec) 55 | except Exception as e: 56 | self.logger.error(e) 57 | return 58 | 59 | try: 60 | dec = base64.b64decode(dec) 61 | except Exception as e: 62 | self.logger.error(e) 63 | return 64 | if self.check_and_write_file(dec): 65 | return True 66 | return False 67 | -------------------------------------------------------------------------------- /src/kavanoz/loader/moqhao.py: -------------------------------------------------------------------------------- 1 | from androguard.core.apk import APK 2 | from kavanoz.unpack_plugin import Unpacker 3 | from kavanoz.utils import xor 4 | 5 | 6 | class LoaderMoqhao(Unpacker): 7 | """ 8 | Read asset files. Try to decrypt with : file[11] is xor key to decrypt file[12:] 9 | """ 10 | 11 | def __init__(self, apk_obj: APK, dvms, output_dir): 12 | super().__init__( 13 | "loader.moqhao", "Unpacker for moqhao", apk_obj, dvms, output_dir 14 | ) 15 | 16 | def start_decrypt(self, native_lib: str = ""): 17 | self.logger.info("Starting to decrypt") 18 | self.decrypted_payload_path = None 19 | self.brute_assets() 20 | 21 | def brute_assets(self): 22 | self.logger.info("Starting brute-force") 23 | asset_list = self.apk_object.get_files() 24 | for filepath in asset_list: 25 | if "assets/" in filepath: 26 | f = self.apk_object.get_file(filepath) 27 | if self.solve_encryption(f): 28 | self.logger.info( 29 | f"Decryption finished! {self.decrypted_payload_path}" 30 | ) 31 | else: 32 | if self.solve_encryption_native(f): 33 | self.logger.info( 34 | f"Decryption finished! {self.decrypted_payload_path}" 35 | ) 36 | 37 | def lazy_check(self, apk_obj, dvms) -> bool: 38 | file_list = apk_obj.get_files() 39 | one_asset = any("assets/" in x for x in file_list) 40 | native_lib = any("lib/" in x for x in file_list) 41 | return one_asset and native_lib 42 | 43 | def solve_encryption(self, file_data): 44 | if len(file_data) < 12: 45 | return 46 | first_12 = file_data[:12] 47 | xor_key = first_12[11].to_bytes(1, "little") 48 | xord_data = xor(file_data[12:], xor_key) 49 | if self.check_and_write_file(xord_data): 50 | return True 51 | else: 52 | return False 53 | 54 | def solve_encryption_native(self, file_data): 55 | if len(file_data) < 24: 56 | return 57 | first_12 = file_data[:24] 58 | xor_key = first_12[16].to_bytes(1, "little") 59 | xord_data = xor(file_data[24:], xor_key) 60 | if self.check_and_write_file(xord_data): 61 | return True 62 | else: 63 | return False 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /src/kavanoz/utils.py: -------------------------------------------------------------------------------- 1 | from itertools import cycle 2 | from loguru import logger 3 | import logging 4 | import sys 5 | import re 6 | from typing import cast 7 | 8 | 9 | def xor(var: bytes, key: bytes) -> bytes: 10 | return bytes(a ^ b for a, b in zip(var, cycle(key))) 11 | 12 | def unpad_pkcs5(data: bytes) -> bytes: 13 | """ 14 | Unpads the data using the PKCS5 padding scheme. 15 | """ 16 | pad_len = data[-1] 17 | if pad_len > 16: 18 | raise ValueError("Invalid padding length") 19 | return data[:-pad_len] 20 | 21 | dex_headers = [ 22 | b"dex\n035\x00", 23 | b"dex\n036\x00", 24 | b"dex\n037\x00", 25 | b"dex\n038\x00", 26 | b"dex\n039\x00", 27 | b"dey\n035\x00", 28 | b"dey\n036\x00", 29 | b"dey\n037\x00", 30 | b"dey\n038\x00", 31 | ] 32 | 33 | pkzip_headers = [ 34 | b"PK\x03\x04", 35 | b"PK\x05\x06", 36 | b"PK\x07\x08", 37 | ] 38 | 39 | zlib_headers = [ 40 | b"\x78\x01", 41 | b"\x78\x9c", 42 | b"\x78\x5e", 43 | b"\x78\xda", 44 | b"\x78\x20", 45 | b"\x78\x7d", 46 | b"\x78\xbb", 47 | b"\x78\xf9", 48 | ] 49 | 50 | 51 | class MyFilter: 52 | def __init__(self, level): 53 | self.level = level 54 | 55 | def __call__(self, record): 56 | levelno = logger.level(self.level).no 57 | return record["level"].no >= levelno 58 | 59 | 60 | def set_log(level): 61 | """ 62 | Sets the log for loguru based on the level being passed. 63 | The possible values are TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL 64 | """ 65 | logger.remove(0) 66 | my_filter = MyFilter(level) 67 | logger.add(sys.stderr, filter=my_filter, level=0) 68 | 69 | 70 | def unescape_unicode(str): 71 | codepoint = re.compile(r"(\\u[0-9a-fA-F]{4})") 72 | 73 | def replace(match): 74 | return chr(int(match.group(1)[2:], 16)) 75 | 76 | return codepoint.sub(replace, str) 77 | 78 | 79 | class InterceptHandler(logging.Handler): 80 | def emit(self, record): 81 | # Get corresponding Loguru level if it exists. 82 | try: 83 | level = logger.level(record.levelname).name 84 | except ValueError: 85 | level = record.levelno 86 | 87 | # Find caller from where originated the logged message. 88 | frame, depth = sys._getframe(6), 6 89 | while frame and frame.f_code.co_filename == logging.__file__: 90 | frame = frame.f_back 91 | depth += 1 92 | 93 | logger.opt(depth=depth, exception=record.exc_info).log( 94 | level, record.getMessage() 95 | ) 96 | -------------------------------------------------------------------------------- /src/kavanoz/loader/simple_xor.py: -------------------------------------------------------------------------------- 1 | import re 2 | from kavanoz.unpack_plugin import Unpacker 3 | from kavanoz.utils import xor 4 | 5 | """ 6 | invoke-direct v10, v14, v15, Ljava/lang/Long;->(J)V 7 | const/4 v9, 0 8 | array-length v13, v4 9 | if-ge v9, v13, +03fh 10 | aget-byte v13, v4, v9 11 | const-string v14, "pAinaTuyPSZcNjEbewHmUaUiFLzjnb" 12 | invoke-virtual v14, Ljava/lang/String;->getBytes()[B 13 | move-result-object v14 14 | invoke-virtual v10, Ljava/lang/Long;->longValue()J 15 | move-result-wide v16 16 | move-wide/from16 v0, v16 17 | long-to-int v15, v0 18 | aget-byte v14, v14, v15 19 | xor-int/2addr v13, v14 20 | int-to-byte v13, v13 21 | aput-byte v13, v8, v9 22 | """ 23 | 24 | find_xor_key = ( 25 | r"const/4 [vp]\d+, 0\s+" 26 | r"array-length [vp]\d+, [vp]\d+\s+" 27 | r"if-ge [vp]\d+, [vp]\d+, \+03fh\s+" 28 | r"aget-byte [vp]\d+, [vp]\d+, [vp]\d+\s+" 29 | r"const-string [vp]\d+, \"(.*)\"\s+" 30 | r"invoke-virtual [vp]\d+, L[^;]+;->getBytes\(\)+\[B\s+" 31 | r"move-result-object [vp]\d+\s+" 32 | r"invoke-virtual [vp]\d+, L[^;]+;->longValue\(\)J\s+" 33 | ) 34 | 35 | 36 | class LoaderSimpleXor(Unpacker): 37 | decrypted_payload_path = None 38 | 39 | def __init__(self, apk_object, dvms, output_dir): 40 | super().__init__( 41 | "loader.simplexor", 42 | "Unpacker for multiple simple unpackers", 43 | apk_object, 44 | dvms, 45 | output_dir, 46 | ) 47 | 48 | def start_decrypt(self): 49 | self.logger.info("Starting to decrypt") 50 | self.xor_key = self.find_xor_key() 51 | if self.xor_key is None: 52 | return 53 | self.decrypt_files(self.xor_key) 54 | 55 | def find_xor_key(self): 56 | application_smali = self.find_main_application() 57 | target_method = self.find_method(application_smali, "attachBaseContext") 58 | if target_method == None: 59 | return 60 | sm = self.get_smali(target_method) 61 | m = re.findall(find_xor_key, sm) 62 | if len(m) == 1: 63 | return bytes(m[0].encode("utf-8")) 64 | else: 65 | return 66 | 67 | def decrypt_files(self, xor_key): 68 | if self.decrypted_payload_path == None: 69 | out_file = "unpacked.dex" 70 | else: 71 | index = re.findall(r"\d+", self.decrypted_payload_path) 72 | if index: 73 | ii = int(index[0]) 74 | out_file = f"unpacked{ii+1}.dex" 75 | else: 76 | out_file = "unpacked1.dex" 77 | 78 | for filepath in self.apk_object.get_files(): 79 | if not (filepath.startswith("assets") or filepath.startswith("res")): 80 | continue 81 | fd = self.apk_object.get_file(filepath) 82 | dec = xor(fd, xor_key) 83 | if self.check_and_write_file(dec): 84 | return True 85 | return False 86 | -------------------------------------------------------------------------------- /src/kavanoz/core.py: -------------------------------------------------------------------------------- 1 | from androguard.core.apk import APK 2 | from androguard.core.dex import DEX 3 | from androguard import util 4 | from kavanoz.unpack_plugin import Unpacker 5 | from kavanoz import plugin_loader, utils 6 | from kavanoz.utils import InterceptHandler 7 | from loguru import logger 8 | import logging 9 | import time 10 | import kavanoz.loader 11 | import click 12 | import sys 13 | from halo import Halo 14 | 15 | 16 | logger.disable("androguard") 17 | 18 | class Kavanoz: 19 | def __init__( 20 | self, 21 | apk_path: str | None = None, 22 | apk_object=None, 23 | output_dir: str | None = None, 24 | ): 25 | self.output_dir = output_dir 26 | mod_logger = logging.getLogger("androidemu") 27 | mod_logger.handlers = [InterceptHandler(level=logging.CRITICAL)] 28 | mod_logger.propagate = False 29 | s = time.time() 30 | if apk_object: 31 | self.apk_object = apk_object 32 | elif apk_path: 33 | self.apk_object = APK(apk_path) 34 | 35 | # load plugins 36 | self.plugins = [ 37 | subplug 38 | for plugin in filter(None, plugin_loader.get_plugins()) 39 | for subplug in plugin 40 | ] 41 | e = time.time() 42 | logger.info(f"Androguard took : {e-s} seconds") 43 | s = time.time() 44 | self.dexes = [DEX(dex) for dex in self.apk_object.get_all_dex()] 45 | e = time.time() 46 | logger.info(f"Androguard dvm took : {e-s} seconds") 47 | 48 | def get_plugin_results(self): 49 | for plugin in self.plugins: 50 | p = plugin(self.apk_object, self.dexes, output_dir=self.output_dir) 51 | yield p.main() 52 | 53 | def is_packed(self): 54 | p = Unpacker( 55 | "test", 56 | "test", 57 | apk_object=self.apk_object, 58 | dexes=self.dexes, 59 | output_dir=self.output_dir, 60 | ) 61 | return p.is_packed() 62 | 63 | 64 | @click.command() 65 | @click.argument("filename", type=click.Path(exists=True)) 66 | @click.option( 67 | "--output-dir", 68 | "-o", 69 | type=click.Path(exists=True), 70 | default=".", 71 | help="Output directory path", 72 | ) 73 | @click.option("-v", "--verbose", count=True) 74 | def cli(filename, output_dir, verbose): 75 | logger.remove() 76 | if verbose > 0: 77 | if verbose > 3: 78 | verbose = 3 79 | logger.add(sys.stderr, level=40-verbose*10) 80 | spinner = Halo(text="Extracting apk/dex information", spinner="star") 81 | spinner.start() 82 | k = Kavanoz(filename, output_dir=output_dir) 83 | spinner.stop() 84 | spinner.start() 85 | if not k.is_packed(): 86 | spinner.warn("Sample is not packed") 87 | for res in k.get_plugin_results(): 88 | spinner.text = f'Plugin {res["tag"]} is running' 89 | spinner.start() 90 | if res["status"] == "success": 91 | m = f"""Plugin tag : {res['tag']} 92 | Plugin description : {res['name']} 93 | Output file : {res['output_file']} """ 94 | spinner.text_color = "green" 95 | spinner.stop_and_persist("✨", "Unpacked successfully!") 96 | print(m) 97 | break 98 | else: 99 | spinner.stop_and_persist("❌", "Cannot unpack") 100 | -------------------------------------------------------------------------------- /src/kavanoz/loader/kangapack.py: -------------------------------------------------------------------------------- 1 | from androguard.core.apk import APK 2 | from kavanoz.unpack_plugin import Unpacker 3 | from Crypto.Cipher import AES 4 | from kavanoz.unpack_plugin import Unpacker 5 | import lief 6 | 7 | 8 | class LoaderKangaPack(Unpacker): 9 | """ 10 | ref: https://cryptax.medium.com/inside-kangapack-the-kangaroo-packer-with-native-decryption-3e7e054679c4 11 | encrypted file is appended to end of classes.dex 12 | aes decryption key/iv is in native library used with openssl evp api, keys are exported 13 | """ 14 | 15 | def __init__(self, apk_obj, dvms, output_dir): 16 | super().__init__( 17 | "loader.kangapack", "Unpacker for kangapack", apk_obj, dvms, output_dir 18 | ) 19 | 20 | def start_decrypt(self, native_lib: str = ""): 21 | # Get encrypted payload 22 | classes_dex = lief.DEX.parse(list(self.apk_object.get_dex())) 23 | dex_headers = classes_dex.header 24 | link_off, link_size = dex_headers.link 25 | enc_offset = 0 26 | if link_off == 0 and link_size == 0: 27 | off, size = dex_headers.data 28 | if dex_headers.file_size > off + size: 29 | enc_offset = off + size 30 | if enc_offset == 0: 31 | return 32 | 33 | enc_payload = self.apk_object.get_dex()[enc_offset:] 34 | payload_size = enc_payload[len(enc_payload) - 4 :] 35 | enc_payload = enc_payload[: len(enc_payload) - 4] 36 | native_libs = [ 37 | filename 38 | for filename in self.apk_object.get_files() 39 | if filename.startswith("lib/arm64-v8a/libapk") 40 | ] 41 | if len(native_libs) == 0: 42 | self.logger.info("No native lib 😔") 43 | return 44 | if len(native_libs) != 1: 45 | self.logger.info("Not sure this is kangapack but continue anyway") 46 | 47 | fname = native_libs[0].split("/")[-1] 48 | self.target_lib = fname 49 | elf_bin = lief.ELF.parse( 50 | list(self.apk_object.get_file(f"lib/arm64-v8a/{self.target_lib}")) 51 | ) 52 | for sym in elf_bin.exported_symbols: 53 | if sym.name == "AES_SECRET_KEY": 54 | rel = elf_bin.get_relocation(sym.value) 55 | # get lots of bytes then split by null byte :( 56 | str_arr = elf_bin.get_content_from_virtual_address(rel.addend, 40) 57 | str_arr = str_arr.tolist() 58 | an = str_arr[: str_arr.index(0)] 59 | secret_key = "".join(chr(x) for x in an).encode() 60 | iv = secret_key 61 | cipher = AES.new(secret_key, AES.MODE_CBC, iv) 62 | decrypted = cipher.decrypt(enc_payload) 63 | if self.check_and_write_file(decrypted): 64 | self.logger.info(f"Decrypted dex with key {secret_key}") 65 | return True 66 | return False 67 | 68 | def lazy_check(self, apk_object: APK, dvms: "list[DEX]") -> bool: 69 | dex_bytes = apk_object.get_dex() 70 | if len(dex_bytes) > 0: 71 | try: 72 | classes_dex = lief.DEX.parse(list(dex_bytes)) 73 | except Exception as e: 74 | # print(e) 75 | return False 76 | dex_headers = classes_dex.header 77 | link_off, link_size = dex_headers.link 78 | 79 | if link_off == 0 and link_size == 0: 80 | off, size = dex_headers.data 81 | if dex_headers.file_size > off + size: 82 | return True 83 | return False 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🫙 kavanoz 🫙 2 | ![](https://img.shields.io/github/license/eybisi/kavanoz) 3 | ![](https://img.shields.io/github/stars/eybisi/kavanoz) 4 | ![](https://img.shields.io/github/issues-closed/eybisi/kavanoz.svg) 5 | ![](https://img.shields.io/github/issues-pr-closed/eybisi/kavanoz.svg) 6 | 7 | Kavanoz (jar in turkish) is a tool to statically unpack common android banker malware. 8 | Do you ever wanted to get payload from packed malware without running android emulator ? Me neither :) But here is a tool anyway. 9 | 10 | ![](assets/unpack.gif) 11 | 12 | ### :eyes: Installation 13 | 14 | ``` 15 | pip install kavanoz 16 | ``` 17 | 18 | To install from source, clone the repository and do an editable install with -e. Which means if you edit or add new plugins to the project it will be used without reinstalling. 19 | 20 | ``` 21 | git clone https://github.com/eybisi/kavanoz.git 22 | cd kavanoz 23 | pip install -e . 24 | ``` 25 | 26 | ### :zap: Usage 27 | 28 | from cmdline 29 | ```bash 30 | kavanoz /tmp/filepath 31 | ``` 32 | You can use `-vvv` parameter to print verbose logs. (useful for debugging plugins) 33 | 34 | as python library 35 | ```py 36 | from kavanoz.core import Kavanoz 37 | from kavanoz import utils 38 | 39 | utils.set_log("DEBUG") 40 | k = Kavanoz(apk_path="tests/test_apk/coper.apk") 41 | for plugin_result in k.get_plugin_results(): 42 | if plugin_result["status"] == "success": 43 | print("Unpacked") 44 | print(plugin_result) 45 | break 46 | ``` 47 | 48 | ### :snake: Scripts: 49 | 50 | - [rc4.py](src/kavanoz/loader/rc4.py) Generic rc4 encrypted asset file. Script covers multiple versions. 51 | - [old_rc4.py](src/kavanoz/loader/old_rc4.py) Another Generic rc4 encrypted asset file. 52 | - [subapp.py](src/kavanozloader/subapp.py) Decryption of file with key derived from Manifest file ProtectKey variable 53 | - [multidex.py](src/kavanoz/loader/multidex.py) Multidex like loader with inflated packed file. (zlib compression) 54 | - [coper.py](src/kavanoz/loader/coper.py) Extract rc4 key from native lib with emulation (AndroidNativeEmu) 55 | - [moqhao.py](src/kavanozloader/moqhao.py) Emulation for moqhau unpacking. 56 | - [sesdex.py](src/kavanoz/loader/sesdex.py) 57 | - [simple_aes.py](src/kavanoz/loader/simple_aes.py) 58 | - [simple_xor.py](src/kavanoz/loader/simple_xor.py) 59 | - [simple_xor2.py](src/kavanoz/loader/simple_xor2.py) 60 | - [simple_xor_zlib.py](src/kavanoz/loader/simple_xor_zlib.py) 61 | - [subapp.py](src/kavanoz/loader/subapp.py) Decrypt asset with package name 62 | 63 | 64 | ### :gear: Development 65 | 66 | Make sure to install kavanoz as editable (with -e). To add new plugins just create new file in loader folder. Extend Unpacker class from unpack_plugin.py file. Define start_decrypt function with your implementation. 67 | ```py 68 | def start_decrypt(self, apk_object: APK, dexes: "list[DEX]"): 69 | ``` 70 | 71 | Add following function to make early exit from plugin. 72 | ```py 73 | def lazy_check(self,apk_object:APK, dexes: "list[DEX]"): 74 | ``` 75 | 76 | If extraction is successful assign self.decrypted_payload_path with extracted file path. 77 | You can use helper functions from unpacker class: 78 | - get_array_data 79 | - get_smali 80 | - find_method(class_name,method_name,descriptor="") 81 | - check_and_write_file(file_data) : checks file has dex, zip and zlib headers and writes unpacked dex with name : "external-{m[:8]}.dex" 82 | 83 | Make sure to run `python -m unittest` before opening a PR. In order to get test apk files, use `git lfs pull` command. 84 | 85 | ### :book: Tips 86 | 87 | - self.dexes hold dex objects. You can get class with `dex.get_class(smali_annotation_of_class)`. 88 | - You can use get_smali function and give target method obj to get smali represantation of target method. Then apply some regex to get data from smali. There are lots of defined regexs in [smali_regexes.py](src/kavanoz/smali_regexes.py) file to lookup. 89 | - Most of the time packers use file from asset folder. You can get files with `self.apk_object.get_files()` 90 | - Most of the time packers use Application class to start unpacking sequence. Use `application = self.apk_object.get_attribute_value("application", "name")` to get application class defined in manifest file. 91 | 92 | ### Thanks: 93 | [apkdetect.com](https://apkdetect.com) for unique samples to work with. 94 | 95 | -------------------------------------------------------------------------------- /src/kavanoz/loader/sesdex.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from androguard.core.apk import APK 3 | from arc4 import ARC4 4 | from androguard.core.dex import DEX 5 | import re 6 | from itertools import combinations 7 | from kavanoz.unpack_plugin import Unpacker 8 | from kavanoz.smali_regexes import Regexs 9 | from kavanoz.utils import xor 10 | 11 | 12 | """ 13 | invoke-virtual v8, v2, Ljava/io/InputStream;->read([B)I 14 | const-string v5, 'bhMIAdCgBYYOymrlRp' 15 | invoke-virtual v5, Ljava/lang/String;->getBytes()[B 16 | move-result-object v5 17 | invoke-static v2, v5, Lorvbreo/ycmgmee;->ZowuWxil([B [B)[B 18 | move-result-object v2 19 | invoke-virtual v8, Ljava/io/InputStream;->close()V 20 | """ 21 | 22 | find_xor_key = ( 23 | r"invoke-virtual [vp]\d+, [vp]\d+, L[^;]+;->read\(\[B\)I\s+" 24 | r"const-string [vp]\d+, \"(.*)\"\s+" 25 | r"invoke-virtual [vp]\d+, L[^;]+;->getBytes\(\)+\[B\s+" 26 | r"move-result-object [vp]\d+\s+" 27 | r"invoke-static [vp]\d+, [vp]\d+, L[^;]+;->([^\(]+)\(\[B \[B\)\[B\s+" 28 | r"move-result-object [vp]\d+\s+" 29 | r"invoke-virtual [vp]\d+, L[^;]+;->close\(\)V\s+" 30 | ) 31 | 32 | find_second_xor_key = ( 33 | r"const-string [vp]\d+, \"(.*)\"\s+" 34 | r"invoke-virtual [vp]\d+, L[^;]+;->getBytes\(\)+\[B\s+" 35 | r"move-result-object [vp]\d+\s+" 36 | r"invoke-static [vp]\d+, [vp]\d+, L[^;]+;->([^\(]+)\(\[B \[B\)\[B\s+" 37 | r"move-result-object [vp]\d+\s+" 38 | r"invoke-virtual [vp]\d+, [vp]\d+, L[^;]+;->write\(\[B\)V\s+" 39 | ) 40 | 41 | 42 | class LoaderSesdex(Unpacker): 43 | regex_class = Regexs() 44 | rc4_string_var = "" 45 | first_inner = [] 46 | second_inner = [] 47 | byte_array_data = [] 48 | decrypted_payload_path = None 49 | 50 | def __init__(self, apk_object, dvms, output_dir): 51 | super().__init__( 52 | "loader.sesdex", 53 | "Unpacker for unknown adware malware", 54 | apk_object, 55 | dvms, 56 | output_dir, 57 | ) 58 | 59 | def start_decrypt(self): 60 | self.second_inner_regex = self.regex_class.get_second_inner_regex() 61 | self.first_encryption_route = self.regex_class.get_encrytion_route_regex() 62 | self.key_class_regex = self.regex_class.get_key_class_regex() 63 | self.logger.info("Starting to decrypt") 64 | self.xor_key = self.find_xor_key() 65 | if self.xor_key is None: 66 | return 67 | self.decrypt_files(self.xor_key) 68 | 69 | for filepath in self.apk_object.get_files(): 70 | second_xor_key = self.find_second_xor_key() 71 | if second_xor_key: 72 | self.decrypt_files(second_xor_key) 73 | 74 | def find_xor_key(self): 75 | application_smali = self.find_main_application() 76 | target_method = self.find_method_re( 77 | application_smali, ".*", "(Ljava/io/InputStream;)Ljava/io/File;" 78 | ) 79 | if target_method == None: 80 | return 81 | sm = self.get_smali(target_method) 82 | if "ses.dex" not in sm: 83 | return 84 | m = re.findall(find_xor_key, sm) 85 | if len(m) == 1: 86 | return bytes(m[0][0].encode("utf-8")) 87 | else: 88 | return None 89 | 90 | def find_second_xor_key(self): 91 | if self.decrypted_payload_path == None: 92 | return 93 | with open(self.decrypted_payload_path, "rb") as fp: 94 | d = fp.read() 95 | dvm = DEX(d) 96 | for c in dvm.get_classes(): 97 | if c.get_superclassname() == "Landroid/app/Application;": 98 | for m in c.get_methods(): 99 | if m.get_name() == "onCreate": 100 | sm = self.get_smali(m) 101 | matches = re.findall(find_second_xor_key, sm) 102 | if matches: 103 | return matches[0][0].encode("utf-8") 104 | 105 | def decrypt_files(self, xor_key): 106 | for filepath in self.apk_object.get_files(): 107 | if not (filepath.startswith("assets") or filepath.startswith("res")): 108 | continue 109 | fd = self.apk_object.get_file(filepath) 110 | dec = xor(fd, xor_key) 111 | if self.check_and_write_file(dec): 112 | return True 113 | return False 114 | -------------------------------------------------------------------------------- /src/kavanoz/loader/simply_xor2.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from androguard.core.apk import APK 3 | from arc4 import ARC4 4 | from androguard.core.dex import DEX 5 | import re 6 | from itertools import combinations 7 | from kavanoz.unpack_plugin import Unpacker 8 | from kavanoz.smali_regexes import Regexs 9 | from kavanoz.utils import xor 10 | 11 | """ 12 | array-length v0, v3 13 | new-array v0, v0, [B 14 | const/4 v1, 0 15 | array-length v2, v3 16 | if-ge v1, v2, +c 17 | aget-byte v2, v3, v1 18 | xor-int/lit8 v2, v2, -43 19 | int-to-byte v2, v2 20 | aput-byte v2, v0, v1 21 | add-int/lit8 v1, v1, 1 22 | goto -c 23 | return-object v0 24 | """ 25 | 26 | find_xor_key = r"xor-int/lit8 [vp]\d+, [vp]\d+, (-?\d+)" 27 | """ 28 | invoke-virtual v0, v2, Ljava/io/InputStream;->read([B)I 29 | invoke-virtual v0, Ljava/io/InputStream;->close()V 30 | invoke-static v2, Lcom/squareup/leakcanary/gutG;->XdB([B)[B 31 | move-result-object v0 32 | invoke-virtual v6, v0, Ljava/io/FileOutputStream;->write([B)V 33 | invoke-virtual v6, Ljava/io/FileOutputStream;->close()V 34 | """ 35 | find_xor_function = ( 36 | r"invoke-virtual [vp]\d+, [vp]\d+, Ljava/io/InputStream;->read\(\[B\)I\s+" 37 | r"invoke-virtual [vp]\d+, Ljava/io/InputStream;->close\(\)V\s+" 38 | r"invoke-static [vp]\d+, (L[^;]+;->[^\(]+)\(\[B\)\[B\s+" 39 | r"move-result-object [vp]\d+\s+" 40 | r"invoke-virtual [vp]\d+, [vp]\d+, Ljava/io/FileOutputStream;->write\(\[B\)V\s+" 41 | ) 42 | 43 | 44 | class LoaderSimpleXor2(Unpacker): 45 | decrypted_payload_path = None 46 | 47 | def __init__(self, apk_object, dvms, output_dir): 48 | super().__init__( 49 | "loader.simplexor2", 50 | "Unpacker for multiple simple unpackers", 51 | apk_object, 52 | dvms, 53 | output_dir, 54 | ) 55 | 56 | def start_decrypt(self): 57 | self.logger.info("Starting to decrypt") 58 | self.xor_key = self.find_xor_key() 59 | if self.xor_key is None: 60 | return 61 | self.decrypt_files(self.xor_key) 62 | 63 | def find_xor_key(self): 64 | asset_filenames = [ 65 | x.replace("assets/", "") 66 | for x in self.apk_object.get_files() 67 | if x.startswith("assets/") 68 | ] 69 | for d in self.dexes: 70 | for c in d.get_classes(): 71 | for m in c.get_methods(): 72 | if ( 73 | m.get_descriptor() 74 | == "(Landroid/content/Context;)Ljava/io/File;" 75 | ): 76 | m_smali = self.get_smali(m) 77 | for fname in asset_filenames: 78 | if fname in m_smali: 79 | matches = re.findall("[a-f0-9]+\.dex", m_smali) 80 | if len(matches) == 0: 81 | self.logger.info("no match") 82 | return 83 | self.logger.info("Found method") 84 | target_method = m 85 | m = re.findall(find_xor_function, m_smali) 86 | klass, method = m[0].split("->") 87 | target_method = self.find_method(klass, method) 88 | if target_method: 89 | xor_key = re.findall( 90 | find_xor_key, self.get_smali(target_method) 91 | ) 92 | if xor_key: 93 | n = int(xor_key[0]) 94 | n = n & 0xFF 95 | self.logger.info(f"Found single xor key : {n}") 96 | return n.to_bytes(1, "little") 97 | return None 98 | 99 | def decrypt_files(self, xor_key): 100 | if self.decrypted_payload_path == None: 101 | out_file = "unpacked.dex" 102 | else: 103 | index = re.findall(r"\d+", self.decrypted_payload_path) 104 | if index: 105 | ii = int(index[0]) 106 | out_file = f"unpacked{ii+1}.dex" 107 | else: 108 | out_file = "unpacked1.dex" 109 | 110 | for filepath in self.apk_object.get_files(): 111 | if not (filepath.startswith("assets") or filepath.startswith("res")): 112 | continue 113 | fd = self.apk_object.get_file(filepath) 114 | dec = xor(fd, xor_key) 115 | if self.check_and_write_file(dec): 116 | return True 117 | return False 118 | -------------------------------------------------------------------------------- /src/kavanoz/loader/pronlocker.py: -------------------------------------------------------------------------------- 1 | import re 2 | from kavanoz.unpack_plugin import Unpacker 3 | from kavanoz.utils import xor 4 | 5 | 6 | # const/16 v0, 0xc 7 | # new-array v0, v0, [B 8 | # fill-array-data v0, :array_8 9 | # return-object v0 10 | byte_array_function = ( 11 | r"const/16 [vp]\d+, \d+\s+" 12 | r"new-array [vp]\d+, [vp]\d+, \[B\s+" 13 | r"fill-array-data [vp]\d+.*\s+" 14 | r"return-object [vp]\d+" 15 | ) 16 | 17 | 18 | class LoaderPr0nLocker(Unpacker): 19 | decrypted_payload_path = None 20 | 21 | def __init__(self, apk_object, dexes, output_dir): 22 | super().__init__( 23 | "loader.pr0nlocker", 24 | "Unpacker for pr0nlocker", 25 | apk_object, 26 | dexes, 27 | output_dir, 28 | ) 29 | 30 | def start_decrypt(self): 31 | self.logger.info("Starting to decrypt") 32 | self.xor_key = self.find_xor_key() 33 | if self.xor_key is None: 34 | return 35 | self.decrypt_files(bytes(self.xor_key)) 36 | 37 | def find_xor_key(self): 38 | """There is distinct string decryption function in the code takes float and string as input 39 | and returns string as output. We can find the string decryption function and then find the 40 | byte array function that is used to decrypt the asset files. We can then find the byte array 41 | data and use it as xor key to decrypt the asset files. 42 | """ 43 | 44 | found_str_decryptor = False 45 | str_decryptor_class = None 46 | for d in self.dexes: 47 | for c in d.get_classes(): 48 | for m in c.get_methods(): 49 | if ( 50 | m.get_descriptor() 51 | == "(Ljava/lang/Float; Ljava/lang/String;)Ljava/lang/String;" 52 | ): 53 | found_str_decryptor = True 54 | str_decryptor_class = c 55 | 56 | if not found_str_decryptor or str_decryptor_class is None: 57 | return None 58 | 59 | for m in str_decryptor_class.get_methods(): 60 | if m.get_descriptor() == "()[B": 61 | self.logger.info("Found byte array function") 62 | smali = self.get_smali(m) 63 | match = re.findall(byte_array_function, smali) 64 | if match: 65 | self.logger.info("Found byte array function") 66 | array_data = self.get_array_data(m) 67 | return array_data[0] 68 | return None 69 | 70 | def decrypt_files(self, xor_key: bytes): 71 | if self.decrypted_payload_path == None: 72 | out_file = "unpacked.dex" 73 | else: 74 | index = re.findall(r"\d+", self.decrypted_payload_path) 75 | if index: 76 | ii = int(index[0]) 77 | out_file = f"unpacked{ii+1}.dex" 78 | else: 79 | out_file = "unpacked1.dex" 80 | 81 | for filepath in self.apk_object.get_files(): 82 | # Assets have 4 files, 1 html,1 json config, 2 dex 83 | if not (filepath.startswith("assets") or filepath.startswith("res")): 84 | continue 85 | fd = self.apk_object.get_file(filepath) 86 | dec = xor(fd, xor_key) 87 | if self.check_and_write_file(dec): 88 | self.logger.info("Found encrypted file: %s", filepath) 89 | self.logger.info( 90 | "Writing decrypted file to: %s", self.decrypted_payload_path 91 | ) 92 | else: 93 | try: 94 | decrypted = dec.decode("utf-8") 95 | if decrypted.startswith(""): 96 | self.logger.info("Found html file:") 97 | calculated_name = self.calculate_name(dec) 98 | calculated_name = calculated_name.replace(".dex", ".html") 99 | self.logger.info( 100 | "Writing decrypted file to: %s", calculated_name 101 | ) 102 | with open(calculated_name, "w") as f: 103 | f.write(decrypted) 104 | else: 105 | self.logger.info("Found config file:") 106 | calculated_name = self.calculate_name(dec) 107 | calculated_name = calculated_name.replace(".dex", ".json") 108 | self.logger.info( 109 | "Writing decrypted file to: %s", calculated_name 110 | ) 111 | with open(calculated_name, "w") as f: 112 | f.write(decrypted) 113 | 114 | except: 115 | pass 116 | return False 117 | -------------------------------------------------------------------------------- /src/kavanoz/loader/old_rc4.py: -------------------------------------------------------------------------------- 1 | from androguard.core.apk import APK 2 | from androguard.core.dex import DEX 3 | import re 4 | from arc4 import ARC4 5 | from kavanoz.unpack_plugin import Unpacker 6 | 7 | 8 | class LoaderOldRc4(Unpacker): 9 | def __init__(self, apk_obj, dvms, output_dir): 10 | super().__init__( 11 | "loader.rc4.v2", 12 | "Unpacker old rc4 based variants", 13 | apk_obj, 14 | dvms, 15 | output_dir, 16 | ) 17 | 18 | def start_decrypt(self, native_lib: str = ""): 19 | self.logger.info("Starting to decrypt") 20 | self.decrypted_payload_path = None 21 | application_oncreate = self.find_application_oncreate() 22 | if not application_oncreate: 23 | return 24 | rc4_caller = self.find_caller_rc4_init(application_oncreate) 25 | if not rc4_caller: 26 | return 27 | rc4_inits = self.get_rc4_init_from_caller(rc4_caller) 28 | for rc4_init in rc4_inits: 29 | rc4_keys = self.get_rc4_key(rc4_init) 30 | for rc4_key in rc4_keys: 31 | x = self.brute_assets(rc4_key) 32 | if x != None: 33 | return 34 | 35 | def get_rc4_key(self, rc4_init_function): 36 | klass_name, method_name = rc4_init_function.split("->") 37 | m = self.find_method(klass_name, method_name, descriptor="()V") 38 | if m: 39 | self.logger.info(m.get_name()) 40 | array_data = self.get_array_data(m) 41 | if len(array_data) > 1: 42 | self.logger.info("Found multiple array data, might be wrong function") 43 | return array_data 44 | return [] 45 | 46 | def get_rc4_init_from_caller(self, class_func_str) -> list: 47 | klass_name, method_name = class_func_str.split("->") 48 | m = self.find_method(klass_name, method_name, "(Landroid/app/Application;)V") 49 | if m == None: 50 | return [] 51 | self.logger.info("Found rc4 init method") 52 | """ 53 | public void xVKoMuDKBel(Application application) { 54 | yQuzIA(); 55 | 56 | invoke-direct v11, Lcom/tnmwagts/rmorecegr/MPqJcHURCv;->yQuzIA()V 57 | """ 58 | smali_str = self.get_smali(m) 59 | # find functions without parameters. 60 | match = re.findall(r"invoke-direct [vp]\d+, (L[^;]+;->[^\s]+)\(\)V", smali_str) 61 | if len(match) == 0: 62 | self.logger.info("Unable to extract variable from target_method") 63 | self.logger.info("Exiting ...") 64 | return [] 65 | if len(match) == 1: 66 | self.logger.info(f"Found variable ! : {match[0]}") 67 | else: 68 | self.logger.info("Found multiple functions to call rc4_init 🤔") 69 | return match 70 | return [] 71 | 72 | def find_application_oncreate(self): 73 | application_smali = self.find_main_application() 74 | return self.find_method(application_smali, "onCreate") 75 | 76 | def find_caller_rc4_init(self, target_method): 77 | """ 78 | invoke-virtual v2, v6, Lcom/tnmwagts/rmorecegr/MPqJcHURCv;->xVKoMuDKBel(Landroid/app/Application;)V 79 | """ 80 | smali_str = self.get_smali(target_method) 81 | match = re.findall( 82 | r"invoke-virtual [vp]\d+, [vp]\d+, (L[^;]+;->[^\s]+)\(Landroid/app/Application;\)V\s+", 83 | smali_str, 84 | ) 85 | if len(match) == 0: 86 | self.logger.info("Unable to extract variable from target_method") 87 | self.logger.info("Exiting ...") 88 | return None 89 | if len(match) == 1: 90 | self.logger.info(f"Found variable ! : {match[0]}") 91 | return match[0] 92 | else: 93 | self.logger.info("Something is wrong .. 🤔") 94 | self.logger.info("Found multiple ?? : {match}") 95 | return None 96 | 97 | def brute_assets(self, key: bytes): 98 | self.logger.info("Starting brute-force") 99 | asset_list = self.apk_object.get_files() 100 | for filepath in asset_list: 101 | f = self.apk_object.get_file(filepath) 102 | if self.solve_encryption(f, key, filepath): 103 | self.logger.info(f"Decryption finished! {self.decrypted_payload_path}") 104 | return self.decrypted_payload_path 105 | self.logger.info(f"No valid file found for {key}") 106 | return None 107 | 108 | def solve_encryption(self, file_data, key: bytes, filepath: str): 109 | arc4 = ARC4(bytes(key)) 110 | filesize = int.from_bytes(file_data[0:4], byteorder="little") 111 | if filesize > len(file_data): 112 | return False 113 | decrypted = arc4.decrypt(file_data[4:]) 114 | decrypted = decrypted[:filesize] 115 | if self.check_and_write_file(decrypted): 116 | return True 117 | return False 118 | -------------------------------------------------------------------------------- /src/kavanoz/loader/simple_aes.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | import base64 3 | from datetime import datetime 4 | from androguard.core.apk import APK 5 | from arc4 import ARC4 6 | from androguard.core.dex import DEX 7 | import re 8 | from itertools import combinations 9 | from kavanoz.unpack_plugin import Unpacker 10 | from kavanoz.smali_regexes import Regexs 11 | from kavanoz.utils import xor 12 | 13 | """ 14 | invoke-direct v2, v0, v3, Ljava/io/File;->(Ljava/io/File; Ljava/lang/String;)V 15 | invoke-direct v7, v8, v1, v2, Lbtewtslyl/vmcdkpllfzrvt/u5a48eebb7c1d4;->a(Landroid/content/Context; Ljava/lang/String; Ljava/io/File;)Z 16 | new-instance v3, Ldalvik/system/DexClassLoader; 17 | """ 18 | find_aes_function = ( 19 | r"invoke-direct [vp]\d+, [vp]\d+, [vp]\d+, Ljava/io/File;->\(Ljava/io/File; Ljava/lang/String;\)V\s+" 20 | r"invoke-direct [vp]\d+, [vp]\d+, [vp]\d+, [vp]\d+, (L[^;]+;->[^\(]+)\(Landroid/content/Context; Ljava/lang/String; Ljava/io/File;\)Z\s+" 21 | r"new-instance [vp]\d+, Ldalvik/system/DexClassLoader;" 22 | ) 23 | """ 24 | new-instance v2, Ljavax/crypto/CipherInputStream; 25 | const-string v3, '7RHkUDPB5fGL4NLPDuehSRjnxYGr0I7KmsqAUwLT1sk=' 26 | invoke-direct v4, v3, Lbtewtslyl/vmcdkpllfzrvt/u5a48eebb7c1d4;->a(Ljava/lang/String;)Ljavax/crypto/Cipher; 27 | move-result-object v3 28 | """ 29 | 30 | find_aes_key = ( 31 | r"new-instance [vp]\d+, Ljavax/crypto/CipherInputStream;\s+" 32 | r"const-string [vp]\d+, \"(.*)\"\s+" 33 | r"invoke-direct [vp]\d+, [vp]\d+, L[^;]+;->[^\(]+\(Ljava/lang/String;\)Ljavax/crypto/Cipher;\s+" 34 | r"move-result-object [vp]\d+" 35 | ) 36 | 37 | 38 | class LoaderSimpleAes(Unpacker): 39 | decrypted_payload_path = None 40 | 41 | def __init__(self, apk_object, dvms, output_dir): 42 | super().__init__( 43 | "loader.simpleaes", 44 | "Unpacker for multiple simple unpackers", 45 | apk_object, 46 | dvms, 47 | output_dir, 48 | ) 49 | 50 | def start_decrypt(self): 51 | self.logger.info("Starting to decrypt") 52 | self.aes_key = self.find_aes_key() 53 | if self.aes_key is None: 54 | return 55 | self.decrypt_files(self.aes_key) 56 | 57 | def find_aes_key(self): 58 | asset_filenames = [ 59 | x.replace("assets/", "") 60 | for x in self.apk_object.get_files() 61 | if x.startswith("assets/") 62 | ] 63 | for d in self.dexes: 64 | for c in d.get_classes(): 65 | for m in c.get_methods(): 66 | if ( 67 | m.get_descriptor() == "(Landroid/content/Context;)V" 68 | and m.get_name() == "" 69 | ): 70 | m_smali = self.get_smali(m) 71 | 72 | for fname in asset_filenames: 73 | if fname in m_smali: 74 | self.logger.info("Found method") 75 | target_method = m 76 | m = re.findall(find_aes_function, m_smali) 77 | if len(m) == 0: 78 | continue 79 | klass, method = m[0].split("->") 80 | target_method = self.find_method( 81 | klass, 82 | method, 83 | "(Landroid/content/Context; Ljava/lang/String; Ljava/io/File;)Z", 84 | ) 85 | if target_method: 86 | aes_key = re.findall( 87 | find_aes_key, self.get_smali(target_method) 88 | ) 89 | if aes_key: 90 | return aes_key[0] 91 | 92 | return None 93 | 94 | def decrypt_files(self, aes_key): 95 | try: 96 | kk = base64.b64decode(aes_key) 97 | except Exception as e: 98 | self.logger.error(e) 99 | return 100 | 101 | key = kk[:16] 102 | iv = kk[16:32] 103 | ai = AES.new(key, AES.MODE_CBC, iv) 104 | if self.decrypted_payload_path == None: 105 | out_file = "unpacked.dex" 106 | else: 107 | index = re.findall(r"\d+", self.decrypted_payload_path) 108 | if index: 109 | ii = int(index[0]) 110 | out_file = f"unpacked{ii+1}.dex" 111 | else: 112 | out_file = "unpacked1.dex" 113 | 114 | for filepath in self.apk_object.get_files(): 115 | if not (filepath.startswith("assets") or filepath.startswith("res")): 116 | continue 117 | fd = self.apk_object.get_file(filepath) 118 | dec = ai.decrypt(fd) 119 | if self.check_and_write_file(dec): 120 | return True 121 | return False 122 | -------------------------------------------------------------------------------- /src/kavanoz/smali_regexes.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class Regexs: 5 | def __init__(self): 6 | self.first_inner_regex = {} 7 | 8 | def set_first_inner_regex(self, rc4_string_var: str): 9 | # invoke-static v0, Lcom/huge/dragon/DEdEoXwGgUxOmDnIdQiBhAeDwDbFbByQwQfQtYuWk;->meattool(B)Ljava/lang/String; 10 | # move-result-object v0 11 | # iput-object v0, v3, Lcom/huge/dragon/DEdEoXwGgUxOmDnIdQiBhAeDwDbFbByQwQfQtYuWk;->XZkYqPfMoCcNtMzDiIpGaYlRuDjFeZfMtPcSq Ljava/lang/String; 12 | 13 | first_inner_1 = ( 14 | rf"invoke-static [vp]\d+, L[^;]+;->([^\(]+)\([a-zA-Z0-9;/]+\)Ljava/lang/String;\s+" 15 | "move-result-object [vp]\d+\s+" 16 | f"iput-object [vp]\d+, [vp]\d+, L[^;]+;->{rc4_string_var} Ljava/lang/String;" 17 | ) 18 | 19 | # invoke-static Lsquare/ivory/purchase/YKxOcNuRkOlYhOySzZjCsYqLcJkYuUlJdTfTqMeMgXuOnUzEjNiSs;->antiquehello()Ljava/lang/StringBuilder; 20 | # move-result-object v0 21 | # invoke-static v0, Ljava/lang/String;->valueOf(Ljava/lang/Object;)Ljava/lang/String; 22 | 23 | first_inner_2 = ( 24 | r"invoke-static L[^;]+;->([^\(]+)\(\w*\)+Ljava/lang/StringBuilder;\s+" 25 | r"move-result-object [vp]\d+\s+" 26 | r"invoke-static [vp]\d+, Ljava/lang/String;->valueOf\(Ljava/lang/Object;\)Ljava/lang/String;\s+" 27 | r"move-result-object [vp]\d+\s+" 28 | rf"iput-object [vp]\d+, [vp]\d+, L[^;]+;->{rc4_string_var} Ljava/lang/String;" 29 | ) 30 | 31 | first_inner_6 = ( 32 | r"invoke-static [vp]\d+, L[^;]+;->([^\(]+)\(\w*\)+Ljava/lang/StringBuilder;\s+" 33 | r"move-result-object [vp]\d+\s+" 34 | r"invoke-static [vp]\d+, Ljava/lang/String;->valueOf\(Ljava/lang/Object;\)Ljava/lang/String;\s+" 35 | r"move-result-object [vp]\d+\s+" 36 | rf"iput-object [vp]\d+, [vp]\d+, L[^;]+;->{rc4_string_var} Ljava/lang/String;" 37 | ) 38 | first_inner_9 = ( 39 | r"invoke-static L[^;]+;->([^\(]+)\(\)+Ljava/lang/StringBuilder;\s+" 40 | r"move-result-object [vp]\d+\s+" 41 | r"invoke-virtual [vp]\d+, Ljava/lang/StringBuilder;->toString\(\)Ljava/lang/String;\s+" 42 | r"move-result-object [vp]\d+\s+" 43 | rf"iput-object [vp]\d+, [vp]\d+, L[^;]+;->{rc4_string_var} Ljava/lang/String;" 44 | ) 45 | # invoke-static Lcom/marine/build/NKlIyWrPrYoKzZyRtDsOnKnJtNkNcEoOePzLtNg;->dressraccoon()Ljava/lang/String; 46 | # move-result-object v0 47 | # iput-object v0, v3, Lcom/marine/build/NKlIyWrPrYoKzZyRtDsOnKnJtNkNcEoOePzLtNg;->QFxOwNaMaJqTgNdOhOc Ljava/lang/String; 48 | 49 | first_inner_3 = ( 50 | "invoke-static L[^;]+;->([^\(]+)\(\w*\)Ljava\/lang\/String;\s+" 51 | "move-result-object [vp]\d+\s+" 52 | f"iput-object [vp]\d+, [vp]\d+, L[^;]+;->{rc4_string_var} Ljava/lang/String;" 53 | ) 54 | 55 | first_inner_7 = ( 56 | "invoke-static [vp]\d+, L[^;]+;->([^\(]+)\(Ljava/lang/String;\)Ljava\/lang\/String;\s+" 57 | "move-result-object [vp]\d+\s+" 58 | f"iput-object [vp]\d+, [vp]\d+, L[^;]+;->{rc4_string_var} Ljava/lang/String;" 59 | ) 60 | first_inner_10 = ( 61 | r"invoke-static [vp]\d+, L[^;]+;->([^\(]+)\(\[I\)Ljava\/lang\/String;\s+" 62 | "move-result-object [vp]\d+\s+" 63 | f"iput-object [vp]\d+, [vp]\d+, L[^;]+;->{rc4_string_var} Ljava/lang/String;" 64 | ) 65 | 66 | # invoke-static {v0}, Lcom/frog/assault/ZQwAlNnFmAdZiOe;->corespy([Ljava/lang/String;)Ljava/lang/String; 67 | # move-result-object v0 68 | # iput-object v0, p0, Lcom/frog/assault/ZQwAlNnFmAdZiOe;->OPxNlTsOuSiJtOg:Ljava/lang/String; 69 | first_inner_11 = ( 70 | r"invoke-static [vp]\d+, L[^;]+;->([^\(]+)\(\[Ljava/lang/String;\)Ljava\/lang\/String;\s+" 71 | r"move-result-object [vp]\d+\s+" 72 | rf"iput-object [vp]\d+, [vp]\d+, L[^;]+;->{rc4_string_var} Ljava/lang/String;" 73 | ) 74 | # invoke-static v0, Ljknl/bgdfdntrwerlfwaxohrcyamosg/rathpswbuyyukhdihs/Qreunionscience;->symbolraise(Ljava/lang/Boolean;)Ljava/lang/StringBuffer; 75 | # move-result-object v0 76 | # invoke-virtual v0, Ljava/lang/StringBuffer;->toString()Ljava/lang/String; 77 | # move-result-object v0 78 | # iput-object v0, v4, Ljknl/bgdfdntrwerlfwaxohrcyamosg/rathpswbuyyukhdihs/Qreunionscience;->Salterrail Ljava/lang/String; 79 | first_inner_4 = ( 80 | r"invoke-static [vp]\d+, L[^;]+;->([^\(]+)\(Ljava/lang/Object\)Ljava/lang/StringBuffer;\s+" 81 | r"move-result-object [vp]\d+\s+" 82 | r"invoke-virtual [vp]\d+, Ljava/lang/StringBuffer;->toString\(\)Ljava/lang/String;\s+" 83 | r"move-result-object [vp]\d+\s+" 84 | rf"iput-object [vp]\d+, [vp]\d+, L[^;]+;->{rc4_string_var} Ljava/lang/String;" 85 | ) 86 | 87 | # invoke-static Lrecall/promote/hidden/IYmMtEjAhYyTpQz;->incomeagain()Ljava/lang/StringBuffer; 88 | # move-result-object v0 89 | # invoke-virtual v0, Ljava/lang/StringBuffer;->toString()Ljava/lang/String; 90 | # move-result-object v0 91 | # iput-object v0, v3, Lrecall/promote/hidden/IYmMtEjAhYyTpQz;->QOlUeNyKnHtWmHdEnUs Ljava/lang/String; 92 | 93 | first_inner_5 = ( 94 | r"invoke-static L[^;]+;->([^\(]+)\(\)Ljava/lang/StringBuffer;\s+" 95 | r"move-result-object [vp]\d+\s+" 96 | r"invoke-virtual [vp]\d+, Ljava/lang/StringBuffer;->toString\(\)Ljava/lang/String;\s+" 97 | r"move-result-object [vp]\d+\s+" 98 | rf"iput-object [vp]\d+, [vp]\d+, L[^;]+;->{rc4_string_var} Ljava/lang/String;" 99 | ) 100 | # invoke-static v0, Lpousozuqamiyngkkoczbahranxo/efcwgecwerpfesmilxxmkco/tpqbn/Pwrongmatch;->trialalert(Z)Ljava/lang/StringBuilder; 101 | # move-result-object v0 102 | # invoke-virtual v0, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; 103 | # move-result-object v0 104 | # iput-object v0, v4, Lpousozuqamiyngkkoczbahranxo/efcwgecwerpfesmilxxmkco/tpqbn/Pwrongmatch;->Qtwintattoo Ljava/lang/String; 105 | 106 | first_inner_8 = ( 107 | r"invoke-static [vp]\d+, L[^;]+;->([^\(]+)\(Z\)Ljava/lang/StringBuilder;\s+" 108 | r"move-result-object [vp]\d+\s+" 109 | r"invoke-virtual [vp]\d+, Ljava/lang/StringBuilder;->toString\(\)Ljava/lang/String;\s+" 110 | r"move-result-object [vp]\d+\s+" 111 | rf"iput-object [vp]\d+, [vp]\d+, L[^;]+;->{rc4_string_var} Ljava/lang/String;" 112 | ) 113 | # invoke-static v0, Lcom/chuckle/define/QXqAuXjWzGjAnCiYyKoKp;->censusasset(Ljava/lang/StringBuilder;)Ljava/lang/String; 114 | # move-result-object v0 115 | # iput-object v0, v3, Lcom/chuckle/define/QXqAuXjWzGjAnCiYyKoKp;->TKiLpUsQmOtFqOoEoYjKpCxGeHjFeMlAoQfQuJmRdOwFwLw Ljava/lang/String; 116 | 117 | first_inner_12 = ( 118 | "invoke-static [vp]\d+, L[^;]+;->([^\(]+)\(Ljava/lang/StringBuilder;\)Ljava/lang/String;\s+" 119 | "move-result-object [vp]\d+\s+" 120 | f"iput-object [vp]\d+, [vp]\d+, L[^;]+;->{rc4_string_var} Ljava/lang/String;" 121 | ) 122 | 123 | # 001e56f2: 7110 af2f 0000 005b: invoke-static {v0}, Lcom/chuckle/sunset/IGlYqYmEaEdKmJiYhUxMyOpKlTrHi;->praiseload(Ljava/io/FileDescriptor;)[C # method@2faf 124 | # 001e56f8: 0c00 005e: move-result-object v0 125 | # 001e56fa: 7110 8a47 0000 005f: invoke-static {v0}, Ljava/lang/String;->valueOf([C)Ljava/lang/String; # method@478a 126 | # 001e5700: 0c00 0062: move-result-object v0 127 | # 001e5702: 5b10 2614 0063: iput-object v0, v1, Lcom/chuckle/sunset/IGlYqYmEaEdKmJiYhUxMyOpKlTrHi;->dDybYjxWbGQoGcmLQGyU_818756:Ljava/lang/String; # field@1426 128 | first_inner_13 = ( 129 | "invoke-static [vp]\d+, L[^;]+;->([^\(]+)\(Ljava/io/FileDescriptor;\)\[C\s+" 130 | "move-result-object [vp]\d+\s+" 131 | "invoke-static [vp]\d+, Ljava/lang/String;->valueOf\(\[C\)Ljava/lang/String;\s+" 132 | "move-result-object [vp]\d+\s+" 133 | f"iput-object [vp]\d+, [vp]\d+, L[^;]+;->{rc4_string_var} Ljava/lang/String;" 134 | ) 135 | 136 | self.first_inner_regex["first_variant"] = re.compile(first_inner_1) 137 | self.first_inner_regex["second_variant"] = re.compile(first_inner_2) 138 | self.first_inner_regex["third_variant"] = re.compile(first_inner_3) 139 | self.first_inner_regex["fourth_variant"] = re.compile(first_inner_4) 140 | self.first_inner_regex["fifth_variant"] = re.compile(first_inner_5) 141 | self.first_inner_regex["six_variant"] = re.compile(first_inner_6) 142 | self.first_inner_regex["seven_variant"] = re.compile(first_inner_7) 143 | self.first_inner_regex["eight_variant"] = re.compile(first_inner_8) 144 | self.first_inner_regex["nine_variant"] = re.compile(first_inner_9) 145 | self.first_inner_regex["ten_variant"] = re.compile(first_inner_10) 146 | self.first_inner_regex["eleven_variant"] = re.compile(first_inner_11) 147 | self.first_inner_regex["12"] = re.compile(first_inner_12) 148 | self.first_inner_regex["13"] = re.compile(first_inner_13) 149 | 150 | def get_first_inner_regexs(self) -> dict: 151 | return self.first_inner_regex 152 | 153 | @staticmethod 154 | def get_encrytion_route_regex() -> re: 155 | first_encrytion_route = ( 156 | "invoke-virtual [vp]\d+, [vp]\d+, L[^\s]+;->([^\s]+)\(Ljava/lang/String;\)Z" 157 | ) 158 | return re.compile(first_encrytion_route) 159 | 160 | @staticmethod 161 | def get_key_class_regex() -> re: 162 | """ 163 | iput-object v0, v4, Lsolve/expect/water/DWcJrLbBhZuZeQxRsByOgHrOgEwAb;->PTuCuGcNoJqKpAnWyFoNfHoSj Landroid/content/Context; 164 | iget-object v0, v4, Lsolve/expect/water/DWcJrLbBhZuZeQxRsByOgHrOgEwAb;->PTuCuGcNoJqKpAnWyFoNfHoSj Landroid/content/Context; 165 | iget-object v1, v4, Lsolve/expect/water/DWcJrLbBhZuZeQxRsByOgHrOgEwAb;->HLcJdEiQiGcGpHyIwXrTiXuQiLqIaRgGt Ljava/lang/String; 166 | invoke-static v5, v0, v1, Lsolve/expect/water/GKfMeQmSxLkSzNmFoUhBxJrOjAiRsAyGbThJnQhOkNiRuIxUf;->shrugbalcony(Ljava/lang/String; Landroid/content/Context; Ljava/lang/String;)Z 167 | move-result v5 168 | return v5 169 | """ 170 | key_class_regex = ( 171 | r"invoke-(virtual|static) ([vp]\d+, ){3,4}(L[^\(]+;)->[^\(]+\(Ljava/lang/String; Landroid/content/Context; Ljava/lang/String;\)Z\s+" 172 | r"move-result [vp]\d+\s+" 173 | r"return [vp]\d+" 174 | ) 175 | return re.compile(key_class_regex) 176 | 177 | @staticmethod 178 | def get_second_inner_regex(): 179 | second_inner = "invoke-static L[^\(]+;->([^\(]+)\(\)Ljava/lang/String;" 180 | return re.compile(second_inner) 181 | -------------------------------------------------------------------------------- /src/kavanoz/loader/rc4.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from androguard.core.apk import APK 3 | from arc4 import ARC4 4 | from androguard.core.dex import DEX 5 | import re 6 | from itertools import combinations 7 | from kavanoz.unpack_plugin import Unpacker 8 | from kavanoz.smali_regexes import Regexs 9 | import string 10 | 11 | 12 | class LoaderRc4(Unpacker): 13 | regex_class = Regexs() 14 | rc4_string_var = "" 15 | first_inner = [] 16 | second_inner = [] 17 | byte_array_data = [] 18 | decrypted_payload_path = None 19 | 20 | def __init__(self, apk_object, dvms, output_dir): 21 | super().__init__( 22 | "loader.rc4.v1", "Unpacker rc4 based variants", apk_object, dvms, output_dir 23 | ) 24 | 25 | def start_decrypt(self): 26 | self.second_inner_regex = self.regex_class.get_second_inner_regex() 27 | self.first_encryption_route = self.regex_class.get_encrytion_route_regex() 28 | self.key_class_regex = self.regex_class.get_key_class_regex() 29 | self.logger.info("Starting to decrypt") 30 | self.attach_class = self.find_attach_class() 31 | if self.attach_class is None: 32 | return 33 | 34 | all_possible_rc4_keys = self.find_rc4_keys_from_attach_class(self.attach_class) 35 | self.logger.info(f"all possible keys : {all_possible_rc4_keys}") 36 | if all_possible_rc4_keys: 37 | if self.decrypt_files(all_possible_rc4_keys): 38 | # More stages 39 | if not self.is_really_unpacked(): 40 | if self.bruteforce_all_strings(): 41 | self.logger.info("Multiple stage is decrypted") 42 | else: 43 | if self.bruteforce_all_strings(): 44 | self.logger.info("Multiple stage is decrypted") 45 | 46 | def bruteforce_all_strings(self): 47 | if not self.is_really_unpacked(): 48 | all_possible_rc4_keys = list( 49 | filter( 50 | lambda x: x != None, 51 | self.find_all_strings(self.dexes[-1]), 52 | ) 53 | ) 54 | if self.decrypt_files(all_possible_rc4_keys): 55 | return self.bruteforce_all_strings() 56 | else: 57 | return False 58 | else: 59 | return True 60 | 61 | def find_attach_class(self): 62 | application_smali = self.find_main_application() 63 | target_method = self.find_method(application_smali, "attachBaseContext") 64 | return target_method 65 | 66 | def find_application_init(self): 67 | application_smali = self.find_main_application() 68 | target_method = self.find_method(application_smali, "") 69 | return target_method 70 | 71 | def find_rc4_keys_from_attach_class(self, target_method): 72 | smali_str = self.get_smali(target_method) 73 | match = self.first_encryption_route.findall(smali_str) 74 | if len(match) == 0: 75 | self.logger.info(f"Unable to extract variable from {target_method}") 76 | self.logger.info("Exiting ...") 77 | 78 | if len(match) == 1: 79 | # self.logger.info(f'HMM : {match[0]}') 80 | method = self.find_method(target_method.get_class_name(), match[0]) 81 | if method == None: 82 | return 83 | smali_str = self.get_smali(method) 84 | # self.logger.info(smali_str) 85 | key_class = self.key_class_regex.findall(smali_str) 86 | if len(key_class) != 1: 87 | return 88 | self.logger.info(f"Key class : {key_class[0]}") 89 | klass = self.find_class_in_dvms(key_class[0][2]) 90 | if klass == None: 91 | return 92 | return self.find_rc4_keys_from_klass_fields(klass) 93 | 94 | def find_all_strings(self, dvm: DEX) -> set: 95 | all_rc4_keys = set() 96 | for klass in dvm.get_classes(): 97 | # all_rc4_keys.update(self.find_rc4_keys_from_klass_fields(klass)) 98 | all_rc4_keys.update(self.find_rc4_keys_from_static_methods(klass)) 99 | return all_rc4_keys 100 | 101 | def find_all_strings_from_application_class(self, dvm: DEX) -> set: 102 | application_smali = self.find_main_application() 103 | klass = self.find_class_in_dvms(application_smali) 104 | all_rc4_keys = set() 105 | all_rc4_keys.update(self.find_rc4_keys_from_klass_fields(klass)) 106 | return all_rc4_keys 107 | 108 | def find_rc4_keys_from_static_methods(self, klass) -> set: 109 | all_possible_rc4_keys = set() 110 | for method in klass.get_methods(): 111 | if ( 112 | "static" in method.get_access_flags_string() 113 | and "Ljava/lang/String;" in method.get_descriptor() 114 | ): 115 | res = self.generate_rc4_keys_from_method(method) 116 | if len(res) > 0: 117 | all_possible_rc4_keys.update(res) 118 | return all_possible_rc4_keys 119 | 120 | def find_rc4_keys_from_klass_fields(self, klass) -> set: 121 | all_possible_rc4_keys = set() 122 | for field in klass.get_fields(): 123 | rc4_string_variable = None 124 | if field.get_descriptor() != "Ljava/lang/String;": 125 | continue 126 | if field.get_init_value() != None and field.get_init_value != "": 127 | self.logger.info( 128 | f"Found static key : {field.get_init_value().get_value()}" 129 | ) 130 | static_rc4_string = field.get_init_value().get_value() 131 | r = set() 132 | r.add(static_rc4_string.encode()) 133 | return r 134 | else: 135 | if ( 136 | "0x0" == field.get_access_flags_string() 137 | or "protected final" == field.get_access_flags_string() 138 | or "" == field.get_access_flags_string() 139 | ): 140 | rc4_string_variable = field.get_name() 141 | if rc4_string_variable is not None: 142 | self.regex_class.set_first_inner_regex(rc4_string_variable) 143 | all_possible_rc4_keys.update(self.get_key_from_init(klass)) 144 | return all_possible_rc4_keys 145 | 146 | def get_key_from_init(self, klass) -> set: 147 | """ 148 | String field is calculated with two inner functions. 149 | Example : 150 | - String SAsDiYdEsXlNsTnXkKoYoSmZp = derivetrouble(new String[98]); 151 | - static String derivetrouble(String[] strArray) { 152 | return leaveangry(); 153 | } 154 | - public static String leaveangry() { 155 | byte[] bArr = {11, 63, 45, 21}; 156 | byte[] bArr2 = new byte[4]; 157 | byte[] bArr3 = {79}; 158 | while (i8 < 4) { 159 | bArr2[i8] = (byte) (bArr[i8] ^ bArr3[i8 % 1]); 160 | i8++; 161 | } 162 | return new String(bArr2); 163 | } 164 | We try to find second inner function that generates rc4 key 165 | """ 166 | possible_rc4_keys = set() 167 | string_gen_0 = [] 168 | klass_name = klass.get_name() 169 | init_method = self.find_method(klass_name, "") 170 | if not init_method: 171 | return possible_rc4_keys 172 | smali_str = self.get_smali(init_method) 173 | for key, regex in self.regex_class.get_first_inner_regexs().items(): 174 | string_gen_0 = regex.findall(smali_str) 175 | if string_gen_0: 176 | break 177 | if string_gen_0: 178 | self.logger.info(f"First inner function: {string_gen_0[0]}") 179 | string_gen_1 = [] 180 | # Find function that uses first found function 181 | first_method = self.find_method(klass_name, string_gen_0[0]) 182 | if not first_method: 183 | return possible_rc4_keys 184 | smali_str = self.get_smali(first_method) 185 | string_gen_1 = self.second_inner_regex.findall(smali_str) 186 | if not string_gen_1: 187 | self.logger.info( 188 | f"Unable to extract second inner function from {first_method.get_name()}" 189 | ) 190 | self.logger.info("Checking if we are already in the last function") 191 | rc4_keys = self.generate_rc4_keys_from_method(first_method) 192 | possible_rc4_keys.update(rc4_keys) 193 | 194 | else: 195 | self.logger.info(f"Second inner function: {string_gen_1[0]}") 196 | if string_gen_1: 197 | second_method = self.find_method(klass_name, string_gen_1[0]) 198 | if second_method: 199 | rc4_keys = self.generate_rc4_keys_from_method(second_method) 200 | possible_rc4_keys.update(rc4_keys) 201 | else: 202 | self.logger.info("Unable to extract first inner function") 203 | 204 | return possible_rc4_keys 205 | 206 | def generate_rc4_keys_from_method(self, method) -> set: 207 | """ 208 | Extract array data from target method. Generaly packer generates rc4 key from two array data. 209 | Or defines constant string. 210 | First regex captures string, 211 | """ 212 | # self.logger.info(self.get_smali(method)) 213 | smali = self.get_smali(method) 214 | match = re.findall( 215 | r"const-string [vp]\d+, \'(.*?)\'\s+" r"return-object [vp]\d+", smali 216 | ) 217 | if len(match) == 1: 218 | self.logger.info(match) 219 | # self.decrypt_files([match[0]]) 220 | r = set() 221 | k = match[0] 222 | if type(k) is bytes or type(k) is str: 223 | r.add(k) 224 | return r 225 | 226 | arrays_in_method = self.get_array_data(method) 227 | if len(arrays_in_method) < 2: 228 | return set() 229 | if len(arrays_in_method) > 2: 230 | self.logger.info( 231 | f"We have {len(self.byte_array_data)} byte arrays, so gonna brute force little bit" 232 | ) 233 | if len(arrays_in_method) == 2: 234 | self.logger.info( 235 | f"RC4 key generators : {arrays_in_method[0]} - {arrays_in_method[1]}" 236 | ) 237 | rc4_keys = self.get_all_rc4_keys(arrays_in_method) 238 | return rc4_keys 239 | 240 | def decrypt_files(self, rc4key): 241 | for filepath in self.apk_object.get_files(): 242 | if filepath.endswith(".json"): 243 | fd = self.apk_object.get_file(filepath) 244 | for rc4k in rc4key: 245 | if len(rc4k) > 0: 246 | dede = ARC4(rc4k) 247 | dec = dede.decrypt(fd[:8]) 248 | if self.check_header(dec): 249 | dede = ARC4(rc4k) 250 | dec = dede.decrypt(fd) 251 | if self.check_and_write_file(dec): 252 | self.logger.info( 253 | f"Decrypted dex is from {filepath} with key {rc4k}" 254 | ) 255 | return True 256 | return False 257 | 258 | def get_all_rc4_keys(self, keys: list) -> set: 259 | rc4_key = set() 260 | if len(keys) > 2: 261 | # combinartion of keys 262 | comb = combinations(keys, 2) 263 | rc4_key = set() 264 | for k in comb: 265 | rc4_key.add(self.generate_rc4_key(k[0], k[1], True)) 266 | for k in keys: 267 | rc4_key.add(bytes(k)) 268 | else: 269 | rc4_key.add(self.generate_rc4_key(keys[0], keys[1])) 270 | rc4_key.add(self.generate_rc4_key(keys[0], keys[1], True)) 271 | return rc4_key 272 | 273 | def generate_rc4_key(self, key0, key1, without_arrange=False): 274 | big_key = key0 if len(key0) > len(key1) else key1 275 | smol_key = key0 if len(key0) < len(key1) else key1 276 | rc4_key = bytearray() 277 | for i in range(len(big_key)): 278 | zz = big_key[i] ^ smol_key[i % len(smol_key)] 279 | rc4_key.append(zz) 280 | 281 | if without_arrange: 282 | rc4_key = bytearray() 283 | for i in range(len(key0)): 284 | rc4_key.append(key0[i] ^ key1[i % len(key1)]) 285 | return bytes(rc4_key) 286 | return bytes(rc4_key) 287 | -------------------------------------------------------------------------------- /src/kavanoz/unpack_plugin.py: -------------------------------------------------------------------------------- 1 | from androguard.core.apk import APK 2 | import re 3 | from androguard.core.dex import DEX, EncodedMethod, ClassDefItem 4 | import time 5 | import io 6 | import zipfile 7 | import hashlib 8 | import zlib 9 | from kavanoz.utils import dex_headers, pkzip_headers, zlib_headers 10 | from loguru import logger 11 | import os 12 | 13 | 14 | class Unpacker: 15 | tag = "DefaultUnpackPlugin" 16 | name = "DefaultUnpackName" 17 | 18 | def __init__( 19 | self, 20 | tag: str, 21 | name: str, 22 | apk_object: APK, 23 | dexes: list[DEX], 24 | output_dir, 25 | ): 26 | """Default unpacking plugin""" 27 | self.tag = tag 28 | self.name = name 29 | self.decrypted_payload_path = None 30 | self.logger = logger 31 | self.apk_object = apk_object 32 | self.dexes = list(filter(self.filter_dvms, dexes)) 33 | if output_dir: 34 | self.output_dir = output_dir 35 | else: 36 | self.output_dir = os.getcwd() 37 | 38 | @staticmethod 39 | def filter_dvms(dvm): 40 | if dvm.classes == None: 41 | return False 42 | return True 43 | 44 | def is_packed(self) -> bool: 45 | """Checks if apk is packed by checking components defined in AndroidManifest.xml is present in dex 46 | 47 | :returns ispacked: Is apk packed 48 | :rtype:bool 49 | """ 50 | ispacked = False 51 | not_found_counter = 0 52 | act_serv_recv = ( 53 | self.apk_object.get_activities() 54 | + self.apk_object.get_receivers() 55 | + self.apk_object.get_services() 56 | ) 57 | for component in act_serv_recv: 58 | if component: 59 | for dex in self.dexes: 60 | try: 61 | dex_classes = dex.get_classes_names() 62 | except Exception as e: 63 | continue 64 | clas_name = "L" + component.replace(".", "/") + ";" 65 | if clas_name in dex_classes: 66 | break 67 | else: 68 | not_found_counter += 1 69 | if len(act_serv_recv) == 0: 70 | return False 71 | score = not_found_counter / len(act_serv_recv) 72 | self.logger.info(f"Packed : Score : {score}") 73 | if score > 0.80: 74 | ispacked = True 75 | elif score == 0.0: 76 | ispacked = False 77 | else: 78 | # Lets check if MainActivity is present 79 | res = self.apk_object.get_main_activity() 80 | if res: 81 | for dex in self.dexes: 82 | try: 83 | dex_classes = dex.get_classes_names() 84 | except Exception as e: 85 | continue 86 | clas_name = "L" + res.replace(".", "/") + ";" 87 | if clas_name in dex_classes: 88 | break 89 | else: 90 | ispacked = True 91 | return ispacked 92 | 93 | def is_really_unpacked(self) -> bool: 94 | """Adds decrypted dex file as dvm and checks if its still packed or not""" 95 | if not self.decrypted_payload_path: 96 | return False 97 | # add last dvm 98 | with open(self.decrypted_payload_path, "rb") as fp: 99 | self.dexes.append(DEX(fp.read())) 100 | return not self.is_packed() 101 | 102 | def get_tag(self) -> str: 103 | return self.tag 104 | 105 | def get_name(self) -> str: 106 | return self.name 107 | 108 | def __str__(self): 109 | return f"Name: {self.name}\nTag: {self.tag}" 110 | 111 | @staticmethod 112 | def get_smali(target_method: EncodedMethod) -> str: 113 | """ 114 | Get smali represantation of target_method 115 | """ 116 | smali_str = "" 117 | for ins in target_method.get_instructions(): 118 | smali_str += f"{ins.get_name()} {ins.get_output()}\n" 119 | return smali_str 120 | 121 | @staticmethod 122 | def get_array_data(target_method: EncodedMethod) -> list: 123 | """ 124 | Get array data from target_method. This is done via parsing instructions 125 | """ 126 | barrays = [] 127 | for ins in target_method.get_instructions(): 128 | if ins.get_name() == "fill-array-data-payload": 129 | # androguard bug 130 | # 00 03 01 00 07 00 00 00 5e 5a 6a 71 5e 6c 74 00 131 | # Following code has wrong data, it retusn 0c,00 instead of 0c 00 00 132 | # 00 03 01 00 02 00 00 00 0c 00 133 | # ins.get_data also return with \x00 appended, we dont need that 134 | raw_data = list(ins.get_raw()) 135 | # print(ins.get_raw()) 136 | # print(ins.get_hex()) 137 | # print(ins.get_data()) 138 | data_size = raw_data[4] 139 | barray = bytearray(raw_data[8 : 8 + data_size]) 140 | barrays.append(barray) 141 | return barrays 142 | 143 | def find_main_application(self) -> str: 144 | """ 145 | Find main application class name from AndroidManifest.xml 146 | If application tag is not present, find first class that extends Application 147 | :returns application_smali: Application class name in smali format 148 | """ 149 | application_smali = None 150 | application = self.apk_object.get_attribute_value("application", "name") 151 | if application == None: 152 | for d in self.dexes: 153 | for c in d.get_classes(): 154 | if c.get_superclassname() == "Landroid/app/Application;": 155 | application_smali = c.get_name() 156 | break 157 | else: 158 | application_smali = "L" + application.replace(".", "/") + ";" 159 | return application_smali 160 | 161 | def find_method( 162 | self, klass_name: str, method_name: str, descriptor: str = "" 163 | ) -> EncodedMethod: 164 | """ 165 | Find method in dvms via class name and method name. Descriptor is optional 166 | :returns EncodedMethod of found method 167 | """ 168 | for dvm in self.dexes: 169 | c = dvm.get_class(klass_name) 170 | if c != None: 171 | methods = c.get_methods() 172 | for method in methods: 173 | if method.get_name() == method_name: 174 | if descriptor == "": 175 | return method 176 | else: 177 | if method.get_descriptor() == descriptor: 178 | return method 179 | return None 180 | 181 | def find_method_re( 182 | self, klass_name: str, method_name: str, descriptor: str = "" 183 | ) -> EncodedMethod: 184 | for dvm in self.dexes: 185 | c = dvm.get_class(klass_name) 186 | if c != None: 187 | methods = c.get_methods() 188 | for method in methods: 189 | if len(re.findall(method_name, method.get_name())) > 1: 190 | if descriptor == "": 191 | return method 192 | else: 193 | if method.get_descriptor() == descriptor: 194 | return method 195 | return None 196 | 197 | def find_class_in_dvms(self, klass_name: str) -> ClassDefItem: 198 | """Search class name in dvms and return first instance""" 199 | for dvm in self.dexes: 200 | c = dvm.get_class(klass_name) 201 | if c != None: 202 | return c 203 | return None 204 | 205 | @staticmethod 206 | def find_method_in_class_m(klass, method_name): 207 | """Find method in klass instance.""" 208 | methods = klass.get_methods() 209 | for method in methods: 210 | if method.get_name() == method_name: 211 | return method 212 | return None 213 | 214 | def lazy_check(self, apk_object: APK, dvms: "list[DEX]") -> bool: 215 | """Check if this plugin should run. This method shouldn't be heavy.""" 216 | return True 217 | 218 | def calculate_name(self, file_data) -> str: 219 | """Calculate external dex file name from file data by taking md5 hash of it""" 220 | m = hashlib.md5(file_data).hexdigest() 221 | return f"external-{m[:8]}.dex" 222 | 223 | def check_header(self, fd) -> bool: 224 | """Check if given data contains dex/pkzip/zlib headers""" 225 | if len(fd) > 7 and fd[:8] in dex_headers: 226 | return True 227 | elif len(fd) > 3 and fd[:4] in pkzip_headers: 228 | return True 229 | elif len(fd) > 1 and fd[:2] in zlib_headers: 230 | return True 231 | return False 232 | 233 | def check_and_write_file(self, dec) -> bool: 234 | """ 235 | Check headers and write extracted dex to output_dir, if output_dir is empty save to current path. ZIP/Zlib streams is decompressed and first instance of dex file is written. 236 | """ 237 | if dec[:8] in dex_headers: 238 | self.decrypted_payload_path = os.path.join( 239 | self.output_dir, self.calculate_name(dec) 240 | ) 241 | self.logger.success( 242 | f"Decryption successful! Output dex : {self.decrypted_payload_path}" 243 | ) 244 | with open(self.decrypted_payload_path, "wb") as fp: 245 | fp.write(dec) 246 | return True 247 | elif dec[:4] in pkzip_headers: 248 | self.logger.success(f"Decryption successful!\t Found zip file") 249 | with zipfile.ZipFile(io.BytesIO(dec), "r") as drop: 250 | for file in drop.filelist: 251 | with drop.open(file.filename) as f: 252 | zip_files_ex = f.read(8) 253 | f.seek(0) 254 | if zip_files_ex in dex_headers: 255 | self.logger.info( 256 | f"Extracting dex from zip file. Output dex : {self.decrypted_payload_path}" 257 | ) 258 | file_data = f.read() 259 | self.decrypted_payload_path = os.path.join( 260 | self.output_dir, self.calculate_name(dec) 261 | ) 262 | with open(self.decrypted_payload_path, "wb") as fp: 263 | fp.write(file_data) 264 | return True 265 | elif dec[:2] in zlib_headers: 266 | try: 267 | decrypted = zlib.decompress(dec) 268 | except Exception as e: 269 | self.logger.error(e) 270 | return False 271 | if decrypted[:8] in dex_headers: 272 | self.decrypted_payload_path = os.path.join( 273 | self.output_dir, self.calculate_name(decrypted) 274 | ) 275 | self.logger.success(f"Decryption successful!\t Found zlib file") 276 | with open(self.decrypted_payload_path, "wb") as fp: 277 | fp.write(decrypted) 278 | return True 279 | return False 280 | 281 | def main(self, native_lib: str = "") -> dict: 282 | """ 283 | Starting point for each plugin. Calls lazy_check then starts start_decrypt. Returns dict of result that contains status, output_file, plugin name and plugin tag 284 | """ 285 | start_time = time.time() 286 | 287 | result = {} 288 | result["name"] = self.get_name() 289 | result["tag"] = self.get_tag() 290 | if not self.lazy_check(self.apk_object, self.dexes): 291 | result["status"] = "success" if self.get_status() else "fail" 292 | return result 293 | # try: 294 | o = self.start_decrypt() 295 | # except Exception as e: 296 | # result["error"] = str(e) 297 | # result["status"] = "error" 298 | # return result 299 | 300 | result["status"] = "success" if self.get_status() else "fail" 301 | if self.get_status(): 302 | result["output_file"] = self.get_path() 303 | end_time = time.time() 304 | 305 | self.logger.info(f"total analysis time = {end_time-start_time}") 306 | return result 307 | 308 | def get_status(self) -> bool: 309 | """ 310 | Get decryption status by checking decrypted_payload_path 311 | """ 312 | return self.decrypted_payload_path != None 313 | 314 | def get_path(self) -> str: 315 | """ 316 | Get decrypted_payload_path 317 | """ 318 | return self.decrypted_payload_path 319 | 320 | def start_decrypt(self): 321 | """ 322 | Start decryption routine. This should be overwritten 323 | """ 324 | pass 325 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import logging 4 | from loguru import logger 5 | from kavanoz.utils import InterceptHandler 6 | 7 | logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True) 8 | # logging.getLogger().setLevel(logging.INFO) 9 | # logging.getLogger("androguard").setLevel(logging.CRITICAL) 10 | logger.remove() 11 | 12 | 13 | from kavanoz.loader.multidex import LoaderMultidex 14 | from kavanoz.loader.rc4 import LoaderRc4 15 | from kavanoz.loader.subapp import LoaderSubapp 16 | from kavanoz.loader.moqhao import LoaderMoqhao 17 | from kavanoz.loader.coper import LoaderCoper 18 | from kavanoz.loader.sesdex import LoaderSesdex 19 | from kavanoz.loader.multidex_header import LoaderMultidexHeader 20 | from kavanoz.loader.simple_xor import LoaderSimpleXor 21 | from kavanoz.loader.simply_xor2 import LoaderSimpleXor2 22 | from kavanoz.loader.simple_xor_zlib import LoaderSimpleXorZlib 23 | from kavanoz.loader.simple_aes import LoaderSimpleAes 24 | from kavanoz.loader.pronlocker import LoaderPr0nLocker 25 | from kavanoz.loader.crocodilus import LoaderCrocodilus 26 | from androguard.core.apk import APK 27 | from kavanoz.loader.kangapack import LoaderKangaPack 28 | from androguard.core.dex import DEX 29 | 30 | 31 | class TestAllLoaders(unittest.TestCase): 32 | def test_rc4(self): 33 | """ 34 | Test that it can sum a list of integers 35 | """ 36 | 37 | filename = os.path.join( 38 | os.path.dirname(__file__), 39 | "./test_apk/loader_rc4_static_key_in_key_class.apk", 40 | ) 41 | apk_object = APK(filename) 42 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 43 | rc4 = LoaderRc4(apk_object, dvms, output_dir=None) 44 | res = rc4.main() 45 | assert res["status"] == "success" 46 | if rc4.decrypted_payload_path: 47 | os.remove(rc4.decrypted_payload_path) 48 | filename = os.path.join( 49 | os.path.dirname(__file__), "./test_apk/loader_rc4_second_key_0.apk" 50 | ) 51 | apk_object = APK(filename) 52 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 53 | rc4 = LoaderRc4(apk_object, dvms, output_dir=None) 54 | res = rc4.main() 55 | assert res["status"] == "success" 56 | if rc4.decrypted_payload_path: 57 | os.remove(rc4.decrypted_payload_path) 58 | filename = os.path.join( 59 | os.path.dirname(__file__), "./test_apk/loader_rc4_key_0.apk" 60 | ) 61 | apk_object = APK(filename) 62 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 63 | rc4 = LoaderRc4(apk_object, dvms, output_dir=None) 64 | res = rc4.main() 65 | assert res["status"] == "success" 66 | if rc4.decrypted_payload_path: 67 | os.remove(rc4.decrypted_payload_path) 68 | 69 | filename = os.path.join( 70 | os.path.dirname(__file__), "./test_apk/loader_rc4_multiple_stage.apk" 71 | ) 72 | apk_object = APK(filename) 73 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 74 | rc4 = LoaderRc4(apk_object, dvms, output_dir=None) 75 | res = rc4.main() 76 | assert res["status"] == "success" 77 | if rc4.decrypted_payload_path: 78 | os.remove(rc4.decrypted_payload_path) 79 | 80 | def test_inflate(self): 81 | """ 82 | Test that it can sum a list of integers 83 | """ 84 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/inflate.apk") 85 | apk_object = APK(filename) 86 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 87 | rc4 = LoaderMultidex(apk_object, dvms, output_dir=None) 88 | res = rc4.main() 89 | assert res["status"] == "success" 90 | if rc4.decrypted_payload_path: 91 | os.remove(rc4.decrypted_payload_path) 92 | 93 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/inflate2.apk") 94 | apk_object = APK(filename) 95 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 96 | rc4 = LoaderMultidex(apk_object, dvms, output_dir=None) 97 | res = rc4.main() 98 | assert res["status"] == "success" 99 | if rc4.decrypted_payload_path: 100 | os.remove(rc4.decrypted_payload_path) 101 | 102 | filename = os.path.join( 103 | os.path.dirname(__file__), "./test_apk/default_dex_protector.apk" 104 | ) 105 | apk_object = APK(filename) 106 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 107 | rc4 = LoaderMultidex(apk_object, dvms, output_dir=None) 108 | res = rc4.main() 109 | assert res["status"] == "success" 110 | if rc4.decrypted_payload_path: 111 | os.remove(rc4.decrypted_payload_path) 112 | 113 | def test_inflate_second(self): 114 | """ 115 | Test that it can sum a list of integers 116 | """ 117 | filename = os.path.join( 118 | os.path.dirname(__file__), 119 | "./test_apk/protect_key_chines_manifest_without_zlib.apk", 120 | ) 121 | apk_object = APK(filename) 122 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 123 | rc4 = LoaderMultidex(apk_object, dvms, output_dir=None) 124 | res = rc4.main() 125 | assert res["status"] == "success" 126 | if rc4.decrypted_payload_path: 127 | os.remove(rc4.decrypted_payload_path) 128 | 129 | def test_subapp(self): 130 | """ 131 | Test that it can sum a list of integers 132 | """ 133 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/subapp.apk") 134 | apk_object = APK(filename) 135 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 136 | rc4 = LoaderSubapp(apk_object, dvms, output_dir=None) 137 | res = rc4.main() 138 | assert res["status"] == "success" 139 | if rc4.decrypted_payload_path: 140 | os.remove(rc4.decrypted_payload_path) 141 | 142 | def test_moqhao(self): 143 | """ 144 | Test that it can sum a list of integers 145 | """ 146 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/moqhao.apk") 147 | apk_object = APK(filename) 148 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 149 | moqhao = LoaderMoqhao(apk_object, dvms, output_dir=None) 150 | res = moqhao.main() 151 | assert res["status"] == "success" 152 | if moqhao.decrypted_payload_path: 153 | os.remove(moqhao.decrypted_payload_path) 154 | 155 | def test_coper(self): 156 | """ 157 | Test that it can sum a list of integers 158 | """ 159 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/coper.apk") 160 | apk_object = APK(filename) 161 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 162 | coper = LoaderCoper(apk_object, dvms, output_dir=None) 163 | res = coper.main() 164 | assert res["status"] == "success" 165 | if coper.decrypted_payload_path: 166 | os.remove(coper.decrypted_payload_path) 167 | 168 | def test_coper2(self): 169 | """ 170 | Test that it can sum a list of integers 171 | """ 172 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/coper2_0.apk") 173 | apk_object = APK(filename) 174 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 175 | coper = LoaderCoper(apk_object, dvms, output_dir=None) 176 | res = coper.main() 177 | assert res["status"] == "success" 178 | if coper.decrypted_payload_path: 179 | os.remove(coper.decrypted_payload_path) 180 | 181 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/coper2_1.apk") 182 | apk_object = APK(filename) 183 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 184 | coper = LoaderCoper(apk_object, dvms, output_dir=None) 185 | res = coper.main() 186 | assert res["status"] == "success" 187 | if coper.decrypted_payload_path: 188 | os.remove(coper.decrypted_payload_path) 189 | 190 | 191 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/coper3_2.apk") 192 | apk_object = APK(filename) 193 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 194 | coper = LoaderCoper(apk_object, dvms, output_dir=None) 195 | res = coper.main() 196 | assert res["status"] == "success" 197 | if coper.decrypted_payload_path: 198 | os.remove(coper.decrypted_payload_path) 199 | 200 | def test_sesdex(self): 201 | """ 202 | Test that it can sum a list of integers 203 | """ 204 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/sesdex.apk") 205 | apk_object = APK(filename) 206 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 207 | sesdex = LoaderSesdex(apk_object, dvms, output_dir=None) 208 | res = sesdex.main() 209 | assert res["status"] == "success" 210 | if sesdex.decrypted_payload_path: 211 | os.remove(sesdex.decrypted_payload_path) 212 | 213 | def test_multidex_header(self): 214 | """ 215 | Test that it can sum a list of integers 216 | """ 217 | 218 | filename = os.path.join( 219 | os.path.dirname(__file__), "./test_apk/multidex_without_header.apk" 220 | ) 221 | apk_object = APK(filename) 222 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 223 | mwheader = LoaderMultidexHeader(apk_object, dvms, output_dir=None) 224 | res = mwheader.main() 225 | assert res["status"] == "success" 226 | if mwheader.decrypted_payload_path: 227 | os.remove(mwheader.decrypted_payload_path) 228 | 229 | def test_simple_xor(self): 230 | """ 231 | Test that it can sum a list of integers 232 | """ 233 | 234 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/simplexor.apk") 235 | apk_object = APK(filename) 236 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 237 | sxorzlib = LoaderSimpleXor(apk_object, dvms, output_dir=None) 238 | res = sxorzlib.main() 239 | assert res["status"] == "success" 240 | if sxorzlib.decrypted_payload_path: 241 | os.remove(sxorzlib.decrypted_payload_path) 242 | 243 | def test_simple_xor2(self): 244 | """ 245 | Test that it can sum a list of integers 246 | """ 247 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/simple_xor2.apk") 248 | apk_object = APK(filename) 249 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 250 | sxor2 = LoaderSimpleXor2(apk_object, dvms, output_dir=None) 251 | res = sxor2.main() 252 | assert res["status"] == "success" 253 | if sxor2.decrypted_payload_path: 254 | os.remove(sxor2.decrypted_payload_path) 255 | 256 | def test_simple_xor_zlib(self): 257 | """ 258 | Test that it can sum a list of integers 259 | """ 260 | filename = os.path.join( 261 | os.path.dirname(__file__), "./test_apk/simple_xor_zlib_base64.apk" 262 | ) 263 | apk_object = APK(filename) 264 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 265 | sxorzlib = LoaderSimpleXorZlib(apk_object, dvms, output_dir=None) 266 | res = sxorzlib.main() 267 | assert res["status"] == "success" 268 | if sxorzlib.decrypted_payload_path: 269 | os.remove(sxorzlib.decrypted_payload_path) 270 | filename = os.path.join( 271 | os.path.dirname(__file__), "./test_apk/simple_skip4_zlib_base64.apk" 272 | ) 273 | apk_object = APK(filename) 274 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 275 | sxorzlib = LoaderSimpleXorZlib(apk_object, dvms, output_dir=None) 276 | res = sxorzlib.main() 277 | assert res["status"] == "success" 278 | if sxorzlib.decrypted_payload_path: 279 | os.remove(sxorzlib.decrypted_payload_path) 280 | 281 | def test_simple_aes(self): 282 | """ 283 | Test that it can sum a list of integers 284 | """ 285 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/simpleaes.apk") 286 | apk_object = APK(filename) 287 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 288 | saes = LoaderSimpleAes(apk_object, dvms, output_dir=None) 289 | res = saes.main() 290 | assert res["status"] == "success" 291 | if saes.decrypted_payload_path: 292 | os.remove(saes.decrypted_payload_path) 293 | 294 | def test_kangapack(self): 295 | """ 296 | Test that it can sum a list of integers 297 | """ 298 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/kangapack.apk") 299 | apk_object = APK(filename) 300 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 301 | skanga = LoaderKangaPack(apk_object, dvms, output_dir=None) 302 | res = skanga.main() 303 | assert res["status"] == "success" 304 | if skanga.decrypted_payload_path: 305 | os.remove(skanga.decrypted_payload_path) 306 | 307 | def test_pronlocker(self): 308 | """ 309 | Test that it can sum a list of integers 310 | """ 311 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/pronlocker.apk") 312 | apk_object = APK(filename) 313 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 314 | spron = LoaderPr0nLocker(apk_object, dvms, output_dir=None) 315 | res = spron.main() 316 | assert res["status"] == "success" 317 | if spron.decrypted_payload_path: 318 | os.remove(spron.decrypted_payload_path) 319 | 320 | def test_crocodilus(self): 321 | """ 322 | Test crocodilus loader 323 | """ 324 | filename = os.path.join(os.path.dirname(__file__), "./test_apk/crocodilus.apk") 325 | apk_object = APK(filename) 326 | dvms = [DEX(dex) for dex in apk_object.get_all_dex()] 327 | spron = LoaderCrocodilus(apk_object, dvms, output_dir=None) 328 | res = spron.main() 329 | assert res["status"] == "success" 330 | if spron.decrypted_payload_path: 331 | os.remove(spron.decrypted_payload_path) 332 | 333 | 334 | if __name__ == "__main__": 335 | unittest.main() 336 | -------------------------------------------------------------------------------- /src/kavanoz/loader/coper.py: -------------------------------------------------------------------------------- 1 | from androguard.core.apk import APK 2 | from androguard.core.axml import ARSCParser, ARSCResType 3 | from androguard.core.dex import DEX 4 | from androidemu.emulator import Emulator 5 | from androidemu.utils.memory_helpers import read_utf8 6 | from unicorn.unicorn_const import UC_HOOK_MEM_READ_UNMAPPED, UC_HOOK_MEM_UNMAPPED 7 | 8 | from unicorn import UC_HOOK_CODE 9 | import unicorn 10 | from unicorn.arm_const import * 11 | import lief 12 | from arc4 import ARC4 13 | from kavanoz.unpack_plugin import Unpacker 14 | import os 15 | 16 | 17 | class LoaderCoper(Unpacker): 18 | def __init__(self, apk_obj, dvms, output_dir): 19 | super().__init__( 20 | "loader.coper", "Unpacker for coper", apk_obj, dvms, output_dir 21 | ) 22 | 23 | def lazy_check(self, apk_object: APK, dvms: list[DEX]) -> bool: 24 | arm32_native_libs = [ 25 | filename 26 | for filename in self.apk_object.get_files() 27 | if filename.startswith("lib/armeabi-v7a") 28 | ] 29 | if len(arm32_native_libs) >= 5: 30 | self.logger.info( 31 | "Found more than 5 native libs, this is probably NOT a coper" 32 | ) 33 | return False 34 | else: 35 | self.logger.info("Found less than 5 native libs, this is probably a coper") 36 | return True 37 | 38 | def start_decrypt(self, native_lib: str = ""): 39 | arm32_native_libs = [ 40 | filename 41 | for filename in self.apk_object.get_files() 42 | if filename.startswith("lib/armeabi-v7a") 43 | ] 44 | if len(arm32_native_libs) == 0: 45 | self.logger.info("No native lib 😔") 46 | return 47 | if len(arm32_native_libs) != 1: 48 | self.logger.info("Not sure this is copper but continue anyway") 49 | 50 | for arm32_lib in arm32_native_libs: 51 | self.logger.info(f"Trying to decrypt with {arm32_lib}") 52 | if self.decrypt_library(arm32_lib): 53 | return 54 | 55 | def decrypt_library(self, native_lib: str) -> bool: 56 | fname = native_lib.split("/")[-1] 57 | with open(fname, "wb") as fp: 58 | fp.write(self.apk_object.get_file(native_lib)) 59 | self.target_lib = fname 60 | # Show loaded modules. 61 | self.resolved_strings = [] 62 | if not self.init_lib(): 63 | return 64 | self.logger.info("Loaded modules:") 65 | if not self.setup_hook(): 66 | self.logger.info("Failed to setup hooks maybe no srtcat symbol ?") 67 | self.logger.info("Trying to find strings in stack") 68 | # self.emulator.mu.hook_add(UC_HOOK_CODE, self.hook_debug_print) 69 | self.emulator.uc.hook_add(UC_HOOK_MEM_READ_UNMAPPED, self.hook_unmapped_read) 70 | 71 | try: 72 | self.emulator.call_symbol(self.target_module, self.target_function.name) 73 | except Exception as e: 74 | self.logger.info(f"Exception while calling symbol: {e}") 75 | if len(self.resolved_strings) == 0: 76 | self.logger.info("No strings found") 77 | return 78 | self.logger.info(f"Androidemu extracted rc4 key: {self.resolved_strings[0]}") 79 | if self.decrypt_files(self.resolved_strings[0]): 80 | self.logger.info("Decryption successful") 81 | os.remove(self.target_lib) 82 | return True 83 | else: 84 | # Now this can be because dex\n035 is added after decryption of thhe file. We can also get file name from resolved strings. 85 | if len(self.resolved_strings) == 2 and self.apk_object.get_package() in self.resolved_strings[1]: 86 | # This is tricky part. Sometimes :raw points to different point from resources.arsc/raw.. 87 | # But instead we can search .../raw/filename in all files 88 | enc_filename = self.resolved_strings[1].split(":")[1] 89 | self.logger.debug(f"Looking for file : {enc_filename}") 90 | for f in self.apk_object.get_files(): 91 | if enc_filename in f: 92 | if self.decrypt_file(self.resolved_strings[0],f): 93 | os.remove(self.target_lib) 94 | return True 95 | else: 96 | # Here we go... 97 | # I dont know this is obfuscation or something else with androguard library 98 | # We need to manually find mapping of res/somefile with raw/targetfile by parsing resources.arsc 99 | arsc_file = self.apk_object.get_file("resources.arsc") 100 | arsc_parser = ARSCParser(arsc_file) 101 | # Find item 102 | # then next item will be rids of raw files [(0, 2130837504), (16, 2130837505)] 103 | items = arsc_parser.get_items(package_name=self.apk_object.get_package()) 104 | for i in range(len(items)): 105 | if type(items[i]) == ARSCResType: 106 | # Found res type 107 | if items[i].get_type() == "raw": 108 | # Found raw type 109 | # items[i+1] hold list of resource ids of raw type files 110 | rids = [x[1] for x in items[i+1]] 111 | self.logger.debug(f"Found rids : {rids}") 112 | for rid in rids: 113 | curr_res_config = arsc_parser.get_resolved_res_configs(rid=rid) 114 | xml_name = arsc_parser.get_resource_xml_name(r_id=rid) 115 | if enc_filename in xml_name and len(curr_res_config) > 0: 116 | curr_res_file_name = curr_res_config[0][1] 117 | if self.decrypt_file(self.resolved_strings[0],curr_res_file_name): 118 | os.remove(self.target_lib) 119 | return True 120 | 121 | return False 122 | os.remove(self.target_lib) 123 | return False 124 | 125 | def decrypt_files(self, rc4key: str): 126 | for filepath in self.apk_object.get_files(): 127 | fd = self.apk_object.get_file(filepath) 128 | dede = ARC4(rc4key.encode("utf-8")) 129 | dec = dede.decrypt(fd) 130 | if self.check_and_write_file(dec): 131 | return True 132 | return False 133 | 134 | def decrypt_file(self,rc4key:str, filename:str): 135 | fd = self.apk_object.get_file(filename) 136 | arc4 = ARC4(rc4key.encode("utf-8")) 137 | dec = arc4.decrypt(fd) 138 | dec = b"dex\n035" + dec 139 | if self.check_and_write_file(dec): 140 | return True 141 | return False 142 | 143 | 144 | def init_lib(self): 145 | target_ELF = lief.ELF.parse(self.target_lib) 146 | java_exports = [ 147 | jf for jf in target_ELF.exported_functions if jf.name.startswith("Java_") 148 | ] 149 | if len(java_exports) == 0: 150 | return False 151 | if len(java_exports) > 1: 152 | self.logger.info("Not sure this is copper but continue anyway") 153 | 154 | self.target_function = java_exports[0] 155 | # Configure logging 156 | 157 | # Initialize emulator 158 | self.emulator = Emulator(vfp_inst_set=True) 159 | libc_path = os.path.join(os.path.dirname(__file__), "androidnativeemu/libc.so") 160 | self.emulator.load_library(libc_path, do_init=False) 161 | self.target_module = self.emulator.load_library(self.target_lib, do_init=False) 162 | return True 163 | 164 | def hook_debug_print(self, uc, address, size, user_data): 165 | instruction = uc.mem_read(address, size) 166 | instruction_str = "".join("{:02x} ".format(x) for x in instruction) 167 | 168 | print( 169 | "# Tracing instruction at 0x%x, instruction size = 0x%x, instruction = %s" 170 | % (address, size, instruction_str) 171 | ) 172 | 173 | def hook_unmapped_read(self, uc, access, address, size, value, user_data): 174 | # Read stack and print it byte per byte 175 | self.logger.debug("Trying to read from address : %x" % address) 176 | sp = uc.reg_read(UC_ARM_REG_SP) 177 | bp = uc.reg_read(UC_ARM_REG_R11) 178 | self.logger.debug(f"Stack pointer: {hex(sp)}, Base pointer: {hex(bp)}") 179 | 180 | # Problem here is we don't know the size of the stack data 181 | # If we read too much we will get unmapped memory error 182 | # But we can extract stack size from function prologue 183 | 184 | stack_size = self.extract_stack_size_from_function_prologue( 185 | self.emulator.uc, self.target_function, self.target_lib_base 186 | ) 187 | if stack_size == 0: 188 | return 189 | stack_data = uc.mem_read(sp, stack_size) 190 | # Stack data contains list of strings ends with \x00 but there are also 191 | # filler \x00 bytes in between them. We need to split them. 192 | stack_data = stack_data.split(b"\x00") 193 | # Filter out empty strings 194 | stack_data = [x for x in stack_data if x != b""] 195 | # Decode strings 196 | stack_data = [x for x in stack_data] 197 | self.logger.debug(f"Stack data: {stack_data}") 198 | self.resolved_strings.append(stack_data[-1].decode("utf-8")) 199 | # Print stack 200 | 201 | def setup_hook(self): 202 | for module in self.emulator.modules: 203 | if module.filename == self.target_lib: 204 | self.logger.info("[0x%x] %s" % (module.base, module.filename)) 205 | self.target_lib_base = module.base 206 | # emulator.mu.hook_add( 207 | # UC_HOOK_CODE, 208 | # hook_code, 209 | # begin=module.base + java_func_obj.address, 210 | # end=module.base + java_func_obj.address + (0x2198 - 0x1FC1), 211 | # ) 212 | strncat = module.find_symbol("__strncat_chk") 213 | if strncat == None: 214 | self.logger.info("No strncat symbol 😔") 215 | 216 | self.logger.info("maybe octo2 ?") 217 | unpack_dynlib = module.find_symbol("_ZN8WrpClass13unpack_dynlibEv") 218 | if unpack_dynlib == None: 219 | self.logger.info("No unpack_dynlib symbol 😔") 220 | return False 221 | self.logger.debug(f"{hex(unpack_dynlib.address)} unpack_dynlib addr") 222 | replace_loader = module.find_symbol("_ZN8WrpClass14replace_loaderEb") 223 | 224 | self.emulator.uc.hook_add( 225 | UC_HOOK_CODE, 226 | self.hook_unpack_dynlib, 227 | begin=unpack_dynlib.address, 228 | end=unpack_dynlib.address + 1, 229 | user_data=replace_loader.address, 230 | ) 231 | return False 232 | self.logger.debug(f"{hex(strncat.address)} strcat_chk addr") 233 | self.emulator.uc.hook_add( 234 | UC_HOOK_CODE, 235 | self.hook_strncat, 236 | begin=strncat.address, 237 | end=strncat.address + 1, 238 | ) 239 | self.emulator.uc.hook_add(UC_HOOK_MEM_UNMAPPED, self.hook_mem_read) 240 | self.emulator.uc.hook_add(UC_HOOK_MEM_READ_UNMAPPED, self.hook_mem_read) 241 | return True 242 | return False 243 | 244 | def hook_mem_read(self, uc, access, address, size, value, user_data): 245 | pc = uc.reg_read(UC_ARM_REG_PC) 246 | data = uc.mem_read(address, size) 247 | self.logger.debug( 248 | ">>> Memory READ at 0x%x, data size = %u, pc: %x, data value = 0x%s" 249 | % (address, size, pc, data.hex()) 250 | ) 251 | 252 | def hook_strncat(self, uc: unicorn.unicorn.Uc, address, size, user_data): 253 | # print(f"current strncat hook addr : {hex(address)}") 254 | r0 = uc.reg_read(UC_ARM_REG_R0) 255 | # print(f"current strncat hook r0 : {hex(r0)}") 256 | r1 = uc.reg_read(UC_ARM_REG_R1) 257 | max_size = uc.reg_read(UC_ARM_REG_R2) 258 | # print(f"current strncat hook r1 : {hex(r1)}") 259 | cur_key = read_utf8(uc, r0) 260 | added = read_utf8(uc, r1) 261 | final_str = cur_key + added 262 | if len(final_str) == max_size - 1: 263 | self.logger.debug(f"current strncat hook final_str : {final_str}") 264 | self.resolved_strings.append(final_str) 265 | if len(self.resolved_strings) > 10: 266 | self.emulator.uc.emu_stop() 267 | 268 | def hook_unpack_dynlib(self, uc: unicorn.unicorn.Uc, address, size, user_data): 269 | 270 | self.logger.debug("unpack_dynlib triggered : %x" % address) 271 | sp = uc.reg_read(UC_ARM_REG_SP) 272 | self.logger.debug(f"Stack pointer: {hex(sp)}") 273 | # Problem here is we don't know the size of the stack data, we can go back calle function 274 | # and read prologue to extract stack size 275 | 276 | # Get stack size from replace_loader 277 | 278 | should_be_subw = uc.mem_read( 279 | user_data - 1 + 8, 0x4 280 | ) 281 | if should_be_subw[1] != 0xb0: 282 | self.logger.debug("Bad instruction to find stack size") 283 | return 0 284 | # a6b0 -> read 0xa6 &= 0x7f 285 | stack_size = (should_be_subw[0] & 0x7f ) << 2 286 | 287 | self.logger.debug(f"Found stack size at replace_loader : {stack_size}") 288 | stack_data = uc.mem_read(sp, stack_size+12) 289 | # Stack data contains list of strings ends with \x00 but there are also 290 | # filler \x00 bytes in between them. We need to split them. 291 | stack_data = stack_data.split(b"\x00") 292 | # Filter out empty strings 293 | stack_data = [x for x in stack_data if x != b""] 294 | # Decode strings 295 | stack_data = [x for x in stack_data] 296 | self.logger.debug(f"Stack data: {stack_data}") 297 | # Some sanity checks 298 | if len(stack_data) > 5: 299 | # example stack strings: 300 | # - Q3DCe5ZIFftphISZwNpNYy4vA1Qvyyjt 301 | # - replace_loader 302 | # - 7a1bc825b313fd55 303 | # - b313fd551c1f219b 304 | # - com.handedfastee5 305 | # - com.handedfastee5:raw/kyndwjzbyg0 306 | # get rc4 key 307 | self.resolved_strings.append(stack_data[-1].decode("utf-8")) 308 | # walk backward and find raw file 309 | stack_data.reverse() 310 | for i in range(0,len(stack_data),2): 311 | if stack_data[i] in stack_data[i+1]: 312 | # we found the file 313 | self.logger.debug(f'RC4 encrypted file name : {stack_data[i+1].decode("utf-8")}') 314 | self.resolved_strings.append(stack_data[i+1].decode("utf-8")) 315 | break 316 | 317 | 318 | self.emulator.uc.emu_stop() 319 | 320 | def extract_stack_size_from_function_prologue( 321 | self, uc, target_function, target_lib_base 322 | ) -> int: 323 | # 00001ee8 f0b5 push {r4, r5, r6, r7, lr} {var_4} {__saved_r7} {__saved_r6} {__saved_r5} {__saved_r4} 324 | # 00001eea 03af add r7, sp, #0xc {__saved_r7} 325 | # 00001eec 2de9000f push {r8, r9, r10, r11} {__saved_r11} {__saved_r10} {__saved_r9} {__saved_r8} 326 | # 00001ef0 adf2144d subw sp, sp, #0x414 327 | 328 | # We need 4th instruction, if its sub/subw with parameters sp,sp then get the value 329 | # of the last parameter 330 | # F it just read bytes 331 | # -1 is for thumb mode 332 | should_be_subw = uc.mem_read( 333 | target_lib_base + target_function.value - 1 + 8, 0x4 334 | ) 335 | if should_be_subw[0] != 0xAD: 336 | return 0 337 | # adf2144d -> read 0x14 and 0x4d bytes -> convert it to 0x414 338 | 339 | stack_size = should_be_subw[2] | (((should_be_subw[3] & 0xF0) >> 4) << 8) 340 | self.logger.info(f"Stack size must be {hex(stack_size)}") 341 | return stack_size 342 | 343 | # def hook_code(self,uc: unicorn.unicorn.Uc, address, size, user_data): 344 | # global rc4_key 345 | # if address == coper_base + java_func_obj.address + (0x2198 - 0x1FC1): 346 | # sp = uc.reg_read(UC_ARM_REG_SP) 347 | # rc4_key = read_utf8(uc, sp + 0x46F) 348 | 349 | # print( 350 | # "# Tracing instruction at 0x%x, instruction size = 0x%x, instruction = %s" 351 | # % (address, size, instruction_str) 352 | # ) 353 | # if instruction[0] == 0xA0 and instruction[1] == 0x47 and len(instruction) == 2: 354 | # r1 = uc.reg_read(UC_ARM_REG_R1) 355 | # print(r1) 356 | # print(uc.mem_read(r1, 1)) 357 | -------------------------------------------------------------------------------- /src/kavanoz/loader/multidex.py: -------------------------------------------------------------------------------- 1 | from androguard.core.apk import APK 2 | import zlib 3 | from androguard.core.dex import DEX, EncodedMethod 4 | import re 5 | import ctypes 6 | import string 7 | from kavanoz import utils 8 | from kavanoz.unpack_plugin import Unpacker 9 | 10 | 11 | def unsigned_rshift(val, n): 12 | unsigned_integer = val % 0x100000000 13 | return unsigned_integer >> n 14 | 15 | 16 | def unsigned_lshift(val, n): 17 | unsigned_integer = val % 0x100000000 18 | return unsigned_integer << n 19 | 20 | 21 | class LoaderMultidex(Unpacker): 22 | ProtectKey = "" 23 | 24 | def __init__(self, apk_obj, dexes, output_dir): 25 | super().__init__( 26 | "loader.multidex", 27 | "Unpacker for multidex variants", 28 | apk_obj, 29 | dexes, 30 | output_dir, 31 | ) 32 | 33 | def start_decrypt(self, native_lib: str = ""): 34 | self.logger.info("Starting to decrypt") 35 | z = self.apk_object.get_android_manifest_xml() 36 | if z != None: 37 | f = z.find("application") 38 | childs = f.getchildren() 39 | self.ProtectKey = None 40 | for child in childs: 41 | if child.tag == "meta-data": 42 | if ( 43 | child.attrib["{http://schemas.android.com/apk/res/android}name"] 44 | == "ProtectKey" 45 | ): 46 | self.ProtectKey = child.attrib[ 47 | "{http://schemas.android.com/apk/res/android}value" 48 | ] 49 | self.logger.info(f"Found protect key {self.ProtectKey}") 50 | if self.ProtectKey != None: 51 | if self.find_decrypt_protect_arrays(): 52 | self.logger.info("Found key in manifest/xor") 53 | return 54 | 55 | self.decrypted_payload_path = None 56 | zip_functions = self.find_zip_function() 57 | for zip_function in zip_functions: 58 | _function, dvm = zip_function 59 | variable = self.extract_variable_from_zip(_function, dvm) 60 | if variable is not None: 61 | key = self.find_clinit_target_variable(variable) 62 | if key is not None: 63 | if self.brute_assets(key): 64 | if self.is_really_unpacked(): 65 | self.logger.info("fully unpacked") 66 | else: 67 | self.logger.info("not fully unpacked") 68 | return 69 | else: 70 | self.logger.info("Cannot find zip function") 71 | self.logger.info("Second plan for zipper") 72 | r = self.second_plan() 73 | if r is not None: 74 | self.logger.info("Second plan worked") 75 | self.logger.info(f"{r}") 76 | return 77 | else: 78 | self.logger.info("Second plan failed") 79 | self.third_plan() 80 | is_default = self.default_dex_protector() 81 | if is_default != None: 82 | for key in is_default: 83 | self.logger.info(f"Trying default dex protector key {key}") 84 | if self.brute_assets(key): 85 | if self.is_really_unpacked(): 86 | self.logger.info("fully unpacked") 87 | else: 88 | self.logger.info("not fully unpacked") 89 | return 90 | 91 | def third_plan(self): 92 | """ 93 | public class ldhgedudr { 94 | public static void fslstmkpgcrup(InputStream input, OutputStream output) throws Exception { 95 | InflaterInputStream is = new InflaterInputStream(input); 96 | InflaterOutputStream os = new InflaterOutputStream(output); 97 | swtj(is, os); 98 | os.close(); 99 | is.close(); 100 | } 101 | 102 | private static void swtj(InputStream inputStream, OutputStream outputStream) throws Exception { 103 | char[] key = rtpgi.kphimwvplfd.toCharArray(); 104 | """ 105 | input_initials = self.find_input_output_stream() 106 | for input_initial in input_initials: 107 | if input_initial is not None: 108 | self.logger.info("Found input output stream") 109 | _function, dvm = input_initial 110 | self.logger.info(f"{_function}") 111 | key = self.extract_variable_for_third_plan(_function, dvm) 112 | if key is not None: 113 | key = utils.unescape_unicode(key) 114 | self.logger.info(f"Found key : {key}") 115 | if self.brute_assets(key): 116 | if self.is_really_unpacked(): 117 | self.logger.info("fully unpacked") 118 | else: 119 | self.logger.info("not fully unpacked") 120 | return 121 | 122 | return None 123 | 124 | def extract_variable_for_third_plan(self, target_method: EncodedMethod, dvm): 125 | smali_str = self.get_smali(target_method) 126 | """ 127 | 0059925c: 6200 9e53 0000: sget-object v0, Lwhg/wwtgweg/mtgmdloqs/tduk/rtpgi;->kphimwvplfd:Ljava/lang/String; # field@539e 128 | 00599260: 6e10 5099 0000 0002: invoke-virtual {v0}, Ljava/lang/String;->toCharArray()[C # method@9950 129 | 00599266: 0c00 0005: move-result-object v0 130 | """ 131 | match = re.findall( 132 | r"sget-object [vp]\d+, (L[^;]+;->[^\(]+) Ljava/lang/String;\s+" 133 | r"invoke-virtual {?[vp]\d+}?, Ljava/lang/String;->toCharArray\(\)\[C", 134 | smali_str, 135 | ) 136 | if len(match) == 0: 137 | self.logger.info( 138 | f"Unable to extract variable from {target_method.get_name()}" 139 | ) 140 | self.logger.info("Exiting ...") 141 | return None 142 | if len(match) == 1: 143 | self.logger.info(f"Found variable ! : {match[0]}") 144 | key_variable = match[0].split("->")[1] 145 | key_class = match[0].split("->")[0] 146 | method = self.find_method(key_class, "") 147 | if method: 148 | smali_str = self.get_smali(method) 149 | # 0059a656: 1a00 7884 0039: const-string v0, "恐恜恞思恕恐恛恟恟恇恁恕恑思恜恊恃恃恑恕恆恛恄思恲恃恃" # string@8478 150 | # 0059a65a: 7120 b897 1000 003b: invoke-static {v0, v1}, Lehl/vlnirvo/rwipgpued/dnhwp/fstmjrrront;->hfuojgtnouejrq(Ljava/lang/String;, I)Ljava/lang/String; # method@97b8 151 | key_variable = re.findall( 152 | r"const-string [vp]\d+, \"(.*)\"\s+" 153 | f"sput-object v0, {match[0]} Ljava/lang/String;", 154 | smali_str, 155 | ) 156 | if len(key_variable) == 1: 157 | self.logger.info( 158 | f"Found key variable from zip class {key_variable[0]}" 159 | ) 160 | 161 | return key_variable[0] 162 | else: 163 | self.logger.info("Not found key variable from clinit") 164 | self.logger.info(f"{smali_str}") 165 | return None 166 | else: 167 | self.logger.info( 168 | f"Not found method for class {target_method.class_name}" 169 | ) 170 | return None 171 | else: 172 | self.logger.info("Something is wrong .. 🤔") 173 | self.logger.info("Found multiple ?? : {match}") 174 | return None 175 | 176 | def default_dex_protector(self): 177 | target_class = self.find_class_in_dvms( 178 | "Landroid/support/dexpro/utils/DexCrypto;" 179 | ) 180 | str_decrypt_keys = set() 181 | if target_class != None: 182 | self.logger.info("Found default dex protector class") 183 | # Find static field with name "KEY" 184 | for field in target_class.get_fields(): 185 | rc4_string_variable = None 186 | if field.get_descriptor() != "Ljava/lang/String;": 187 | continue 188 | if field.get_init_value() != None and field.get_init_value != "": 189 | self.logger.info( 190 | f"Found static key : {field.get_init_value().get_value()}" 191 | ) 192 | static_rc4_string = field.get_init_value().get_value() 193 | if static_rc4_string != None: 194 | str_decrypt_keys.add(static_rc4_string) 195 | else: 196 | if ( 197 | "0x0" == field.get_access_flags_string() 198 | or "protected final" == field.get_access_flags_string() 199 | or "" == field.get_access_flags_string() 200 | ): 201 | rc4_string_variable = field.get_name() 202 | 203 | # Find ProxyApplication 204 | # application = self.apk_object.get_attribute_value("application", "name") 205 | application_smali = self.find_main_application() 206 | target_method = self.find_method(application_smali, "") 207 | self.logger.info( 208 | f"Found application class : {application_smali} target_method : {target_method}" 209 | ) 210 | if target_method == None: 211 | self.logger.info("Unable to find target_method class") 212 | return 213 | smali_str = self.get_smali(target_method) 214 | 215 | # const-string v0, '4743504252544340435744245230474050425254' 216 | # invoke-static v0, Landroid/support/dexpro/utils/DexCrypto;->ab(Ljava/lang/String;)Ljava/lang/String; 217 | # move-result-object v0 218 | # iput-object v0, v1, Lxyz/magicph/dexpro/ProxyApplication;->protectKey Ljava/lang/String 219 | # const-string v0, "4743504252544340435744245230474050425254" 220 | # invoke-static v0, Landroid/support/dexpro/utils/DexCrypto;->ab(Ljava/lang/String;)Ljava/lang/String; 221 | # move-result-object v0 222 | # iput-object v0, v1, Lxyz/magicph/dexpro/ProxyApplication;->protectKey Ljava/lang/String; 223 | # Get const string from smali_str 224 | key_variable = re.findall( 225 | r"const-string(?:/jumbo)? [vp]\d+, \"(.*)\"\s+" 226 | r"invoke-static [vp]\d+, Landroid/support/dexpro/utils/DexCrypto;->[^\(]+\(Ljava/lang/String;\)Ljava/lang/String;\s+" 227 | r"move-result-object [vp]\d+\s+" 228 | r"iput-object [vp]\d+, [vp]\d+, L[^;]+;->protectKey Ljava/lang/String", 229 | smali_str, 230 | ) 231 | r = set() 232 | if len(key_variable) == 1: 233 | self.logger.info( 234 | f"Found key variable from zip class {key_variable[0]}" 235 | ) 236 | x = bytes.fromhex(key_variable[0]) 237 | for s in str_decrypt_keys: 238 | file_dec_key = utils.xor(x, s.encode()) 239 | r.add(file_dec_key) 240 | 241 | # return set of file_Dec_key 242 | return r 243 | 244 | def second_plan(self): 245 | application_smali = self.find_main_application() 246 | target_method = self.find_method(application_smali, "") 247 | 248 | if target_method == None: 249 | return None 250 | smali_str = self.get_smali(target_method) 251 | """ 252 | sget-object v0, Lb;->f:Ljava/lang/String; 253 | invoke-static {v0}, Lc;->b(Ljava/lang/String;)Ljava/lang/String; 254 | move-result-object v0 255 | """ 256 | match = re.findall( 257 | r"sget-object [vp]\d+, (L[^;]+;->[^ ]+) Ljava/lang/String;\s+" 258 | r"invoke-static {?[vp]\d+}?, L[^;]+;->[^\(]+\(Ljava/lang/String;\)Ljava/lang/String", 259 | smali_str, 260 | ) 261 | for matched_field in match: 262 | key = self.find_clinit_target_variable(matched_field) 263 | key = utils.unescape_unicode(key) 264 | 265 | if key != None: 266 | xor_k = 0x6033 267 | tmp_key = "".join(chr(xor_k ^ ord(c)) for c in key) 268 | self.logger.info(f"Is this a key ??? {tmp_key}") 269 | if tmp_key is not None: 270 | if all(c in string.printable for c in tmp_key): 271 | asset_list = self.apk_object.get_files() 272 | for filepath in asset_list: 273 | f = self.apk_object.get_file(filepath) 274 | if self.solve_encryption( 275 | f, tmp_key 276 | ) or self.solve_encryption2(f, tmp_key): 277 | return True 278 | else: 279 | return False 280 | 281 | return None 282 | 283 | def find_zip_function(self): 284 | target_methods = [] 285 | target_method = None 286 | for d in self.dexes: 287 | for c in d.get_classes(): 288 | for m in c.get_methods(): 289 | if ( 290 | m.get_descriptor() 291 | == "(Ljava/util/zip/ZipFile; Ljava/util/zip/ZipEntry; Ljava/io/File; Ljava/lang/String;)V" 292 | ): 293 | self.logger.info("Found method") 294 | target_method = m 295 | target_methods.append((target_method,d)) 296 | return target_methods 297 | 298 | def find_input_output_stream(self): 299 | target_method = None 300 | target_method_and_dvms = [] 301 | for d in self.dexes: 302 | for c in d.get_classes(): 303 | for m in c.get_methods(): 304 | if ( 305 | m.get_descriptor() 306 | == "(Ljava/io/InputStream; Ljava/io/OutputStream;)V" or m.get_descriptor() == "(Ljava/lang/String; Ljava/io/InputStream; Ljava/io/OutputStream;)V" 307 | ): 308 | if m.access_flags & 0x2 == 0x2: 309 | self.logger.info("Found method with private access") 310 | 311 | target_method = m 312 | target_method_and_dvms.append((target_method, d)) 313 | return target_method_and_dvms 314 | 315 | def find_decrypt_protect_arrays(self): 316 | for d in self.dexes: 317 | for c in d.get_classes(): 318 | for m in c.get_methods(): 319 | if m.get_descriptor() == "(I)[C": 320 | self.logger.info( 321 | f"Found decrypt protect arrays method {m.get_name()}" 322 | ) 323 | smali_str = self.get_smali(m) 324 | """ 325 | const/16 v6, 11 326 | const/4 v5, 3 327 | const/4 v4, 2 328 | const/4 v3, 1 329 | const/4 v2, 0 330 | if-eqz v7, +1d6h 331 | if-eq v7, v3, +1c8h 332 | if-eq v7, v4, +1bdh 333 | if-eq v7, v5, +005h 334 | new-array v0, v2, [C 335 | return-object v0 336 | const/16 v0, 75 337 | oto/16 -1b5 338 | new-array v0, v3, [C 339 | const/16 v1, 24627 340 | int-to-char v1, v1 341 | aput-char v1, v0, v2 342 | goto/16 -1be 343 | new-array v0, v4, [C 344 | const/16 v1, 12293 345 | aput-char v1, v0, v2 346 | const/16 v1, 12294 347 | aput-char v1, v0, v3 348 | goto/16 -1ca 349 | """ 350 | match = re.findall( 351 | r"new-array [vp]\d+, [vp]\d+, \[C\s+" 352 | r"const/16 [vp]\d+, (-?\d+)\s+" 353 | r"int-to-char [vp]\d+, [vp]\d+\s+" 354 | r"aput-char [vp]\d+, [vp]\d+, [vp]\d+\s+" 355 | r"goto/16 -?[a-f0-9]+h\s+", 356 | smali_str, 357 | ) 358 | for m in match: 359 | try: 360 | xor_k = int(m) 361 | except: 362 | self.logger.info("bad match", m) 363 | continue 364 | if self.ProtectKey != None: 365 | tmp_key = "".join( 366 | chr(xor_k ^ ord(c)) for c in self.ProtectKey 367 | ) 368 | if self.brute_assets(tmp_key): 369 | self.logger.info("Decrypted from manifest") 370 | return True 371 | else: 372 | self.logger.info("no protect key found in manifest..") 373 | else: 374 | # new-array v0, v0, [C 375 | # const/16 v1, 24627 376 | # aput-char v1, v0, v2 377 | # goto -fh 378 | # Or we can extract data from fill-array-data 379 | match = re.findall( 380 | r"new-array [vp]\d+, [vp]\d+, \[C\s+" 381 | r"const/16 [vp]\d+, (-?\d+)\s+" 382 | r"aput-char [vp]\d+, [vp]\d+, [vp]\d+\s+" 383 | r"goto -?[a-f0-9]+h\s+", 384 | smali_str, 385 | ) 386 | for m in match: 387 | try: 388 | xor_k = int(m) 389 | print(xor_k) 390 | except: 391 | self.logger.info("bad match", m) 392 | continue 393 | if self.ProtectKey != None: 394 | tmp_key = "".join( 395 | chr(xor_k ^ ord(c)) for c in self.ProtectKey 396 | ) 397 | if self.brute_assets(tmp_key): 398 | self.logger.info("Decrypted from manifest") 399 | return True 400 | else: 401 | self.logger.info( 402 | "no protect key found in manifest.." 403 | ) 404 | return False 405 | 406 | def extract_variable_from_zip(self, target_method: EncodedMethod, dvm): 407 | smali_str = self.get_smali(target_method) 408 | """ 409 | 5 invoke-virtual v3, v0, Ljava/util/zip/ZipOutputStream;->putNextEntry(Ljava/util/zip/ZipEntry;)V 410 | 6 sget-object v0, Lcom/icecream/sandwich/c;->l Ljava/lang/String; 411 | 7 new-instance v4, Ljava/util/zip/InflaterInputStream; 412 | """ 413 | match = re.findall( 414 | r"invoke-virtual [vp]\d+, [vp]\d+, [vp]\d+, Ljava/util/zip/ZipEntry;->setTime\(J\)V\s+" 415 | r"invoke-virtual {?[vp]\d+, [vp]\d+}?, L[^;]+;->[^\(]+\(Ljava/util/zip/ZipEntry;\)V\s+" 416 | r"sget-object [vp]\d+, (L[^;]+;->[^\(]+) Ljava/lang/String;\s+", 417 | smali_str, 418 | ) 419 | if len(match) == 0: 420 | self.logger.info( 421 | f"Unable to extract variable from {target_method}" 422 | ) 423 | self.logger.info("Exiting ...") 424 | return None 425 | if len(match) == 1: 426 | self.logger.info(f"Found variable ! : {match[0]}") 427 | method = self.find_method(target_method.class_name, "") 428 | if method: 429 | smali_str = self.get_smali(method) 430 | key_variable = re.findall( 431 | r"sget-object [vp]\d+, (L[^;]+;->[^\s]+) Ljava/lang/String;\s+" 432 | f"sput-object v0, {match[0]} Ljava/lang/String;", 433 | smali_str, 434 | ) 435 | if len(key_variable) == 1: 436 | self.logger.info( 437 | f"Found key variable from zip class {key_variable[0]}" 438 | ) 439 | return key_variable[0] 440 | else: 441 | self.logger.info("Not found key variable from clinit") 442 | return None 443 | else: 444 | self.logger.info("Something is wrong .. 🤔") 445 | self.logger.info("Found multiple ?? : {match}") 446 | return None 447 | 448 | def for_fun(self, variable_string): 449 | variable_class, variable_field = variable_string.split("->") 450 | key_class = self.find_class_in_dvms(variable_class) 451 | if key_class == None: 452 | self.logger.info(f"No key class found {key_class}") 453 | return None 454 | 455 | self.logger.info(f"Key class found ! {key_class}") 456 | key_clinit = self.find_method(variable_class, "") 457 | if key_clinit is not None: 458 | smali_str = self.get_smali(key_clinit) 459 | # self.logger.info(smali_str) 460 | match = re.findall( 461 | r"const-string [vp]\d+, \"(.*)\"\s+" rf"sput-object [vp]\d+, .*\s+", 462 | smali_str, 463 | ) 464 | for m in match: 465 | xor_k = 0x6033 466 | tmp_key = "".join(chr(xor_k ^ ord(c)) for c in m) 467 | self.logger.info(f"zaa??? {tmp_key}") 468 | 469 | def find_clinit_target_variable(self, variable_string): 470 | variable_class, variable_field = variable_string.split("->") 471 | key_class = self.find_class_in_dvms(variable_class) 472 | if key_class == None: 473 | self.logger.info(f"No key class found {key_class}") 474 | return None 475 | 476 | self.logger.info(f"Key class found ! {key_class}") 477 | key_clinit = self.find_method(variable_class, "") 478 | if key_clinit is not None: 479 | smali_str = self.get_smali(key_clinit) 480 | # self.logger.info(smali_str) 481 | match = re.findall( 482 | r"const-string [vp]\d+, \"(.*)\"\s+" 483 | rf"sput-object [vp]\d+, {variable_string} Ljava/lang/String;", 484 | smali_str, 485 | ) 486 | if len(match) == 0: 487 | self.logger.info( 488 | f"Cannot find string definition in clinit for target variable {variable_string}" 489 | ) 490 | # If its using apkprotecttor, we can try some other method 491 | match = re.findall( 492 | r"const-string(?:/jumbo)? [vp]\d+, \"(.*)\"\s+" 493 | r"invoke-static [vp]\d+, [vp]\d+, L[^;]+;->[^\(]+\(Ljava\/lang\/String; I\)Ljava\/lang\/String;\s+" 494 | r"move-result-object [vp]\d+\s+" 495 | rf"sput-object [vp]\d+, {variable_string} Ljava/lang/String;", 496 | smali_str, 497 | ) 498 | if len(match) == 0: 499 | match = re.findall( 500 | r"const-string(?:/jumbo)? [vp]\d+, \"(.*)\"\s+" 501 | r"invoke-static [vp]\d+, L[^;]+;->[^\(]+\(Ljava\/lang\/String;\)Ljava\/lang\/String;\s+" 502 | r"move-result-object [vp]\d+\s+" 503 | rf"sput-object [vp]\d+, {variable_string} Ljava/lang/String;", 504 | smali_str, 505 | ) 506 | 507 | if len(match) == 1: 508 | xor_k = 0x6033 509 | tmp_key = "".join(chr(xor_k ^ ord(c)) for c in match[0]) 510 | self.logger.info(f"Is this a key ??? {tmp_key}") 511 | return tmp_key 512 | if len(match) == 1: 513 | self.logger.info(f"Found key ! {match[0]}") 514 | return match[0] 515 | else: 516 | self.logger.info(f"Multiple key ? {match}") 517 | if key_clinit is None: 518 | self.logger.info(f"No clinit for {variable_class}") 519 | return None 520 | 521 | def brute_assets(self, key: str): 522 | asset_list = self.apk_object.get_files() 523 | for filepath in asset_list: 524 | f = self.apk_object.get_file(filepath) 525 | if self.solve_encryption(f, key) or self.solve_encryption2(f, key): 526 | self.logger.info("Decryption finished!!") 527 | return self.decrypted_payload_path 528 | return None 529 | 530 | def solve_encryption2(self, file_data, key): 531 | if len(file_data) < 8 or len(key) < 12: 532 | return False 533 | 534 | if file_data[0] == 0x78 and file_data[1] == 0x9C: 535 | try: 536 | encrypted = zlib.decompress(file_data) 537 | except Exception as e: 538 | self.logger.error(e) 539 | return False 540 | else: 541 | encrypted = file_data 542 | 543 | iArr = [] # 2 544 | iArr2 = [] # 4 545 | iArr3 = [None] * 27 # 27 546 | iArr4 = [] # 3 547 | if type(key) == str: 548 | key = [ord(x) for x in key] 549 | iArr = [key[8] | (key[9] << 16), key[11] << 16 | key[10]] 550 | iArr2.extend( 551 | [ 552 | key[0] | (key[1] << 16), 553 | key[2] | (key[3] << 16), 554 | key[4] | (key[5] << 16), 555 | key[6] | (key[7] << 16), 556 | ] 557 | ) 558 | iArr3[0] = iArr2[0] 559 | iArr4.extend([iArr2[1], iArr2[2], iArr2[3]]) 560 | i2 = iArr2[0] 561 | i = 0 562 | while i < 26: 563 | i3 = i % 3 564 | iArr4[i3] = ( 565 | ( 566 | (unsigned_rshift(ctypes.c_int32(iArr4[i3]).value, 8)) 567 | | ctypes.c_int32((iArr4[i3]) << 24).value 568 | ) 569 | + i2 570 | ) ^ i 571 | i2 = ( 572 | ctypes.c_int32(i2 << 3).value 573 | | (unsigned_rshift(ctypes.c_int32(i2).value, 29)) 574 | ) ^ ctypes.c_int32(iArr4[i3]).value 575 | i += 1 576 | iArr3[i] = i2 577 | 578 | decrypted_bytes = bytearray() 579 | # self.logger.info(f"{iArr3}") 580 | z = 0 581 | for b in encrypted: 582 | if z % 8 == 0: 583 | h0 = iArr[0] 584 | h1 = iArr[1] 585 | for k in iArr3: 586 | tmp0 = ((unsigned_rshift(h1, 8) | (h1 << 24) & 0xFFFFFFFF) + h0) ^ k 587 | tmp1 = ((h0 << 3) & 0xFFFFFFFF | unsigned_rshift(h0, 29)) ^ tmp0 588 | h0 = tmp1 & 0xFFFFFFFF 589 | h1 = tmp0 & 0xFFFFFFFF 590 | iArr[0] = h0 591 | iArr[1] = h1 592 | b ^= iArr[int((z % 8) / 4)] >> (8 * (z % 4)) & 0xFF 593 | if (z == 0 and b != 0x78) or (z == 1 and b != 0x9C): 594 | return False 595 | z += 1 596 | decrypted_bytes.append(b) 597 | if self.check_and_write_file(decrypted_bytes): 598 | self.logger.info("Found in second algo finished") 599 | return True 600 | return False 601 | 602 | def solve_encryption(self, file_data: bytes, key: str): 603 | if len(file_data) < 8 or len(key) < 12: 604 | return False 605 | if file_data[0] == 0x78 and file_data[1] == 0x9C: 606 | try: 607 | encrypted = zlib.decompress(file_data) 608 | except Exception as e: 609 | self.logger.error(e) 610 | return False 611 | else: 612 | encrypted = file_data 613 | decrypted_bytes = bytearray() 614 | indexes = [0, 0, 0, 0, 1, 1, 1, 1] 615 | bits = [0, 8, 16, 24] 616 | if type(key) == str: 617 | c = [ord(x) for x in key] 618 | else: 619 | c = key 620 | poolArr = [(c[9] << 16) | c[8], (c[11] << 16) | c[10]] 621 | check_0 = (poolArr[indexes[0]]) >> bits[0] & 0xFF ^ encrypted[0] 622 | check_1 = (poolArr[indexes[0]]) >> bits[0] & 0xFF ^ encrypted[1] 623 | if check_0 != 0x78 and check_1 != 0x9C: 624 | return False 625 | for i, b in enumerate(encrypted): 626 | b ^= (poolArr[indexes[i % 8]]) >> bits[i % 4] & 0xFF 627 | decrypted_bytes.append(b) 628 | 629 | if self.check_and_write_file(decrypted_bytes): 630 | self.logger.info("Found in first algo") 631 | return True 632 | else: 633 | return False 634 | --------------------------------------------------------------------------------