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 +21 -0
- stitch-0.0.21/PKG-INFO +54 -0
- stitch-0.0.21/README.md +42 -0
- stitch-0.0.21/pyproject.toml +24 -0
- stitch-0.0.21/setup.cfg +4 -0
- stitch-0.0.21/src/stitch/__init__.py +1 -0
- stitch-0.0.21/src/stitch/apk_utils.py +125 -0
- stitch-0.0.21/src/stitch/artifactory_generator/SimpleArtifactoryFinder.py +19 -0
- stitch-0.0.21/src/stitch/artifactory_generator/__init__.py +0 -0
- stitch-0.0.21/src/stitch/artifactory_generator/generate_artifactory.py +26 -0
- stitch-0.0.21/src/stitch/bin/apktool_2.12.1.jar +0 -0
- stitch-0.0.21/src/stitch/bin/uber-apk-signer-1.2.1.jar +0 -0
- stitch-0.0.21/src/stitch/common.py +28 -0
- stitch-0.0.21/src/stitch/patcher.py +158 -0
- stitch-0.0.21/src/stitch/stitch.py +148 -0
- stitch-0.0.21/src/stitch.egg-info/PKG-INFO +54 -0
- stitch-0.0.21/src/stitch.egg-info/SOURCES.txt +18 -0
- stitch-0.0.21/src/stitch.egg-info/dependency_links.txt +1 -0
- stitch-0.0.21/src/stitch.egg-info/requires.txt +2 -0
- stitch-0.0.21/src/stitch.egg-info/top_level.txt +1 -0
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.
|
stitch-0.0.21/README.md
ADDED
|
@@ -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"]
|
stitch-0.0.21/setup.cfg
ADDED
|
@@ -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
|
|
File without changes
|
|
@@ -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
|
|
Binary file
|
|
Binary file
|
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
stitch
|