anais-apk-forensic 1.0.0

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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -0
  3. package/anais.sh +669 -0
  4. package/analysis_tools/__pycache__/apk_basic_info.cpython-313.pyc +0 -0
  5. package/analysis_tools/__pycache__/apk_basic_info.cpython-314.pyc +0 -0
  6. package/analysis_tools/__pycache__/check_zip_encryption.cpython-313.pyc +0 -0
  7. package/analysis_tools/__pycache__/check_zip_encryption.cpython-314.pyc +0 -0
  8. package/analysis_tools/__pycache__/detect_obfuscation.cpython-313.pyc +0 -0
  9. package/analysis_tools/__pycache__/detect_obfuscation.cpython-314.pyc +0 -0
  10. package/analysis_tools/__pycache__/dex_payload_hunter.cpython-314.pyc +0 -0
  11. package/analysis_tools/__pycache__/entropy_analyzer.cpython-314.pyc +0 -0
  12. package/analysis_tools/__pycache__/error_logger.cpython-313.pyc +0 -0
  13. package/analysis_tools/__pycache__/error_logger.cpython-314.pyc +0 -0
  14. package/analysis_tools/__pycache__/find_encrypted_payload.cpython-314.pyc +0 -0
  15. package/analysis_tools/__pycache__/fix_apk_headers.cpython-313.pyc +0 -0
  16. package/analysis_tools/__pycache__/fix_apk_headers.cpython-314.pyc +0 -0
  17. package/analysis_tools/__pycache__/manifest_analyzer.cpython-313.pyc +0 -0
  18. package/analysis_tools/__pycache__/manifest_analyzer.cpython-314.pyc +0 -0
  19. package/analysis_tools/__pycache__/network_analyzer.cpython-313.pyc +0 -0
  20. package/analysis_tools/__pycache__/network_analyzer.cpython-314.pyc +0 -0
  21. package/analysis_tools/__pycache__/report_generator.cpython-313.pyc +0 -0
  22. package/analysis_tools/__pycache__/report_generator.cpython-314.pyc +0 -0
  23. package/analysis_tools/__pycache__/report_generator_modular.cpython-314.pyc +0 -0
  24. package/analysis_tools/__pycache__/sast_scanner.cpython-313.pyc +0 -0
  25. package/analysis_tools/__pycache__/sast_scanner.cpython-314.pyc +0 -0
  26. package/analysis_tools/__pycache__/so_string_analyzer.cpython-314.pyc +0 -0
  27. package/analysis_tools/__pycache__/yara_enhanced_analyzer.cpython-314.pyc +0 -0
  28. package/analysis_tools/__pycache__/yara_results_processor.cpython-314.pyc +0 -0
  29. package/analysis_tools/apk_basic_info.py +85 -0
  30. package/analysis_tools/check_zip_encryption.py +142 -0
  31. package/analysis_tools/detect_obfuscation.py +650 -0
  32. package/analysis_tools/dex_payload_hunter.py +734 -0
  33. package/analysis_tools/entropy_analyzer.py +335 -0
  34. package/analysis_tools/error_logger.py +75 -0
  35. package/analysis_tools/find_encrypted_payload.py +485 -0
  36. package/analysis_tools/fix_apk_headers.py +154 -0
  37. package/analysis_tools/manifest_analyzer.py +214 -0
  38. package/analysis_tools/network_analyzer.py +287 -0
  39. package/analysis_tools/report_generator.py +506 -0
  40. package/analysis_tools/report_generator_modular.py +885 -0
  41. package/analysis_tools/sast_scanner.py +412 -0
  42. package/analysis_tools/so_string_analyzer.py +406 -0
  43. package/analysis_tools/yara_enhanced_analyzer.py +330 -0
  44. package/analysis_tools/yara_results_processor.py +368 -0
  45. package/analyzer_config.json +113 -0
  46. package/apkid/__init__.py +32 -0
  47. package/apkid/__pycache__/__init__.cpython-313.pyc +0 -0
  48. package/apkid/__pycache__/__init__.cpython-314.pyc +0 -0
  49. package/apkid/__pycache__/apkid.cpython-313.pyc +0 -0
  50. package/apkid/__pycache__/apkid.cpython-314.pyc +0 -0
  51. package/apkid/__pycache__/main.cpython-313.pyc +0 -0
  52. package/apkid/__pycache__/main.cpython-314.pyc +0 -0
  53. package/apkid/__pycache__/output.cpython-313.pyc +0 -0
  54. package/apkid/__pycache__/rules.cpython-313.pyc +0 -0
  55. package/apkid/apkid.py +266 -0
  56. package/apkid/main.py +98 -0
  57. package/apkid/output.py +177 -0
  58. package/apkid/rules/apk/common.yara +68 -0
  59. package/apkid/rules/apk/obfuscators.yara +118 -0
  60. package/apkid/rules/apk/packers.yara +1197 -0
  61. package/apkid/rules/apk/protectors.yara +301 -0
  62. package/apkid/rules/dex/abnormal.yara +104 -0
  63. package/apkid/rules/dex/anti-vm.yara +568 -0
  64. package/apkid/rules/dex/common.yara +60 -0
  65. package/apkid/rules/dex/compilers.yara +434 -0
  66. package/apkid/rules/dex/obfuscators.yara +602 -0
  67. package/apkid/rules/dex/packers.yara +761 -0
  68. package/apkid/rules/dex/protectors.yara +520 -0
  69. package/apkid/rules/dll/common.yara +38 -0
  70. package/apkid/rules/dll/obfuscators.yara +43 -0
  71. package/apkid/rules/elf/anti-vm.yara +43 -0
  72. package/apkid/rules/elf/common.yara +54 -0
  73. package/apkid/rules/elf/obfuscators.yara +991 -0
  74. package/apkid/rules/elf/packers.yara +1128 -0
  75. package/apkid/rules/elf/protectors.yara +794 -0
  76. package/apkid/rules/res/common.yara +43 -0
  77. package/apkid/rules/res/obfuscators.yara +46 -0
  78. package/apkid/rules/res/protectors.yara +46 -0
  79. package/apkid/rules.py +77 -0
  80. package/bin/anais +3 -0
  81. package/dist/cli.js +82 -0
  82. package/dist/index.js +123 -0
  83. package/dist/types/index.js +2 -0
  84. package/dist/utils/index.js +21 -0
  85. package/dist/utils/output.js +44 -0
  86. package/dist/utils/paths.js +107 -0
  87. package/docs/ARCHITECTURE.txt +353 -0
  88. package/docs/Workflow and Reference.md +445 -0
  89. package/package.json +70 -0
  90. package/rules/yara_general_rules.yar +323 -0
  91. package/scripts/dynamic_analysis_helper.sh +334 -0
  92. package/scripts/frida/dpt_dex_dumper.js +145 -0
  93. package/scripts/frida/frida_dex_dump.js +145 -0
  94. package/scripts/frida/frida_hooks.js +437 -0
  95. package/scripts/frida/frida_websocket_extractor.js +154 -0
  96. package/scripts/setup.sh +206 -0
  97. package/scripts/validate_framework.sh +224 -0
  98. package/src/cli.ts +91 -0
  99. package/src/index.ts +123 -0
  100. package/src/types/index.ts +44 -0
  101. package/src/utils/index.ts +6 -0
  102. package/src/utils/output.ts +50 -0
  103. package/src/utils/paths.ts +72 -0
  104. package/tsconfig.json +14 -0
