stitch 0.0.21__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
stitch-0.0.21/LICENSE ADDED
@@ -0,0 +1,21 @@
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.
stitch-0.0.21/PKG-INFO ADDED
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: stitch
3
+ Version: 0.0.21
4
+ Author-email: Alon Schwartzblat <alon.ponch@gmail.com>
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: PyYAML
10
+ Requires-Dist: androguard
11
+ Dynamic: license-file
12
+
13
+ # Stitch
14
+ Stitch is a powerful APK patching python library that allows you to inject your own java module into any APK, bundle or XAPK file.
15
+
16
+ ## How it works
17
+
18
+ Will be added soon.
19
+
20
+ ## Projects using Stitch:
21
+ - [MakoPatcher][https://github.com/Schwartzblat/MakoPatcher]- A patcher for 12+ app.
22
+
23
+ ## How to use
24
+ ### Installation
25
+ You can install Stitch using pip:
26
+ ```bash
27
+ # For now, install the test version from TestPyPI
28
+ pip install stitch
29
+ ```
30
+
31
+ ### Basic Usage
32
+ Create an Android Gradle project like my smali_generator (Check it out in on of the examples) that generates the java module you want to inject.
33
+
34
+ Then, use the following code to patch an APK:
35
+ ```python
36
+ from stitch import Stitch
37
+
38
+ with Stitch(
39
+ apk_path='./input.apk',
40
+ output_apk='./output.apk',
41
+ temp_path='./temp',
42
+ external_module='./smali_generator'
43
+ ) as stitch:
44
+ stitch.patch()
45
+ ```
46
+ And that's it! Your APK will be patched with the injected module.
47
+
48
+ ## Contributing
49
+
50
+ I will be happy if you want to contribute to this project. Feel free to open issues or submit pull requests.
51
+
52
+ ## Disclaimer
53
+
54
+ For educational purpose only or something like that. I am not responsible for any misuse of this software.
@@ -0,0 +1,42 @@
1
+ # Stitch
2
+ Stitch is a powerful APK patching python library that allows you to inject your own java module into any APK, bundle or XAPK file.
3
+
4
+ ## How it works
5
+
6
+ Will be added soon.
7
+
8
+ ## Projects using Stitch:
9
+ - [MakoPatcher][https://github.com/Schwartzblat/MakoPatcher]- A patcher for 12+ app.
10
+
11
+ ## How to use
12
+ ### Installation
13
+ You can install Stitch using pip:
14
+ ```bash
15
+ # For now, install the test version from TestPyPI
16
+ pip install stitch
17
+ ```
18
+
19
+ ### Basic Usage
20
+ Create an Android Gradle project like my smali_generator (Check it out in on of the examples) that generates the java module you want to inject.
21
+
22
+ Then, use the following code to patch an APK:
23
+ ```python
24
+ from stitch import Stitch
25
+
26
+ with Stitch(
27
+ apk_path='./input.apk',
28
+ output_apk='./output.apk',
29
+ temp_path='./temp',
30
+ external_module='./smali_generator'
31
+ ) as stitch:
32
+ stitch.patch()
33
+ ```
34
+ And that's it! Your APK will be patched with the injected module.
35
+
36
+ ## Contributing
37
+
38
+ I will be happy if you want to contribute to this project. Feel free to open issues or submit pull requests.
39
+
40
+ ## Disclaimer
41
+
42
+ For educational purpose only or something like that. I am not responsible for any misuse of this software.
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "stitch"
3
+ version = "0.0.21"
4
+ description = ""
5
+ authors = [
6
+ { name = "Alon Schwartzblat", email = "alon.ponch@gmail.com" },
7
+ ]
8
+ requires-python = ">=3.11"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ dependencies = [
13
+ "PyYAML",
14
+ "androguard"
15
+ ]
16
+ [build-system]
17
+ requires = ["setuptools>=81.0.0"]
18
+ build-backend = "setuptools.build_meta"
19
+
20
+ [tool.setuptools.packages.find]
21
+ where = ["src"]
22
+
23
+ [tool.setuptools.package-data]
24
+ "*" = ["bin/*.jar"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ from stitch.stitch import Stitch
@@ -0,0 +1,125 @@
1
+ import glob
2
+ import os
3
+ from pathlib import Path
4
+ import shutil
5
+ import subprocess
6
+ import typing
7
+ import zipfile
8
+ import yaml
9
+ from stitch.common import APKTOOL_PATH, UBER_APK_SIGNER_PATH, EXTRACTED_PATH, BUNDLE_APK_EXTRACTED_PATH, BUNDLE_DIR_PATH
10
+
11
+ main_apk_name = 'base.apk'
12
+
13
+
14
+ def is_bundle(path: os.PathLike) -> bool:
15
+ with zipfile.ZipFile(path, 'r') as zip_file:
16
+ for file in zip_file.namelist():
17
+ if file.endswith('.apk'):
18
+ return True
19
+ return False
20
+
21
+
22
+ def extract_apk(apk_path: os.PathLike, temp_path: Path, extracted_path: typing.Optional[Path] = None) -> None:
23
+ if str(apk_path).endswith('.xapk'):
24
+ global main_apk_name
25
+ with zipfile.ZipFile(apk_path, 'r') as zip_file:
26
+ apk_files = [file for file in zip_file.namelist() if file.endswith('.apk')]
27
+ if len(apk_files) == 0:
28
+ raise ValueError('No APK files found in the XAPK.')
29
+ main_apk_name = max(apk_files, key=lambda x: zip_file.getinfo(x).file_size)
30
+ if is_bundle(apk_path):
31
+ with zipfile.ZipFile(apk_path, 'r') as zip_file:
32
+ zip_file.extractall(temp_path / BUNDLE_APK_EXTRACTED_PATH)
33
+ os.makedirs(temp_path / BUNDLE_DIR_PATH, exist_ok=True)
34
+ for apk_file in glob.iglob(str(temp_path / BUNDLE_APK_EXTRACTED_PATH / '*.apk')):
35
+ if os.path.basename(apk_file) != main_apk_name:
36
+ shutil.copy(apk_file, temp_path / BUNDLE_DIR_PATH)
37
+ extract_apk(temp_path / BUNDLE_APK_EXTRACTED_PATH / main_apk_name, temp_path)
38
+ return
39
+ subprocess.check_call(
40
+ [
41
+ "java",
42
+ "-jar",
43
+ APKTOOL_PATH,
44
+ "d",
45
+ "-q",
46
+ "-r",
47
+ "--output",
48
+ extracted_path if extracted_path is not None else temp_path / EXTRACTED_PATH,
49
+ apk_path,
50
+ ],
51
+ timeout=20 * 60,
52
+ )
53
+
54
+
55
+ def compile_apk(input_path: Path, output_path: Path) -> None:
56
+ yml_path = input_path / 'apktool.yml'
57
+ if yml_path.exists():
58
+ with open(yml_path, 'r') as file:
59
+ apktool_yml = yaml.safe_load(file)
60
+ if 'so' not in apktool_yml['doNotCompress']:
61
+ apktool_yml['doNotCompress'].append('so')
62
+ with open(yml_path, 'w') as file:
63
+ yaml.safe_dump(apktool_yml, file, default_flow_style=False, sort_keys=False)
64
+ for i in range(2):
65
+ try:
66
+ subprocess.check_call([
67
+ "java",
68
+ "-jar",
69
+ APKTOOL_PATH,
70
+ "build",
71
+ "-q",
72
+ str(input_path),
73
+ "--output",
74
+ str(output_path)
75
+ ], timeout=20 * 60
76
+ )
77
+ break
78
+ except Exception as e:
79
+ if i == 1:
80
+ raise e
81
+
82
+
83
+ def sign_apk(bundle_dir: Path, apk_path: Path, output_path: Path, is_bundle_file: bool) -> None:
84
+ apk_files = [str(apk_path)]
85
+ for file in glob.glob(str(bundle_dir / '*.apk')):
86
+ apk_files.append(str(file))
87
+ for file in apk_files:
88
+ args = ["java", "-jar", UBER_APK_SIGNER_PATH]
89
+ if os.environ.get('KEYSTORE_PATH') is not None:
90
+ args.extend(["--ks", os.environ['KEYSTORE_PATH']])
91
+ if os.environ.get('KEY_ALIAS') is not None:
92
+ args.extend(["--ksAlias", os.environ['KEY_ALIAS']])
93
+ if os.environ.get('KEYSTORE_PASSWORD') is not None:
94
+ args.extend(["--ksPass", os.environ['KEYSTORE_PASSWORD']])
95
+ if os.environ.get('KEY_PASSWORD') is not None:
96
+ args.extend(["--ksKeyPass", os.environ['KEY_PASSWORD']])
97
+ args.extend(['--allowResign', '--apks', file])
98
+ subprocess.check_call(args, timeout=20 * 60)
99
+ os.remove(file)
100
+ if is_bundle_file:
101
+ os.rename(
102
+ f'{file.removesuffix(".apk")}-aligned-signed.apk',
103
+ f'{file.removesuffix(".apk")}.apk',
104
+ )
105
+ else:
106
+ os.rename(f'{str(apk_path).removesuffix(".apk")}-aligned-signed.apk', output_path)
107
+
108
+
109
+ def _recursive_search_class(parent: Path, class_path: list) -> typing.Optional[Path]:
110
+ for child in parent.iterdir():
111
+ if len(class_path) == 1 and child.is_file() and child.name == f'{class_path[0]}.smali':
112
+ return child
113
+ elif child.is_dir() and child.name == class_path[0]:
114
+ return _recursive_search_class(child, class_path[1:])
115
+ return None
116
+
117
+
118
+ def find_smali_file_by_class_name(parent: Path, class_name: str) -> typing.Optional[Path]:
119
+ for child in parent.iterdir():
120
+ if not child.is_dir() or not str(child.name).startswith('smali'):
121
+ continue
122
+ file_path = _recursive_search_class(child, class_name.split('.'))
123
+ if file_path:
124
+ return file_path
125
+ return None
@@ -0,0 +1,19 @@
1
+ from abc import abstractmethod
2
+ import re
3
+
4
+ CLASS_NAME_RE = re.compile(r'\.class public.*L(?P<name>[\w/]+)')
5
+
6
+
7
+ class SimpleArtifactoryFinder:
8
+ def __init__(self, args):
9
+ self.args = args
10
+ self.is_once = True
11
+ self.is_found = False
12
+
13
+ @abstractmethod
14
+ def class_filter(self, class_data: str) -> bool:
15
+ pass
16
+
17
+ @abstractmethod
18
+ def extract_artifacts(self, artifacts: dict, class_data: str) -> None:
19
+ pass
@@ -0,0 +1,26 @@
1
+ """
2
+ This module supposed to be edited by the user to generate the artifactory for their project.
3
+ """
4
+ import glob
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from stitch.common import EXTRACTED_PATH
9
+
10
+
11
+ def generate_artifactory(temp_path: Path, artifacts_finders: list):
12
+ artifacts = dict()
13
+ for filename in glob.iglob(os.path.join(temp_path, EXTRACTED_PATH, "**", "*.smali"), recursive=True):
14
+ if len(artifacts_finders) == 0:
15
+ break
16
+ with open(filename, "r", encoding="utf8") as f:
17
+ data = f.read()
18
+ for artifact_finder in artifacts_finders:
19
+ if not artifact_finder.class_filter(data):
20
+ continue
21
+ artifact_finder.extract_artifacts(artifacts, data)
22
+ if artifact_finder.is_once and artifact_finder.is_found:
23
+ artifacts_finders.remove(artifact_finder)
24
+
25
+ print(f'[+] Found artifacts:\n{artifacts}')
26
+ return artifacts
@@ -0,0 +1,28 @@
1
+ import dataclasses
2
+ from pathlib import Path
3
+ from importlib.resources import files
4
+ import enum
5
+
6
+ APKTOOL_PATH = files('stitch').joinpath('bin/apktool_2.12.1.jar')
7
+ UBER_APK_SIGNER_PATH = files('stitch').joinpath('./bin/uber-apk-signer-1.2.1.jar')
8
+ EXTRACTED_PATH = 'extracted'
9
+ BUNDLE_DIR_PATH = 'bundle_apks'
10
+ BUNDLE_APK_EXTRACTED_PATH = Path('bundle')
11
+ SMALI_GENERATOR_TEMP_PATH = './smali_generator'
12
+ SMALI_EXTRACTED_PATH = './smali_extracted'
13
+ SMALI_GENERATOR_OUTPUT_PATH = './smali_generator.apk'
14
+ ARTIFACTORY_PATH = Path('artifactory.json')
15
+
16
+
17
+ class ManifestKeys(enum.StrEnum):
18
+ EXPORTED = '{http://schemas.android.com/apk/res/android}exported'
19
+ NAME = '{http://schemas.android.com/apk/res/android}name'
20
+ TARGET_ACTIVITY = '{http://schemas.android.com/apk/res/android}targetActivity'
21
+
22
+
23
+ ANDROID_MANIFEST_RELEVANT_TAGS = ['activity', 'activity-alias', 'provider', 'receiver', 'service']
24
+
25
+ @dataclasses.dataclass
26
+ class ExternalModule:
27
+ module_path: Path
28
+ invoke_line: str
@@ -0,0 +1,158 @@
1
+ import glob
2
+ import os
3
+ from pathlib import Path
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ from typing import Optional
8
+
9
+ import lxml.etree
10
+ from androguard.core.apk import APK
11
+ from androguard.util import set_log
12
+ from androguard.core.axml import ARSCParser
13
+ from stitch.apk_utils import find_smali_file_by_class_name, extract_apk, is_bundle
14
+ from stitch.common import ManifestKeys, SMALI_GENERATOR_TEMP_PATH, SMALI_GENERATOR_OUTPUT_PATH, \
15
+ SMALI_EXTRACTED_PATH, \
16
+ BUNDLE_APK_EXTRACTED_PATH, EXTRACTED_PATH
17
+
18
+ set_log('CRITICAL')
19
+ INVOKE_LINE = '\n\tinvoke-static {}, Lcom/smali_generator/TheAmazingPatch;->on_load()V\n\t'
20
+
21
+
22
+ def patch_artifacts(artifactory: dict, smali_generator_temp_path: Path) -> None:
23
+ for file in glob.iglob(str(smali_generator_temp_path / '**' / '**'), recursive=True, include_hidden=True):
24
+ if os.path.isdir(file):
25
+ continue
26
+ with open(file, 'rb') as f:
27
+ old_data = f.read()
28
+ data = old_data
29
+ for key, value in artifactory.items():
30
+ data = data.replace(f'{{{{{key}}}}}'.encode(), value.encode())
31
+ if data != old_data:
32
+ with open(file, 'wb') as f:
33
+ f.write(data)
34
+
35
+
36
+ def prepare_smali(temp_path: Path, external_module: Path, artifactory: dict) -> None:
37
+ smali_generator_temp_path = temp_path / SMALI_GENERATOR_TEMP_PATH
38
+ print('[+] Copying the smali generator...')
39
+ shutil.copytree(external_module, smali_generator_temp_path)
40
+ print('[+] Patching the artifacts...')
41
+ patch_artifacts(artifactory, smali_generator_temp_path)
42
+ print('[+] Assembling the java...')
43
+ subprocess.check_call(['./gradlew', 'assembleRelease'], cwd=smali_generator_temp_path)
44
+ print('[+] Extracting the smali...')
45
+ extract_apk(smali_generator_temp_path / SMALI_GENERATOR_OUTPUT_PATH, temp_path,
46
+ smali_generator_temp_path / SMALI_EXTRACTED_PATH)
47
+
48
+
49
+ def get_activities_with_entry_points(apk_path: Path) -> list:
50
+ manifest: lxml.etree.Element = APK(str(apk_path)).get_android_manifest_xml()
51
+ activities = []
52
+ for element in manifest.find('.//application').getchildren():
53
+ should_patch = False
54
+ if element.tag == 'activity' or element.tag == 'activity-alias':
55
+ should_patch = element.get(ManifestKeys.EXPORTED) == 'true'
56
+ elif element.tag == 'provider' or element.tag == 'receiver' or element.tag == 'service':
57
+ should_patch = True
58
+ if should_patch:
59
+ activities.append(element)
60
+ return activities
61
+
62
+
63
+ def patch_or_add_function(smali_file_path: Path, function_name: str, invoke_line: str) -> None:
64
+ with open(smali_file_path, 'r') as file:
65
+ smali_file = file.read()
66
+ matches = re.findall(fr'\.method \w+ [^\n]*{function_name}[^\n]*\n[^\n]+', smali_file)
67
+ if len(matches) == 0:
68
+ pass
69
+ for match in matches:
70
+ smali_file = smali_file.replace(match, f'{match}\n\t{invoke_line}\n\t')
71
+ with open(smali_file_path, 'w') as file:
72
+ file.write(smali_file)
73
+
74
+
75
+ def add_static_call_to_on_load(temp_path: Path, class_name: str, function_name: str, invoke_line: str) -> None:
76
+ smali_file_path = find_smali_file_by_class_name(temp_path / EXTRACTED_PATH, class_name)
77
+ if smali_file_path is None:
78
+ print(f'[-] Failed to find smali file for {class_name}')
79
+ return
80
+ patch_or_add_function(smali_file_path, function_name, invoke_line)
81
+
82
+
83
+ def patch_entries(apk_path: Path, temp_path: Path, invoke_line: str) -> None:
84
+ from stitch.apk_utils import main_apk_name
85
+ print('[+] Searching for activities with entry points...')
86
+ activities_to_patch = get_activities_with_entry_points(
87
+ Path(temp_path) / BUNDLE_APK_EXTRACTED_PATH / main_apk_name if is_bundle(
88
+ apk_path) else apk_path)
89
+ print(f'[+] Found {len(activities_to_patch)} activities with entry points')
90
+ for activity in activities_to_patch:
91
+ add_static_call_to_on_load(temp_path, activity.get(
92
+ ManifestKeys.TARGET_ACTIVITY if activity.tag == 'activity-alias' else ManifestKeys.NAME),
93
+ 'onCreate' if 'activity' in activity.tag else '<init>', invoke_line)
94
+
95
+
96
+ def patch_google_api_key(temp_path: Path, package_name: str, custom_google_api_key: str) -> None:
97
+ print('[+] Searching for google api key...')
98
+ resources_path = temp_path / EXTRACTED_PATH / 'resources.arsc'
99
+ resources = ARSCParser(resources_path.read_bytes())
100
+ _, original_google_api_key = resources.get_string(package_name, 'google_api_key')
101
+ print(f'[+] Original google api key: {original_google_api_key}')
102
+ with open(resources_path, 'rb') as file:
103
+ resources_data = file.read()
104
+ resources_data = resources_data.replace(original_google_api_key.encode(), custom_google_api_key.encode())
105
+ with open(resources_path, 'wb') as file:
106
+ file.write(resources_data)
107
+
108
+
109
+ def get_new_smali_folder(smali_path: Path) -> Path:
110
+ smali_folders = [folder for folder in smali_path.iterdir() if
111
+ folder.is_dir() and folder.name.startswith('smali_classes')]
112
+ if not smali_folders:
113
+ return smali_path / 'smali'
114
+ smali_folders.sort(key=lambda x: int(x.name.replace('smali_classes', '')))
115
+ smali_index = int(smali_folders[-1].name.replace('smali_classes', '')) + 1
116
+ (smali_path / f'smali_classes{smali_index}').mkdir()
117
+ return smali_path / f'smali_classes{smali_index}'
118
+
119
+
120
+ def patch_apk(apk_path: Path, temp_path: Path, external_module: Path, artifactory: dict, arch: str,
121
+ api_key: Optional[str] = None) -> None:
122
+ print('[+] Preparing the smali...')
123
+ prepare_smali(temp_path, external_module, artifactory)
124
+
125
+ new_smali_folder = get_new_smali_folder(temp_path / EXTRACTED_PATH)
126
+
127
+ print(f'[+] Applying the custom smali into {new_smali_folder.name}...')
128
+ shutil.copytree(temp_path / SMALI_GENERATOR_TEMP_PATH / SMALI_EXTRACTED_PATH / 'smali',
129
+ new_smali_folder,
130
+ dirs_exist_ok=True)
131
+
132
+ smali_folders = [folder for folder in
133
+ (temp_path / EXTRACTED_PATH).iterdir() if
134
+ folder.is_dir() and (folder.name.startswith('smali_classes') or folder.name == 'smali')]
135
+ for folder in smali_folders:
136
+ # move every first folder within to the new smali folder
137
+ for file in folder.iterdir():
138
+ if not (new_smali_folder / file.name).exists():
139
+ shutil.move(file, new_smali_folder)
140
+ break
141
+
142
+ print('[+] Injecting the custom so...')
143
+ os.makedirs(temp_path / EXTRACTED_PATH / 'lib' / arch, exist_ok=True)
144
+ shutil.copytree(
145
+ temp_path / SMALI_GENERATOR_TEMP_PATH / SMALI_EXTRACTED_PATH / 'lib' / arch,
146
+ temp_path / EXTRACTED_PATH / 'lib' / arch,
147
+ dirs_exist_ok=True)
148
+ print('[+] Adding calls to the custom smali...')
149
+ patch_entries(apk_path, temp_path)
150
+
151
+ if api_key is not None:
152
+ print('[+] Patching google api key...')
153
+ if is_bundle(apk_path):
154
+ from stitch.apk_utils import main_apk_name
155
+ package_name = APK(str(temp_path / BUNDLE_APK_EXTRACTED_PATH / main_apk_name)).get_package()
156
+ else:
157
+ package_name = APK(str(apk_path)).get_package()
158
+ patch_google_api_key(temp_path, package_name, api_key)
@@ -0,0 +1,148 @@
1
+ import json
2
+ import os
3
+ import shutil
4
+ from pathlib import Path
5
+ from typing import List
6
+
7
+ from androguard.core.apk import APK
8
+ from stitch.apk_utils import is_bundle
9
+ from stitch.artifactory_generator.SimpleArtifactoryFinder import SimpleArtifactoryFinder
10
+ from stitch.common import BUNDLE_APK_EXTRACTED_PATH
11
+
12
+ from .apk_utils import compile_apk, sign_apk
13
+ from .artifactory_generator.generate_artifactory import generate_artifactory
14
+ from .common import SMALI_EXTRACTED_PATH, SMALI_GENERATOR_TEMP_PATH, EXTRACTED_PATH, ExternalModule
15
+ from . import apk_utils
16
+ from . import patcher
17
+
18
+
19
+ class Stitch:
20
+ apk_path: Path
21
+ output_apk: Path
22
+ temp_path: Path
23
+ artifactory: Path
24
+ external_modules: List[ExternalModule]
25
+ arch: str
26
+ google_api_key: str
27
+ should_sign: bool
28
+ extra_artifacts: dict
29
+
30
+ def __init__(self, apk_path: str, output_apk: str = 'out.apk', temp_path: str = './temp',
31
+ external_modules: List[ExternalModule] = None,
32
+ arch: str = 'arm64-v8a', artifactory_list: List[SimpleArtifactoryFinder] = None,
33
+ google_api_key: str = None, should_sign=True, extra_artifacts: dict = None):
34
+ if external_modules is None:
35
+ external_modules = [ExternalModule(
36
+ Path('./smali_generator'),
37
+ 'invoke-static {}, Lcom/smali_generator/TheAmazingPatch;->on_load()V'
38
+ )]
39
+ self.apk_path = Path(apk_path)
40
+ self.output_apk = Path(output_apk)
41
+ self.temp_path = Path(temp_path)
42
+ if self.temp_path.exists():
43
+ raise Exception('[!] The temp path already exists')
44
+ self.external_modules = external_modules
45
+ self.arch = arch
46
+ self.artifactory_list = [] if artifactory_list is None else artifactory_list
47
+ os.makedirs(str(self.temp_path), exist_ok=True)
48
+ self.google_api_key = google_api_key
49
+ self.should_sign = should_sign
50
+ if extra_artifacts is None:
51
+ extra_artifacts = {}
52
+ self.extra_artifacts = extra_artifacts
53
+ self.is_bundle_file = is_bundle(self.apk_path)
54
+
55
+ def prepare_artifactory(self):
56
+ if self.artifactory.exists():
57
+ try:
58
+ with open(self.artifactory, 'r') as file:
59
+ json.load(file)
60
+ except json.decoder.JSONDecodeError:
61
+ pass
62
+ else:
63
+ return
64
+ else:
65
+ with open(self.artifactory, 'w') as file:
66
+ json.dump({'SOME_CONST_KEY': 'VALUE'}, file)
67
+
68
+ def patch(self):
69
+ apk_utils.extract_apk(self.apk_path, self.temp_path)
70
+
71
+ artifactory = generate_artifactory(self.temp_path, self.artifactory_list)
72
+ artifactory.update(self.extra_artifacts)
73
+
74
+ smali_folders = [folder for folder in
75
+ (self.temp_path / EXTRACTED_PATH).iterdir() if
76
+ folder.is_dir() and (folder.name.startswith('smali_classes') or folder.name == 'smali')]
77
+ new_smali_folders = [patcher.get_new_smali_folder(self.temp_path / EXTRACTED_PATH) for _ in
78
+ range(len(smali_folders))]
79
+ print(f'[+] Applying the custom smali into {new_smali_folders[0].name}...')
80
+
81
+ for i, folder in enumerate(smali_folders):
82
+ # move every first folder within to the new smali folder
83
+ for file in folder.iterdir():
84
+ target_folder = new_smali_folders[i % len(new_smali_folders)]
85
+ if not (target_folder / file.name).exists():
86
+ shutil.move(file, target_folder)
87
+ break
88
+ target_smali_folder = patcher.get_new_smali_folder(self.temp_path / EXTRACTED_PATH)
89
+
90
+ for module in self.external_modules:
91
+ print('[+] Preparing the smali...')
92
+ patcher.prepare_smali(self.temp_path, module.module_path, artifactory)
93
+
94
+ shutil.copytree(self.temp_path / SMALI_GENERATOR_TEMP_PATH / SMALI_EXTRACTED_PATH / 'smali',
95
+ target_smali_folder,
96
+ dirs_exist_ok=True)
97
+ if (self.temp_path / SMALI_GENERATOR_TEMP_PATH / SMALI_EXTRACTED_PATH / 'lib' / self.arch).exists():
98
+ print('[+] Injecting the custom so...')
99
+ os.makedirs(self.temp_path / EXTRACTED_PATH / 'lib' / self.arch, exist_ok=True)
100
+ shutil.copytree(
101
+ self.temp_path / SMALI_GENERATOR_TEMP_PATH / SMALI_EXTRACTED_PATH / 'lib' / self.arch,
102
+ self.temp_path / EXTRACTED_PATH / 'lib' / self.arch,
103
+ dirs_exist_ok=True)
104
+ shutil.rmtree(self.temp_path / SMALI_GENERATOR_TEMP_PATH, ignore_errors=True)
105
+
106
+ invoke_lines = '\n\t'.join([module.invoke_line for module in self.external_modules])
107
+
108
+ print('[+] Adding calls to the custom smali...')
109
+ patcher.patch_entries(self.apk_path, self.temp_path, invoke_lines)
110
+
111
+ if self.google_api_key is not None:
112
+ print('[+] Patching google api key...')
113
+ if self.is_bundle_file:
114
+ from stitch.apk_utils import main_apk_name
115
+ package_name = APK(str(self.temp_path / BUNDLE_APK_EXTRACTED_PATH / main_apk_name)).get_package()
116
+ else:
117
+ package_name = APK(str(self.apk_path)).get_package()
118
+ patcher.patch_google_api_key(self.temp_path, package_name, self.google_api_key)
119
+
120
+ temp_output_apk = self.temp_path / 'unsigned.apk'
121
+
122
+ print('[+] Compiling APK...')
123
+ compile_apk(self.temp_path / EXTRACTED_PATH, temp_output_apk)
124
+
125
+ if self.should_sign:
126
+ print('[+] Signing APK...')
127
+ sign_apk(self.temp_path / BUNDLE_APK_EXTRACTED_PATH, temp_output_apk, self.output_apk, self.is_bundle_file)
128
+
129
+ if self.is_bundle_file:
130
+ from stitch.apk_utils import main_apk_name
131
+ shutil.move(temp_output_apk, self.temp_path / BUNDLE_APK_EXTRACTED_PATH / main_apk_name)
132
+ shutil.move(self.temp_path / BUNDLE_APK_EXTRACTED_PATH, 'output_bundle_apks')
133
+ temp_output_path = shutil.make_archive(str(self.temp_path / 'temp_output'), 'zip', 'output_bundle_apks')
134
+ shutil.move(temp_output_path, self.output_apk)
135
+ shutil.rmtree('output_bundle_apks', ignore_errors=True)
136
+ else:
137
+ if not self.should_sign:
138
+ shutil.move(temp_output_apk, self.output_apk)
139
+
140
+ def __enter__(self):
141
+ return self
142
+
143
+ def __exit__(self, exc_type, exc_val, exc_tb):
144
+ print('[+] Cleaning up...')
145
+ self.clean_up()
146
+
147
+ def clean_up(self):
148
+ shutil.rmtree(self.temp_path, ignore_errors=True)
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: stitch
3
+ Version: 0.0.21
4
+ Author-email: Alon Schwartzblat <alon.ponch@gmail.com>
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: PyYAML
10
+ Requires-Dist: androguard
11
+ Dynamic: license-file
12
+
13
+ # Stitch
14
+ Stitch is a powerful APK patching python library that allows you to inject your own java module into any APK, bundle or XAPK file.
15
+
16
+ ## How it works
17
+
18
+ Will be added soon.
19
+
20
+ ## Projects using Stitch:
21
+ - [MakoPatcher][https://github.com/Schwartzblat/MakoPatcher]- A patcher for 12+ app.
22
+
23
+ ## How to use
24
+ ### Installation
25
+ You can install Stitch using pip:
26
+ ```bash
27
+ # For now, install the test version from TestPyPI
28
+ pip install stitch
29
+ ```
30
+
31
+ ### Basic Usage
32
+ Create an Android Gradle project like my smali_generator (Check it out in on of the examples) that generates the java module you want to inject.
33
+
34
+ Then, use the following code to patch an APK:
35
+ ```python
36
+ from stitch import Stitch
37
+
38
+ with Stitch(
39
+ apk_path='./input.apk',
40
+ output_apk='./output.apk',
41
+ temp_path='./temp',
42
+ external_module='./smali_generator'
43
+ ) as stitch:
44
+ stitch.patch()
45
+ ```
46
+ And that's it! Your APK will be patched with the injected module.
47
+
48
+ ## Contributing
49
+
50
+ I will be happy if you want to contribute to this project. Feel free to open issues or submit pull requests.
51
+
52
+ ## Disclaimer
53
+
54
+ For educational purpose only or something like that. I am not responsible for any misuse of this software.
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/stitch/__init__.py
5
+ src/stitch/apk_utils.py
6
+ src/stitch/common.py
7
+ src/stitch/patcher.py
8
+ src/stitch/stitch.py
9
+ src/stitch.egg-info/PKG-INFO
10
+ src/stitch.egg-info/SOURCES.txt
11
+ src/stitch.egg-info/dependency_links.txt
12
+ src/stitch.egg-info/requires.txt
13
+ src/stitch.egg-info/top_level.txt
14
+ src/stitch/artifactory_generator/SimpleArtifactoryFinder.py
15
+ src/stitch/artifactory_generator/__init__.py
16
+ src/stitch/artifactory_generator/generate_artifactory.py
17
+ src/stitch/bin/apktool_2.12.1.jar
18
+ src/stitch/bin/uber-apk-signer-1.2.1.jar
@@ -0,0 +1,2 @@
1
+ PyYAML
2
+ androguard
@@ -0,0 +1 @@
1
+ stitch