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.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/anais.sh +669 -0
- package/analysis_tools/__pycache__/apk_basic_info.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/apk_basic_info.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/check_zip_encryption.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/check_zip_encryption.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/detect_obfuscation.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/detect_obfuscation.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/dex_payload_hunter.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/entropy_analyzer.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/error_logger.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/error_logger.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/find_encrypted_payload.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/fix_apk_headers.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/fix_apk_headers.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/manifest_analyzer.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/manifest_analyzer.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/network_analyzer.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/network_analyzer.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/report_generator.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/report_generator.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/report_generator_modular.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/sast_scanner.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/sast_scanner.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/so_string_analyzer.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/yara_enhanced_analyzer.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/yara_results_processor.cpython-314.pyc +0 -0
- package/analysis_tools/apk_basic_info.py +85 -0
- package/analysis_tools/check_zip_encryption.py +142 -0
- package/analysis_tools/detect_obfuscation.py +650 -0
- package/analysis_tools/dex_payload_hunter.py +734 -0
- package/analysis_tools/entropy_analyzer.py +335 -0
- package/analysis_tools/error_logger.py +75 -0
- package/analysis_tools/find_encrypted_payload.py +485 -0
- package/analysis_tools/fix_apk_headers.py +154 -0
- package/analysis_tools/manifest_analyzer.py +214 -0
- package/analysis_tools/network_analyzer.py +287 -0
- package/analysis_tools/report_generator.py +506 -0
- package/analysis_tools/report_generator_modular.py +885 -0
- package/analysis_tools/sast_scanner.py +412 -0
- package/analysis_tools/so_string_analyzer.py +406 -0
- package/analysis_tools/yara_enhanced_analyzer.py +330 -0
- package/analysis_tools/yara_results_processor.py +368 -0
- package/analyzer_config.json +113 -0
- package/apkid/__init__.py +32 -0
- package/apkid/__pycache__/__init__.cpython-313.pyc +0 -0
- package/apkid/__pycache__/__init__.cpython-314.pyc +0 -0
- package/apkid/__pycache__/apkid.cpython-313.pyc +0 -0
- package/apkid/__pycache__/apkid.cpython-314.pyc +0 -0
- package/apkid/__pycache__/main.cpython-313.pyc +0 -0
- package/apkid/__pycache__/main.cpython-314.pyc +0 -0
- package/apkid/__pycache__/output.cpython-313.pyc +0 -0
- package/apkid/__pycache__/rules.cpython-313.pyc +0 -0
- package/apkid/apkid.py +266 -0
- package/apkid/main.py +98 -0
- package/apkid/output.py +177 -0
- package/apkid/rules/apk/common.yara +68 -0
- package/apkid/rules/apk/obfuscators.yara +118 -0
- package/apkid/rules/apk/packers.yara +1197 -0
- package/apkid/rules/apk/protectors.yara +301 -0
- package/apkid/rules/dex/abnormal.yara +104 -0
- package/apkid/rules/dex/anti-vm.yara +568 -0
- package/apkid/rules/dex/common.yara +60 -0
- package/apkid/rules/dex/compilers.yara +434 -0
- package/apkid/rules/dex/obfuscators.yara +602 -0
- package/apkid/rules/dex/packers.yara +761 -0
- package/apkid/rules/dex/protectors.yara +520 -0
- package/apkid/rules/dll/common.yara +38 -0
- package/apkid/rules/dll/obfuscators.yara +43 -0
- package/apkid/rules/elf/anti-vm.yara +43 -0
- package/apkid/rules/elf/common.yara +54 -0
- package/apkid/rules/elf/obfuscators.yara +991 -0
- package/apkid/rules/elf/packers.yara +1128 -0
- package/apkid/rules/elf/protectors.yara +794 -0
- package/apkid/rules/res/common.yara +43 -0
- package/apkid/rules/res/obfuscators.yara +46 -0
- package/apkid/rules/res/protectors.yara +46 -0
- package/apkid/rules.py +77 -0
- package/bin/anais +3 -0
- package/dist/cli.js +82 -0
- package/dist/index.js +123 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/index.js +21 -0
- package/dist/utils/output.js +44 -0
- package/dist/utils/paths.js +107 -0
- package/docs/ARCHITECTURE.txt +353 -0
- package/docs/Workflow and Reference.md +445 -0
- package/package.json +70 -0
- package/rules/yara_general_rules.yar +323 -0
- package/scripts/dynamic_analysis_helper.sh +334 -0
- package/scripts/frida/dpt_dex_dumper.js +145 -0
- package/scripts/frida/frida_dex_dump.js +145 -0
- package/scripts/frida/frida_hooks.js +437 -0
- package/scripts/frida/frida_websocket_extractor.js +154 -0
- package/scripts/setup.sh +206 -0
- package/scripts/validate_framework.sh +224 -0
- package/src/cli.ts +91 -0
- package/src/index.ts +123 -0
- package/src/types/index.ts +44 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/output.ts +50 -0
- package/src/utils/paths.ts +72 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
DEX Payload Hunter
|
|
4
|
+
Finds encrypted/hidden DEX payloads in APK files
|
|
5
|
+
Predicts runtime unpacking locations for dynamic analysis
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
import zipfile
|
|
12
|
+
import struct
|
|
13
|
+
import math
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from error_logger import setup_logger
|
|
19
|
+
except ImportError:
|
|
20
|
+
class DummyLogger:
|
|
21
|
+
def info(self, msg): pass
|
|
22
|
+
def warning(self, msg): print(f"[WARNING] {msg}", file=sys.stderr)
|
|
23
|
+
def error(self, msg, detail=None): print(f"[ERROR] {msg}", file=sys.stderr)
|
|
24
|
+
def setup_logger(name, log_file=None, verbose=False):
|
|
25
|
+
return DummyLogger()
|
|
26
|
+
|
|
27
|
+
class DEXPayloadHunter:
|
|
28
|
+
"""Hunt for hidden/encrypted DEX payloads in APK"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, apk_path):
|
|
31
|
+
self.apk_path = Path(apk_path)
|
|
32
|
+
self.logger = setup_logger('dex_payload_hunter')
|
|
33
|
+
|
|
34
|
+
self.results = {
|
|
35
|
+
'stub_dex_detected': False,
|
|
36
|
+
'encrypted_payloads': [],
|
|
37
|
+
'suspicious_files': [],
|
|
38
|
+
'likely_unpack_locations': [],
|
|
39
|
+
'protection_indicators': [],
|
|
40
|
+
'recommendations': []
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def calculate_entropy(self, data):
|
|
44
|
+
"""Calculate Shannon entropy of data"""
|
|
45
|
+
if len(data) == 0:
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
freq = defaultdict(int)
|
|
49
|
+
for byte in data:
|
|
50
|
+
freq[byte] += 1
|
|
51
|
+
|
|
52
|
+
entropy = 0
|
|
53
|
+
for count in freq.values():
|
|
54
|
+
p = count / len(data)
|
|
55
|
+
entropy -= p * math.log2(p)
|
|
56
|
+
|
|
57
|
+
return entropy
|
|
58
|
+
|
|
59
|
+
def is_dex_file(self, data):
|
|
60
|
+
"""Check if data starts with DEX magic"""
|
|
61
|
+
if len(data) < 8:
|
|
62
|
+
return False
|
|
63
|
+
return data[:4] == b'dex\n'
|
|
64
|
+
|
|
65
|
+
def is_encrypted_dex(self, data):
|
|
66
|
+
"""Detect if data might be encrypted DEX based on patterns"""
|
|
67
|
+
if len(data) < 112: # DEX header size
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
# Check for common encryption markers
|
|
71
|
+
markers = [
|
|
72
|
+
b'ENCRYPTED',
|
|
73
|
+
b'ENCODED',
|
|
74
|
+
b'PACKED',
|
|
75
|
+
b'DPT',
|
|
76
|
+
b'SHELL',
|
|
77
|
+
b'PROTECTED'
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
for marker in markers:
|
|
81
|
+
if marker in data[:200]: # Check first 200 bytes
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
# Check entropy - encrypted data has high entropy
|
|
85
|
+
entropy = self.calculate_entropy(data[:min(10000, len(data))])
|
|
86
|
+
if entropy > 7.5:
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def analyze_stub_dex(self, dex_path, dex_data):
|
|
92
|
+
"""Analyze if DEX is a stub/loader"""
|
|
93
|
+
indicators = []
|
|
94
|
+
is_stub = False
|
|
95
|
+
|
|
96
|
+
# 1. Check size - stubs are usually small
|
|
97
|
+
size = len(dex_data)
|
|
98
|
+
if size < 100 * 1024: # < 100KB
|
|
99
|
+
indicators.append(f'Small DEX size: {size / 1024:.1f}KB (typical stub)')
|
|
100
|
+
is_stub = True
|
|
101
|
+
|
|
102
|
+
# 2. Check for DPT/protection strings in DEX
|
|
103
|
+
protection_strings = [
|
|
104
|
+
b'libdpt.so',
|
|
105
|
+
b'JniBridge',
|
|
106
|
+
b'ProxyApplication',
|
|
107
|
+
b'ProxyComponentFactory',
|
|
108
|
+
b'com/jx/shell',
|
|
109
|
+
b'attachBaseContext',
|
|
110
|
+
b'loadDex',
|
|
111
|
+
b'getDexFromNative',
|
|
112
|
+
b'unpack',
|
|
113
|
+
b'decrypt'
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
found_strings = []
|
|
117
|
+
for ps in protection_strings:
|
|
118
|
+
if ps in dex_data:
|
|
119
|
+
found_strings.append(ps.decode('utf-8', errors='ignore'))
|
|
120
|
+
is_stub = True
|
|
121
|
+
|
|
122
|
+
if found_strings:
|
|
123
|
+
indicators.append(f'Protection strings found: {", ".join(found_strings)}')
|
|
124
|
+
|
|
125
|
+
# 3. Check method/class count (stubs have few methods)
|
|
126
|
+
try:
|
|
127
|
+
# Parse DEX header
|
|
128
|
+
header = struct.unpack('<8sIII', dex_data[0:24])
|
|
129
|
+
# Get method count from header
|
|
130
|
+
method_ids_off = struct.unpack('<I', dex_data[88:92])[0]
|
|
131
|
+
method_ids_size = struct.unpack('<I', dex_data[84:88])[0]
|
|
132
|
+
|
|
133
|
+
if method_ids_size < 500:
|
|
134
|
+
indicators.append(f'Low method count: {method_ids_size} (typical stub)')
|
|
135
|
+
is_stub = True
|
|
136
|
+
except:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
return is_stub, indicators
|
|
140
|
+
|
|
141
|
+
def find_encrypted_payloads(self):
|
|
142
|
+
"""Find encrypted DEX payloads in APK"""
|
|
143
|
+
payloads = []
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
with zipfile.ZipFile(self.apk_path, 'r') as zf:
|
|
147
|
+
for file_info in zf.filelist:
|
|
148
|
+
filename = file_info.filename
|
|
149
|
+
|
|
150
|
+
# Skip standard files
|
|
151
|
+
if filename.startswith('META-INF/') or filename.startswith('res/'):
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
# Check suspicious locations
|
|
155
|
+
suspicious_patterns = [
|
|
156
|
+
'assets/',
|
|
157
|
+
'lib/',
|
|
158
|
+
'.dat',
|
|
159
|
+
'.bin',
|
|
160
|
+
'.jar',
|
|
161
|
+
'.dex',
|
|
162
|
+
'.so',
|
|
163
|
+
'.odex',
|
|
164
|
+
'.zip',
|
|
165
|
+
'classes',
|
|
166
|
+
'shell',
|
|
167
|
+
'stub',
|
|
168
|
+
'encrypted'
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
is_suspicious = any(pattern in filename.lower() for pattern in suspicious_patterns)
|
|
172
|
+
|
|
173
|
+
if is_suspicious and file_info.file_size > 1024: # > 1KB
|
|
174
|
+
try:
|
|
175
|
+
data = zf.read(filename)
|
|
176
|
+
entropy = self.calculate_entropy(data[:min(10000, len(data))])
|
|
177
|
+
|
|
178
|
+
payload_info = {
|
|
179
|
+
'location': filename,
|
|
180
|
+
'size': file_info.file_size,
|
|
181
|
+
'size_kb': file_info.file_size / 1024,
|
|
182
|
+
'entropy': round(entropy, 2),
|
|
183
|
+
'compressed': file_info.compress_type != 0,
|
|
184
|
+
'indicators': []
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# Analyze file
|
|
188
|
+
if self.is_dex_file(data):
|
|
189
|
+
payload_info['type'] = 'DEX'
|
|
190
|
+
payload_info['indicators'].append('Valid DEX header')
|
|
191
|
+
elif self.is_encrypted_dex(data):
|
|
192
|
+
payload_info['type'] = 'Encrypted DEX (suspected)'
|
|
193
|
+
payload_info['indicators'].append('Encryption markers or high entropy')
|
|
194
|
+
elif entropy > 7.5:
|
|
195
|
+
payload_info['type'] = 'Encrypted data'
|
|
196
|
+
payload_info['indicators'].append(f'High entropy: {entropy:.2f}')
|
|
197
|
+
elif entropy > 6.5:
|
|
198
|
+
payload_info['type'] = 'Compressed/Obfuscated'
|
|
199
|
+
payload_info['indicators'].append(f'Medium-high entropy: {entropy:.2f}')
|
|
200
|
+
else:
|
|
201
|
+
payload_info['type'] = 'Unknown'
|
|
202
|
+
|
|
203
|
+
# Check for specific protection patterns
|
|
204
|
+
if b'DPT' in data[:1000]:
|
|
205
|
+
payload_info['indicators'].append('DPT signature detected')
|
|
206
|
+
if b'BANGCLE' in data[:1000]:
|
|
207
|
+
payload_info['indicators'].append('Bangcle signature detected')
|
|
208
|
+
|
|
209
|
+
payloads.append(payload_info)
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
self.logger.error(f'Error analyzing {filename}', str(e))
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
self.logger.error('Error reading APK', str(e))
|
|
216
|
+
|
|
217
|
+
return payloads
|
|
218
|
+
|
|
219
|
+
def extract_strings_from_so(self, data, min_length=4):
|
|
220
|
+
"""Extract printable strings from binary data"""
|
|
221
|
+
strings = []
|
|
222
|
+
current_string = []
|
|
223
|
+
|
|
224
|
+
for byte in data:
|
|
225
|
+
if 32 <= byte < 127: # Printable ASCII
|
|
226
|
+
current_string.append(chr(byte))
|
|
227
|
+
else:
|
|
228
|
+
if len(current_string) >= min_length:
|
|
229
|
+
strings.append(''.join(current_string))
|
|
230
|
+
current_string = []
|
|
231
|
+
|
|
232
|
+
# Don't forget last string
|
|
233
|
+
if len(current_string) >= min_length:
|
|
234
|
+
strings.append(''.join(current_string))
|
|
235
|
+
|
|
236
|
+
return strings
|
|
237
|
+
|
|
238
|
+
def analyze_dpt_shell_library(self, zf):
|
|
239
|
+
"""Specifically analyze libdpt.so for DPT-shell protection artifacts"""
|
|
240
|
+
dpt_findings = []
|
|
241
|
+
|
|
242
|
+
# Find libdpt.so in any architecture or assets directory
|
|
243
|
+
libdpt_files = [f for f in zf.filelist if 'libdpt' in f.filename.lower() and f.filename.endswith('.so')]
|
|
244
|
+
|
|
245
|
+
if not libdpt_files:
|
|
246
|
+
return dpt_findings
|
|
247
|
+
|
|
248
|
+
for libdpt in libdpt_files:
|
|
249
|
+
try:
|
|
250
|
+
data = zf.read(libdpt.filename)
|
|
251
|
+
|
|
252
|
+
# Extract all strings from the library
|
|
253
|
+
all_strings = self.extract_strings_from_so(data, min_length=4)
|
|
254
|
+
|
|
255
|
+
# Enhanced suspicious pattern detection
|
|
256
|
+
found_artifacts = {
|
|
257
|
+
'zip_files': [],
|
|
258
|
+
'dex_files': [],
|
|
259
|
+
'jar_files': [],
|
|
260
|
+
'dat_files': [],
|
|
261
|
+
'code_cache_refs': [],
|
|
262
|
+
'file_paths': [],
|
|
263
|
+
'encryption_refs': [],
|
|
264
|
+
'shell_refs': [],
|
|
265
|
+
'class_loader_refs': []
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for string in all_strings:
|
|
269
|
+
string_lower = string.lower()
|
|
270
|
+
|
|
271
|
+
# CRITICAL: Check for .zip files (encrypted DEX container)
|
|
272
|
+
if '.zip' in string_lower and len(string) < 100:
|
|
273
|
+
if len(found_artifacts['zip_files']) < 10:
|
|
274
|
+
found_artifacts['zip_files'].append(string)
|
|
275
|
+
|
|
276
|
+
# CRITICAL: Check for .dex files
|
|
277
|
+
if '.dex' in string_lower and len(string) < 100:
|
|
278
|
+
if len(found_artifacts['dex_files']) < 10:
|
|
279
|
+
found_artifacts['dex_files'].append(string)
|
|
280
|
+
|
|
281
|
+
# Check for .jar files
|
|
282
|
+
if '.jar' in string_lower and len(string) < 100:
|
|
283
|
+
if len(found_artifacts['jar_files']) < 10:
|
|
284
|
+
found_artifacts['jar_files'].append(string)
|
|
285
|
+
|
|
286
|
+
# Check for .dat files
|
|
287
|
+
if '.dat' in string_lower and len(string) < 100:
|
|
288
|
+
if len(found_artifacts['dat_files']) < 10:
|
|
289
|
+
found_artifacts['dat_files'].append(string)
|
|
290
|
+
|
|
291
|
+
# CRITICAL: Check for code_cache path (where encrypted zip is stored)
|
|
292
|
+
if 'code_cache' in string_lower:
|
|
293
|
+
if len(found_artifacts['code_cache_refs']) < 10:
|
|
294
|
+
found_artifacts['code_cache_refs'].append(string)
|
|
295
|
+
|
|
296
|
+
# Check for file path patterns
|
|
297
|
+
if any(path in string_lower for path in ['/data/data/', '/files/', '/cache/', '/app_dex/']):
|
|
298
|
+
if len(found_artifacts['file_paths']) < 10:
|
|
299
|
+
found_artifacts['file_paths'].append(string)
|
|
300
|
+
|
|
301
|
+
# Check for encryption-related strings
|
|
302
|
+
if any(keyword in string_lower for keyword in ['encrypt', 'decrypt', 'cipher', 'aes', 'des', 'crypto']):
|
|
303
|
+
if len(found_artifacts['encryption_refs']) < 10:
|
|
304
|
+
found_artifacts['encryption_refs'].append(string)
|
|
305
|
+
|
|
306
|
+
# Check for shell/unpack/load related strings
|
|
307
|
+
if any(keyword in string_lower for keyword in ['shell', 'unpack', 'load', 'jni_', 'proxy', 'wrapper']):
|
|
308
|
+
if len(found_artifacts['shell_refs']) < 10:
|
|
309
|
+
found_artifacts['shell_refs'].append(string)
|
|
310
|
+
|
|
311
|
+
# Check for ClassLoader references
|
|
312
|
+
if any(keyword in string_lower for keyword in ['classloader', 'dexclassloader', 'basedexclassloader', 'pathclassloader']):
|
|
313
|
+
if len(found_artifacts['class_loader_refs']) < 10:
|
|
314
|
+
found_artifacts['class_loader_refs'].append(string)
|
|
315
|
+
|
|
316
|
+
# Remove empty categories
|
|
317
|
+
found_artifacts = {k: v for k, v in found_artifacts.items() if v}
|
|
318
|
+
|
|
319
|
+
if found_artifacts:
|
|
320
|
+
dpt_findings.append({
|
|
321
|
+
'library': libdpt.filename,
|
|
322
|
+
'finding': 'šØ DPT-Shell Protection Binary [CRITICAL]',
|
|
323
|
+
'size': libdpt.file_size / 1024,
|
|
324
|
+
'category': 'DPT-Shell Binary Code',
|
|
325
|
+
'importance': 'CRITICAL',
|
|
326
|
+
'artifacts': found_artifacts,
|
|
327
|
+
'description': 'This is the DPT-Shell protection binary that handles DEX decryption and loading'
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
self.logger.info(f'šØ DPT-shell CRITICAL binary detected: {libdpt.filename} with {len(found_artifacts)} artifact types')
|
|
331
|
+
|
|
332
|
+
except Exception as e:
|
|
333
|
+
self.logger.error(f'Error analyzing {libdpt.filename}', str(e))
|
|
334
|
+
|
|
335
|
+
return dpt_findings
|
|
336
|
+
|
|
337
|
+
def analyze_native_libraries(self):
|
|
338
|
+
"""Check native libraries for embedded DEX and zip references"""
|
|
339
|
+
lib_findings = []
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
with zipfile.ZipFile(self.apk_path, 'r') as zf:
|
|
343
|
+
# First, check specifically for DPT-shell
|
|
344
|
+
dpt_findings = self.analyze_dpt_shell_library(zf)
|
|
345
|
+
if dpt_findings:
|
|
346
|
+
lib_findings.extend(dpt_findings)
|
|
347
|
+
|
|
348
|
+
lib_files = [f for f in zf.filelist if f.filename.startswith('lib/') and f.filename.endswith('.so')]
|
|
349
|
+
|
|
350
|
+
for lib_file in lib_files:
|
|
351
|
+
try:
|
|
352
|
+
data = zf.read(lib_file.filename)
|
|
353
|
+
|
|
354
|
+
# Check for embedded DEX
|
|
355
|
+
dex_offset = data.find(b'dex\n')
|
|
356
|
+
if dex_offset != -1:
|
|
357
|
+
lib_findings.append({
|
|
358
|
+
'library': lib_file.filename,
|
|
359
|
+
'finding': 'Embedded DEX detected',
|
|
360
|
+
'offset': dex_offset,
|
|
361
|
+
'size': lib_file.file_size / 1024
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
# Check for ZIP file references (DPT-shell stores encrypted zip)
|
|
365
|
+
zip_patterns = [
|
|
366
|
+
rb'\.zip',
|
|
367
|
+
rb'\.jar',
|
|
368
|
+
rb'\.dat',
|
|
369
|
+
rb'code_cache',
|
|
370
|
+
rb'/files/',
|
|
371
|
+
rb'/app_dex/',
|
|
372
|
+
rb'encrypted',
|
|
373
|
+
rb'shell\.zip',
|
|
374
|
+
rb'dex\.zip',
|
|
375
|
+
rb'classes\.zip'
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
zip_refs = []
|
|
379
|
+
for pattern in zip_patterns:
|
|
380
|
+
if pattern in data:
|
|
381
|
+
# Try to extract readable string around the reference
|
|
382
|
+
offset = data.find(pattern)
|
|
383
|
+
# Get 50 bytes before and after for context
|
|
384
|
+
start = max(0, offset - 50)
|
|
385
|
+
end = min(len(data), offset + 50)
|
|
386
|
+
context = data[start:end]
|
|
387
|
+
# Extract printable string
|
|
388
|
+
try:
|
|
389
|
+
readable = ''.join(chr(b) if 32 <= b < 127 else '.' for b in context)
|
|
390
|
+
zip_refs.append({
|
|
391
|
+
'pattern': pattern.decode('utf-8', errors='ignore'),
|
|
392
|
+
'offset': offset,
|
|
393
|
+
'context': readable.strip('.')
|
|
394
|
+
})
|
|
395
|
+
except:
|
|
396
|
+
zip_refs.append({
|
|
397
|
+
'pattern': pattern.decode('utf-8', errors='ignore'),
|
|
398
|
+
'offset': offset
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
if zip_refs:
|
|
402
|
+
lib_findings.append({
|
|
403
|
+
'library': lib_file.filename,
|
|
404
|
+
'finding': 'ZIP/encrypted file references found',
|
|
405
|
+
'size': lib_file.file_size / 1024,
|
|
406
|
+
'references': zip_refs[:5] # Limit to 5 references
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
# Check for protection library patterns
|
|
410
|
+
protection_libs = {
|
|
411
|
+
'libdpt': 'DPT-Shell',
|
|
412
|
+
'libjiagu': 'Qihoo 360',
|
|
413
|
+
'libbangcle': 'Bangcle',
|
|
414
|
+
'libprotect': 'DexProtector',
|
|
415
|
+
'libsecexe': 'SecNeo',
|
|
416
|
+
'libshell': 'Generic Shell'
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
lib_name = lib_file.filename.lower()
|
|
420
|
+
for pattern, protector in protection_libs.items():
|
|
421
|
+
if pattern in lib_name:
|
|
422
|
+
lib_findings.append({
|
|
423
|
+
'library': lib_file.filename,
|
|
424
|
+
'finding': f'Protection library: {protector}',
|
|
425
|
+
'size': lib_file.file_size / 1024
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
except Exception as e:
|
|
429
|
+
self.logger.error(f'Error analyzing {lib_file.filename}', str(e))
|
|
430
|
+
|
|
431
|
+
except Exception as e:
|
|
432
|
+
self.logger.error('Error analyzing native libraries', str(e))
|
|
433
|
+
|
|
434
|
+
return lib_findings
|
|
435
|
+
|
|
436
|
+
def predict_unpack_locations(self, package_name='<package>'):
|
|
437
|
+
"""Predict where DEX will be unpacked at runtime"""
|
|
438
|
+
locations = [
|
|
439
|
+
{
|
|
440
|
+
'path': f'/data/data/{package_name}/code_cache/',
|
|
441
|
+
'description': 'Code cache directory (DPT-shell stores encrypted zip here)',
|
|
442
|
+
'priority': 'HIGH'
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
'path': f'/data/data/{package_name}/app_dex/',
|
|
446
|
+
'description': 'Common DEX cache directory',
|
|
447
|
+
'priority': 'HIGH'
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
'path': f'/data/data/{package_name}/files/',
|
|
451
|
+
'description': 'App files directory',
|
|
452
|
+
'priority': 'HIGH'
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
'path': f'/data/data/{package_name}/cache/',
|
|
456
|
+
'description': 'App cache directory',
|
|
457
|
+
'priority': 'MEDIUM'
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
'path': f'/data/data/{package_name}/lib/',
|
|
461
|
+
'description': 'Native libraries directory',
|
|
462
|
+
'priority': 'MEDIUM'
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
'path': f'/data/data/{package_name}/app_xxx/',
|
|
466
|
+
'description': 'Custom app directory (xxx = variant name)',
|
|
467
|
+
'priority': 'HIGH'
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
'path': f'/data/data/{package_name}/databases/',
|
|
471
|
+
'description': 'Sometimes used for hidden storage',
|
|
472
|
+
'priority': 'LOW'
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
'path': f'/sdcard/Android/data/{package_name}/',
|
|
476
|
+
'description': 'External storage (less common)',
|
|
477
|
+
'priority': 'LOW'
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
'path': '/data/local/tmp/',
|
|
481
|
+
'description': 'Temporary directory (requires root)',
|
|
482
|
+
'priority': 'LOW'
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
return locations
|
|
487
|
+
|
|
488
|
+
def analyze(self, package_name=None):
|
|
489
|
+
"""Perform complete payload hunting analysis"""
|
|
490
|
+
|
|
491
|
+
self.logger.info(f'Analyzing APK: {self.apk_path.name}')
|
|
492
|
+
|
|
493
|
+
# 1. Check main DEX files
|
|
494
|
+
try:
|
|
495
|
+
with zipfile.ZipFile(self.apk_path, 'r') as zf:
|
|
496
|
+
dex_files = [f for f in zf.filelist if f.filename.startswith('classes') and f.filename.endswith('.dex')]
|
|
497
|
+
|
|
498
|
+
for dex_file in dex_files:
|
|
499
|
+
data = zf.read(dex_file.filename)
|
|
500
|
+
is_stub, indicators = self.analyze_stub_dex(dex_file.filename, data)
|
|
501
|
+
|
|
502
|
+
if is_stub:
|
|
503
|
+
self.results['stub_dex_detected'] = True
|
|
504
|
+
self.results['protection_indicators'].append({
|
|
505
|
+
'file': dex_file.filename,
|
|
506
|
+
'type': 'Stub DEX',
|
|
507
|
+
'indicators': indicators
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
except Exception as e:
|
|
511
|
+
self.logger.error('Error analyzing DEX files', str(e))
|
|
512
|
+
|
|
513
|
+
# 2. Find encrypted payloads
|
|
514
|
+
self.results['encrypted_payloads'] = self.find_encrypted_payloads()
|
|
515
|
+
|
|
516
|
+
# 3. Analyze native libraries
|
|
517
|
+
lib_findings = self.analyze_native_libraries()
|
|
518
|
+
if lib_findings:
|
|
519
|
+
self.results['suspicious_files'].extend(lib_findings)
|
|
520
|
+
|
|
521
|
+
# 4. Predict unpack locations
|
|
522
|
+
self.results['likely_unpack_locations'] = self.predict_unpack_locations(package_name or '<package>')
|
|
523
|
+
|
|
524
|
+
# 5. Generate recommendations
|
|
525
|
+
self.generate_recommendations()
|
|
526
|
+
|
|
527
|
+
return self.results
|
|
528
|
+
|
|
529
|
+
def generate_recommendations(self):
|
|
530
|
+
"""Generate recommendations based on findings"""
|
|
531
|
+
recs = []
|
|
532
|
+
|
|
533
|
+
if self.results['stub_dex_detected']:
|
|
534
|
+
recs.append({
|
|
535
|
+
'priority': 'HIGH',
|
|
536
|
+
'action': 'Dynamic Analysis Required',
|
|
537
|
+
'details': 'Stub DEX detected - real DEX is encrypted/hidden and will be unpacked at runtime'
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
recs.append({
|
|
541
|
+
'priority': 'HIGH',
|
|
542
|
+
'action': 'Monitor Runtime Unpacking',
|
|
543
|
+
'details': 'Use Frida hooks to intercept DexFile.loadDex, DexClassLoader, and file writes'
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
if self.results['encrypted_payloads']:
|
|
547
|
+
high_entropy = [p for p in self.results['encrypted_payloads'] if p.get('entropy', 0) > 7.5]
|
|
548
|
+
if high_entropy:
|
|
549
|
+
recs.append({
|
|
550
|
+
'priority': 'HIGH',
|
|
551
|
+
'action': 'Extract Encrypted Payloads',
|
|
552
|
+
'details': f'Found {len(high_entropy)} high-entropy files that may contain encrypted DEX'
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
if self.results['suspicious_files']:
|
|
556
|
+
# Check if DPT-shell was detected
|
|
557
|
+
dpt_detected = any('DPT-Shell' in f.get('finding', '') for f in self.results['suspicious_files'])
|
|
558
|
+
|
|
559
|
+
if dpt_detected:
|
|
560
|
+
recs.append({
|
|
561
|
+
'priority': 'HIGH',
|
|
562
|
+
'action': 'DPT-Shell Protection Detected',
|
|
563
|
+
'details': 'libdpt.so found with suspicious strings - check artifacts for encrypted zip filename and paths'
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
recs.append({
|
|
567
|
+
'priority': 'MEDIUM',
|
|
568
|
+
'action': 'Analyze Native Libraries',
|
|
569
|
+
'details': 'Protection libraries detected - analyze for DEX loading mechanisms'
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
recs.append({
|
|
573
|
+
'priority': 'HIGH',
|
|
574
|
+
'action': 'Setup Runtime Monitoring',
|
|
575
|
+
'details': 'Monitor predicted unpack locations during app execution'
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
recs.append({
|
|
579
|
+
'priority': 'MEDIUM',
|
|
580
|
+
'action': 'Use Frida DEX Dumper',
|
|
581
|
+
'details': 'Run scripts/frida/frida_dex_dump.js to capture unpacked DEX from memory'
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
self.results['recommendations'] = recs
|
|
585
|
+
|
|
586
|
+
def main():
|
|
587
|
+
import argparse
|
|
588
|
+
|
|
589
|
+
parser = argparse.ArgumentParser(description='Hunt for encrypted DEX payloads in APK')
|
|
590
|
+
parser.add_argument('apk_path', help='Path to APK file')
|
|
591
|
+
parser.add_argument('output_json', help='Output JSON file')
|
|
592
|
+
parser.add_argument('package_name', nargs='?', default=None, help='Package name')
|
|
593
|
+
parser.add_argument('--brief', action='store_true', help='Brief console output')
|
|
594
|
+
|
|
595
|
+
args = parser.parse_args()
|
|
596
|
+
|
|
597
|
+
hunter = DEXPayloadHunter(args.apk_path)
|
|
598
|
+
results = hunter.analyze(args.package_name)
|
|
599
|
+
|
|
600
|
+
# Save results
|
|
601
|
+
with open(args.output_json, 'w') as f:
|
|
602
|
+
json.dump(results, f, indent=2)
|
|
603
|
+
|
|
604
|
+
# Print summary (brief or detailed)
|
|
605
|
+
if args.brief:
|
|
606
|
+
# Brief output - just key findings
|
|
607
|
+
if results['stub_dex_detected']:
|
|
608
|
+
print("ā ļø Stub DEX detected - real DEX is hidden", file=sys.stderr)
|
|
609
|
+
if results['encrypted_payloads']:
|
|
610
|
+
print(f"š Found {len(results['encrypted_payloads'])} encrypted payload(s)", file=sys.stderr)
|
|
611
|
+
if results['suspicious_files']:
|
|
612
|
+
so_with_refs = [f for f in results['suspicious_files'] if 'references' in f]
|
|
613
|
+
if so_with_refs:
|
|
614
|
+
print(f"š Found {len(so_with_refs)} .so file(s) with zip references", file=sys.stderr)
|
|
615
|
+
else:
|
|
616
|
+
# Detailed output
|
|
617
|
+
print(f"\n{'='*60}")
|
|
618
|
+
print(f"DEX PAYLOAD HUNTER RESULTS")
|
|
619
|
+
print(f"{'='*60}")
|
|
620
|
+
print(f"APK: {Path(args.apk_path).name}\n")
|
|
621
|
+
|
|
622
|
+
if results['stub_dex_detected']:
|
|
623
|
+
print("ā ļø STUB DEX DETECTED - Real DEX is hidden!")
|
|
624
|
+
for indicator in results['protection_indicators']:
|
|
625
|
+
print(f"\nš¦ {indicator['file']}:")
|
|
626
|
+
for ind in indicator['indicators']:
|
|
627
|
+
print(f" ⢠{ind}")
|
|
628
|
+
|
|
629
|
+
if results['encrypted_payloads']:
|
|
630
|
+
print(f"\nš ENCRYPTED PAYLOADS FOUND: {len(results['encrypted_payloads'])}")
|
|
631
|
+
for payload in results['encrypted_payloads'][:5]: # Show first 5
|
|
632
|
+
print(f"\n š {payload['location']}")
|
|
633
|
+
print(f" Type: {payload['type']}")
|
|
634
|
+
print(f" Size: {payload['size_kb']:.1f} KB")
|
|
635
|
+
print(f" Entropy: {payload['entropy']}")
|
|
636
|
+
for ind in payload['indicators']:
|
|
637
|
+
print(f" ⢠{ind}")
|
|
638
|
+
|
|
639
|
+
if results['suspicious_files']:
|
|
640
|
+
print(f"\nš SUSPICIOUS FILES: {len(results['suspicious_files'])}")
|
|
641
|
+
for sf in results['suspicious_files']:
|
|
642
|
+
# Highlight DPT-Shell binaries differently
|
|
643
|
+
if sf.get('category') == 'DPT-Shell Binary Code':
|
|
644
|
+
print(f"\n {sf['finding']}")
|
|
645
|
+
print(f" š Location: {sf['library']}")
|
|
646
|
+
print(f" š Size: {sf['size']:.2f} KB")
|
|
647
|
+
print(f" ā ļø Importance: {sf.get('importance', 'HIGH')}")
|
|
648
|
+
if sf.get('description'):
|
|
649
|
+
print(f" š {sf['description']}")
|
|
650
|
+
else:
|
|
651
|
+
print(f" ⢠{sf['library']}: {sf['finding']}")
|
|
652
|
+
|
|
653
|
+
# Show DPT-shell specific artifacts
|
|
654
|
+
if 'artifacts' in sf:
|
|
655
|
+
print(f" š Artifacts Extracted:")
|
|
656
|
+
artifacts = sf['artifacts']
|
|
657
|
+
|
|
658
|
+
# CRITICAL: Show .dex files first
|
|
659
|
+
if 'dex_files' in artifacts:
|
|
660
|
+
print(f" šÆ DEX files ({len(artifacts['dex_files'])} found):")
|
|
661
|
+
for item in artifacts['dex_files'][:5]:
|
|
662
|
+
print(f" - {item}")
|
|
663
|
+
|
|
664
|
+
# CRITICAL: Show .zip files
|
|
665
|
+
if 'zip_files' in artifacts:
|
|
666
|
+
print(f" š ZIP files ({len(artifacts['zip_files'])} found):")
|
|
667
|
+
for item in artifacts['zip_files'][:5]:
|
|
668
|
+
print(f" - {item}")
|
|
669
|
+
|
|
670
|
+
# Show .jar files
|
|
671
|
+
if 'jar_files' in artifacts:
|
|
672
|
+
print(f" š¦ JAR files ({len(artifacts['jar_files'])} found):")
|
|
673
|
+
for item in artifacts['jar_files'][:3]:
|
|
674
|
+
print(f" - {item}")
|
|
675
|
+
|
|
676
|
+
# Show .dat files
|
|
677
|
+
if 'dat_files' in artifacts:
|
|
678
|
+
print(f" š DAT files ({len(artifacts['dat_files'])} found):")
|
|
679
|
+
for item in artifacts['dat_files'][:3]:
|
|
680
|
+
print(f" - {item}")
|
|
681
|
+
|
|
682
|
+
# CRITICAL: Show code_cache references
|
|
683
|
+
if 'code_cache_refs' in artifacts:
|
|
684
|
+
print(f" š Code cache references:")
|
|
685
|
+
for item in artifacts['code_cache_refs'][:3]:
|
|
686
|
+
print(f" - {item}")
|
|
687
|
+
|
|
688
|
+
# Show file paths
|
|
689
|
+
if 'file_paths' in artifacts:
|
|
690
|
+
print(f" š File paths ({len(artifacts['file_paths'])} found):")
|
|
691
|
+
for item in artifacts['file_paths'][:3]:
|
|
692
|
+
print(f" - {item}")
|
|
693
|
+
|
|
694
|
+
# Show encryption references
|
|
695
|
+
if 'encryption_refs' in artifacts:
|
|
696
|
+
print(f" š Encryption references:")
|
|
697
|
+
for item in artifacts['encryption_refs'][:3]:
|
|
698
|
+
print(f" - {item}")
|
|
699
|
+
|
|
700
|
+
# Show shell references
|
|
701
|
+
if 'shell_refs' in artifacts:
|
|
702
|
+
print(f" š Shell references ({len(artifacts['shell_refs'])} found):")
|
|
703
|
+
for item in artifacts['shell_refs'][:3]:
|
|
704
|
+
print(f" - {item}")
|
|
705
|
+
|
|
706
|
+
# Show ClassLoader references
|
|
707
|
+
if 'class_loader_refs' in artifacts:
|
|
708
|
+
print(f" š§ ClassLoader references:")
|
|
709
|
+
for item in artifacts['class_loader_refs'][:3]:
|
|
710
|
+
print(f" - {item}")
|
|
711
|
+
|
|
712
|
+
# Show general zip references (for non-DPT files)
|
|
713
|
+
elif 'references' in sf:
|
|
714
|
+
for ref in sf['references'][:3]: # Show first 3
|
|
715
|
+
print(f" - {ref.get('pattern', 'N/A')} at offset {ref.get('offset', 0)}")
|
|
716
|
+
if 'context' in ref:
|
|
717
|
+
print(f" Context: {ref['context'][:50]}")
|
|
718
|
+
|
|
719
|
+
print(f"\nš PREDICTED UNPACK LOCATIONS:")
|
|
720
|
+
for loc in results['likely_unpack_locations']:
|
|
721
|
+
if loc['priority'] == 'HIGH':
|
|
722
|
+
print(f" š“ {loc['path']}")
|
|
723
|
+
print(f" {loc['description']}")
|
|
724
|
+
|
|
725
|
+
print(f"\nš” RECOMMENDATIONS:")
|
|
726
|
+
for rec in results['recommendations']:
|
|
727
|
+
priority_icon = 'š“' if rec['priority'] == 'HIGH' else 'š”'
|
|
728
|
+
print(f" {priority_icon} {rec['action']}")
|
|
729
|
+
print(f" {rec['details']}")
|
|
730
|
+
|
|
731
|
+
print(f"\n{'='*60}\n")
|
|
732
|
+
|
|
733
|
+
if __name__ == '__main__':
|
|
734
|
+
main()
|