package/apkid/apkid.py ADDED
@@ -0,0 +1,266 @@
1
+ """
2
+ Copyright (C) 2024 RedNaga. https://rednaga.io
3
+ All rights reserved. Contact: rednaga@protonmail.com
4
+
5
+
6
+ This file is part of APKiD
7
+
8
+
9
+ Commercial License Usage
10
+ ------------------------
11
+ Licensees holding valid commercial APKiD licenses may use this file
12
+ in accordance with the commercial license agreement provided with the
13
+ Software or, alternatively, in accordance with the terms contained in
14
+ a written agreement between you and RedNaga.
15
+
16
+
17
+ GNU General Public License Usage
18
+ --------------------------------
19
+ Alternatively, this file may be used under the terms of the GNU General
20
+ Public License version 3.0 as published by the Free Software Foundation
21
+ and appearing in the file LICENSE.GPL included in the packaging of this
22
+ file. Please visit http://www.gnu.org/copyleft/gpl.html and review the
23
+ information to ensure the GNU General Public License version 3.0
24
+ requirements will be met.
25
+ """
26
+
27
+ import io
28
+ import lzma
29
+ import os
30
+ import struct
31
+ import sys
32
+ import traceback
33
+ import zipfile
34
+ from typing import Union, IO, List, Dict, Set
35
+
36
+ import yara
37
+
38
+ from .output import OutputFormatter
39
+ from .rules import RulesManager
40
+
41
+ SCANNABLE_FILE_MAGICS: Dict[str, Set[bytes]] = {
42
+ 'zip': {b'PK\x03\x04', b'PK\x05\x06', b'PK\x07\x08'},
43
+ 'dex': {b'dex\n', b'dey\n'},
44
+ 'elf': {b'\x7fELF'},
45
+ 'res': {b'\x02\x00\x0c\x00'},
46
+ 'dll': {b'MZ\x90\x00'},
47
+ # TODO: implement axml yara module
48
+ # 'axml': set(),
49
+ }
50
+ XZ_COMPRESSION_TYPE = 95
51
+ ZIP_LFH_SIG_SIZE = 4
52
+ ZIP_LFH_FIELDS_SIZE = 26
53
+ ZIP_LFH_HEADER_SIZE = ZIP_LFH_SIG_SIZE + ZIP_LFH_FIELDS_SIZE
54
+
55
+
56
+ class Options(object):
57
+
58
+ def __init__(self, timeout: int = 10, verbose: bool = False, json: bool = False, output_dir: Union[str, None] = None,
59
+ typing: Union[str, None] = 'magic', entry_max_scan_size: int = 0, scan_depth=2, recursive: bool = False,
60
+ include_types: bool = False):
61
+ """Scan options.
62
+ Holds user-supplied options governing how APKiD behaves.
63
+
64
+ Parameters
65
+ ----------
66
+ timeout : integer, optional (default=10)
67
+ The number of seconds before Yara match should time out.
68
+ json : boolean, optional (default=False)
69
+ If the output should be JSON format.
70
+ output_dir : string or None, optional (default=None)
71
+ Directory to write individual scan results to. If this is true, it implies `json_output=True`.
72
+ Note: This is useful for feature extraction of a bunch of APKs.
73
+ verbose : boolean, optional (default=False)
74
+ When set to `True`, log warnings and other debug information.
75
+ typing : string or None, optional (default="magic")
76
+ Determines how the scanner decides if a file should be scanned.
77
+ If "magic", then require the file match a built-in list of supported file magics
78
+ If "filename", then scan files which have names known to be supported (e.g. ".dex")
79
+ If None, pass every file to Yara for matching.
80
+ Note: This option defines a trade-off between performance and accuracy. For example, if you're scanning a large APK file, it is expensive
81
+ to uncompress every ZIP entry for Yara matching. It's much faster to only decompress files which have a known extension such as ".dex".
82
+ But in many cases files may not have the correct extension, e.g. a DEX file may be named "notmalware.gif". On the other extreme, one could
83
+ simply decompress every file and scan it, but this is wasteful because most files in an APK are typically not interesting, e.g. images.
84
+ The default behavior of "magic" only needs to read a few bytes to decide if a file should be completely uncompressed.
85
+ entry_max_scan_size : integer, optional (default=0)
86
+ If > 0, only scan APK entries if their uncompressed size is less than this value.
87
+ scan_depth : integer, optional (default=2)
88
+ Determines how many times scanner should recurse into nested archives.
89
+ If 0, don't recurse into nested archives.
90
+ Note: It's possible to construct a malicious ZIP which can be infinitely nested. It's therefore necessary to limit the scan depth.
91
+ Don't get cheeky and think you can set this value to 1000 and scan random malware without blowing up your memory.
92
+ recursive : boolean, optional (default=True)
93
+ If true, when scanning a directory, will recurse into subdirectories.
94
+ """
95
+ self.timeout = timeout
96
+ self.verbose = verbose
97
+ self.typing = typing
98
+ self.entry_max_scan_size = entry_max_scan_size
99
+ self.scan_depth = scan_depth
100
+ self.recursive = recursive
101
+ self.rules_manager = RulesManager()
102
+ self.output = OutputFormatter(
103
+ json_output=json,
104
+ output_dir=output_dir,
105
+ rules_manager=self.rules_manager,
106
+ include_types=include_types
107
+ )
108
+
109
+
110
+ class Scanner(object):
111
+
112
+ def __init__(self, rules: yara.Rules, options: Options):
113
+ self.rules = rules
114
+ self.options = options
115
+
116
+ def scan(self, path: str) -> None:
117
+ if os.path.isfile(path):
118
+ results = self.scan_file(path)
119
+ if len(results) > 0:
120
+ self.options.output.write(results)
121
+ elif os.path.isdir(path):
122
+ self.scan_directory(path)
123
+
124
+ def scan_directory(self, dir_path: str) -> None:
125
+ for file_path in self._yield_file_paths(dir_path):
126
+ self.scan(file_path)
127
+
128
+ def scan_file(self, file_path: str) -> Dict[str, List[yara.Match]]:
129
+ results = []
130
+ with open(file_path, 'rb') as f:
131
+ try:
132
+ results: Dict[str, List[yara.Match]] = self.scan_file_obj(f, file_path)
133
+ except Exception as e:
134
+ stack = traceback.format_exc()
135
+ print(f"Exception scanning {file_path}: {stack}")
136
+ return results
137
+
138
+ def scan_file_obj(self, file: IO, file_path: str = '$FILE$'):
139
+ if file_path == '$FILE$':
140
+ file_name = file_path
141
+ else:
142
+ file_name = os.path.basename(file_path)
143
+
144
+ results: Dict[str, List[yara.Match]] = {}
145
+ if not self._should_scan(file, file_name):
146
+ return results
147
+
148
+ matches: List[yara.Matches] = self.rules.match(data=file.read(), timeout=self.options.timeout)
149
+ if len(matches) > 0:
150
+ results[file_path] = matches
151
+ if self._is_zipfile(file, file_name):
152
+ with zipfile.ZipFile(file) as zf:
153
+ zip_results = self._scan_zip(zf)
154
+ for entry_name, entry_matches in zip_results.items():
155
+ results[f'{file_path}!{entry_name}'] = entry_matches
156
+ return results
157
+
158
+ def _scan_zip(self, zf: zipfile.ZipFile, depth=0) -> Dict[str, List[yara.Match]]:
159
+ results: Dict[str, List[yara.Match]] = {}
160
+ for info in zf.infolist():
161
+ if info.is_dir():
162
+ continue
163
+ try:
164
+ self._scan_zip_entry(zf, info, results, depth)
165
+ except Exception as e:
166
+ stack = traceback.format_exc()
167
+ print(f"Exception scanning {info.filename} in {zf.filename}, depth={depth}: {stack}",
168
+ file=sys.stderr)
169
+ return results
170
+
171
+ def _scan_zip_entry(self, zf, info, results, depth) -> None:
172
+ try:
173
+ with zf.open(info) as entry:
174
+ # Python 3.6 zip entries are not seek'able :(
175
+ entry_buffer: IO = io.BytesIO(entry.read(4))
176
+ entry_buffer.seek(0)
177
+ if not self._should_scan(entry_buffer, info.filename):
178
+ return
179
+ entry_buffer.seek(4)
180
+ entry_buffer.write(entry.read())
181
+ except zipfile.BadZipFile as e:
182
+ if "Overlapped entries" in str(e) or "possible zip bomb" in str(e):
183
+ if self.options.verbose:
184
+ print(f"[W] Skipping overlapped entry {info.filename} (possible zip bomb)")
185
+ return
186
+ else:
187
+ raise
188
+ except NotImplementedError:
189
+ # XZ-compression
190
+ if info.compress_type == XZ_COMPRESSION_TYPE:
191
+ with open(zf.filename, 'rb') as raw_zip:
192
+ raw_zip.seek(info.header_offset + ZIP_LFH_FIELDS_SIZE)
193
+ filename_len = struct.unpack('<H', raw_zip.read(2))[0]
194
+ extra_len = struct.unpack('<H', raw_zip.read(2))[0]
195
+ data_offset = info.header_offset + ZIP_LFH_HEADER_SIZE + filename_len + extra_len
196
+ raw_zip.seek(data_offset)
197
+ compressed_data = raw_zip.read(info.compress_size)
198
+ try:
199
+ decompressed_data = lzma.decompress(compressed_data)
200
+ entry_buffer = io.BytesIO(decompressed_data)
201
+ except Exception as e:
202
+ print(f"[E] Failed to decompress {info.filename}: {e}")
203
+ return
204
+ else:
205
+ print(f"[E] Unsupported compression method {info.compress_type} for {info.filename}")
206
+ return
207
+
208
+
209
+ entry_buffer.seek(0)
210
+ matches = self.rules.match(data=entry_buffer.read(), timeout=self.options.timeout)
211
+
212
+ if len(matches) > 0:
213
+ results[info.filename] = matches
214
+
215
+ if depth < self.options.scan_depth and self._is_zipfile(entry_buffer, info.filename):
216
+ with zipfile.ZipFile(entry_buffer) as zip_entry:
217
+ nested_results = self._scan_zip(zip_entry, depth=depth + 1)
218
+ for nested_name, nested_matches in nested_results.items():
219
+ results[f'{info.filename}!{nested_name}'] = nested_matches
220
+
221
+ @staticmethod
222
+ def _type_file(file: IO) -> Union[None, str]:
223
+ magic = file.read(4)
224
+ file.seek(0)
225
+ for file_type, magics in SCANNABLE_FILE_MAGICS.items():
226
+ if magic in magics:
227
+ return file_type
228
+ return None
229
+
230
+ def _is_zipfile(self, file: IO, name: str) -> bool:
231
+ if self.options.typing == 'filename':
232
+ name = name.lower()
233
+ return name.endswith('.apk') or name.endswith('.zip') or name.endswith('.jar')
234
+ else:
235
+ # Look at file magic since zipfile.is_zipfile isn't perfect
236
+ # Some elfs may be considered zips and this causes apkid to barf errors
237
+ file.seek(0)
238
+ return Scanner._type_file(file) == 'zip' and zipfile.is_zipfile(file)
239
+
240
+ def _should_scan(self, file: IO, name: str) -> bool:
241
+ if self.options.typing == 'magic':
242
+ file_type = Scanner._type_file(file)
243
+ return file_type is not None
244
+ elif self.options.typing == 'filename':
245
+ name = name.lower()
246
+ return name.startswith('classes') \
247
+ or name.startswith('AndroidManifest.xml') \
248
+ or name.startswith('lib/') \
249
+ or name.endswith('.so') \
250
+ or name.endswith('.dex') \
251
+ or name.endswith('.apk') \
252
+ or name.endswith('.arsc') \
253
+ or name.endswith('.dll')
254
+ return True
255
+
256
+ def _yield_file_paths(self, dir_path: str):
257
+ if self.options.recursive:
258
+ for root, _, filenames in os.walk(dir_path):
259
+ for path in filenames:
260
+ yield os.path.join(root, path)
261
+ else:
262
+ for path in os.listdir(dir_path):
263
+ full_path = os.path.join(dir_path, path)
264
+ if os.path.isdir(full_path):
265
+ continue
266
+ yield full_path
package/apkid/main.py ADDED
@@ -0,0 +1,98 @@
1
+ """
2
+ Copyright (C) 2023 RedNaga. https://rednaga.io
3
+ All rights reserved. Contact: rednaga@protonmail.com
4
+
5
+
6
+ This file is part of APKiD
7
+
8
+
9
+ Commercial License Usage
10
+ ------------------------
11
+ Licensees holding valid commercial APKiD licenses may use this file
12
+ in accordance with the commercial license agreement provided with the
13
+ Software or, alternatively, in accordance with the terms contained in
14
+ a written agreement between you and RedNaga.
15
+
16
+
17
+ GNU General Public License Usage
18
+ --------------------------------
19
+ Alternatively, this file may be used under the terms of the GNU General
20
+ Public License version 3.0 as published by the Free Software Foundation
21
+ and appearing in the file LICENSE.GPL included in the packaging of this
22
+ file. Please visit http://www.gnu.org/copyleft/gpl.html and review the
23
+ information to ensure the GNU General Public License version 3.0
24
+ requirements will be met.
25
+ """
26
+
27
+ import argparse
28
+
29
+ from apkid.apkid import Scanner, Options
30
+ from . import __version__
31
+
32
+
33
+ def get_parser():
34
+ formatter = lambda prog: argparse.ArgumentDefaultsHelpFormatter(prog, max_help_position=50, width=100)
35
+
36
+ parser = argparse.ArgumentParser(
37
+ description=f"APKiD - Android Application Identifier v{__version__}",
38
+ formatter_class=formatter
39
+ )
40
+ parser.add_argument('input', metavar='FILE', type=str, nargs='*',
41
+ help="apk, dex, or directory")
42
+ parser.add_argument('-v', '--verbose', action='store_true',
43
+ help="log debug messages")
44
+
45
+ scanning = parser.add_argument_group('scanning')
46
+ scanning.add_argument('-t', '--timeout', type=int, default=30,
47
+ help="Yara scan timeout (in seconds)")
48
+ scanning.add_argument('-r', '--recursive', action='store_true', default=False,
49
+ help="recurse into subdirectories")
50
+ scanning.add_argument('--scan-depth', type=int, default=2,
51
+ help="how deep to go when scanning nested zips")
52
+ scanning.add_argument('--entry-max-scan-size', type=int, default=100 * 1024 * 1024,
53
+ help="max zip entry size to scan in bytes, 0 = no limit")
54
+ scanning.add_argument('--typing', choices=('magic', 'filename', 'none'), default='magic',
55
+ help="method to decide which files to scan")
56
+
57
+ output = parser.add_argument_group('output')
58
+ output.add_argument('-j', '--json', action='store_true',
59
+ help="output scan results in JSON format", )
60
+ output.add_argument('-o', '--output-dir', metavar='DIR', default=None,
61
+ help="write individual results here (implies --json)")
62
+ output.add_argument('--include-types', action='store_true', default=False,
63
+ help="include file type info for matched files")
64
+
65
+ return parser
66
+
67
+
68
+ def build_options(args) -> Options:
69
+ return Options(
70
+ timeout=args.timeout,
71
+ verbose=args.verbose,
72
+ json=args.json,
73
+ output_dir=args.output_dir,
74
+ typing=args.typing,
75
+ entry_max_scan_size=args.entry_max_scan_size,
76
+ scan_depth=args.scan_depth,
77
+ recursive=args.recursive,
78
+ include_types=args.include_types,
79
+ )
80
+
81
+
82
+ def main():
83
+ parser = get_parser()
84
+ args = parser.parse_args()
85
+ options = build_options(args)
86
+
87
+ if not options.output.json:
88
+ print(f"[+] APKiD {__version__} :: from RedNaga :: rednaga.io")
89
+
90
+ rules = options.rules_manager.load()
91
+ scanner = Scanner(rules, options)
92
+
93
+ for input in args.input:
94
+ scanner.scan(input)
95
+
96
+
97
+ if __name__ == '__main__':
98
+ main()
@@ -0,0 +1,177 @@
1
+ """
2
+ Copyright (C) 2023 RedNaga. https://rednaga.io
3
+ All rights reserved. Contact: rednaga@protonmail.com
4
+
5
+
6
+ This file is part of APKiD
7
+
8
+
9
+ Commercial License Usage
10
+ ------------------------
11
+ Licensees holding valid commercial APKiD licenses may use this file
12
+ in accordance with the commercial license agreement provided with the
13
+ Software or, alternatively, in accordance with the terms contained in
14
+ a written agreement between you and RedNaga.
15
+
16
+
17
+ GNU General Public License Usage
18
+ --------------------------------
19
+ Alternatively, this file may be used under the terms of the GNU General
20
+ Public License version 3.0 as published by the Free Software Foundation
21
+ and appearing in the file LICENSE.GPL included in the packaging of this
22
+ file. Please visit http://www.gnu.org/copyleft/gpl.html and review the
23
+ information to ensure the GNU General Public License version 3.0
24
+ requirements will be met.
25
+ """
26
+
27
+ import json
28
+ import os
29
+ import sys
30
+ from typing import Dict, List, Union
31
+
32
+ import yara
33
+
34
+ from .rules import RulesManager
35
+
36
+ prt_red = lambda s: f"\033[91m{s}\033[00m"
37
+ prt_green = lambda s: f"\033[92m{s}\033[00m"
38
+ prt_yellow = lambda s: f"\033[93m{s}\033[00m"
39
+ prt_light_purple = lambda s: f"\033[94m{s}\033[00m"
40
+ prt_purple = lambda s: f"\033[95m{s}\033[00m"
41
+ prt_cyan = lambda s: f"\033[36m{s}\033[00m"
42
+ prt_light_cyan = lambda s: f"\033[96m{s}\033[00m"
43
+ prt_light_gray = lambda s: f"\033[97m{s}\033[00m"
44
+ prt_orange = lambda s: f"\033[33m{s}\033[00m"
45
+ prt_pink = lambda s: f"\033[35m{s}\033[00m"
46
+
47
+ def is_windows_cmd():
48
+ """
49
+ Check if the current environment is a Windows command prompt.
50
+
51
+ Returns:
52
+ bool: True if the operating system is Windows and the SESSIONNAME
53
+ environment variable is not set, indicating a command prompt.
54
+ """
55
+ return os.name == 'nt' and (
56
+ os.getenv('SESSIONNAME') is None
57
+ )
58
+
59
+ def colorize_tag(tag) -> str:
60
+ if tag == 'compiler':
61
+ return prt_cyan(tag)
62
+ elif tag == 'manipulator':
63
+ return prt_light_cyan(tag)
64
+ elif tag == 'abnormal':
65
+ return prt_light_gray(tag)
66
+ elif tag in ['anti_vm', 'anti_disassembly', 'anti_debug', 'anti_root']:
67
+ return prt_purple(tag)
68
+ elif tag in ['packer', 'protector', 'anticheat']:
69
+ return prt_red(tag)
70
+ elif tag == 'obfuscator':
71
+ return prt_yellow(tag)
72
+ elif tag == 'dropper':
73
+ return prt_green(tag)
74
+ elif tag == 'embedded':
75
+ return prt_light_purple(tag)
76
+ elif tag == 'file_type':
77
+ return prt_orange(tag)
78
+ elif tag == 'internal':
79
+ return prt_pink(tag)
80
+ else:
81
+ return tag
82
+
83
+
84
+ class OutputFormatter(object):
85
+ def __init__(self, json_output: bool, output_dir: Union[str, None], rules_manager: RulesManager, include_types: bool):
86
+ from apkid import __version__
87
+ self.output_dir = output_dir
88
+ self.json = json_output or output_dir
89
+ self.version = __version__
90
+ self.rules_hash = rules_manager.hash
91
+ self.include_types = include_types
92
+
93
+ def write(self, results: Dict[str, List[yara.Match]]) -> None:
94
+ """
95
+ Example yara.Match:
96
+ {
97
+ 'tags': ['foo', 'bar'],
98
+ 'matches': True,
99
+ 'namespace': 'default',
100
+ 'rule': 'my_rule',
101
+ 'meta': {},
102
+ 'strings': [(81L, '$a', 'abc'), (141L, '$b', 'def')]
103
+ }
104
+ """
105
+
106
+ if self.output_dir:
107
+ # Result keys are file paths. Shortest key is base file in the case of archives.
108
+ base_file = sorted(results.keys(), key=lambda k: len(k))[0]
109
+ out_file = os.path.join(self.output_dir, *base_file.split(os.path.sep))
110
+ out_path = os.path.dirname(out_file)
111
+ if not os.path.exists(out_path):
112
+ os.makedirs(out_path)
113
+ output = self.build_json_output(results)
114
+ with open(out_file, 'w') as f:
115
+ f.write(json.dumps(output))
116
+ else:
117
+ if self.json:
118
+ self._print_json(results)
119
+ else:
120
+ self._print_console(results)
121
+
122
+ def build_json_output(self, results: Dict[str, List[yara.Match]]):
123
+ output = {
124
+ 'apkid_version': self.version,
125
+ 'rules_sha256': self.rules_hash,
126
+ 'files': [],
127
+ }
128
+ for filename, matches in results.items():
129
+ match_results = self._build_match_results(matches)
130
+ if len(match_results) == 0:
131
+ continue
132
+ result = {
133
+ 'filename': filename,
134
+ 'matches': match_results,
135
+ }
136
+ output['files'].append(result)
137
+ return output
138
+
139
+ def _print_json(self, results: Dict[str, List[yara.Match]]) -> None:
140
+ output = self.build_json_output(results)
141
+ print(json.dumps(output, sort_keys=True))
142
+
143
+ def _print_console(self, results: Dict[str, List[yara.Match]]) -> None:
144
+ for key, raw_matches in results.items():
145
+ match_results = self._build_match_results(raw_matches)
146
+ if len(match_results) == 0:
147
+ continue
148
+ print(f"[*] {key}")
149
+ for tags in sorted(match_results):
150
+ descriptions = ', '.join(sorted(match_results[tags]))
151
+ if sys.stdout.isatty() and not is_windows_cmd():
152
+ tags_str = OutputFormatter._colorize_tags(tags)
153
+ else:
154
+ tags_str = tags
155
+ print(f" |-> {tags_str} : {descriptions}")
156
+
157
+ def _build_match_results(self, matches) -> Dict[str, List[str]]:
158
+ results: Dict[str, List[str]] = {}
159
+ for m in matches:
160
+ if 'file_type' in m.tags and not self.include_types:
161
+ continue
162
+ tags = ', '.join(sorted(m.tags))
163
+ description = m.meta.get('description', m)
164
+ if tags in results:
165
+ if description not in results[tags]:
166
+ results[tags].append(description)
167
+ else:
168
+ results[tags] = [description]
169
+ return results
170
+
171
+ @staticmethod
172
+ def _colorize_tags(tags) -> str:
173
+ colored_tags = []
174
+ for tag in tags.split(', '):
175
+ colored_tag = colorize_tag(tag)
176
+ colored_tags.append(colored_tag)
177
+ return ', '.join(colored_tags)
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright (C) 2023 RedNaga. https://rednaga.io
3
+ * All rights reserved. Contact: rednaga@protonmail.com
4
+ *
5
+ *
6
+ * This file is part of APKiD
7
+ *
8
+ *
9
+ * Commercial License Usage
10
+ * ------------------------
11
+ * Licensees holding valid commercial APKiD licenses may use this file
12
+ * in accordance with the commercial license agreement provided with the
13
+ * Software or, alternatively, in accordance with the terms contained in
14
+ * a written agreement between you and RedNaga.
15
+ *
16
+ *
17
+ * GNU General Public License Usage
18
+ * --------------------------------
19
+ * Alternatively, this file may be used under the terms of the GNU General
20
+ * Public License version 3.0 as published by the Free Software Foundation
21
+ * and appearing in the file LICENSE.GPL included in the packaging of this
22
+ * file. Please visit http://www.gnu.org/copyleft/gpl.html and review the
23
+ * information to ensure the GNU General Public License version 3.0
24
+ * requirements will be met.
25
+ *
26
+ **/
27
+
28
+ rule is_apk : file_type
29
+ {
30
+ meta:
31
+ description = "APK"
32
+
33
+ strings:
34
+ $zip_head = "PK"
35
+ $manifest = "AndroidManifest.xml"
36
+
37
+ condition:
38
+ $zip_head at 0 and $manifest and #manifest >= 2
39
+ }
40
+
41
+ private rule is_signed_apk : internal
42
+ {
43
+ meta:
44
+ description = "Resembles a signed APK that is likely not corrupt"
45
+
46
+ strings:
47
+ $meta_inf = "META-INF/"
48
+ $ext_rsa = ".RSA"
49
+ $ext_dsa = ".DSA"
50
+ $ext_ec = ".EC"
51
+ $apk_sig_block_footer = { 41 50 4B 20 53 69 67 20 42 6C 6F 63 6B 20 34 32 50 4B 01 02 }
52
+
53
+ condition:
54
+ is_apk and
55
+ (
56
+ for all of ($meta_inf*) : ($ext_rsa or $ext_dsa or $ext_ec in (@ + 9..@ + 9 + 100)) or
57
+ $apk_sig_block_footer
58
+ )
59
+ }
60
+
61
+ private rule is_unsigned_apk : internal
62
+ {
63
+ meta:
64
+ description = "Resembles an unsigned APK that is likely not corrupt"
65
+
66
+ condition:
67
+ is_apk and not is_signed_apk
68
+ }