android-persistence 1.0.0__py3-none-any.whl
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.
- android_persistence/__init__.py +29 -0
- android_persistence/data_parser.py +325 -0
- android_persistence/defensive_mitigations.py +378 -0
- android_persistence/persistence_detector.py +441 -0
- android_persistence/report_generator.py +326 -0
- android_persistence/utils/__init__.py +3 -0
- android_persistence/utils/hex_analyzer.py +162 -0
- android_persistence/utils/logger.py +74 -0
- android_persistence/utils/manifest_parser.py +164 -0
- android_persistence/utils/signature_matcher.py +217 -0
- android_persistence-1.0.0.dist-info/METADATA +484 -0
- android_persistence-1.0.0.dist-info/RECORD +16 -0
- android_persistence-1.0.0.dist-info/WHEEL +5 -0
- android_persistence-1.0.0.dist-info/entry_points.txt +2 -0
- android_persistence-1.0.0.dist-info/licenses/LICENSE +21 -0
- android_persistence-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Manifest Parser for AndroidManifest.xml binary format analysis.
|
|
3
|
+
|
|
4
|
+
Handles parsing of compiled AndroidManifest.xml files which are in binary AXML format.
|
|
5
|
+
This module provides tools for extracting components, permissions, and intent filters.
|
|
6
|
+
|
|
7
|
+
Author: Security Research Team
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Dict, List, Optional, Any
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Permission:
|
|
17
|
+
"""Represents a permission declared in manifest."""
|
|
18
|
+
name: str
|
|
19
|
+
protection_level: str = "normal"
|
|
20
|
+
description: str = ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Component:
|
|
25
|
+
"""Represents an Android component (Activity, Service, BroadcastReceiver, etc)."""
|
|
26
|
+
name: str
|
|
27
|
+
component_type: str # activity, service, receiver, provider
|
|
28
|
+
exported: bool = False
|
|
29
|
+
permissions: List[str] = None
|
|
30
|
+
intent_filters: List[Dict[str, Any]] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ManifestParser:
|
|
34
|
+
"""
|
|
35
|
+
Parser for Android manifest files.
|
|
36
|
+
|
|
37
|
+
Note: This is a simplified parser. For full AXML binary parsing,
|
|
38
|
+
use androguard library.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, manifest_path: str = None, manifest_data: bytes = None):
|
|
42
|
+
"""
|
|
43
|
+
Initialize manifest parser.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
manifest_path: Path to AndroidManifest.xml
|
|
47
|
+
manifest_data: Raw manifest bytes
|
|
48
|
+
"""
|
|
49
|
+
self.manifest_path = manifest_path
|
|
50
|
+
self.manifest_data = manifest_data
|
|
51
|
+
self.components: List[Component] = []
|
|
52
|
+
self.permissions: List[Permission] = []
|
|
53
|
+
self.package_name: str = ""
|
|
54
|
+
|
|
55
|
+
def parse(self) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
Parse manifest file.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
True if parsing succeeded
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
if self.manifest_path:
|
|
64
|
+
with open(self.manifest_path, 'rb') as f:
|
|
65
|
+
self.manifest_data = f.read()
|
|
66
|
+
|
|
67
|
+
if not self.manifest_data:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
self._extract_basic_info()
|
|
71
|
+
self._extract_components()
|
|
72
|
+
self._extract_permissions()
|
|
73
|
+
|
|
74
|
+
return True
|
|
75
|
+
except Exception as e:
|
|
76
|
+
print(f"Error parsing manifest: {str(e)}")
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def _extract_basic_info(self) -> None:
|
|
80
|
+
"""Extract basic package information from manifest."""
|
|
81
|
+
# This is a simplified extraction - real parsing would decode AXML format
|
|
82
|
+
# Looking for package attribute pattern
|
|
83
|
+
try:
|
|
84
|
+
# Search for package name in manifest
|
|
85
|
+
if b'package=' in self.manifest_data:
|
|
86
|
+
idx = self.manifest_data.find(b'package=')
|
|
87
|
+
if idx != -1:
|
|
88
|
+
# Extract package name (simplified approach)
|
|
89
|
+
data_after = self.manifest_data[idx+8:idx+100]
|
|
90
|
+
end = data_after.find(b'\x00')
|
|
91
|
+
if end > 0:
|
|
92
|
+
self.package_name = data_after[:end].decode('utf-8', errors='ignore')
|
|
93
|
+
except Exception:
|
|
94
|
+
self.package_name = "Unknown"
|
|
95
|
+
|
|
96
|
+
def _extract_components(self) -> None:
|
|
97
|
+
"""Extract Android components from manifest."""
|
|
98
|
+
# Simulate component extraction
|
|
99
|
+
component_types = {
|
|
100
|
+
b'<activity': 'activity',
|
|
101
|
+
b'<service': 'service',
|
|
102
|
+
b'<receiver': 'receiver',
|
|
103
|
+
b'<provider': 'provider',
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for tag, comp_type in component_types.items():
|
|
107
|
+
offset = 0
|
|
108
|
+
while True:
|
|
109
|
+
offset = self.manifest_data.find(tag, offset)
|
|
110
|
+
if offset == -1:
|
|
111
|
+
break
|
|
112
|
+
# Extract component name (simplified)
|
|
113
|
+
offset += 1
|
|
114
|
+
|
|
115
|
+
def _extract_permissions(self) -> None:
|
|
116
|
+
"""Extract declared permissions from manifest."""
|
|
117
|
+
# Simulate permission extraction
|
|
118
|
+
try:
|
|
119
|
+
offset = self.manifest_data.find(b'<uses-permission')
|
|
120
|
+
if offset != -1:
|
|
121
|
+
# Found permission declaration
|
|
122
|
+
pass
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
def get_components(self, component_type: str = None) -> List[Component]:
|
|
127
|
+
"""
|
|
128
|
+
Get components from manifest.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
component_type: Filter by type (activity, service, receiver, provider)
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of components
|
|
135
|
+
"""
|
|
136
|
+
if not component_type:
|
|
137
|
+
return self.components
|
|
138
|
+
return [c for c in self.components if c.component_type == component_type]
|
|
139
|
+
|
|
140
|
+
def get_permissions(self) -> List[Permission]:
|
|
141
|
+
"""Get declared permissions."""
|
|
142
|
+
return self.permissions
|
|
143
|
+
|
|
144
|
+
def get_package_name(self) -> str:
|
|
145
|
+
"""Get package name."""
|
|
146
|
+
return self.package_name
|
|
147
|
+
|
|
148
|
+
def has_boot_receiver(self) -> bool:
|
|
149
|
+
"""Check if manifest contains BOOT_COMPLETED receiver."""
|
|
150
|
+
return any(
|
|
151
|
+
'BOOT_COMPLETED' in str(c.intent_filters or [])
|
|
152
|
+
for c in self.get_components('receiver')
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def get_exported_components(self) -> List[Component]:
|
|
156
|
+
"""Get all exported components (potential attack surface)."""
|
|
157
|
+
return [c for c in self.components if c.exported]
|
|
158
|
+
|
|
159
|
+
def print_summary(self) -> None:
|
|
160
|
+
"""Print manifest summary."""
|
|
161
|
+
print(f"\nPackage: {self.package_name}")
|
|
162
|
+
print(f"Components: {len(self.components)}")
|
|
163
|
+
print(f"Permissions: {len(self.permissions)}")
|
|
164
|
+
print(f"Exported: {len(self.get_exported_components())}")
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Signature Matcher for identifying known persistence patterns and attack signatures.
|
|
3
|
+
|
|
4
|
+
Provides pattern matching capabilities to identify known Android persistence
|
|
5
|
+
techniques, malware signatures, and suspicious code patterns.
|
|
6
|
+
|
|
7
|
+
Author: Security Research Team
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Dict, List, Tuple, Optional
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SignatureSeverity(Enum):
|
|
17
|
+
"""Severity levels for matched signatures."""
|
|
18
|
+
CRITICAL = 5
|
|
19
|
+
HIGH = 4
|
|
20
|
+
MEDIUM = 3
|
|
21
|
+
LOW = 2
|
|
22
|
+
INFO = 1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Signature:
|
|
27
|
+
"""Pattern signature definition."""
|
|
28
|
+
name: str
|
|
29
|
+
pattern: str # Regex or string pattern
|
|
30
|
+
description: str
|
|
31
|
+
severity: SignatureSeverity
|
|
32
|
+
cve: Optional[str] = None
|
|
33
|
+
references: List[str] = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SignatureMatcher:
|
|
37
|
+
"""
|
|
38
|
+
Matcher for code and binary signatures.
|
|
39
|
+
|
|
40
|
+
Maintains a database of known persistence patterns and suspicious signatures
|
|
41
|
+
to identify potential threats during APK analysis.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
"""Initialize signature matcher with known patterns."""
|
|
46
|
+
self.signatures = self._load_signatures()
|
|
47
|
+
|
|
48
|
+
def _load_signatures(self) -> List[Signature]:
|
|
49
|
+
"""
|
|
50
|
+
Load known persistence signatures.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of Signature objects
|
|
54
|
+
"""
|
|
55
|
+
return [
|
|
56
|
+
Signature(
|
|
57
|
+
name="BootReceiver",
|
|
58
|
+
pattern="android.intent.action.BOOT_COMPLETED",
|
|
59
|
+
description="Broadcast receiver triggered on device boot",
|
|
60
|
+
severity=SignatureSeverity.HIGH,
|
|
61
|
+
),
|
|
62
|
+
Signature(
|
|
63
|
+
name="AutoStartService",
|
|
64
|
+
pattern="startForeground|startService",
|
|
65
|
+
description="Auto-starting service detected",
|
|
66
|
+
severity=SignatureSeverity.MEDIUM,
|
|
67
|
+
),
|
|
68
|
+
Signature(
|
|
69
|
+
name="ScreenOnReceiver",
|
|
70
|
+
pattern="android.intent.action.SCREEN_ON",
|
|
71
|
+
description="Receiver triggered on screen activation",
|
|
72
|
+
severity=SignatureSeverity.MEDIUM,
|
|
73
|
+
),
|
|
74
|
+
Signature(
|
|
75
|
+
name="JobScheduler",
|
|
76
|
+
pattern="JobScheduler|JobService",
|
|
77
|
+
description="Job scheduling mechanism for background tasks",
|
|
78
|
+
severity=SignatureSeverity.MEDIUM,
|
|
79
|
+
),
|
|
80
|
+
Signature(
|
|
81
|
+
name="WorkManager",
|
|
82
|
+
pattern="WorkManager|Worker",
|
|
83
|
+
description="WorkManager background task execution",
|
|
84
|
+
severity=SignatureSeverity.MEDIUM,
|
|
85
|
+
),
|
|
86
|
+
Signature(
|
|
87
|
+
name="NativeExecution",
|
|
88
|
+
pattern="System\\.load|Runtime\\.exec",
|
|
89
|
+
description="Native code or shell command execution",
|
|
90
|
+
severity=SignatureSeverity.HIGH,
|
|
91
|
+
),
|
|
92
|
+
Signature(
|
|
93
|
+
name="HiddenComponent",
|
|
94
|
+
pattern="android:enabled=\"false\"",
|
|
95
|
+
description="Disabled component that could be enabled at runtime",
|
|
96
|
+
severity=SignatureSeverity.MEDIUM,
|
|
97
|
+
),
|
|
98
|
+
Signature(
|
|
99
|
+
name="KernelHook",
|
|
100
|
+
pattern="ptrace|syscall|mmap.*exec",
|
|
101
|
+
description="Potential kernel-level hook or manipulation",
|
|
102
|
+
severity=SignatureSeverity.CRITICAL,
|
|
103
|
+
),
|
|
104
|
+
Signature(
|
|
105
|
+
name="ObfuscationIndicator",
|
|
106
|
+
pattern="aaaa|bbbb|cccc|\\u[0-9a-f]{4}",
|
|
107
|
+
description="Code obfuscation indicators",
|
|
108
|
+
severity=SignatureSeverity.MEDIUM,
|
|
109
|
+
),
|
|
110
|
+
Signature(
|
|
111
|
+
name="ReflectionUsage",
|
|
112
|
+
pattern="reflect|getMethod|invoke|forName",
|
|
113
|
+
description="Reflection-based code execution",
|
|
114
|
+
severity=SignatureSeverity.MEDIUM,
|
|
115
|
+
),
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
def match_signatures(self, data: str) -> List[Tuple[Signature, List[str]]]:
|
|
119
|
+
"""
|
|
120
|
+
Match signatures in provided data.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
data: Text data to scan
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
List of (Signature, matches) tuples
|
|
127
|
+
"""
|
|
128
|
+
results = []
|
|
129
|
+
|
|
130
|
+
for signature in self.signatures:
|
|
131
|
+
matches = self._find_pattern(data, signature.pattern)
|
|
132
|
+
if matches:
|
|
133
|
+
results.append((signature, matches))
|
|
134
|
+
|
|
135
|
+
return results
|
|
136
|
+
|
|
137
|
+
def _find_pattern(self, text: str, pattern: str) -> List[str]:
|
|
138
|
+
"""
|
|
139
|
+
Find all occurrences of pattern in text.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
text: Text to search
|
|
143
|
+
pattern: Pattern to find
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of matching strings
|
|
147
|
+
"""
|
|
148
|
+
import re
|
|
149
|
+
try:
|
|
150
|
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
|
151
|
+
return matches
|
|
152
|
+
except re.error:
|
|
153
|
+
# If regex fails, try literal string match
|
|
154
|
+
if pattern in text:
|
|
155
|
+
return [pattern]
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
def get_risk_score(self, matches: List[Tuple[Signature, List[str]]]) -> float:
|
|
159
|
+
"""
|
|
160
|
+
Calculate risk score based on signature matches.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
matches: Matched signatures
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Risk score from 0 to 100
|
|
167
|
+
"""
|
|
168
|
+
if not matches:
|
|
169
|
+
return 0.0
|
|
170
|
+
|
|
171
|
+
max_severity = max(m[0].severity.value for m in matches)
|
|
172
|
+
match_count = sum(len(m[1]) for m in matches)
|
|
173
|
+
|
|
174
|
+
base_score = (max_severity / 5) * 100
|
|
175
|
+
bonus = min(match_count * 5, 30) # Cap bonus at 30
|
|
176
|
+
|
|
177
|
+
return min(base_score + bonus, 100)
|
|
178
|
+
|
|
179
|
+
def generate_report(self, matches: List[Tuple[Signature, List[str]]]) -> str:
|
|
180
|
+
"""
|
|
181
|
+
Generate human-readable report of matches.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
matches: Matched signatures
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Formatted report string
|
|
188
|
+
"""
|
|
189
|
+
lines = ["Signature Matching Report", "=" * 50]
|
|
190
|
+
|
|
191
|
+
risk_score = self.get_risk_score(matches)
|
|
192
|
+
lines.append(f"Overall Risk Score: {risk_score:.1f}/100")
|
|
193
|
+
lines.append("")
|
|
194
|
+
|
|
195
|
+
if not matches:
|
|
196
|
+
lines.append("No suspicious signatures detected.")
|
|
197
|
+
else:
|
|
198
|
+
lines.append(f"Found {len(matches)} signature matches:\n")
|
|
199
|
+
|
|
200
|
+
for signature, match_list in sorted(
|
|
201
|
+
matches,
|
|
202
|
+
key=lambda x: x[0].severity.value,
|
|
203
|
+
reverse=True
|
|
204
|
+
):
|
|
205
|
+
lines.append(f"[{signature.severity.name}] {signature.name}")
|
|
206
|
+
lines.append(f" Description: {signature.description}")
|
|
207
|
+
lines.append(f" Matches: {len(match_list)}")
|
|
208
|
+
if len(match_list) <= 3:
|
|
209
|
+
for match in match_list:
|
|
210
|
+
lines.append(f" - {match}")
|
|
211
|
+
else:
|
|
212
|
+
for match in match_list[:3]:
|
|
213
|
+
lines.append(f" - {match}")
|
|
214
|
+
lines.append(f" ... and {len(match_list) - 3} more")
|
|
215
|
+
lines.append("")
|
|
216
|
+
|
|
217
|
+
return "\n".join(lines)
|