trueseeing 2.2.2__tar.gz → 2.2.4__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.
- {trueseeing-2.2.2 → trueseeing-2.2.4}/PKG-INFO +4 -3
- {trueseeing-2.2.2 → trueseeing-2.2.4}/README.md +1 -1
- {trueseeing-2.2.2 → trueseeing-2.2.4}/pyproject.toml +3 -1
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/__init__.py +1 -1
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/api.py +4 -1
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/cmd/__init__.py +1 -2
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/cmd/android/asm.py +13 -5
- trueseeing-2.2.2/trueseeing/app/cmd/android/exploit.py → trueseeing-2.2.4/trueseeing/app/cmd/android/engage.py +355 -49
- trueseeing-2.2.2/trueseeing/app/cmd/android/device.py → trueseeing-2.2.4/trueseeing/app/cmd/android/recon.py +46 -125
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/cmd/android/search.py +18 -111
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/cmd/android/show.py +32 -61
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/cmd/config.py +30 -2
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/cmd/info.py +7 -14
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/cmd/scan.py +2 -2
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/cmd/search.py +1 -1
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/inspect.py +101 -38
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/shell.py +5 -0
- trueseeing-2.2.4/trueseeing/core/android/analysis/flow.py +357 -0
- trueseeing-2.2.4/trueseeing/core/android/analysis/op.py +55 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/android/asm.py +37 -9
- trueseeing-2.2.4/trueseeing/core/android/context.py +422 -0
- trueseeing-2.2.4/trueseeing/core/android/db.py +136 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/android/device.py +13 -7
- trueseeing-2.2.4/trueseeing/core/android/model.py +60 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/context.py +51 -14
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/env.py +1 -1
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/model/issue.py +3 -5
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/store.py +6 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/tools.py +25 -16
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/ui.py +76 -1
- trueseeing-2.2.4/trueseeing/libs/android/store.0.sql +10 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/libs/store.s.sql +1 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/sig/__init__.py +1 -2
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/sig/android/crypto.py +60 -59
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/sig/android/fingerprint.py +14 -14
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/sig/android/manifest.py +8 -8
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/sig/android/privacy.py +14 -14
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/sig/android/security.py +75 -73
- trueseeing-2.2.2/trueseeing/core/android/analysis/flow.py +0 -331
- trueseeing-2.2.2/trueseeing/core/android/analysis/smali.py +0 -144
- trueseeing-2.2.2/trueseeing/core/android/context.py +0 -216
- trueseeing-2.2.2/trueseeing/core/android/db.py +0 -187
- trueseeing-2.2.2/trueseeing/core/android/model/code.py +0 -55
- trueseeing-2.2.2/trueseeing/libs/android/store.0.sql +0 -4
- trueseeing-2.2.2/trueseeing/libs/android/store.1.sql +0 -77
- trueseeing-2.2.2/trueseeing/sig/android/__init__.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/.dockerignore +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/.github/workflows/deploy.yaml +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/.github/workflows/lint.yaml +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/.github/workflows/publish.yaml +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/.github/workflows/stale.yaml +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/.gitignore +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/COPYING +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/Dockerfile +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/__init__.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/cmd/alias.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/cmd/analyze.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/cmd/android/__init__.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/cmd/report.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/cmd/show.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/app/scan.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/__init__.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/android/analysis/__init__.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/android/store.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/android/tools.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/config.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/cvss.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/db.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/exc.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/ext.py +0 -0
- {trueseeing-2.2.2/trueseeing/core/android → trueseeing-2.2.4/trueseeing/core}/model/__init__.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/model/cmd.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/model/sig.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/report.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/scan.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/core/z.py +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/libs/LICENSE.md +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/libs/android/abe.jar +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/libs/android/apkeditor.jar +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/libs/android/apksigner.jar +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/libs/android/frida-app.smali +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/libs/android/frida-scriptdir.config +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/libs/android/nsc.xml +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/libs/files.0.sql +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/libs/public_suffix_list.dat +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/libs/store.0.sql +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/libs/template/report.html +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/libs/tlds.txt +0 -0
- {trueseeing-2.2.2 → trueseeing-2.2.4}/trueseeing/py.typed +0 -0
- {trueseeing-2.2.2/trueseeing/core/model → trueseeing-2.2.4/trueseeing/sig/android}/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: trueseeing
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.4
|
|
4
4
|
Summary: Trueseeing is a non-decompiling Android application vulnerability scanner.
|
|
5
5
|
Keywords: android,security,pentest,hacking
|
|
6
6
|
Author-email: Takahiro Yoshimura <alterakey@protonmail.com>
|
|
@@ -16,7 +16,6 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
|
|
|
16
16
|
Requires-Dist: lxml~=5.0
|
|
17
17
|
Requires-Dist: pyyaml~=6.0
|
|
18
18
|
Requires-Dist: jinja2~=3.1
|
|
19
|
-
Requires-Dist: attrs~=23.2
|
|
20
19
|
Requires-Dist: pypubsub~=4.0
|
|
21
20
|
Requires-Dist: termcolor~=2.4
|
|
22
21
|
Requires-Dist: progressbar2~=4.3
|
|
@@ -25,6 +24,8 @@ Requires-Dist: asn1crypto~=1.5
|
|
|
25
24
|
Requires-Dist: zstandard~=0.22
|
|
26
25
|
Requires-Dist: aiohttp~=3.9
|
|
27
26
|
Requires-Dist: lief~=0.14
|
|
27
|
+
Requires-Dist: pyaxmlparser~=0.3
|
|
28
|
+
Requires-Dist: prompt-toolkit~=3.0
|
|
28
29
|
Requires-Dist: mypy~=1.7 ; extra == "dev"
|
|
29
30
|
Requires-Dist: pyproject-flake8~=6.1 ; extra == "dev"
|
|
30
31
|
Requires-Dist: typing_extensions~=4.1 ; extra == "dev"
|
|
@@ -83,7 +84,7 @@ Alternatively, you can install our package with pip as follows. This form of ins
|
|
|
83
84
|
You can interactively scan/analyze/patch/etc. apps -- making it the ideal choice for manual analysis:
|
|
84
85
|
|
|
85
86
|
$ trueseeing target.apk
|
|
86
|
-
[+] trueseeing 2.2.
|
|
87
|
+
[+] trueseeing 2.2.4
|
|
87
88
|
ts[target.apk]> ?
|
|
88
89
|
...
|
|
89
90
|
ts[target.apk]> i # show generic information
|
|
@@ -50,7 +50,7 @@ Alternatively, you can install our package with pip as follows. This form of ins
|
|
|
50
50
|
You can interactively scan/analyze/patch/etc. apps -- making it the ideal choice for manual analysis:
|
|
51
51
|
|
|
52
52
|
$ trueseeing target.apk
|
|
53
|
-
[+] trueseeing 2.2.
|
|
53
|
+
[+] trueseeing 2.2.4
|
|
54
54
|
ts[target.apk]> ?
|
|
55
55
|
...
|
|
56
56
|
ts[target.apk]> i # show generic information
|
|
@@ -22,7 +22,6 @@ dependencies = [
|
|
|
22
22
|
"lxml~=5.0",
|
|
23
23
|
"pyyaml~=6.0",
|
|
24
24
|
"jinja2~=3.1",
|
|
25
|
-
"attrs~=23.2",
|
|
26
25
|
"pypubsub~=4.0",
|
|
27
26
|
"termcolor~=2.4",
|
|
28
27
|
"progressbar2~=4.3",
|
|
@@ -31,6 +30,8 @@ dependencies = [
|
|
|
31
30
|
"zstandard~=0.22",
|
|
32
31
|
"aiohttp~=3.9",
|
|
33
32
|
"lief~=0.14",
|
|
33
|
+
"pyaxmlparser~=0.3",
|
|
34
|
+
"prompt-toolkit~=3.0",
|
|
34
35
|
]
|
|
35
36
|
requires-python = ">=3.9"
|
|
36
37
|
dynamic = ['version', 'description']
|
|
@@ -59,6 +60,7 @@ module = [
|
|
|
59
60
|
"jinja2",
|
|
60
61
|
"pubsub",
|
|
61
62
|
"asn1crypto.*",
|
|
63
|
+
"pyaxmlparser.*",
|
|
62
64
|
]
|
|
63
65
|
ignore_missing_imports = true
|
|
64
66
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""Trueseeing is a non-decompiling Android application vulnerability scanner."""
|
|
2
|
-
__version__ = '2.2.
|
|
2
|
+
__version__ = '2.2.4'
|
|
@@ -10,9 +10,12 @@ if TYPE_CHECKING:
|
|
|
10
10
|
from trueseeing.core.android.context import APKContext
|
|
11
11
|
from trueseeing.core.model.issue import Issue, IssueConfidence
|
|
12
12
|
|
|
13
|
+
ModifierEvent = Literal['begin', 'end']
|
|
14
|
+
|
|
13
15
|
CommandEntrypoint = Callable[[deque[str]], Coroutine[Any, Any, None]]
|
|
14
16
|
CommandlineEntrypoint = Callable[[str], Coroutine[Any, Any, None]]
|
|
15
17
|
CommandPatternEntrypoints = Union[CommandEntrypoint, CommandlineEntrypoint]
|
|
18
|
+
ModifierListenerEntrypoint = Callable[[ModifierEvent, str], Coroutine[Any, Any, None]]
|
|
16
19
|
SignatureEntrypoint = Callable[[], Coroutine[Any, Any, None]]
|
|
17
20
|
FormatHandlerEntrypoint = Callable[[str], Optional[Context]]
|
|
18
21
|
ConfigGetterEntrypoint = Callable[[], Any]
|
|
@@ -34,7 +37,7 @@ if TYPE_CHECKING:
|
|
|
34
37
|
pass
|
|
35
38
|
|
|
36
39
|
class ModifierEntry(Entry):
|
|
37
|
-
|
|
40
|
+
e: Optional[ModifierListenerEntrypoint] # type: ignore[misc]
|
|
38
41
|
|
|
39
42
|
class ConfigEntry(TypedDict):
|
|
40
43
|
g: ConfigGetterEntrypoint
|
|
@@ -8,11 +8,10 @@ if TYPE_CHECKING:
|
|
|
8
8
|
def discover() -> Iterator[Type[Command]]:
|
|
9
9
|
from trueseeing.api import Command
|
|
10
10
|
from importlib import import_module
|
|
11
|
-
from trueseeing.core.model.cmd import CommandMixin
|
|
12
11
|
from trueseeing.core.tools import get_public_subclasses, get_missing_methods, discover_modules_under
|
|
13
12
|
|
|
14
13
|
for mod in discover_modules_under('trueseeing.app.cmd'):
|
|
15
14
|
m = import_module(mod)
|
|
16
|
-
for c in get_public_subclasses(m, Command,
|
|
15
|
+
for c in get_public_subclasses(m, Command, 'CommandMixin'): # type:ignore[type-abstract]
|
|
17
16
|
assert not get_missing_methods(c)
|
|
18
17
|
yield c
|
|
@@ -47,16 +47,24 @@ class AssembleCommand(CommandMixin):
|
|
|
47
47
|
ui.fatal('need root path')
|
|
48
48
|
|
|
49
49
|
import os
|
|
50
|
+
import re
|
|
50
51
|
import time
|
|
51
52
|
from tempfile import TemporaryDirectory
|
|
52
53
|
from trueseeing.core.android.asm import APKAssembler
|
|
53
54
|
from trueseeing.core.android.tools import move_apk
|
|
54
55
|
|
|
55
56
|
root = args.popleft()
|
|
56
|
-
origapk =
|
|
57
|
+
origapk = re.sub(r'(\.x?apk)$', r'\1.orig', apk)
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
stem = re.sub(r'\.x?apk$', '', apk)
|
|
60
|
+
for typ in ['apk', 'xapk']:
|
|
61
|
+
print(origapk, f'{stem}.{typ}.orig')
|
|
62
|
+
if os.path.exists(f'{stem}.{typ}.orig') and not cmd.endswith('!'):
|
|
63
|
+
ui.fatal('backup file exists; force (!) to overwrite')
|
|
64
|
+
|
|
65
|
+
if apk.endswith('.xapk'):
|
|
66
|
+
ui.warn('assembling xapk is not supported; assembling as merged apk')
|
|
67
|
+
apk = apk.replace('.xapk', '.apk')
|
|
60
68
|
|
|
61
69
|
opts = self._helper.get_effective_options(self._helper.get_modifiers(args))
|
|
62
70
|
|
|
@@ -140,7 +148,7 @@ class AssembleCommand(CommandMixin):
|
|
|
140
148
|
at = time.time()
|
|
141
149
|
|
|
142
150
|
with TemporaryDirectory() as td:
|
|
143
|
-
await APKDisassembler.disassemble_to_path(apk, td, nodex=nodex)
|
|
151
|
+
await APKDisassembler.disassemble_to_path(apk, td, nodex=nodex, merge=apk.endswith('.xapk'))
|
|
144
152
|
|
|
145
153
|
if not archive:
|
|
146
154
|
with FileTransferProgressReporter('disassemble: writing').scoped() as progress:
|
|
@@ -185,7 +193,7 @@ class AssembleCommand(CommandMixin):
|
|
|
185
193
|
|
|
186
194
|
at = time.time()
|
|
187
195
|
extracted = 0
|
|
188
|
-
context = self._helper.get_context()
|
|
196
|
+
context = self._helper.get_context()
|
|
189
197
|
q = context.store().query()
|
|
190
198
|
|
|
191
199
|
if not archive:
|
|
@@ -7,45 +7,55 @@ from trueseeing.core.model.cmd import CommandMixin
|
|
|
7
7
|
from trueseeing.core.ui import ui
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
|
-
from typing import Any, Optional, Dict, Mapping
|
|
10
|
+
from typing import Any, Optional, Dict, Mapping, List, Iterator, Tuple
|
|
11
11
|
from trueseeing.api import CommandHelper, Command, CommandMap, OptionMap
|
|
12
12
|
from trueseeing.core.android.context import APKContext
|
|
13
|
+
from trueseeing.core.android.model import XAPKManifest
|
|
13
14
|
|
|
14
|
-
class
|
|
15
|
+
class EngageCommand(CommandMixin):
|
|
15
16
|
def __init__(self, helper: CommandHelper) -> None:
|
|
16
17
|
self._helper = helper
|
|
17
18
|
|
|
18
19
|
@staticmethod
|
|
19
20
|
def create(helper: CommandHelper) -> Command:
|
|
20
|
-
return
|
|
21
|
+
return EngageCommand(helper)
|
|
21
22
|
|
|
22
23
|
def get_commands(self) -> CommandMap:
|
|
23
24
|
return {
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'xco':dict(e=self.
|
|
38
|
-
'
|
|
39
|
-
'xci':dict(e=self.
|
|
40
|
-
'
|
|
25
|
+
'xtq':dict(e=self._engage_tamper_discard, n='xtq', d='engage: discard changes'),
|
|
26
|
+
'xtx':dict(e=self._engage_tamper_apply, n='xtx[!]', d='engage: apply and rebuild apk'),
|
|
27
|
+
'xtx!':dict(e=self._engage_tamper_apply),
|
|
28
|
+
'xtf':dict(e=self._engage_tamper_inject_frida, n='xtf[!] [config]', d='engage; inject frida gadget'),
|
|
29
|
+
'xtf!':dict(e=self._engage_tamper_inject_frida),
|
|
30
|
+
'xtfs':dict(e=self._engage_tamper_inject_frida_scriptdir, n='xtfs[!] [path]', d='engage; inject frida gadget in script dir mode'),
|
|
31
|
+
'xtfs!':dict(e=self._engage_tamper_inject_frida_scriptdir),
|
|
32
|
+
'xtn':dict(e=self._engage_tamper_disable_pinning, n='xtn', d='engage: patch NSC to disable SSL/TLS pinning'),
|
|
33
|
+
'xtd':dict(e=self._engage_tamper_enable_debug, n='xtd', d='engage: make debuggable'),
|
|
34
|
+
'xtb':dict(e=self._engage_tamper_enable_backup, n='xtb', d='engage: make backupable'),
|
|
35
|
+
'xtt':dict(e=self._engage_tamper_patch_target_api_level, n='xtt[!] <api level>', d='engage: patch target api level'),
|
|
36
|
+
'xtt!':dict(e=self._engage_tamper_patch_target_api_level),
|
|
37
|
+
'xco':dict(e=self._engage_device_copyout, n='xco[!] package [data.tar]', d='engage: copy-out package data'),
|
|
38
|
+
'xco!':dict(e=self._engage_device_copyout),
|
|
39
|
+
'xci':dict(e=self._engage_device_copyin, n='xci[!] package [data.tar]', d='engage: copy-in package data'),
|
|
40
|
+
'xci!':dict(e=self._engage_device_copyin),
|
|
41
|
+
'xpd':dict(e=self._engage_deploy_package, n='xpd[!]', d='engage: deploy target package'),
|
|
42
|
+
'xpd!':dict(e=self._engage_deploy_package),
|
|
43
|
+
'xpu':dict(e=self._engage_undeploy_package, n='xpu', d='engage: remove target package'),
|
|
44
|
+
'xz':dict(e=self._engage_fuzz_intent, n='xz[!] "am-cmdline-template" [output.txt]', d='engage: fuzz intent'),
|
|
45
|
+
'xz!':dict(e=self._engage_fuzz_intent),
|
|
46
|
+
'xzr':dict(e=self._engage_fuzz_command, n='xzr[!] "cmdline-template" [output.txt]', d='engage: fuzz cmdline'),
|
|
47
|
+
'xzr!':dict(e=self._engage_fuzz_command),
|
|
48
|
+
'xg':dict(e=self._engage_grab_package, n='xg[!] package [output.apk]', d='engage: grab package'),
|
|
49
|
+
'xg!':dict(e=self._engage_grab_package),
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
def get_options(self) -> OptionMap:
|
|
44
53
|
return {
|
|
45
|
-
'vers':dict(n='vers=X.Y.Z', d='specify frida-gadget version to use [xf,xfs]')
|
|
54
|
+
'vers':dict(n='vers=X.Y.Z', d='specify frida-gadget version to use [xf,xfs]'),
|
|
55
|
+
'w':dict(n='wNAME=FN', d='wordlist, use as {NAME} [xz]'),
|
|
46
56
|
}
|
|
47
57
|
|
|
48
|
-
async def
|
|
58
|
+
async def _engage_tamper_discard(self, args: deque[str]) -> None:
|
|
49
59
|
apk = self._helper.require_target()
|
|
50
60
|
|
|
51
61
|
_ = args.popleft()
|
|
@@ -63,7 +73,7 @@ class ExploitCommand(CommandMixin):
|
|
|
63
73
|
|
|
64
74
|
ui.success('done ({t:.02f} sec.)'.format(t=(time.time() - at)))
|
|
65
75
|
|
|
66
|
-
async def
|
|
76
|
+
async def _engage_tamper_apply(self, args: deque[str]) -> None:
|
|
67
77
|
apk = self._helper.require_target()
|
|
68
78
|
|
|
69
79
|
cmd = args.popleft()
|
|
@@ -107,7 +117,7 @@ class ExploitCommand(CommandMixin):
|
|
|
107
117
|
|
|
108
118
|
ui.success('done ({t:.02f} sec.)'.format(t=(time.time() - at)))
|
|
109
119
|
|
|
110
|
-
async def
|
|
120
|
+
async def _engage_tamper_disable_pinning(self, args: deque[str]) -> None:
|
|
111
121
|
apk = self._helper.require_target()
|
|
112
122
|
|
|
113
123
|
_ = args.popleft()
|
|
@@ -157,7 +167,7 @@ class ExploitCommand(CommandMixin):
|
|
|
157
167
|
|
|
158
168
|
ui.success('done ({t:.02f} sec.)'.format(t=(time.time() - at)))
|
|
159
169
|
|
|
160
|
-
async def
|
|
170
|
+
async def _engage_tamper_enable_debug(self, args: deque[str]) -> None:
|
|
161
171
|
apk = self._helper.require_target()
|
|
162
172
|
|
|
163
173
|
_ = args.popleft()
|
|
@@ -179,7 +189,7 @@ class ExploitCommand(CommandMixin):
|
|
|
179
189
|
|
|
180
190
|
ui.success('done ({t:.02f} sec.)'.format(t=(time.time() - at)))
|
|
181
191
|
|
|
182
|
-
async def
|
|
192
|
+
async def _engage_tamper_enable_backup(self, args: deque[str]) -> None:
|
|
183
193
|
apk = self._helper.require_target()
|
|
184
194
|
|
|
185
195
|
_ = args.popleft()
|
|
@@ -203,7 +213,7 @@ class ExploitCommand(CommandMixin):
|
|
|
203
213
|
|
|
204
214
|
ui.success('done ({t:.02f} sec.)'.format(t=(time.time() - at)))
|
|
205
215
|
|
|
206
|
-
async def
|
|
216
|
+
async def _engage_tamper_patch_target_api_level(self, args: deque[str]) -> None:
|
|
207
217
|
apk = self._helper.require_target()
|
|
208
218
|
|
|
209
219
|
cmd = args.popleft()
|
|
@@ -239,7 +249,7 @@ class ExploitCommand(CommandMixin):
|
|
|
239
249
|
ui.success('done ({t:.02f} sec.)'.format(t=(time.time() - at)))
|
|
240
250
|
|
|
241
251
|
# XXX: long and ugly
|
|
242
|
-
async def
|
|
252
|
+
async def _engage_tamper_inject_frida(self, args: deque[str], script_dir_mode: bool = False) -> None:
|
|
243
253
|
configfn: Optional[str] = None
|
|
244
254
|
config_override: Optional[str] = None
|
|
245
255
|
dev_frida_dir: Optional[str] = None
|
|
@@ -419,8 +429,8 @@ class ExploitCommand(CommandMixin):
|
|
|
419
429
|
|
|
420
430
|
ui.success('done ({t:.02f} sec.)'.format(t=(time.time() - at)))
|
|
421
431
|
|
|
422
|
-
async def
|
|
423
|
-
await self.
|
|
432
|
+
async def _engage_tamper_inject_frida_scriptdir(self, args: deque[str]) -> None:
|
|
433
|
+
await self._engage_tamper_inject_frida(args, script_dir_mode=True)
|
|
424
434
|
|
|
425
435
|
def _as_dalvik_classname(self, jn: str) -> str:
|
|
426
436
|
return 'L{};'.format(jn.replace('.', '/'))
|
|
@@ -475,24 +485,7 @@ class ExploitCommand(CommandMixin):
|
|
|
475
485
|
except ClientConnectionError:
|
|
476
486
|
raise InvalidResponseError()
|
|
477
487
|
|
|
478
|
-
async def
|
|
479
|
-
_ = args.popleft()
|
|
480
|
-
|
|
481
|
-
ui.info('listing packages')
|
|
482
|
-
|
|
483
|
-
import time
|
|
484
|
-
import re
|
|
485
|
-
from trueseeing.core.android.device import AndroidDevice
|
|
486
|
-
|
|
487
|
-
at = time.time()
|
|
488
|
-
nr = 0
|
|
489
|
-
for m in re.finditer(r'^package:(.*)', await AndroidDevice().invoke_adb('shell pm list package'), re.MULTILINE):
|
|
490
|
-
p = m.group(1)
|
|
491
|
-
ui.info(p)
|
|
492
|
-
nr += 1
|
|
493
|
-
ui.success('done, {nr} packages found ({t:.02f} sec.)'.format(nr=nr, t=(time.time() - at)))
|
|
494
|
-
|
|
495
|
-
async def _exploit_device_copyout(self, args: deque[str]) -> None:
|
|
488
|
+
async def _engage_device_copyout(self, args: deque[str]) -> None:
|
|
496
489
|
success: bool = False
|
|
497
490
|
|
|
498
491
|
cmd = args.popleft()
|
|
@@ -585,7 +578,7 @@ class ExploitCommand(CommandMixin):
|
|
|
585
578
|
else:
|
|
586
579
|
ui.failure('copyout failed')
|
|
587
580
|
|
|
588
|
-
async def
|
|
581
|
+
async def _engage_device_copyin(self, args: deque[str]) -> None:
|
|
589
582
|
success: bool = False
|
|
590
583
|
|
|
591
584
|
_ = args.popleft()
|
|
@@ -677,6 +670,272 @@ class ExploitCommand(CommandMixin):
|
|
|
677
670
|
else:
|
|
678
671
|
ui.failure('copyin failed')
|
|
679
672
|
|
|
673
|
+
async def _engage_fuzz_command(self, args: deque[str], am: bool = False) -> None:
|
|
674
|
+
outfn: Optional[str] = None
|
|
675
|
+
|
|
676
|
+
cmd = args.popleft()
|
|
677
|
+
|
|
678
|
+
if not args:
|
|
679
|
+
if am:
|
|
680
|
+
ui.fatal('an "am" command line pattern required; try giving whatever you would to "adb shell am" (e.g. {} "start-activity .." ..)'.format(cmd))
|
|
681
|
+
else:
|
|
682
|
+
ui.fatal('command line pattern required; try giving you would to "adb shell"')
|
|
683
|
+
|
|
684
|
+
pat = args.popleft()
|
|
685
|
+
if am:
|
|
686
|
+
pat = f'am {pat}'
|
|
687
|
+
|
|
688
|
+
if args and not args[0].startswith('@'):
|
|
689
|
+
import os
|
|
690
|
+
outfn = args.popleft()
|
|
691
|
+
if os.path.exists(outfn) and not cmd.endswith('!'):
|
|
692
|
+
ui.fatal('outfile exists; force (!) to overwrite')
|
|
693
|
+
|
|
694
|
+
wordlist: Dict[str, List[str]] = dict()
|
|
695
|
+
for name, fn in self._helper.get_effective_options(self._helper.get_modifiers(args)).items():
|
|
696
|
+
if name.startswith('w'):
|
|
697
|
+
name = name[1:]
|
|
698
|
+
try:
|
|
699
|
+
with open(fn, 'r') as f:
|
|
700
|
+
wordlist[name] = [x.rstrip() for x in f]
|
|
701
|
+
except OSError as e:
|
|
702
|
+
ui.fatal(f'cannot open wordlist: {e}')
|
|
703
|
+
|
|
704
|
+
if not wordlist:
|
|
705
|
+
ui.fatal('need a wordlist (try @o:wNAME=FN)')
|
|
706
|
+
|
|
707
|
+
ui.info('wordlist built: {} words in {} keys ({})'.format(sum([len(v) for v in wordlist.values()]), len(wordlist), ','.join(wordlist.keys())))
|
|
708
|
+
|
|
709
|
+
def _expand(pat: str, wordlist: Mapping[str, List[str]]) -> Iterator[Tuple[int, int, str]]:
|
|
710
|
+
tries = min(len(v) for v in wordlist.values())
|
|
711
|
+
for nr in range(tries):
|
|
712
|
+
d = {k:v[nr] for k,v in wordlist.items()}
|
|
713
|
+
try:
|
|
714
|
+
yield nr, tries, pat.format(*[], **d)
|
|
715
|
+
except KeyError as e:
|
|
716
|
+
ui.fatal(f'unknown wordlist specified: {e}')
|
|
717
|
+
|
|
718
|
+
ui.info('starting fuzzing, opening log system-wide{}'.format(' [{}]'.format(outfn) if outfn else ''))
|
|
719
|
+
|
|
720
|
+
from trueseeing.core.android.device import AndroidDevice
|
|
721
|
+
|
|
722
|
+
dev = AndroidDevice()
|
|
723
|
+
|
|
724
|
+
async def _log(outfn: Optional[str]) -> None:
|
|
725
|
+
import sys
|
|
726
|
+
nr = 0
|
|
727
|
+
|
|
728
|
+
if not outfn:
|
|
729
|
+
f = sys.stdout.buffer
|
|
730
|
+
else:
|
|
731
|
+
f = open(outfn, 'wb')
|
|
732
|
+
|
|
733
|
+
try:
|
|
734
|
+
async for l in dev.invoke_adb_streaming('logcat -T1'):
|
|
735
|
+
f.write(l)
|
|
736
|
+
nr += 1
|
|
737
|
+
if outfn and nr % 256 == 0:
|
|
738
|
+
ui.info(' ... captured: {}')
|
|
739
|
+
finally:
|
|
740
|
+
if outfn:
|
|
741
|
+
f.close()
|
|
742
|
+
|
|
743
|
+
async def _fuzz(pat: str, wordlist: Mapping[str, List[str]]) -> None:
|
|
744
|
+
from asyncio import sleep
|
|
745
|
+
from subprocess import CalledProcessError
|
|
746
|
+
for nr, tries, t in _expand(pat, wordlist):
|
|
747
|
+
await sleep(.05)
|
|
748
|
+
prog = dict(nr=nr+1, max=tries, cmd=t)
|
|
749
|
+
try:
|
|
750
|
+
await dev.invoke_adb(f'shell {t}')
|
|
751
|
+
ui.info('[{nr}/{max}] {cmd}'.format(**prog))
|
|
752
|
+
except CalledProcessError as e:
|
|
753
|
+
ui.failure('[{nr}/{max}] {cmd}: failed: {code}'.format(code=e.returncode, **prog))
|
|
754
|
+
|
|
755
|
+
from asyncio import create_task, wait, FIRST_COMPLETED, ALL_COMPLETED
|
|
756
|
+
task_log = create_task(_log(outfn))
|
|
757
|
+
task_fuzz = create_task(_fuzz(pat, wordlist))
|
|
758
|
+
|
|
759
|
+
done, pending = await wait([task_log, task_fuzz], return_when=FIRST_COMPLETED)
|
|
760
|
+
for t in pending:
|
|
761
|
+
t.cancel()
|
|
762
|
+
done, _ = await wait([task_log, task_fuzz], return_when=ALL_COMPLETED)
|
|
763
|
+
for t in done:
|
|
764
|
+
exc = t.exception()
|
|
765
|
+
if exc:
|
|
766
|
+
ui.error('unhandled exception', exc=exc)
|
|
767
|
+
|
|
768
|
+
async def _engage_fuzz_intent(self, args: deque[str]) -> None:
|
|
769
|
+
await self._engage_fuzz_command(args, am=True)
|
|
770
|
+
|
|
771
|
+
async def _engage_deploy_package(self, args: deque[str]) -> None:
|
|
772
|
+
cmd = args.popleft()
|
|
773
|
+
|
|
774
|
+
context: APKContext = self._helper.get_context().require_type('apk')
|
|
775
|
+
apk = context.target
|
|
776
|
+
|
|
777
|
+
from time import time
|
|
778
|
+
from pubsub import pub
|
|
779
|
+
from trueseeing.core.ui import AndroidInstallProgressReporter
|
|
780
|
+
from trueseeing.core.android.device import AndroidDevice
|
|
781
|
+
from subprocess import CalledProcessError
|
|
782
|
+
|
|
783
|
+
dev = AndroidDevice()
|
|
784
|
+
at = time()
|
|
785
|
+
pkg = context.get_package_name()
|
|
786
|
+
|
|
787
|
+
ui.info(f'deploying package: {pkg}')
|
|
788
|
+
|
|
789
|
+
if cmd.endswith('!'):
|
|
790
|
+
try:
|
|
791
|
+
async for l in dev.invoke_adb_streaming(f'uninstall {pkg}', redir_stderr=True):
|
|
792
|
+
pub.sendMessage('progress.android.adb.update')
|
|
793
|
+
if b'success' in l.lower():
|
|
794
|
+
ui.warn('removing existing package')
|
|
795
|
+
except CalledProcessError as e:
|
|
796
|
+
ui.fatal('uninstall failed: {}'.format(e.stdout.decode().rstrip()))
|
|
797
|
+
|
|
798
|
+
with AndroidInstallProgressReporter().scoped():
|
|
799
|
+
pub.sendMessage('progress.android.adb.begin', what='installing ... ')
|
|
800
|
+
try:
|
|
801
|
+
async for l in dev.invoke_adb_streaming(f'install --no-streaming {apk}', redir_stderr=True):
|
|
802
|
+
pub.sendMessage('progress.android.adb.update')
|
|
803
|
+
if b'failure' in l.lower():
|
|
804
|
+
ui.stderr('')
|
|
805
|
+
if not cmd.endswith('!'):
|
|
806
|
+
ui.fatal('install failed; force (!) to replace ({})'.format(l.decode('UTF-8')))
|
|
807
|
+
else:
|
|
808
|
+
ui.fatal('install failed ({})'.format(l.decode('UTF-8')))
|
|
809
|
+
|
|
810
|
+
pub.sendMessage('progress.android.adb.done')
|
|
811
|
+
except CalledProcessError as e:
|
|
812
|
+
ui.fatal('install failed: {}'.format(e.stdout.decode().rstrip()))
|
|
813
|
+
|
|
814
|
+
ui.success('done ({t:.02f} sec){trailer}'.format(t=time() - at, trailer=' '*8))
|
|
815
|
+
|
|
816
|
+
async def _engage_undeploy_package(self, args: deque[str]) -> None:
|
|
817
|
+
_ = args.popleft()
|
|
818
|
+
|
|
819
|
+
context: APKContext = self._helper.get_context().require_type('apk')
|
|
820
|
+
|
|
821
|
+
from time import time
|
|
822
|
+
from pubsub import pub
|
|
823
|
+
from trueseeing.core.android.device import AndroidDevice
|
|
824
|
+
from subprocess import CalledProcessError
|
|
825
|
+
|
|
826
|
+
dev = AndroidDevice()
|
|
827
|
+
at = time()
|
|
828
|
+
pkg = context.get_package_name()
|
|
829
|
+
|
|
830
|
+
ui.info(f'removing package: {pkg}')
|
|
831
|
+
|
|
832
|
+
try:
|
|
833
|
+
async for l in dev.invoke_adb_streaming(f'uninstall {pkg}', redir_stderr=True):
|
|
834
|
+
pub.sendMessage('progress.android.adb.update')
|
|
835
|
+
if b'failure' in l.lower():
|
|
836
|
+
import re
|
|
837
|
+
packages = await dev.invoke_adb('shell pm list packages', redir_stderr=True)
|
|
838
|
+
if not re.match(f'{pkg}$', packages, re.MULTILINE):
|
|
839
|
+
ui.fatal('package not found')
|
|
840
|
+
else:
|
|
841
|
+
ui.fatal('uninstall failed ({})'.format(l.decode()))
|
|
842
|
+
except CalledProcessError as e:
|
|
843
|
+
ui.fatal('uninstall failed: {}'.format(e.stdout.decode().rstrip()))
|
|
844
|
+
|
|
845
|
+
ui.success('done ({t:.02f} sec)'.format(t=time() - at))
|
|
846
|
+
|
|
847
|
+
async def _engage_grab_package(self, args: deque[str]) -> None:
|
|
848
|
+
cmd = args.popleft()
|
|
849
|
+
|
|
850
|
+
import os
|
|
851
|
+
if not args:
|
|
852
|
+
ui.fatal('need the package name')
|
|
853
|
+
|
|
854
|
+
pkg = args.popleft()
|
|
855
|
+
|
|
856
|
+
if args:
|
|
857
|
+
outfn = args.popleft()
|
|
858
|
+
else:
|
|
859
|
+
outfn = f'{pkg}.apk'
|
|
860
|
+
|
|
861
|
+
if os.path.exists(outfn):
|
|
862
|
+
if not cmd.endswith('!'):
|
|
863
|
+
ui.fatal('output file exists; force (!) to overwrite')
|
|
864
|
+
else:
|
|
865
|
+
os.remove(outfn)
|
|
866
|
+
|
|
867
|
+
import re
|
|
868
|
+
from time import time
|
|
869
|
+
from tempfile import TemporaryDirectory
|
|
870
|
+
from pubsub import pub
|
|
871
|
+
from trueseeing.core.android.device import AndroidDevice
|
|
872
|
+
|
|
873
|
+
dev = AndroidDevice()
|
|
874
|
+
at = time()
|
|
875
|
+
outfn = os.path.realpath(outfn)
|
|
876
|
+
|
|
877
|
+
ui.info(f'grabbing package: {pkg} -> {outfn}')
|
|
878
|
+
|
|
879
|
+
basepath: Optional[bytes] = None
|
|
880
|
+
splits: List[bytes] = []
|
|
881
|
+
|
|
882
|
+
async for l in dev.invoke_adb_streaming(f'shell pm dump {pkg}', redir_stderr=True):
|
|
883
|
+
pub.sendMessage('progress.android.adb.update')
|
|
884
|
+
if f'unable to find package: {pkg}'.encode() in l.lower():
|
|
885
|
+
ui.fatal(f'package not found: {pkg}')
|
|
886
|
+
|
|
887
|
+
m = re.search(rb'codePath=(/.+)', l)
|
|
888
|
+
if m:
|
|
889
|
+
basepath = m.group(1)
|
|
890
|
+
m = re.search(rb'splits=\[(.+)\]', l)
|
|
891
|
+
if m:
|
|
892
|
+
splits = re.split(rb', *', m.group(1))
|
|
893
|
+
|
|
894
|
+
assert basepath
|
|
895
|
+
assert splits
|
|
896
|
+
|
|
897
|
+
with TemporaryDirectory() as td:
|
|
898
|
+
from os import chdir, getcwd
|
|
899
|
+
from shlex import quote
|
|
900
|
+
from zipfile import ZipFile, ZIP_STORED
|
|
901
|
+
cd = getcwd()
|
|
902
|
+
try:
|
|
903
|
+
chdir(td)
|
|
904
|
+
slicemap = dict()
|
|
905
|
+
if len(splits) == 1:
|
|
906
|
+
if outfn.endswith('.xapk'):
|
|
907
|
+
ui.warn('target has only one slice; using apk format')
|
|
908
|
+
outfn = outfn.replace('.xapk', '.apk')
|
|
909
|
+
ui.info('getting {nr} slice'.format(nr=len(splits)))
|
|
910
|
+
await dev.invoke_adb('pull {path}/base.apk {outfn}'.format(path=quote(basepath.decode()), outfn=outfn))
|
|
911
|
+
else:
|
|
912
|
+
if outfn.endswith('.apk'):
|
|
913
|
+
ui.warn('target has multiple slices; using xapk format')
|
|
914
|
+
outfn = outfn.replace('.apk', '.xapk')
|
|
915
|
+
ui.info('getting {nr} slices'.format(nr=len(splits)))
|
|
916
|
+
for s in splits:
|
|
917
|
+
slice = s.decode()
|
|
918
|
+
if slice == 'base':
|
|
919
|
+
fn = f'{pkg}.apk'
|
|
920
|
+
else:
|
|
921
|
+
fn = f'{slice}.apk'
|
|
922
|
+
await dev.invoke_adb('pull {path}/{typ}{slice}.apk {fn}'.format(
|
|
923
|
+
path=quote(basepath.decode()),
|
|
924
|
+
typ='' if slice == 'base' else 'split_',
|
|
925
|
+
slice=slice,
|
|
926
|
+
fn=fn,
|
|
927
|
+
))
|
|
928
|
+
slicemap[slice] = fn
|
|
929
|
+
XAPKManifestGenerator(slicemap).generate()
|
|
930
|
+
with ZipFile(outfn, 'w', ZIP_STORED) as zf:
|
|
931
|
+
from glob import glob
|
|
932
|
+
for n in glob('*'):
|
|
933
|
+
with open(n, 'rb') as g:
|
|
934
|
+
zf.writestr(n, g.read())
|
|
935
|
+
finally:
|
|
936
|
+
chdir(cd)
|
|
937
|
+
ui.success('done ({t:.02f} sec)'.format(t=time() - at))
|
|
938
|
+
|
|
680
939
|
def _generate_tempfilename_for_device(self, dir: Optional[str] = None) -> str:
|
|
681
940
|
import random
|
|
682
941
|
return (f'{dir}/' if dir is not None else '/data/local/tmp/') + ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=16))
|
|
@@ -692,3 +951,50 @@ class ExploitCommand(CommandMixin):
|
|
|
692
951
|
|
|
693
952
|
class InvalidResponseError(Exception):
|
|
694
953
|
pass
|
|
954
|
+
|
|
955
|
+
class XAPKManifestGenerator:
|
|
956
|
+
def __init__(self, slicemap: Dict[str, str]) -> None:
|
|
957
|
+
self._slicemap = slicemap
|
|
958
|
+
|
|
959
|
+
def generate(self) -> None:
|
|
960
|
+
from os import stat
|
|
961
|
+
from pyaxmlparser import APK
|
|
962
|
+
manif: XAPKManifest = dict(
|
|
963
|
+
xapk_version='2',
|
|
964
|
+
total_size=0,
|
|
965
|
+
locales_name=dict(),
|
|
966
|
+
split_apks=[],
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
for slice, fn in self._slicemap.items():
|
|
970
|
+
conf = slice.split('.')[-1]
|
|
971
|
+
manif['total_size'] += stat(fn).st_size
|
|
972
|
+
manif['split_apks'].append(dict(id=slice, file=fn))
|
|
973
|
+
if slice == 'base':
|
|
974
|
+
apk = APK(fn)
|
|
975
|
+
manif.update(dict(
|
|
976
|
+
name=apk.get_app_name(),
|
|
977
|
+
icon='icon.png',
|
|
978
|
+
package_name=apk.get_package(),
|
|
979
|
+
version_code=apk.version_code,
|
|
980
|
+
version_name=apk.version_name,
|
|
981
|
+
min_sdk_version=apk.get_min_sdk_version(),
|
|
982
|
+
target_sdk_version=apk.get_target_sdk_version(),
|
|
983
|
+
permissions=apk.get_permissions(),
|
|
984
|
+
))
|
|
985
|
+
with open('icon.png', 'wb') as f:
|
|
986
|
+
f.write(apk.icon_data)
|
|
987
|
+
elif len(conf) == 2:
|
|
988
|
+
apk = APK(fn)
|
|
989
|
+
country_code = conf
|
|
990
|
+
manif['locales_name'].update({
|
|
991
|
+
country_code:apk.get_app_name(),
|
|
992
|
+
})
|
|
993
|
+
if manif['locales_name']:
|
|
994
|
+
ln: Dict[str, str] = manif['locales_name']
|
|
995
|
+
for k in ln.keys():
|
|
996
|
+
if not ln[k]:
|
|
997
|
+
ln[k] = manif['name']
|
|
998
|
+
with open('manifest.json', 'w') as g:
|
|
999
|
+
from json import dumps
|
|
1000
|
+
g.write(dumps(manif, separators=(',', ':')))
|