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,485 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Encrypted Payload Locator
|
|
4
|
+
Finds the actual source location of encrypted DEX payloads within the APK
|
|
5
|
+
before they are unpacked to code_cache at runtime.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import zipfile
|
|
10
|
+
import struct
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
class EncryptedPayloadLocator:
|
|
16
|
+
def __init__(self, apk_path, target_filename=None):
|
|
17
|
+
self.apk_path = apk_path
|
|
18
|
+
self.target_filename = target_filename
|
|
19
|
+
self.results = {
|
|
20
|
+
'found_payloads': [],
|
|
21
|
+
'zip_signatures': [],
|
|
22
|
+
'embedded_in_so': [],
|
|
23
|
+
'obfuscated_assets': [],
|
|
24
|
+
'suspicious_files': []
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def search_by_magic_bytes(self, zf):
|
|
28
|
+
"""Search for ZIP magic bytes (PK signature) in all files"""
|
|
29
|
+
print("\n🔍 Searching for ZIP magic bytes (PK signatures)...")
|
|
30
|
+
|
|
31
|
+
# ZIP file signatures
|
|
32
|
+
ZIP_SIGNATURES = [
|
|
33
|
+
b'PK\x03\x04', # Local file header
|
|
34
|
+
b'PK\x05\x06', # End of central directory
|
|
35
|
+
b'PK\x01\x02', # Central directory file header
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
for file_info in zf.filelist:
|
|
39
|
+
try:
|
|
40
|
+
data = zf.read(file_info.filename)
|
|
41
|
+
|
|
42
|
+
# Check for ZIP signatures
|
|
43
|
+
for sig in ZIP_SIGNATURES:
|
|
44
|
+
offset = 0
|
|
45
|
+
while True:
|
|
46
|
+
offset = data.find(sig, offset)
|
|
47
|
+
if offset == -1:
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
# Found a signature - check if it's a valid ZIP
|
|
51
|
+
if sig == b'PK\x03\x04': # Local file header
|
|
52
|
+
# Try to read ZIP header
|
|
53
|
+
try:
|
|
54
|
+
if offset + 30 <= len(data):
|
|
55
|
+
header = data[offset:offset+30]
|
|
56
|
+
# Parse local file header
|
|
57
|
+
signature, version, flags, method, mod_time, mod_date, crc32, comp_size, uncomp_size, fname_len, extra_len = struct.unpack('<IHHHHIIIHH', header)
|
|
58
|
+
|
|
59
|
+
if fname_len > 0 and fname_len < 256: # Reasonable filename length
|
|
60
|
+
# Extract filename
|
|
61
|
+
if offset + 30 + fname_len <= len(data):
|
|
62
|
+
zip_fname = data[offset+30:offset+30+fname_len].decode('utf-8', errors='ignore')
|
|
63
|
+
|
|
64
|
+
# Check if this matches our target
|
|
65
|
+
match_target = False
|
|
66
|
+
if self.target_filename and self.target_filename.lower() in zip_fname.lower():
|
|
67
|
+
match_target = True
|
|
68
|
+
|
|
69
|
+
result = {
|
|
70
|
+
'location': file_info.filename,
|
|
71
|
+
'offset': offset,
|
|
72
|
+
'embedded_filename': zip_fname,
|
|
73
|
+
'compressed_size': comp_size,
|
|
74
|
+
'uncompressed_size': uncomp_size,
|
|
75
|
+
'compression_method': method,
|
|
76
|
+
'matches_target': match_target
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
self.results['zip_signatures'].append(result)
|
|
80
|
+
|
|
81
|
+
if match_target:
|
|
82
|
+
print(f"\n✅ FOUND TARGET: {self.target_filename}")
|
|
83
|
+
print(f" Location: {file_info.filename}")
|
|
84
|
+
print(f" Offset: 0x{offset:08x} ({offset} bytes)")
|
|
85
|
+
print(f" Embedded filename: {zip_fname}")
|
|
86
|
+
print(f" Compressed: {comp_size} bytes")
|
|
87
|
+
print(f" Uncompressed: {uncomp_size} bytes")
|
|
88
|
+
except Exception as e:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
offset += 1
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
def search_obfuscated_assets(self, zf):
|
|
97
|
+
"""Search assets folder for obfuscated files that might be the payload"""
|
|
98
|
+
print("\n🔍 Searching assets folder for obfuscated files...")
|
|
99
|
+
|
|
100
|
+
asset_files = [f for f in zf.filelist if f.filename.startswith('assets/')]
|
|
101
|
+
|
|
102
|
+
for asset in asset_files:
|
|
103
|
+
try:
|
|
104
|
+
data = zf.read(asset.filename)
|
|
105
|
+
|
|
106
|
+
# Check if it's actually a ZIP file (starts with PK)
|
|
107
|
+
if data[:2] == b'PK':
|
|
108
|
+
is_valid_zip = True
|
|
109
|
+
embedded_files = []
|
|
110
|
+
|
|
111
|
+
# Try to read as ZIP
|
|
112
|
+
try:
|
|
113
|
+
import io
|
|
114
|
+
with zipfile.ZipFile(io.BytesIO(data)) as inner_zip:
|
|
115
|
+
embedded_files = [f.filename for f in inner_zip.filelist]
|
|
116
|
+
|
|
117
|
+
# Check if any embedded file contains .dex
|
|
118
|
+
has_dex = any('.dex' in f.lower() for f in embedded_files)
|
|
119
|
+
|
|
120
|
+
result = {
|
|
121
|
+
'location': asset.filename,
|
|
122
|
+
'size': len(data),
|
|
123
|
+
'is_zip': True,
|
|
124
|
+
'embedded_files': embedded_files,
|
|
125
|
+
'contains_dex': has_dex,
|
|
126
|
+
'potential_payload': has_dex
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
self.results['obfuscated_assets'].append(result)
|
|
130
|
+
|
|
131
|
+
print(f"\n📦 Found ZIP in assets: {asset.filename}")
|
|
132
|
+
print(f" Size: {len(data)} bytes")
|
|
133
|
+
print(f" Contains {len(embedded_files)} file(s)")
|
|
134
|
+
if has_dex:
|
|
135
|
+
print(f" ⚠️ Contains DEX files!")
|
|
136
|
+
print(f" Files: {', '.join(embedded_files[:5])}")
|
|
137
|
+
|
|
138
|
+
except zipfile.BadZipFile:
|
|
139
|
+
# Not a valid ZIP despite PK signature
|
|
140
|
+
result = {
|
|
141
|
+
'location': asset.filename,
|
|
142
|
+
'size': len(data),
|
|
143
|
+
'is_zip': False,
|
|
144
|
+
'has_pk_signature': True,
|
|
145
|
+
'potential_encrypted': True
|
|
146
|
+
}
|
|
147
|
+
self.results['suspicious_files'].append(result)
|
|
148
|
+
print(f"\n🔒 Suspicious file: {asset.filename}")
|
|
149
|
+
print(f" Has PK signature but not a valid ZIP (likely encrypted)")
|
|
150
|
+
print(f" Size: {len(data)} bytes")
|
|
151
|
+
|
|
152
|
+
# Check for files with suspicious names or no extension
|
|
153
|
+
elif '.' not in Path(asset.filename).name or len(Path(asset.filename).name) > 20:
|
|
154
|
+
# Check entropy (high entropy = encrypted/compressed)
|
|
155
|
+
entropy = self.calculate_entropy(data[:min(1024, len(data))])
|
|
156
|
+
|
|
157
|
+
if entropy > 7.0: # High entropy
|
|
158
|
+
result = {
|
|
159
|
+
'location': asset.filename,
|
|
160
|
+
'size': len(data),
|
|
161
|
+
'entropy': entropy,
|
|
162
|
+
'suspicious': True
|
|
163
|
+
}
|
|
164
|
+
self.results['suspicious_files'].append(result)
|
|
165
|
+
print(f"\n⚠️ High entropy file: {asset.filename}")
|
|
166
|
+
print(f" Size: {len(data)} bytes")
|
|
167
|
+
print(f" Entropy: {entropy:.2f} (likely encrypted)")
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
def search_in_native_libs(self, zf):
|
|
173
|
+
"""Search for embedded payloads in .so files"""
|
|
174
|
+
print("\n🔍 Searching native libraries for embedded payloads...")
|
|
175
|
+
|
|
176
|
+
so_files = [f for f in zf.filelist if f.filename.endswith('.so')]
|
|
177
|
+
|
|
178
|
+
for so_file in so_files:
|
|
179
|
+
try:
|
|
180
|
+
data = zf.read(so_file.filename)
|
|
181
|
+
|
|
182
|
+
# Search for PK signatures
|
|
183
|
+
pk_offsets = []
|
|
184
|
+
offset = 0
|
|
185
|
+
while True:
|
|
186
|
+
offset = data.find(b'PK\x03\x04', offset)
|
|
187
|
+
if offset == -1:
|
|
188
|
+
break
|
|
189
|
+
pk_offsets.append(offset)
|
|
190
|
+
offset += 1
|
|
191
|
+
|
|
192
|
+
if pk_offsets:
|
|
193
|
+
# Found embedded ZIP
|
|
194
|
+
for pk_offset in pk_offsets:
|
|
195
|
+
# Try to extract the embedded ZIP
|
|
196
|
+
try:
|
|
197
|
+
# Find the end of the ZIP (look for end of central directory signature)
|
|
198
|
+
end_offset = data.find(b'PK\x05\x06', pk_offset)
|
|
199
|
+
if end_offset != -1:
|
|
200
|
+
# Extract ZIP data
|
|
201
|
+
end_offset += 22 # Size of end of central directory record
|
|
202
|
+
zip_data = data[pk_offset:end_offset]
|
|
203
|
+
|
|
204
|
+
# Try to read it
|
|
205
|
+
import io
|
|
206
|
+
try:
|
|
207
|
+
with zipfile.ZipFile(io.BytesIO(zip_data)) as embedded_zip:
|
|
208
|
+
embedded_files = [f.filename for f in embedded_zip.filelist]
|
|
209
|
+
|
|
210
|
+
result = {
|
|
211
|
+
'location': so_file.filename,
|
|
212
|
+
'offset': pk_offset,
|
|
213
|
+
'size': len(zip_data),
|
|
214
|
+
'embedded_files': embedded_files,
|
|
215
|
+
'contains_dex': any('.dex' in f.lower() for f in embedded_files)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
self.results['embedded_in_so'].append(result)
|
|
219
|
+
|
|
220
|
+
print(f"\n📦 Found embedded ZIP in: {so_file.filename}")
|
|
221
|
+
print(f" Offset: 0x{pk_offset:08x} ({pk_offset} bytes)")
|
|
222
|
+
print(f" Size: {len(zip_data)} bytes")
|
|
223
|
+
print(f" Files: {', '.join(embedded_files)}")
|
|
224
|
+
|
|
225
|
+
except zipfile.BadZipFile:
|
|
226
|
+
pass
|
|
227
|
+
except Exception as e:
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
except Exception as e:
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
def search_by_name_pattern(self, zf):
|
|
234
|
+
"""Search for files matching the target filename pattern"""
|
|
235
|
+
if not self.target_filename:
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
print(f"\n🔍 Searching for files matching: {self.target_filename}")
|
|
239
|
+
|
|
240
|
+
# Also search where the filename is referenced (in .so files)
|
|
241
|
+
target_bytes = self.target_filename.encode()
|
|
242
|
+
|
|
243
|
+
# Find all .so files that reference this filename
|
|
244
|
+
so_references = []
|
|
245
|
+
for file_info in zf.filelist:
|
|
246
|
+
if file_info.filename.endswith('.so'):
|
|
247
|
+
try:
|
|
248
|
+
data = zf.read(file_info.filename)
|
|
249
|
+
if target_bytes in data:
|
|
250
|
+
offset = data.find(target_bytes)
|
|
251
|
+
so_references.append({
|
|
252
|
+
'so_file': file_info.filename,
|
|
253
|
+
'offset': offset
|
|
254
|
+
})
|
|
255
|
+
print(f"\n📌 Found reference in: {file_info.filename}")
|
|
256
|
+
print(f" Offset: 0x{offset:08x}")
|
|
257
|
+
except:
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
# If found in .so files, the actual encrypted data is likely in assets
|
|
261
|
+
if so_references:
|
|
262
|
+
print(f"\n💡 The filename '{self.target_filename}' is used at RUNTIME")
|
|
263
|
+
print(f" The ENCRYPTED source is likely in assets/ with an obfuscated name")
|
|
264
|
+
print(f"\n🔍 Checking for large suspicious assets...")
|
|
265
|
+
|
|
266
|
+
# Find large files in assets (potential encrypted payloads)
|
|
267
|
+
large_assets = []
|
|
268
|
+
for file_info in zf.filelist:
|
|
269
|
+
if file_info.filename.startswith('assets/') and file_info.file_size > 1_000_000: # > 1MB
|
|
270
|
+
# Check if it has weird name
|
|
271
|
+
filename = Path(file_info.filename).name
|
|
272
|
+
is_suspicious = (
|
|
273
|
+
'.' not in filename or # No extension
|
|
274
|
+
filename.count(filename[0]) > len(filename) // 2 or # Repetitive chars
|
|
275
|
+
not any(c.isalnum() and c.islower() for c in filename) # No lowercase letters
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if is_suspicious:
|
|
279
|
+
large_assets.append(file_info)
|
|
280
|
+
|
|
281
|
+
data = zf.read(file_info.filename)
|
|
282
|
+
entropy = self.calculate_entropy(data[:min(4096, len(data))])
|
|
283
|
+
|
|
284
|
+
result = {
|
|
285
|
+
'location': file_info.filename,
|
|
286
|
+
'size': len(data),
|
|
287
|
+
'compressed_size': file_info.compress_size,
|
|
288
|
+
'entropy': entropy,
|
|
289
|
+
'likely_encrypted': entropy > 5.0,
|
|
290
|
+
'target_runtime_name': self.target_filename,
|
|
291
|
+
'referenced_in': [ref['so_file'] for ref in so_references]
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
self.results['found_payloads'].append(result)
|
|
295
|
+
|
|
296
|
+
print(f"\n✅ POTENTIAL SOURCE: {file_info.filename}")
|
|
297
|
+
print(f" Size: {len(data):,} bytes ({len(data)/1024/1024:.2f} MB)")
|
|
298
|
+
print(f" Entropy: {entropy:.2f}")
|
|
299
|
+
if entropy > 5.0:
|
|
300
|
+
print(f" ⚠️ High entropy - likely encrypted!")
|
|
301
|
+
print(f" Runtime name: {self.target_filename}")
|
|
302
|
+
print(f" Referenced in: {', '.join(ref['so_file'] for ref in so_references)}")
|
|
303
|
+
|
|
304
|
+
# Original search for exact filename
|
|
305
|
+
# Extract base name without extension
|
|
306
|
+
target_base = self.target_filename.replace('.zip', '').replace('.jar', '').replace('.dat', '')
|
|
307
|
+
|
|
308
|
+
for file_info in zf.filelist:
|
|
309
|
+
# Check if filename contains target pattern
|
|
310
|
+
if target_base.lower() in file_info.filename.lower():
|
|
311
|
+
data = zf.read(file_info.filename)
|
|
312
|
+
|
|
313
|
+
result = {
|
|
314
|
+
'location': file_info.filename,
|
|
315
|
+
'size': len(data),
|
|
316
|
+
'compressed_size': file_info.compress_size,
|
|
317
|
+
'exact_match': file_info.filename.endswith(self.target_filename)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
self.results['found_payloads'].append(result)
|
|
321
|
+
|
|
322
|
+
print(f"\n✅ FOUND: {file_info.filename}")
|
|
323
|
+
print(f" Size: {len(data)} bytes")
|
|
324
|
+
print(f" Compressed: {file_info.compress_size} bytes")
|
|
325
|
+
|
|
326
|
+
# Check if it's a valid ZIP
|
|
327
|
+
if data[:2] == b'PK':
|
|
328
|
+
print(f" ✓ Valid ZIP signature")
|
|
329
|
+
|
|
330
|
+
# Try to list contents
|
|
331
|
+
try:
|
|
332
|
+
import io
|
|
333
|
+
with zipfile.ZipFile(io.BytesIO(data)) as inner_zip:
|
|
334
|
+
files = [f.filename for f in inner_zip.filelist]
|
|
335
|
+
print(f" Contains: {', '.join(files)}")
|
|
336
|
+
except:
|
|
337
|
+
print(f" ⚠️ Has PK signature but cannot read (encrypted?)")
|
|
338
|
+
else:
|
|
339
|
+
entropy = self.calculate_entropy(data[:min(1024, len(data))])
|
|
340
|
+
print(f" Entropy: {entropy:.2f}")
|
|
341
|
+
|
|
342
|
+
def calculate_entropy(self, data):
|
|
343
|
+
"""Calculate Shannon entropy of data"""
|
|
344
|
+
if not data:
|
|
345
|
+
return 0
|
|
346
|
+
|
|
347
|
+
import math
|
|
348
|
+
from collections import Counter
|
|
349
|
+
|
|
350
|
+
counter = Counter(data)
|
|
351
|
+
length = len(data)
|
|
352
|
+
entropy = 0
|
|
353
|
+
|
|
354
|
+
for count in counter.values():
|
|
355
|
+
p = count / length
|
|
356
|
+
entropy -= p * math.log2(p)
|
|
357
|
+
|
|
358
|
+
return entropy
|
|
359
|
+
|
|
360
|
+
def extract_payload(self, output_path, location):
|
|
361
|
+
"""Extract the found payload to a file"""
|
|
362
|
+
print(f"\n💾 Extracting payload from: {location}")
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
with zipfile.ZipFile(self.apk_path, 'r') as zf:
|
|
366
|
+
data = zf.read(location)
|
|
367
|
+
|
|
368
|
+
with open(output_path, 'wb') as f:
|
|
369
|
+
f.write(data)
|
|
370
|
+
|
|
371
|
+
print(f"✅ Extracted to: {output_path}")
|
|
372
|
+
print(f" Size: {len(data)} bytes")
|
|
373
|
+
|
|
374
|
+
# Try to analyze
|
|
375
|
+
if data[:2] == b'PK':
|
|
376
|
+
print(f" Type: ZIP archive")
|
|
377
|
+
try:
|
|
378
|
+
import io
|
|
379
|
+
with zipfile.ZipFile(io.BytesIO(data)) as inner_zip:
|
|
380
|
+
files = [f.filename for f in inner_zip.filelist]
|
|
381
|
+
print(f" Contains {len(files)} file(s):")
|
|
382
|
+
for f in files:
|
|
383
|
+
print(f" • {f}")
|
|
384
|
+
except:
|
|
385
|
+
print(f" ⚠️ Encrypted or corrupted ZIP")
|
|
386
|
+
else:
|
|
387
|
+
entropy = self.calculate_entropy(data[:min(1024, len(data))])
|
|
388
|
+
print(f" Entropy: {entropy:.2f}")
|
|
389
|
+
if entropy > 7.5:
|
|
390
|
+
print(f" Likely encrypted")
|
|
391
|
+
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
except Exception as e:
|
|
395
|
+
print(f"❌ Error: {e}")
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
def search_all(self):
|
|
399
|
+
"""Perform all searches"""
|
|
400
|
+
print(f"\n{'='*70}")
|
|
401
|
+
print(f"🔍 ENCRYPTED PAYLOAD LOCATOR")
|
|
402
|
+
print(f"{'='*70}")
|
|
403
|
+
print(f"APK: {self.apk_path}")
|
|
404
|
+
if self.target_filename:
|
|
405
|
+
print(f"Target: {self.target_filename}")
|
|
406
|
+
print(f"{'='*70}")
|
|
407
|
+
|
|
408
|
+
with zipfile.ZipFile(self.apk_path, 'r') as zf:
|
|
409
|
+
# Search by name pattern first
|
|
410
|
+
if self.target_filename:
|
|
411
|
+
self.search_by_name_pattern(zf)
|
|
412
|
+
|
|
413
|
+
# Search in assets
|
|
414
|
+
self.search_obfuscated_assets(zf)
|
|
415
|
+
|
|
416
|
+
# Search in native libraries
|
|
417
|
+
self.search_in_native_libs(zf)
|
|
418
|
+
|
|
419
|
+
# Search by magic bytes (comprehensive but slow)
|
|
420
|
+
# self.search_by_magic_bytes(zf) # Uncomment for deep search
|
|
421
|
+
|
|
422
|
+
return self.results
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def main():
|
|
426
|
+
parser = argparse.ArgumentParser(
|
|
427
|
+
description='Find encrypted DEX payload source location in APK',
|
|
428
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
429
|
+
epilog="""
|
|
430
|
+
Examples:
|
|
431
|
+
# Find specific encrypted zip file
|
|
432
|
+
python3 find_encrypted_payload.py app.apk --target i11111i111.zip
|
|
433
|
+
|
|
434
|
+
# Search for all suspicious files
|
|
435
|
+
python3 find_encrypted_payload.py app.apk
|
|
436
|
+
|
|
437
|
+
# Extract found payload
|
|
438
|
+
python3 find_encrypted_payload.py app.apk --target i11111i111.zip --extract payload.zip
|
|
439
|
+
|
|
440
|
+
# Export results to JSON
|
|
441
|
+
python3 find_encrypted_payload.py app.apk --json results.json
|
|
442
|
+
"""
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
parser.add_argument('apk', help='APK file to analyze')
|
|
446
|
+
parser.add_argument('--target', '-t', help='Target filename to search for (e.g., i11111i111.zip)')
|
|
447
|
+
parser.add_argument('--extract', '-e', help='Extract found payload to this path')
|
|
448
|
+
parser.add_argument('--json', '-j', help='Export results to JSON file')
|
|
449
|
+
|
|
450
|
+
args = parser.parse_args()
|
|
451
|
+
|
|
452
|
+
if not Path(args.apk).exists():
|
|
453
|
+
print(f"Error: APK not found: {args.apk}", file=sys.stderr)
|
|
454
|
+
sys.exit(1)
|
|
455
|
+
|
|
456
|
+
# Create locator
|
|
457
|
+
locator = EncryptedPayloadLocator(args.apk, args.target)
|
|
458
|
+
|
|
459
|
+
# Search
|
|
460
|
+
results = locator.search_all()
|
|
461
|
+
|
|
462
|
+
# Summary
|
|
463
|
+
print(f"\n{'='*70}")
|
|
464
|
+
print(f"📊 SEARCH SUMMARY")
|
|
465
|
+
print(f"{'='*70}")
|
|
466
|
+
print(f"Direct matches: {len(results['found_payloads'])}")
|
|
467
|
+
print(f"Obfuscated assets: {len(results['obfuscated_assets'])}")
|
|
468
|
+
print(f"Embedded in .so: {len(results['embedded_in_so'])}")
|
|
469
|
+
print(f"Suspicious files: {len(results['suspicious_files'])}")
|
|
470
|
+
print(f"{'='*70}\n")
|
|
471
|
+
|
|
472
|
+
# Extract if requested
|
|
473
|
+
if args.extract and results['found_payloads']:
|
|
474
|
+
location = results['found_payloads'][0]['location']
|
|
475
|
+
locator.extract_payload(args.extract, location)
|
|
476
|
+
|
|
477
|
+
# Export JSON
|
|
478
|
+
if args.json:
|
|
479
|
+
with open(args.json, 'w') as f:
|
|
480
|
+
json.dump(results, f, indent=2)
|
|
481
|
+
print(f"✅ Results exported to: {args.json}")
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
if __name__ == '__main__':
|
|
485
|
+
main()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
APK Header Fixer
|
|
4
|
+
Attempts to fix corrupted or manipulated ZIP headers in APK files
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import struct
|
|
9
|
+
import shutil
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
class APKHeaderFixer:
|
|
13
|
+
def __init__(self, input_apk, output_apk):
|
|
14
|
+
self.input_apk = Path(input_apk)
|
|
15
|
+
self.output_apk = Path(output_apk)
|
|
16
|
+
|
|
17
|
+
def fix_local_file_headers(self, data):
|
|
18
|
+
"""Remove encryption flags from local file headers"""
|
|
19
|
+
output = bytearray(data)
|
|
20
|
+
offset = 0
|
|
21
|
+
|
|
22
|
+
while offset < len(output) - 30:
|
|
23
|
+
# Look for local file header signature (PK\x03\x04)
|
|
24
|
+
if output[offset:offset+4] == b'PK\x03\x04':
|
|
25
|
+
# Get flag bytes at offset + 6
|
|
26
|
+
flags = struct.unpack('<H', output[offset+6:offset+8])[0]
|
|
27
|
+
|
|
28
|
+
# Clear encryption bit (bit 0)
|
|
29
|
+
new_flags = flags & ~0x0001
|
|
30
|
+
|
|
31
|
+
# Clear data descriptor bit (bit 3) if needed
|
|
32
|
+
new_flags = new_flags & ~0x0008
|
|
33
|
+
|
|
34
|
+
# Update flags
|
|
35
|
+
output[offset+6:offset+8] = struct.pack('<H', new_flags)
|
|
36
|
+
|
|
37
|
+
# Get filename and extra field lengths
|
|
38
|
+
filename_len = struct.unpack('<H', output[offset+26:offset+28])[0]
|
|
39
|
+
extra_len = struct.unpack('<H', output[offset+28:offset+30])[0]
|
|
40
|
+
|
|
41
|
+
# Move to next header
|
|
42
|
+
offset += 30 + filename_len + extra_len
|
|
43
|
+
else:
|
|
44
|
+
offset += 1
|
|
45
|
+
|
|
46
|
+
return bytes(output)
|
|
47
|
+
|
|
48
|
+
def fix_central_directory_headers(self, data):
|
|
49
|
+
"""Remove encryption flags from central directory headers"""
|
|
50
|
+
output = bytearray(data)
|
|
51
|
+
offset = 0
|
|
52
|
+
|
|
53
|
+
while offset < len(output) - 46:
|
|
54
|
+
# Look for central directory header signature (PK\x01\x02)
|
|
55
|
+
if output[offset:offset+4] == b'PK\x01\x02':
|
|
56
|
+
# Get flag bytes at offset + 8
|
|
57
|
+
flags = struct.unpack('<H', output[offset+8:offset+10])[0]
|
|
58
|
+
|
|
59
|
+
# Clear encryption bit (bit 0)
|
|
60
|
+
new_flags = flags & ~0x0001
|
|
61
|
+
|
|
62
|
+
# Clear data descriptor bit (bit 3) if needed
|
|
63
|
+
new_flags = new_flags & ~0x0008
|
|
64
|
+
|
|
65
|
+
# Update flags
|
|
66
|
+
output[offset+8:offset+10] = struct.pack('<H', new_flags)
|
|
67
|
+
|
|
68
|
+
# Get lengths
|
|
69
|
+
filename_len = struct.unpack('<H', output[offset+28:offset+30])[0]
|
|
70
|
+
extra_len = struct.unpack('<H', output[offset+30:offset+32])[0]
|
|
71
|
+
comment_len = struct.unpack('<H', output[offset+32:offset+34])[0]
|
|
72
|
+
|
|
73
|
+
# Move to next header
|
|
74
|
+
offset += 46 + filename_len + extra_len + comment_len
|
|
75
|
+
else:
|
|
76
|
+
offset += 1
|
|
77
|
+
|
|
78
|
+
return bytes(output)
|
|
79
|
+
|
|
80
|
+
def remove_encryption_header(self, data):
|
|
81
|
+
"""Remove encryption header if present"""
|
|
82
|
+
# Some protectors add fake encryption headers
|
|
83
|
+
# Look for and remove them
|
|
84
|
+
output = bytearray(data)
|
|
85
|
+
|
|
86
|
+
# Search for encryption header patterns
|
|
87
|
+
# This is heuristic-based and may need adjustment
|
|
88
|
+
patterns = [
|
|
89
|
+
b'\x09\x99\x07', # Common encryption header
|
|
90
|
+
b'\x09\x99\x08',
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
for pattern in patterns:
|
|
94
|
+
idx = output.find(pattern)
|
|
95
|
+
if idx != -1:
|
|
96
|
+
print(f"Found potential encryption header at offset {idx}")
|
|
97
|
+
# Remove the pattern (careful with this)
|
|
98
|
+
# output = output[:idx] + output[idx+len(pattern):]
|
|
99
|
+
|
|
100
|
+
return bytes(output)
|
|
101
|
+
|
|
102
|
+
def fix_apk(self):
|
|
103
|
+
"""Main fixing routine"""
|
|
104
|
+
try:
|
|
105
|
+
# Read entire APK
|
|
106
|
+
with open(self.input_apk, 'rb') as f:
|
|
107
|
+
data = f.read()
|
|
108
|
+
|
|
109
|
+
print(f"Original APK size: {len(data)} bytes")
|
|
110
|
+
|
|
111
|
+
# Apply fixes
|
|
112
|
+
print("Fixing local file headers...")
|
|
113
|
+
data = self.fix_local_file_headers(data)
|
|
114
|
+
|
|
115
|
+
print("Fixing central directory headers...")
|
|
116
|
+
data = self.fix_central_directory_headers(data)
|
|
117
|
+
|
|
118
|
+
print("Checking for encryption headers...")
|
|
119
|
+
data = self.remove_encryption_header(data)
|
|
120
|
+
|
|
121
|
+
# Write fixed APK
|
|
122
|
+
with open(self.output_apk, 'wb') as f:
|
|
123
|
+
f.write(data)
|
|
124
|
+
|
|
125
|
+
print(f"Fixed APK size: {len(data)} bytes")
|
|
126
|
+
print(f"Fixed APK saved to: {self.output_apk}")
|
|
127
|
+
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print(f"Error fixing APK: {e}")
|
|
132
|
+
import traceback
|
|
133
|
+
traceback.print_exc()
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def main():
|
|
137
|
+
if len(sys.argv) < 3:
|
|
138
|
+
print("Usage: fix_apk_headers.py <input_apk> <output_apk>")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
input_apk = sys.argv[1]
|
|
142
|
+
output_apk = sys.argv[2]
|
|
143
|
+
|
|
144
|
+
fixer = APKHeaderFixer(input_apk, output_apk)
|
|
145
|
+
|
|
146
|
+
if fixer.fix_apk():
|
|
147
|
+
print("APK headers fixed successfully!")
|
|
148
|
+
print("Try decompiling the fixed APK now.")
|
|
149
|
+
else:
|
|
150
|
+
print("Failed to fix APK headers.")
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
|
|
153
|
+
if __name__ == '__main__':
|
|
154
|
+
main()
|