├── .gitignore ├── README.md ├── pyxamstore ├── __init__.py ├── constants.py └── explorer.py ├── requirements.txt └── setup.py /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 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 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | out/ 163 | assemblies.* 164 | *.apk 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xamarin AssemblyStore Explorer (pyxamstore) 2 | This is an alpha release of an `assemblies.blob` AssemblyStore parser written in Python. The tool is capable of unpack and repackaging `assemblies.blob` and `assemblies.manifest` Xamarin files from an APK. 3 | 4 | ## Installing 5 | Run the installer script: 6 | 7 | python setup.py install 8 | 9 | You can then use the tool by calling `pyxamstore` 10 | 11 | ## Usage 12 | ### Unpacking 13 | I recommend using the tool in conjunction with `apktool`. The following commands can be used to unpack an APK and unpack the Xamarin DLLs: 14 | 15 | apktool d yourapp.apk 16 | pyxamstore unpack -d yourapp/unknown/assemblies/ 17 | 18 | Assemblies that are detected as compressed with LZ4 will be automatically decompressed in the extraction process. 19 | 20 | ### Repacking 21 | If you want to make changes to the DLLs within the AssemblyStore, you can use `pyxamstore` along with the `assemblies.json` generated during the unpack to create a new `assemblies.blob` file(s). The following command from the directory where your `assemblies.json` file exists: 22 | 23 | pyxamstore pack 24 | 25 | From here you'll need to copy the new manifest and blobs as well as repackage/sign the APK. 26 | 27 | # Additional Details 28 | Additional file format details can be found on my [personal website](https://www.thecobraden.com/posts/unpacking_xamarin_assembly_stores/). 29 | 30 | # Known Limitations 31 | * Python3 support (working on it!) 32 | * DLLs that have debug/config data associated with them 33 | -------------------------------------------------------------------------------- /pyxamstore/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakev/pyxamstore/23cd52612702d08495570f96f60883d6047ae429/pyxamstore/__init__.py -------------------------------------------------------------------------------- /pyxamstore/constants.py: -------------------------------------------------------------------------------- 1 | """Global Values""" 2 | 3 | 4 | # AssemblyStore Constants 5 | ASSEMBLY_STORE_MAGIC = b"XABA" 6 | ASSEMBLY_STORE_FORMAT_VERSION = 1 7 | 8 | COMPRESSED_DATA_MAGIC = b"XALZ" 9 | 10 | # Assemblies related 11 | FILE_ASSEMBLIES_BLOB = "assemblies.blob" 12 | FILE_ASSEMBLIES_BLOB_ARM = "assemblies.armeabi_v7a.blob" 13 | FILE_ASSEMBLIES_BLOB_ARM_64 = "assemblies.arm64_v8a.blob" 14 | FILE_ASSEMBLIES_BLOB_x86 = "assemblies.x86.blob" 15 | FILE_ASSEMBLIES_BLOB_x86_64 = "assemblies.x86_64.blob" 16 | 17 | ARCHITECTURE_MAP = {"arm": FILE_ASSEMBLIES_BLOB_ARM, 18 | "arm64": FILE_ASSEMBLIES_BLOB_ARM_64, 19 | "x86": FILE_ASSEMBLIES_BLOB_x86, 20 | "x86_64": FILE_ASSEMBLIES_BLOB_x86_64} 21 | 22 | FILE_ASSEMBLIES_MANIFEST = "assemblies.manifest" 23 | 24 | # Output / Internal 25 | FILE_ASSEMBLIES_JSON = "assemblies.json" 26 | -------------------------------------------------------------------------------- /pyxamstore/explorer.py: -------------------------------------------------------------------------------- 1 | """Pack and unpack Xamarin AssemblyStore files""" 2 | 3 | from __future__ import print_function 4 | from builtins import object 5 | import struct 6 | import argparse 7 | import os 8 | import os.path 9 | import sys 10 | import json 11 | import shutil 12 | 13 | import lz4.block 14 | import xxhash 15 | 16 | from . import constants 17 | 18 | # Enable debugging here. 19 | DEBUG = False 20 | 21 | def debug(message): 22 | 23 | """Print a debuggable message""" 24 | 25 | if DEBUG: 26 | print("[debug] %s" % message) 27 | 28 | 29 | class ManifestEntry(object): 30 | 31 | """Element in Manifest""" 32 | 33 | hash32 = "" 34 | hash64 = "" 35 | blob_id = 0 36 | blob_idx = 0 37 | name = "" 38 | 39 | def __init__(self, hash32, hash64, blob_id, blob_idx, name): 40 | 41 | """Initialize item""" 42 | 43 | self.hash32 = hash32 44 | self.hash64 = hash64 45 | self.blob_id = int(blob_id) 46 | self.blob_idx = int(blob_idx) 47 | self.name = name 48 | 49 | 50 | class ManifestList(list): 51 | 52 | """List of manifest entries""" 53 | 54 | def get_idx(self, blob_id, blob_idx): 55 | 56 | """Find entry by ID""" 57 | 58 | for entry in self: 59 | if entry.blob_idx == blob_idx and entry.blob_id == blob_id: 60 | return entry 61 | return None 62 | 63 | 64 | class AssemblyStoreAssembly(object): 65 | 66 | """Assembly Details""" 67 | 68 | data_offset = 0 69 | data_size = 0 70 | debug_data_offset = 0 71 | debug_data_size = 0 72 | config_data_offset = 0 73 | config_data_size = 0 74 | 75 | def __init__(self): 76 | pass 77 | 78 | 79 | class AssemblyStoreHashEntry(object): 80 | 81 | """Hash Details""" 82 | 83 | hash_val = "" 84 | mapping_index = 0 85 | local_store_index = 0 86 | store_id = 0 87 | 88 | def __init__(self): 89 | pass 90 | 91 | 92 | class AssemblyStore(object): 93 | 94 | """AssemblyStore object""" 95 | 96 | raw = "" 97 | 98 | file_name = "" 99 | 100 | manifest_entries = None 101 | 102 | hdr_magic = "" 103 | hdr_version = 0 104 | hdr_lec = 0 105 | hdr_gec = 0 106 | hdr_store_id = 0 107 | 108 | assembly_list = None 109 | global_hash32 = None 110 | global_hash64 = None 111 | 112 | def __init__(self, in_file_name, manifest_entries, primary=True): 113 | 114 | """Parse and read store""" 115 | 116 | self.manifest_entries = manifest_entries 117 | self.file_name = os.path.basename(in_file_name) 118 | 119 | blob_file = open(in_file_name, "rb") 120 | 121 | self.raw = blob_file.read() 122 | 123 | blob_file.seek(0) 124 | 125 | # Header Section 126 | # 127 | # 0 - 3: Magic 128 | # 4 - 7: Version 129 | # 8 - 11: LocalEntryCount 130 | # 12 - 15: GlobalEntryCount 131 | # 16 - 19: StoreID 132 | 133 | magic = blob_file.read(4) 134 | if magic != constants.ASSEMBLY_STORE_MAGIC: 135 | raise Exception("Invalid Magic: %s" % magic) 136 | 137 | version = struct.unpack("I", blob_file.read(4))[0] 138 | if version > constants.ASSEMBLY_STORE_FORMAT_VERSION: 139 | raise Exception("This version is higher than expected! Max = %d, got %d" 140 | % constants.ASSEMBLY_STORE_FORMAT_VERSION, version) 141 | 142 | self.hdr_version = version 143 | 144 | self.hdr_lec = struct.unpack("I", blob_file.read(4))[0] 145 | self.hdr_gec = struct.unpack("I", blob_file.read(4))[0] 146 | self.hdr_store_id = struct.unpack("I", blob_file.read(4))[0] 147 | 148 | debug("Local entry count: %d" % self.hdr_lec) 149 | debug("Global entry count: %d" % self.hdr_gec) 150 | 151 | self.assemblies_list = list() 152 | 153 | debug("Entries start at: %d (0x%x)" % (blob_file.tell(), blob_file.tell())) 154 | 155 | i = 0 156 | while i < self.hdr_lec: 157 | 158 | # 0 - 3: DataOffset 159 | # 4 - 7: DataSize 160 | # 8 - 11: DebugDataOffset 161 | # 12 - 15: DebugDataSize 162 | # 16 - 19: ConfigDataOffset 163 | # 20 - 23: ConfigDataSize 164 | 165 | debug("Extracting Assembly: %d (0x%x)" % (blob_file.tell(), blob_file.tell())) 166 | entry = blob_file.read(24) 167 | 168 | assembly = AssemblyStoreAssembly() 169 | 170 | assembly.data_offset = struct.unpack("I", entry[0:4])[0] 171 | assembly.data_size = struct.unpack("I", entry[4:8])[0] 172 | assembly.debug_data_offset = struct.unpack("I", entry[8:12])[0] 173 | assembly.debug_data_size = struct.unpack("I", entry[12:16])[0] 174 | assembly.config_data_offset = struct.unpack("I", entry[16:20])[0] 175 | assembly.config_data_size = struct.unpack("I", entry[20:24])[0] 176 | 177 | self.assemblies_list.append(assembly) 178 | 179 | debug(" Data Offset: %d (0x%x)" % (assembly.data_offset, assembly.data_offset)) 180 | debug(" Data Size: %d (0x%x)" % (assembly.data_size, assembly.data_size)) 181 | debug(" Config Offset: %d (0x%x)" % (assembly.config_data_offset, assembly.config_data_offset)) 182 | debug(" Config Size: %d (0x%x)" % (assembly.config_data_size, assembly.config_data_size)) 183 | debug(" Debug Offset: %d (0x%x)" % (assembly.debug_data_offset, assembly.debug_data_offset)) 184 | debug(" Debug Size: %d (0x%x)" % (assembly.debug_data_size, assembly.debug_data_size)) 185 | 186 | i += 1 187 | 188 | if not primary: 189 | debug("Skipping hash sections in non-primary store") 190 | return 191 | 192 | # Parse Hash data 193 | # 194 | # The following 2 sections are _required_ to be in order from 195 | # lowest to highest (e.g. 0x00000000 to 0xffffffff). 196 | # Since you're very likely not going to be adding assemblies 197 | # (or renaming) to the store, I'm going to store the hashes with the 198 | # assemblies.json to make sorting easier when packing. 199 | 200 | debug("Hash32 start at: %d (0x%x)" % (blob_file.tell(), blob_file.tell())) 201 | self.global_hash32 = list() 202 | 203 | i = 0 204 | while i < self.hdr_lec: 205 | 206 | entry = blob_file.read(20) 207 | 208 | hash_entry = AssemblyStoreHashEntry() 209 | 210 | hash_entry.hash_val = "0x%08x" % struct.unpack("") 402 | print("") 403 | print(" MODES:") 404 | print("\tunpack Unpack assembly blobs.") 405 | print("\tpack Repackage assembly blobs.") 406 | print("\thash file_name Generate xxHash values.") 407 | print("\thelp Print this message.") 408 | 409 | return 0 410 | 411 | 412 | def do_unpack(in_directory, in_arch, force): 413 | 414 | """Unpack a assemblies.blob/manifest""" 415 | 416 | arch_assemblies = False 417 | 418 | if force and os.path.isdir("out/"): 419 | shutil.rmtree("out/") 420 | 421 | # First check if all files exist. 422 | if os.path.isdir("out/"): 423 | print("Out directory already exists!") 424 | return 3 425 | 426 | manifest_path = os.path.join(in_directory, constants.FILE_ASSEMBLIES_MANIFEST) 427 | assemblies_path = os.path.join(in_directory, constants.FILE_ASSEMBLIES_BLOB) 428 | 429 | if not os.path.isfile(manifest_path): 430 | print("Manifest file '%s' does not exist!" % manifest_path) 431 | return 4 432 | elif not os.path.isfile(assemblies_path): 433 | print("Main assemblies blob '%s' does not exist!" % assemblies_path) 434 | return 4 435 | 436 | # The manifest will have all entries (regardless of which 437 | # *.blob they're found in. Parse this first, and then handle 438 | # each blob. 439 | 440 | manifest_entries = read_manifest(manifest_path) 441 | if manifest_entries is None: 442 | print("Unable to parse assemblies.manifest file!") 443 | return 5 444 | 445 | json_data = dict() 446 | json_data['stores'] = list() 447 | json_data['assemblies'] = list() 448 | 449 | os.mkdir("out/") 450 | 451 | assembly_store = AssemblyStore(assemblies_path, manifest_entries) 452 | 453 | if assembly_store.hdr_lec != assembly_store.hdr_gec: 454 | arch_assemblies = True 455 | debug("There are more assemblies to unpack here!") 456 | 457 | # Do extraction. 458 | json_data = assembly_store.extract_all(json_data) 459 | 460 | # What about architecture assemblies? 461 | if arch_assemblies: 462 | arch_assemblies_path = os.path.join(in_directory, 463 | constants.ARCHITECTURE_MAP[in_arch]) 464 | 465 | arch_assembly_store = AssemblyStore(arch_assemblies_path, 466 | manifest_entries, 467 | primary=False) 468 | json_data = arch_assembly_store.extract_all(json_data) 469 | 470 | # Save the large config out. 471 | with open(constants.FILE_ASSEMBLIES_JSON, 'w') as assembly_file: 472 | assembly_file.write(json.dumps(json_data, indent=4)) 473 | 474 | def do_pack(in_json_config): 475 | 476 | """Create new assemblies.blob/manifest""" 477 | 478 | if not os.path.isfile(in_json_config): 479 | print("Config file '%s' does not exist!" % in_json_config) 480 | return -1 481 | 482 | if os.path.isfile("assemblies.manifest.new"): 483 | print("Output manifest exists!") 484 | return -2 485 | 486 | 487 | if os.path.isfile("assemblies.blob.new"): 488 | print("Output blob exists!") 489 | return -3 490 | 491 | json_data = None 492 | with open(in_json_config, "r") as json_f: 493 | json_data = json.load(json_f) 494 | 495 | # Write new assemblies.manifest 496 | print("Writing 'assemblies.manifest.new'...") 497 | assemblies_manifest_f = open("assemblies.manifest.new", "w") 498 | 499 | assemblies_manifest_f.write("Hash 32 Hash 64 ") 500 | assemblies_manifest_f.write("Blob ID Blob idx Name\r\n") 501 | 502 | #for _, store_json in json_data['stores'].items(): 503 | for assembly in json_data['assemblies']: 504 | hash32, hash64 = gen_xxhash(assembly['name']) 505 | 506 | line = ("0x%08s 0x%016s %03d %04d %s\r\n" 507 | % (hash32, hash64, assembly['store_id'], 508 | assembly['blob_idx'], assembly['name'])) 509 | 510 | assemblies_manifest_f.write(line) 511 | 512 | assemblies_manifest_f.close() 513 | 514 | # This is hacky, but we need the lec/gec if there are multiple stores. 515 | store_zero_lec = 0 516 | for assembly_store in json_data['stores']: 517 | for store_name, store_data in list(assembly_store.items()): 518 | if store_name == "assemblies.blob": 519 | store_zero_lec = store_data['header']['lec'] 520 | 521 | # Next do the blobs. 522 | for assembly_store in json_data['stores']: 523 | for store_name, store_data in list(assembly_store.items()): 524 | 525 | out_store_name = "%s.new" % store_name 526 | 527 | # Pack the new AssemblyStore structure 528 | print("Writing '%s'..." % out_store_name) 529 | assemblies_blob_f = open(out_store_name, "wb") 530 | 531 | # Write header 532 | json_hdr = store_data['header'] 533 | assemblies_blob_f.write(struct.pack("4sIIII", 534 | constants.ASSEMBLY_STORE_MAGIC, 535 | json_hdr['version'], 536 | json_hdr['lec'], 537 | json_hdr['gec'], 538 | json_hdr['store_id'])) 539 | 540 | # Offsets are weird. 541 | # If this is a primary store, the data is: 542 | # -header 543 | # -ASA header 544 | # -hash32 545 | # -hash64 546 | # -ASA data 547 | # But a non-primary does not have hashes. Best to determine early 548 | # if this is primary and act accordingly throughout. 549 | primary = bool(json_hdr['store_id'] == 0) 550 | 551 | next_entry_offset = 20 552 | next_data_offset = 20 + (json_hdr['lec'] * 24) + (json_hdr['gec'] * 40) 553 | 554 | if not primary: 555 | next_data_offset = 20 + (json_hdr['lec'] * 24) 556 | 557 | # First pass: Write the entries + DLL content. 558 | for assembly in json_data['assemblies']: 559 | 560 | if assembly['store_id'] != json_hdr['store_id']: 561 | debug("Skipping assembly for another store") 562 | continue 563 | 564 | assembly_data = open(assembly['file'], "rb").read() 565 | if assembly['lz4']: 566 | assembly_data = lz4_compress(assembly_data, 567 | assembly['lz4_desc_idx']) 568 | 569 | data_size = len(assembly_data) 570 | 571 | # Write the entry data 572 | assemblies_blob_f.seek(next_entry_offset) 573 | assemblies_blob_f.write(struct.pack("IIIIII", 574 | next_data_offset, 575 | data_size, 576 | 0, 0, 0, 0)) 577 | 578 | # Write binary data 579 | assemblies_blob_f.seek(next_data_offset) 580 | assemblies_blob_f.write(assembly_data) 581 | 582 | # Move all offsets forward. 583 | next_data_offset += data_size 584 | next_entry_offset += 24 585 | 586 | # Second + third pass: sort the hashes and write them 587 | # But skip if not primary. 588 | if not primary: 589 | assemblies_blob_f.close() 590 | continue 591 | 592 | next_hash32_offset = 20 + (json_hdr['lec'] * 24) 593 | next_hash64_offset = 20 + (json_hdr['lec'] * 24) + (json_hdr['gec'] * 20) 594 | 595 | assembly_data = json_data["assemblies"] 596 | 597 | # hash32 598 | for assembly in sorted(assembly_data, key=lambda d: d['hash32']): 599 | 600 | # Hash sections 601 | hash32, hash64 = gen_xxhash(assembly['name'], raw=True) 602 | mapping_id = assembly['blob_idx'] if assembly['store_id'] == 0 else store_zero_lec + assembly['blob_idx'] 603 | 604 | # Write the hash32 605 | assemblies_blob_f.seek(next_hash32_offset) 606 | assemblies_blob_f.write(struct.pack("4sIIII", 607 | hash32, 608 | 0, 609 | mapping_id, 610 | assembly['blob_idx'], 611 | assembly['store_id'])) 612 | 613 | next_hash32_offset += 20 614 | 615 | # hash64 616 | for assembly in sorted(assembly_data, key=lambda d: d['hash64']): 617 | 618 | # Hash sections 619 | hash32, hash64 = gen_xxhash(assembly['name'], raw=True) 620 | mapping_id = assembly['blob_idx'] if assembly['store_id'] == 0 else store_zero_lec + assembly['blob_idx'] 621 | 622 | # Write the hash64 623 | assemblies_blob_f.seek(next_hash64_offset) 624 | assemblies_blob_f.write(struct.pack("8sIII", 625 | hash64, 626 | mapping_id, 627 | assembly['blob_idx'], 628 | assembly['store_id'])) 629 | 630 | next_hash64_offset += 20 631 | 632 | # Done! 633 | assemblies_blob_f.close() 634 | 635 | return 0 636 | 637 | 638 | def unpack_store(args): 639 | 640 | """Unpack an assemblies store""" 641 | 642 | parser = argparse.ArgumentParser(prog='pyxamstore unpack', 643 | description='Unpack DLLs from assemblies.blob store.') 644 | parser.add_argument('--dir', '-d', type=str, metavar='val', 645 | default='./', 646 | dest='directory', 647 | help='Where to load blobs/manifest from.') 648 | parser.add_argument('--arch', '-a', type=str, metavar='val', 649 | default='arm64', 650 | dest='architecture', 651 | help='Which architecture to unpack: arm(64), x86(_64)') 652 | parser.add_argument('--force', '-f', action='store_const', 653 | dest='force', const=True, default=False, 654 | help="Force re-create out/ directory.") 655 | 656 | parsed_args = parser.parse_args(args) 657 | 658 | return do_unpack(parsed_args.directory, 659 | parsed_args.architecture, 660 | parsed_args.force) 661 | 662 | 663 | def pack_store(args): 664 | 665 | """Pack an assemblies store""" 666 | 667 | parser = argparse.ArgumentParser(prog='pyxamstore pack', 668 | description='Repackage DLLs into assemblies.blob.') 669 | parser.add_argument('--config', '-c', type=str, metavar='val', 670 | default='assemblies.json', 671 | dest='config_json', 672 | help='Input assemblies.json file.') 673 | 674 | parsed_args = parser.parse_args(args) 675 | 676 | if not os.path.isfile(parsed_args.config_json): 677 | print("File '%s' doesn't exist!" % parsed_args.config_json) 678 | return -3 679 | 680 | return do_pack(parsed_args.config_json) 681 | 682 | 683 | def gen_hash(args): 684 | 685 | """Generate xxhashes for a given file path/string, mostly for testing""" 686 | 687 | if len(args) < 1: 688 | print("Need to provide a string to hash!") 689 | return -1 690 | 691 | file_name = args.pop(0) 692 | hash_name = os.path.splitext(os.path.basename(file_name))[0] 693 | 694 | print("Generating hashes for string '%s' (%s)" % (file_name, hash_name)) 695 | hash32, hash64 = gen_xxhash(hash_name) 696 | 697 | print("Hash32: 0x%s" % hash32) 698 | print("Hash64: 0x%s" % hash64) 699 | 700 | return 0 701 | 702 | 703 | def main(): 704 | 705 | """Main Loop""" 706 | 707 | if len(sys.argv) < 2: 708 | print("Mode is required!") 709 | usage() 710 | return -1 711 | 712 | sys.argv.pop(0) 713 | mode = sys.argv.pop(0) 714 | 715 | if mode == "unpack": 716 | return unpack_store(sys.argv) 717 | elif mode == "pack": 718 | return pack_store(sys.argv) 719 | elif mode == "hash": 720 | return gen_hash(sys.argv) 721 | elif mode in ['-h', '--h', 'help']: 722 | return usage() 723 | 724 | print("Unknown mode: '%s'" % mode) 725 | return -2 726 | 727 | 728 | if __name__ == "__main__": 729 | main() 730 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future==0.18.3 2 | lz4==4.3.1 3 | xxhash==3.2.0 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup Script""" 2 | 3 | from __future__ import absolute_import 4 | from setuptools import setup 5 | 6 | 7 | VERSION = '1.0.0' 8 | 9 | setup( 10 | name='pyxamstore', 11 | version=VERSION, 12 | description='Xamarin AssemblyStore Explorer (pyxamstore)', 13 | 14 | download_url='https://github.com/jakev/pyxamstore', 15 | 16 | author='Jake Valletta', 17 | author_email='javallet@gmail.com', 18 | 19 | classifiers=[ 20 | 'Development Status :: 4 - Beta', 21 | 'Intended Audience :: Information Technology', 22 | 'Topic :: Security', 23 | 'Operating System :: POSIX :: Linux', 24 | 'Programming Language :: Python :: 2', 25 | 'Programming Language :: Python :: 2.7'], 26 | 27 | keywords='android device security mobile reverse-engineering Xamarin AssemblyStore', 28 | 29 | packages=["pyxamstore"], 30 | 31 | install_requires=open("requirements.txt", "rb").read().decode("utf-8").split("\n"), 32 | 33 | entry_points={ 34 | 'console_scripts': [ 35 | 'pyxamstore = pyxamstore.explorer:main', 36 | ], 37 | }, 38 | ) 39 | --------------------------------------------------------------------------------