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,650 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Obfuscation Detection Module
|
|
4
|
+
Detects various obfuscation and protection mechanisms in Android APKs
|
|
5
|
+
including ProGuard, R8, DPT-Shell, DexProtector, Bangcle, etc.
|
|
6
|
+
Integrates with APKiD for enhanced detection.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from collections import Counter
|
|
16
|
+
|
|
17
|
+
# Import error logger
|
|
18
|
+
try:
|
|
19
|
+
from error_logger import setup_logger
|
|
20
|
+
except ImportError:
|
|
21
|
+
# Fallback if error_logger not available
|
|
22
|
+
class DummyLogger:
|
|
23
|
+
def info(self, msg): pass
|
|
24
|
+
def warning(self, msg): print(f"[WARNING] {msg}", file=sys.stderr)
|
|
25
|
+
def error(self, msg, detail=None): print(f"[ERROR] {msg}", file=sys.stderr)
|
|
26
|
+
def exception(self, msg, exc_info=True): print(f"[ERROR] {msg}", file=sys.stderr)
|
|
27
|
+
|
|
28
|
+
def setup_logger(name, log_file=None, verbose=False):
|
|
29
|
+
return DummyLogger()
|
|
30
|
+
|
|
31
|
+
class ObfuscationDetector:
|
|
32
|
+
def __init__(self, decompiled_dir, jadx_dir, apk_path=None):
|
|
33
|
+
self.decompiled_dir = Path(decompiled_dir) if decompiled_dir else None
|
|
34
|
+
self.jadx_dir = Path(jadx_dir) if jadx_dir else None
|
|
35
|
+
self.apk_path = apk_path
|
|
36
|
+
self.logger = setup_logger('detect_obfuscation')
|
|
37
|
+
|
|
38
|
+
self.results = {
|
|
39
|
+
'type': 'none',
|
|
40
|
+
'confidence': 0,
|
|
41
|
+
'indicators': [],
|
|
42
|
+
'protection_details': {},
|
|
43
|
+
'apkid_results': {},
|
|
44
|
+
'recommendations': []
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def check_dex_files(self):
|
|
48
|
+
"""Check DEX files for protection shells"""
|
|
49
|
+
indicators = []
|
|
50
|
+
|
|
51
|
+
if not self.decompiled_dir:
|
|
52
|
+
return indicators
|
|
53
|
+
|
|
54
|
+
# Look for shell DEX files
|
|
55
|
+
dex_files = list(self.decompiled_dir.rglob('*.dex'))
|
|
56
|
+
|
|
57
|
+
# Check for abnormal DEX structure
|
|
58
|
+
for dex_file in dex_files:
|
|
59
|
+
try:
|
|
60
|
+
with open(dex_file, 'rb') as f:
|
|
61
|
+
header = f.read(112) # DEX header is 112 bytes
|
|
62
|
+
|
|
63
|
+
# Check magic
|
|
64
|
+
if header[:4] != b'dex\n':
|
|
65
|
+
indicators.append(f'Invalid DEX magic in {dex_file.name}')
|
|
66
|
+
|
|
67
|
+
# Check for encryption markers
|
|
68
|
+
if b'DPT' in header or b'dpt' in header:
|
|
69
|
+
indicators.append('DPT-Shell signature detected')
|
|
70
|
+
self.results['type'] = 'dpt-shell'
|
|
71
|
+
self.results['confidence'] = 90
|
|
72
|
+
|
|
73
|
+
if b'BANGCLE' in header or b'bangcle' in header:
|
|
74
|
+
indicators.append('Bangcle signature detected')
|
|
75
|
+
self.results['type'] = 'bangcle'
|
|
76
|
+
self.results['confidence'] = 90
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
indicators.append(f'Error reading {dex_file.name}: {e}')
|
|
80
|
+
|
|
81
|
+
return indicators
|
|
82
|
+
|
|
83
|
+
def check_native_libs(self):
|
|
84
|
+
"""Check for native protection libraries"""
|
|
85
|
+
indicators = []
|
|
86
|
+
|
|
87
|
+
if not self.decompiled_dir:
|
|
88
|
+
return indicators
|
|
89
|
+
|
|
90
|
+
lib_dir = self.decompiled_dir / 'lib'
|
|
91
|
+
if lib_dir.exists():
|
|
92
|
+
so_files = list(lib_dir.rglob('*.so'))
|
|
93
|
+
|
|
94
|
+
# Known protection library names
|
|
95
|
+
protection_libs = {
|
|
96
|
+
'libdpt': 'DPT-Shell',
|
|
97
|
+
'libexec': 'DexProtector',
|
|
98
|
+
'libprotect': 'DexProtector',
|
|
99
|
+
'libjiagu': 'Qihoo 360',
|
|
100
|
+
'libbangcle': 'Bangcle',
|
|
101
|
+
'libsecexe': 'SecNeo',
|
|
102
|
+
'libshell': 'Generic Shell',
|
|
103
|
+
'libmobisec': 'MobiSec',
|
|
104
|
+
'libnqshield': 'NetQin',
|
|
105
|
+
'libprotection': 'Generic Protection',
|
|
106
|
+
'libapp': 'App Encryption',
|
|
107
|
+
'libjiag': 'Qihoo 360',
|
|
108
|
+
'libddog': 'Ijiami',
|
|
109
|
+
'libAPKProtect': 'APK Protect',
|
|
110
|
+
'libexecmain': 'Execmain Protection',
|
|
111
|
+
'libtosprotection': 'Tencent Protection',
|
|
112
|
+
'libshella': 'Shell Variant A',
|
|
113
|
+
'libfake': 'Fake Dex Protection',
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for so_file in so_files:
|
|
117
|
+
name = so_file.name.lower()
|
|
118
|
+
for lib_pattern, protector in protection_libs.items():
|
|
119
|
+
if lib_pattern in name:
|
|
120
|
+
indicators.append(f'Protection library detected: {so_file.name} ({protector})')
|
|
121
|
+
# Use most specific match
|
|
122
|
+
if 'dpt' in lib_pattern:
|
|
123
|
+
self.results['type'] = 'dpt-shell'
|
|
124
|
+
elif self.results['type'] == 'none':
|
|
125
|
+
self.results['type'] = protector.lower().replace(' ', '-')
|
|
126
|
+
self.results['confidence'] = max(self.results['confidence'], 90)
|
|
127
|
+
|
|
128
|
+
return indicators
|
|
129
|
+
|
|
130
|
+
def check_manifest_indicators(self):
|
|
131
|
+
"""Check AndroidManifest for protection indicators"""
|
|
132
|
+
indicators = []
|
|
133
|
+
|
|
134
|
+
if not self.decompiled_dir:
|
|
135
|
+
return indicators
|
|
136
|
+
|
|
137
|
+
manifest = self.decompiled_dir / 'AndroidManifest.xml'
|
|
138
|
+
if manifest.exists():
|
|
139
|
+
try:
|
|
140
|
+
content = manifest.read_text(errors='ignore')
|
|
141
|
+
|
|
142
|
+
# Check for known protection application classes
|
|
143
|
+
protection_apps = {
|
|
144
|
+
'com.jx.shell': 'DPT-Shell (jx.shell)',
|
|
145
|
+
'com.jx': 'DPT-Shell variant',
|
|
146
|
+
'JniBridge': 'JNI Bridge Protection (DPT)',
|
|
147
|
+
'ProxyApplication': 'Proxy Application Shell (DPT)',
|
|
148
|
+
'ProxyComponentFactory': 'Proxy Component Shell (DPT)',
|
|
149
|
+
'com.secneo.apkwrapper': 'SecNeo',
|
|
150
|
+
'com.bangcle.app': 'Bangcle',
|
|
151
|
+
'com.qihoo.util': 'Qihoo 360',
|
|
152
|
+
'com.shell.NativeApplication': 'Generic Shell',
|
|
153
|
+
's.h.e.l.l': 'Obfuscated Shell',
|
|
154
|
+
'com.stub.StubApp': 'Stub Application Shell',
|
|
155
|
+
'com.wrapper.': 'Wrapper Shell',
|
|
156
|
+
'StubApplication': 'Stub Shell',
|
|
157
|
+
'com.metaapp': 'Meta Application Shell',
|
|
158
|
+
'com.ali.mobisecenhance': 'Alibaba MobiSec',
|
|
159
|
+
'com.tencent.StubShell': 'Tencent Shell',
|
|
160
|
+
'com.baidu.protect': 'Baidu Protect',
|
|
161
|
+
'com.qihoo360.repacker': 'Qihoo 360 Packer',
|
|
162
|
+
'net.lingala.zip4j': 'Zip4j Encryption',
|
|
163
|
+
'DPTApplication': 'DPT Shell',
|
|
164
|
+
'com.dpt.': 'DPT Protection',
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for pattern, protector in protection_apps.items():
|
|
168
|
+
if pattern in content:
|
|
169
|
+
indicators.append(f'Protection application class: {pattern} ({protector})')
|
|
170
|
+
# Specific handling for jx.shell/DPT variants
|
|
171
|
+
if any(x in pattern.lower() for x in ['jx', 'dpt', 'jnibridge', 'proxy']):
|
|
172
|
+
self.results['type'] = 'dpt-shell'
|
|
173
|
+
self.results['confidence'] = 98
|
|
174
|
+
self.results['protection_details']['shell_type'] = protector
|
|
175
|
+
elif self.results['type'] == 'none' or self.results['confidence'] < 80:
|
|
176
|
+
self.results['type'] = protector.lower().replace(' ', '-').replace('(', '').replace(')', '')
|
|
177
|
+
self.results['confidence'] = 85
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
indicators.append(f'Error reading manifest: {e}')
|
|
181
|
+
|
|
182
|
+
return indicators
|
|
183
|
+
|
|
184
|
+
def check_proguard_r8(self):
|
|
185
|
+
"""Detect ProGuard/R8 obfuscation"""
|
|
186
|
+
indicators = []
|
|
187
|
+
|
|
188
|
+
if not self.jadx_dir or not self.jadx_dir.exists():
|
|
189
|
+
return indicators
|
|
190
|
+
|
|
191
|
+
# Sample Java files to check
|
|
192
|
+
java_files = list(self.jadx_dir.rglob('*.java'))[:50] # Check first 50
|
|
193
|
+
|
|
194
|
+
if not java_files:
|
|
195
|
+
return indicators
|
|
196
|
+
|
|
197
|
+
# Indicators of ProGuard/R8
|
|
198
|
+
obfuscated_patterns = {
|
|
199
|
+
'short_class_names': 0, # Classes with single letter names
|
|
200
|
+
'short_method_names': 0, # Methods like a(), b(), c()
|
|
201
|
+
'short_field_names': 0, # Fields like a, b, c
|
|
202
|
+
'package_flattening': 0, # All classes in root package
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for java_file in java_files:
|
|
206
|
+
try:
|
|
207
|
+
content = java_file.read_text(errors='ignore')
|
|
208
|
+
|
|
209
|
+
# Check for short class names (a.java, b.java, etc.)
|
|
210
|
+
if len(java_file.stem) <= 2 and java_file.stem.isalpha():
|
|
211
|
+
obfuscated_patterns['short_class_names'] += 1
|
|
212
|
+
|
|
213
|
+
# Check for short method names
|
|
214
|
+
short_methods = re.findall(r'\s+[a-z]\s*\([^)]*\)\s*{', content)
|
|
215
|
+
obfuscated_patterns['short_method_names'] += len(short_methods)
|
|
216
|
+
|
|
217
|
+
# Check for short field names
|
|
218
|
+
short_fields = re.findall(r'^\s+(private|public|protected)?\s+\w+\s+[a-z];', content, re.MULTILINE)
|
|
219
|
+
obfuscated_patterns['short_field_names'] += len(short_fields)
|
|
220
|
+
|
|
221
|
+
# Check package structure
|
|
222
|
+
package_match = re.search(r'package\s+([\w.]+);', content)
|
|
223
|
+
if package_match:
|
|
224
|
+
package = package_match.group(1)
|
|
225
|
+
# If package has no dots, it's likely flattened
|
|
226
|
+
if '.' not in package or len(package) <= 3:
|
|
227
|
+
obfuscated_patterns['package_flattening'] += 1
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
# Determine if ProGuard/R8 is used
|
|
233
|
+
total_indicators = sum(obfuscated_patterns.values())
|
|
234
|
+
|
|
235
|
+
if obfuscated_patterns['short_class_names'] > 10 or total_indicators > 30:
|
|
236
|
+
indicators.append('ProGuard/R8 obfuscation detected')
|
|
237
|
+
indicators.append(f" - Short class names: {obfuscated_patterns['short_class_names']}")
|
|
238
|
+
indicators.append(f" - Short method names: {obfuscated_patterns['short_method_names']}")
|
|
239
|
+
indicators.append(f" - Short field names: {obfuscated_patterns['short_field_names']}")
|
|
240
|
+
|
|
241
|
+
if self.results['type'] == 'none':
|
|
242
|
+
self.results['type'] = 'proguard'
|
|
243
|
+
self.results['confidence'] = min(70 + (total_indicators // 10), 95)
|
|
244
|
+
|
|
245
|
+
return indicators
|
|
246
|
+
|
|
247
|
+
def check_dpt_jx_shell_sources(self):
|
|
248
|
+
"""Check Java sources for DPT/jx.shell specific patterns"""
|
|
249
|
+
indicators = []
|
|
250
|
+
|
|
251
|
+
if not self.jadx_dir or not self.jadx_dir.exists():
|
|
252
|
+
return indicators
|
|
253
|
+
|
|
254
|
+
# Look for jx.shell specific files
|
|
255
|
+
jx_shell_patterns = [
|
|
256
|
+
'**/jx/shell/*.java',
|
|
257
|
+
'**/com/jx/**/*.java',
|
|
258
|
+
'**/JniBridge.java',
|
|
259
|
+
'**/ProxyApplication.java',
|
|
260
|
+
'**/ProxyComponentFactory.java',
|
|
261
|
+
'**/DPTApplication.java',
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
found_files = []
|
|
265
|
+
for pattern in jx_shell_patterns:
|
|
266
|
+
found_files.extend(self.jadx_dir.glob(pattern))
|
|
267
|
+
|
|
268
|
+
if found_files:
|
|
269
|
+
indicators.append(f'DPT-Shell/jx.shell source files detected: {len(found_files)} files')
|
|
270
|
+
for f in found_files[:10]: # Show first 10
|
|
271
|
+
indicators.append(f' - {f.relative_to(self.jadx_dir)}')
|
|
272
|
+
|
|
273
|
+
self.results['type'] = 'dpt-shell'
|
|
274
|
+
self.results['confidence'] = 99
|
|
275
|
+
self.results['protection_details']['detected_files'] = [str(f.relative_to(self.jadx_dir)) for f in found_files]
|
|
276
|
+
return indicators
|
|
277
|
+
|
|
278
|
+
# Check for DPT-shell code patterns in any Java file
|
|
279
|
+
java_files = list(self.jadx_dir.rglob('*.java'))[:200] # Increased to check more files
|
|
280
|
+
|
|
281
|
+
dpt_patterns = {
|
|
282
|
+
r'libdpt\.so': 'DPT native library',
|
|
283
|
+
r'JniBridge\b': 'JNI Bridge class',
|
|
284
|
+
r'ProxyApplication': 'Proxy Application',
|
|
285
|
+
r'ProxyComponentFactory': 'Proxy Component Factory',
|
|
286
|
+
r'com\.jx\.shell': 'jx.shell package',
|
|
287
|
+
r'attachBaseContext.*native': 'Native context attach',
|
|
288
|
+
r'System\.load.*libdpt': 'DPT library loading',
|
|
289
|
+
r'\.dex.*decrypt': 'DEX decryption',
|
|
290
|
+
r'\.dex.*unpack': 'DEX unpacking',
|
|
291
|
+
r'loadDex.*native': 'Native DEX loading',
|
|
292
|
+
r'getDexFromNative': 'Get DEX from native',
|
|
293
|
+
r'attachBaseContext.*classloader': 'Classloader manipulation',
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
matches = 0
|
|
297
|
+
matched_files = []
|
|
298
|
+
pattern_matches = Counter()
|
|
299
|
+
|
|
300
|
+
for java_file in java_files:
|
|
301
|
+
try:
|
|
302
|
+
content = java_file.read_text(errors='ignore')
|
|
303
|
+
|
|
304
|
+
for pattern, desc in dpt_patterns.items():
|
|
305
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
306
|
+
matches += 1
|
|
307
|
+
pattern_matches[desc] += 1
|
|
308
|
+
if java_file not in matched_files:
|
|
309
|
+
matched_files.append(java_file)
|
|
310
|
+
|
|
311
|
+
except Exception:
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
if matches > 2: # Lowered threshold for better detection
|
|
315
|
+
indicators.append(f'DPT-Shell code patterns detected ({matches} matches in {len(matched_files)} files)')
|
|
316
|
+
for desc, count in pattern_matches.most_common(5):
|
|
317
|
+
indicators.append(f' - {desc}: {count} occurrence(s)')
|
|
318
|
+
self.results['type'] = 'dpt-shell'
|
|
319
|
+
self.results['confidence'] = max(self.results['confidence'], min(70 + matches * 5, 95))
|
|
320
|
+
self.results['protection_details']['pattern_matches'] = dict(pattern_matches)
|
|
321
|
+
|
|
322
|
+
return indicators
|
|
323
|
+
|
|
324
|
+
def run_apkid(self):
|
|
325
|
+
"""Run APKiD for advanced obfuscation detection"""
|
|
326
|
+
indicators = []
|
|
327
|
+
|
|
328
|
+
if not self.apk_path or not Path(self.apk_path).exists():
|
|
329
|
+
return indicators
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
# Get the directory where this script is located
|
|
333
|
+
script_dir = Path(__file__).parent.parent
|
|
334
|
+
apkid_dir = script_dir / 'apkid'
|
|
335
|
+
|
|
336
|
+
# Check if APKiD is available
|
|
337
|
+
if self.logger.info('APKiD not found, skipping enhanced detection'):
|
|
338
|
+
indicators.append('APKiD not found, skipping enhanced detection')
|
|
339
|
+
return indicators
|
|
340
|
+
|
|
341
|
+
# Run APKiD with JSON output
|
|
342
|
+
cmd = [
|
|
343
|
+
sys.executable, '-m', 'apkid.main',
|
|
344
|
+
'--json',
|
|
345
|
+
'--timeout', '60',
|
|
346
|
+
str(self.apk_path)
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
# Set PYTHONPATH to include apkid directory
|
|
350
|
+
env = os.environ.copy()
|
|
351
|
+
env['PYTHONPATH'] = str(script_dir) + ':' + env.get('PYTHONPATH', '')
|
|
352
|
+
|
|
353
|
+
result = subprocess.run(
|
|
354
|
+
cmd,
|
|
355
|
+
capture_output=True,
|
|
356
|
+
text=True,
|
|
357
|
+
timeout=70,
|
|
358
|
+
env=env,
|
|
359
|
+
cwd=str(script_dir)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if result.returncode == 0 and result.stdout:
|
|
363
|
+
apkid_data = json.loads(result.stdout)
|
|
364
|
+
self.results['apkid_results'] = apkid_data
|
|
365
|
+
|
|
366
|
+
# Parse APKiD results
|
|
367
|
+
apk_name = Path(self.apk_path).name
|
|
368
|
+
if apk_name in apkid_data:
|
|
369
|
+
apk_data = apkid_data[apk_name]
|
|
370
|
+
|
|
371
|
+
# Check for compilers
|
|
372
|
+
if 'compiler' in apk_data:
|
|
373
|
+
for compiler_info in apk_data['compiler']:
|
|
374
|
+
indicators.append(f'APKiD - Compiler: {compiler_info}')
|
|
375
|
+
|
|
376
|
+
# Check for obfuscators
|
|
377
|
+
if 'obfuscator' in apk_data:
|
|
378
|
+
for obf_info in apk_data['obfuscator']:
|
|
379
|
+
indicators.append(f'APKiD - Obfuscator: {obf_info}')
|
|
380
|
+
if 'dpt' in obf_info.lower() or 'jx' in obf_info.lower():
|
|
381
|
+
self.results['type'] = 'dpt-shell'
|
|
382
|
+
self.results['confidence'] = 95
|
|
383
|
+
|
|
384
|
+
# Check for packers
|
|
385
|
+
if 'packer' in apk_data:
|
|
386
|
+
for packer_info in apk_data['packer']:
|
|
387
|
+
indicators.append(f'APKiD - Packer: {packer_info}')
|
|
388
|
+
if self.results['type'] == 'none':
|
|
389
|
+
self.results['type'] = 'packed'
|
|
390
|
+
self.results['confidence'] = 85
|
|
391
|
+
|
|
392
|
+
# Check for protectors
|
|
393
|
+
if 'protector' in apk_data:
|
|
394
|
+
for prot_info in apk_data['protector']:
|
|
395
|
+
indicators.append(f'APKiD - Protector: {prot_info}')
|
|
396
|
+
if 'dpt' in prot_info.lower():
|
|
397
|
+
self.results['type'] = 'dpt-shell'
|
|
398
|
+
self.results['confidence'] = 95
|
|
399
|
+
|
|
400
|
+
# Check for anti-VM
|
|
401
|
+
if 'anti_vm' in apk_data:
|
|
402
|
+
for antivm_info in apk_data['anti_vm']:
|
|
403
|
+
indicators.append(f'APKiD - Anti-VM: {antivm_info}')
|
|
404
|
+
|
|
405
|
+
# Check for abnormal features
|
|
406
|
+
if 'abnormal' in apk_data:
|
|
407
|
+
for abnormal_info in apk_data['abnormal']:
|
|
408
|
+
indicators.append(f'APKiD - Abnormal: {abnormal_info}')
|
|
409
|
+
else:
|
|
410
|
+
if result.stderr:
|
|
411
|
+
self.logger.warning(f'APKiD warning: {result.stderr[:200]}')
|
|
412
|
+
|
|
413
|
+
except subprocess.TimeoutExpired:
|
|
414
|
+
self.logger.warning('APKiD scan timed out')
|
|
415
|
+
indicators.append('APKiD scan timed out')
|
|
416
|
+
except json.JSONDecodeError as e:
|
|
417
|
+
self.logger.error('APKiD output parse error', str(e))
|
|
418
|
+
except Exception as e:
|
|
419
|
+
self.logger.exception('APKiD scan error')
|
|
420
|
+
indicators.append('APKiD scan error (check logs)')
|
|
421
|
+
indicators.append(f'APKiD scan error: {e}')
|
|
422
|
+
|
|
423
|
+
return indicators
|
|
424
|
+
|
|
425
|
+
def check_additional_obfuscation(self):
|
|
426
|
+
"""Check for additional obfuscation techniques"""
|
|
427
|
+
indicators = []
|
|
428
|
+
|
|
429
|
+
if not self.jadx_dir or not self.jadx_dir.exists():
|
|
430
|
+
return indicators
|
|
431
|
+
|
|
432
|
+
java_files = list(self.jadx_dir.rglob('*.java'))[:100]
|
|
433
|
+
|
|
434
|
+
obf_patterns = {
|
|
435
|
+
'control_flow': {
|
|
436
|
+
'pattern': r'(goto|switch|if.*goto)',
|
|
437
|
+
'desc': 'Control flow obfuscation',
|
|
438
|
+
'count': 0
|
|
439
|
+
},
|
|
440
|
+
'reflection': {
|
|
441
|
+
'pattern': r'(Class\.forName|getDeclaredMethod|getDeclaredField|invoke)',
|
|
442
|
+
'desc': 'Reflection-based obfuscation',
|
|
443
|
+
'count': 0
|
|
444
|
+
},
|
|
445
|
+
'dynamic_loading': {
|
|
446
|
+
'pattern': r'(DexClassLoader|PathClassLoader|loadDex|loadClass)',
|
|
447
|
+
'desc': 'Dynamic class loading',
|
|
448
|
+
'count': 0
|
|
449
|
+
},
|
|
450
|
+
'native_methods': {
|
|
451
|
+
'pattern': r'native\s+\w+',
|
|
452
|
+
'desc': 'Native method usage',
|
|
453
|
+
'count': 0
|
|
454
|
+
},
|
|
455
|
+
'string_concat': {
|
|
456
|
+
'pattern': r'new\s+StringBuilder.*append',
|
|
457
|
+
'desc': 'String concatenation obfuscation',
|
|
458
|
+
'count': 0
|
|
459
|
+
},
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
for java_file in java_files:
|
|
463
|
+
try:
|
|
464
|
+
content = java_file.read_text(errors='ignore')
|
|
465
|
+
|
|
466
|
+
for key, info in obf_patterns.items():
|
|
467
|
+
matches = re.findall(info['pattern'], content, re.IGNORECASE)
|
|
468
|
+
info['count'] += len(matches)
|
|
469
|
+
|
|
470
|
+
except Exception:
|
|
471
|
+
continue
|
|
472
|
+
|
|
473
|
+
# Report findings
|
|
474
|
+
for key, info in obf_patterns.items():
|
|
475
|
+
if info['count'] > 20: # Threshold for significance
|
|
476
|
+
indicators.append(f"{info['desc']} detected ({info['count']} occurrences)")
|
|
477
|
+
self.results['protection_details'][key] = info['count']
|
|
478
|
+
|
|
479
|
+
return indicators
|
|
480
|
+
|
|
481
|
+
def check_string_encryption(self):
|
|
482
|
+
"""Detect string encryption"""
|
|
483
|
+
indicators = []
|
|
484
|
+
|
|
485
|
+
if not self.jadx_dir or not self.jadx_dir.exists():
|
|
486
|
+
return indicators
|
|
487
|
+
|
|
488
|
+
java_files = list(self.jadx_dir.rglob('*.java'))[:30]
|
|
489
|
+
|
|
490
|
+
encrypted_string_patterns = [
|
|
491
|
+
r'decrypt\s*\(',
|
|
492
|
+
r'deobfuscate\s*\(',
|
|
493
|
+
r'base64\s*\(',
|
|
494
|
+
r'cipher\.doFinal',
|
|
495
|
+
r'Cipher\.getInstance',
|
|
496
|
+
r'SecretKeySpec',
|
|
497
|
+
]
|
|
498
|
+
|
|
499
|
+
matches = 0
|
|
500
|
+
for java_file in java_files:
|
|
501
|
+
try:
|
|
502
|
+
content = java_file.read_text(errors='ignore')
|
|
503
|
+
|
|
504
|
+
for pattern in encrypted_string_patterns:
|
|
505
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
506
|
+
matches += 1
|
|
507
|
+
|
|
508
|
+
except Exception:
|
|
509
|
+
continue
|
|
510
|
+
|
|
511
|
+
if matches > 5:
|
|
512
|
+
indicators.append(f'String encryption detected ({matches} indicators)')
|
|
513
|
+
self.results['protection_details']['string_encryption'] = True
|
|
514
|
+
|
|
515
|
+
return indicators
|
|
516
|
+
|
|
517
|
+
def generate_recommendations(self):
|
|
518
|
+
"""Generate recommendations based on detected protection"""
|
|
519
|
+
|
|
520
|
+
if self.results['type'] in ['dpt-shell', 'dpt-shell-jx.shell', 'dexprotector', 'bangcle', 'qihoo-360', 'generic-shell', 'ijiami', 'secneo']:
|
|
521
|
+
self.results['recommendations'] = [
|
|
522
|
+
'DYNAMIC ANALYSIS REQUIRED - NATIVE PROTECTION DETECTED',
|
|
523
|
+
'1. Install APK on rooted Android device or emulator (Android 7-10 recommended)',
|
|
524
|
+
'2. Use Frida with frida-dexdump or similar tool',
|
|
525
|
+
'3. Run the application to trigger DEX unpacking',
|
|
526
|
+
'4. Dump decrypted DEX from memory:',
|
|
527
|
+
' - frida-dexdump -U -f <package_name>',
|
|
528
|
+
' - Or use frida -U -f <package_name> -l scripts/frida/frida_dex_dump.js',
|
|
529
|
+
'5. Analyze dumped DEX files with JADX/APKTool',
|
|
530
|
+
'6. Alternative: Use FART, DexHunter, or ZjDroid',
|
|
531
|
+
'7. Monitor native library loading with Frida hooks',
|
|
532
|
+
'8. Consider using Android Hooking/Instrumentation for API monitoring',
|
|
533
|
+
'',
|
|
534
|
+
'SPECIFIC TOOLS FOR THIS PROTECTION:',
|
|
535
|
+
]
|
|
536
|
+
|
|
537
|
+
if self.results['type'] == 'dpt-shell':
|
|
538
|
+
self.results['recommendations'].extend([
|
|
539
|
+
'- DPT-Shell unpacker tools',
|
|
540
|
+
'- Frida script: scripts/frida/frida_dex_dump.js (included)',
|
|
541
|
+
'- FRIDA command: frida -U -f com.package.name -l scripts/frida/frida_dex_dump.js --no-pause',
|
|
542
|
+
'- Monitor JniBridge and libdpt.so loading',
|
|
543
|
+
'- Hook ProxyApplication.attachBaseContext()',
|
|
544
|
+
])
|
|
545
|
+
elif 'bangcle' in self.results['type']:
|
|
546
|
+
self.results['recommendations'].extend([
|
|
547
|
+
'- Bangcle unpacker',
|
|
548
|
+
'- ZjDroid for Bangcle',
|
|
549
|
+
])
|
|
550
|
+
elif 'qihoo' in self.results['type']:
|
|
551
|
+
self.results['recommendations'].extend([
|
|
552
|
+
'- Qihoo 360 unpacker',
|
|
553
|
+
'- FART for Qihoo protection',
|
|
554
|
+
])
|
|
555
|
+
|
|
556
|
+
elif self.results['type'] in ['proguard', 'r8']:
|
|
557
|
+
self.results['recommendations'] = [
|
|
558
|
+
'STATIC ANALYSIS POSSIBLE (with limitations)',
|
|
559
|
+
'1. Use JADX with deobfuscation options enabled',
|
|
560
|
+
'2. Apply class/method renaming for better readability',
|
|
561
|
+
'3. Focus on analyzing:',
|
|
562
|
+
' - Network communications',
|
|
563
|
+
' - Cryptographic operations',
|
|
564
|
+
' - Permission usage',
|
|
565
|
+
' - Sensitive API calls',
|
|
566
|
+
'4. Use dynamic analysis for runtime behavior',
|
|
567
|
+
'5. Consider using JADX-GUI for manual analysis',
|
|
568
|
+
]
|
|
569
|
+
else:
|
|
570
|
+
self.results['recommendations'] = [
|
|
571
|
+
'STANDARD STATIC ANALYSIS APPLICABLE',
|
|
572
|
+
'1. Proceed with SAST tools',
|
|
573
|
+
'2. Manual code review of critical components',
|
|
574
|
+
'3. Check for security vulnerabilities',
|
|
575
|
+
'4. Analyze network traffic',
|
|
576
|
+
'5. Review permissions and components',
|
|
577
|
+
]
|
|
578
|
+
|
|
579
|
+
def analyze(self):
|
|
580
|
+
"""Run all detection checks"""
|
|
581
|
+
|
|
582
|
+
# Run APKiD first for comprehensive detection
|
|
583
|
+
self.results['indicators'].extend(self.run_apkid())
|
|
584
|
+
|
|
585
|
+
# Check for DEX-level protection
|
|
586
|
+
self.results['indicators'].extend(self.check_dex_files())
|
|
587
|
+
|
|
588
|
+
# Check for native libraries
|
|
589
|
+
self.results['indicators'].extend(self.check_native_libs())
|
|
590
|
+
|
|
591
|
+
# Check manifest (this can detect jx.shell directly)
|
|
592
|
+
self.results['indicators'].extend(self.check_manifest_indicators())
|
|
593
|
+
|
|
594
|
+
# Check Java sources for DPT/jx.shell patterns (priority check)
|
|
595
|
+
self.results['indicators'].extend(self.check_dpt_jx_shell_sources())
|
|
596
|
+
|
|
597
|
+
# Check for additional obfuscation techniques
|
|
598
|
+
self.results['indicators'].extend(self.check_additional_obfuscation())
|
|
599
|
+
|
|
600
|
+
# Check for ProGuard/R8 (only if no advanced protection detected)
|
|
601
|
+
if self.results['type'] == 'none':
|
|
602
|
+
self.results['indicators'].extend(self.check_proguard_r8())
|
|
603
|
+
|
|
604
|
+
# Check string encryption
|
|
605
|
+
self.results['indicators'].extend(self.check_string_encryption())
|
|
606
|
+
|
|
607
|
+
# Generate recommendations
|
|
608
|
+
self.generate_recommendations()
|
|
609
|
+
|
|
610
|
+
return self.results
|
|
611
|
+
|
|
612
|
+
def main():
|
|
613
|
+
if len(sys.argv) < 5:
|
|
614
|
+
print("Usage: detect_obfuscation.py <apk_path> <decompiled_dir> <jadx_dir> <output_json>")
|
|
615
|
+
sys.exit(1)
|
|
616
|
+
|
|
617
|
+
apk_path = sys.argv[1] if os.path.exists(sys.argv[1]) else None
|
|
618
|
+
decompiled_dir = sys.argv[2] if os.path.exists(sys.argv[2]) else None
|
|
619
|
+
jadx_dir = sys.argv[3] if os.path.exists(sys.argv[3]) else None
|
|
620
|
+
output_json = sys.argv[4]
|
|
621
|
+
|
|
622
|
+
detector = ObfuscationDetector(decompiled_dir, jadx_dir, apk_path)
|
|
623
|
+
results = detector.analyze()
|
|
624
|
+
|
|
625
|
+
# Save results
|
|
626
|
+
with open(output_json, 'w') as f:
|
|
627
|
+
json.dump(results, f, indent=2)
|
|
628
|
+
|
|
629
|
+
# Print summary
|
|
630
|
+
print(f"\n{'='*60}")
|
|
631
|
+
print(f"OBFUSCATION DETECTION RESULTS")
|
|
632
|
+
print(f"{'='*60}")
|
|
633
|
+
print(f"Type: {results['type'].upper()}")
|
|
634
|
+
print(f"Confidence: {results['confidence']}%")
|
|
635
|
+
print(f"\nIndicators Found: {len(results['indicators'])}")
|
|
636
|
+
for indicator in results['indicators']:
|
|
637
|
+
print(f" • {indicator}")
|
|
638
|
+
|
|
639
|
+
if results['apkid_results']:
|
|
640
|
+
print(f"\nAPKiD Enhanced Detection: Enabled")
|
|
641
|
+
|
|
642
|
+
if results['recommendations']:
|
|
643
|
+
print(f"\nRECOMMENDATIONS:")
|
|
644
|
+
for rec in results['recommendations']:
|
|
645
|
+
print(f" {rec}")
|
|
646
|
+
|
|
647
|
+
print(f"\n{'='*60}\n")
|
|
648
|
+
|
|
649
|
+
if __name__ == '__main__':
|
|
650
|
+
main()
|