Skydata001 commited on
Commit
e7bb0c8
·
verified ·
1 Parent(s): 1c7cff5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +267 -92
app.py CHANGED
@@ -1,34 +1,40 @@
1
  import os
2
  import re
3
  import json
 
 
 
4
  import asyncio
5
  from datetime import datetime
6
  from typing import List, Dict, Any
7
 
8
- from fastapi import FastAPI, UploadFile, File, Request, WebSocket, WebSocketDisconnect
9
  from fastapi.responses import HTMLResponse, FileResponse
10
  from fastapi.templating import Jinja2Templates
11
 
12
- app = FastAPI(title="Multi-SO Binary String Extractor")
13
 
14
- WORKSPACE_DIR = "/tmp/so_workspace"
15
- OUTPUT_DIR = "/tmp/so_output"
16
  os.makedirs(WORKSPACE_DIR, exist_ok=True)
17
  os.makedirs(OUTPUT_DIR, exist_ok=True)
18
 
19
  templates = Jinja2Templates(directory="templates")
20
 
21
- # الكلمات المفتاحية المطلوبة للبحث
22
- TARGET_KEYWORDS = [
23
- "https://stuartmods.xyz",
24
- "discord",
25
- "discord.gg",
26
- "stuartmods.xyz",
27
- "stuartmods",
28
- "StURTMODS",
29
- ".xyz"
 
30
  ]
31
 
 
 
32
  class ConnectionManager:
33
  def __init__(self):
34
  self.active_connections: List[WebSocket] = []
@@ -41,11 +47,12 @@ class ConnectionManager:
41
  if websocket in self.active_connections:
42
  self.active_connections.remove(websocket)
43
 
44
- async def broadcast_log(self, level: str, message: str):
45
  log_entry = {
46
  "timestamp": datetime.now().strftime("%H:%M:%S"),
47
  "level": level,
48
- "message": message
 
49
  }
50
  for connection in self.active_connections:
51
  try:
@@ -55,20 +62,54 @@ class ConnectionManager:
55
 
56
  manager = ConnectionManager()
57
 
58
- def extract_strings_from_binary(file_path: str, min_length: int = 3) -> List[str]:
59
- """
60
- استخراج كافة النصوص القابلة للقراءة (ASCII & UTF-8) من الملف الثنائي .so
61
- """
62
- strings_found = []
63
- with open(file_path, "rb") as f:
64
- content = f.read()
65
- pattern = re.compile(rb'[\x20-\x7e]{' + str(min_length).encode() + rb',}')
66
- for match in pattern.finditer(content):
67
  try:
68
- strings_found.append(match.group().decode("ascii"))
69
- except UnicodeDecodeError:
70
- pass
71
- return strings_found
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  @app.get("/", response_class=HTMLResponse)
74
  async def serve_index(request: Request):
@@ -83,75 +124,209 @@ async def websocket_logs(websocket: WebSocket):
83
  except WebSocketDisconnect:
84
  manager.disconnect(websocket)
85
 
86
- @app.post("/analyze-so")
87
- async def analyze_multiple_so(files: List[UploadFile] = File(...)):
88
- so_files = [f for f in files if f.filename.endswith(".so")]
89
- if not so_files:
90
- return {"status": "error", "message": "الرجاء اختيار ملفات بصيغة .so فقط"}
91
-
92
- await manager.broadcast_log("INFO", f"بدء استقبال {len(so_files)} ملف/ملفات .so...")
93
-
94
- all_file_results = []
95
- overall_keyword_summary = {kw: 0 for kw in TARGET_KEYWORDS}
96
- total_matches_all_files = 0
97
-
98
- for file in so_files:
99
- file_path = os.path.join(WORKSPACE_DIR, file.filename)
100
- content = await file.read()
101
- with open(file_path, "wb") as buffer:
102
- buffer.write(content)
103
-
104
- file_size_mb = round(len(content) / (1024 * 1024), 2)
105
- await manager.broadcast_log("INFO", f"جاري فحص: {file.filename} ({file_size_mb} MB)...")
106
-
107
- # استخراج النصوص
108
- extracted_strings = extract_strings_from_binary(file_path)
109
- matches_by_kw = {kw: [] for kw in TARGET_KEYWORDS}
110
- file_matches_count = 0
111
-
112
- for s in extracted_strings:
113
- for kw in TARGET_KEYWORDS:
114
- if kw.lower() in s.lower():
115
- matches_by_kw[kw].append(s)
116
- file_matches_count += 1
117
- overall_keyword_summary[kw] += 1
118
-
119
- total_matches_all_files += file_matches_count
120
-
121
- await manager.broadcast_log(
122
- "SUCCESS" if file_matches_count > 0 else "INFO",
123
- f"الملف {file.filename}: تم استخراج {len(extracted_strings)} نص والعثور على {file_matches_count} تطابق."
124
- )
125
-
126
- all_file_results.append({
127
- "filename": file.filename,
128
- "file_size": f"{file_size_mb} MB",
129
- "total_strings": len(extracted_strings),
130
- "file_matches_count": file_matches_count,
131
- "matches": matches_by_kw
132
- })
133
-
134
- await manager.broadcast_log("SUCCESS", f"اكتمل الفحص بالكامل! إجمالي التطابقات لجميع الملفات: {total_matches_all_files}")
135
-
136
- # حفظ النتيجة في ملف JSON شامل
137
- result_data = {
138
- "total_files_scanned": len(so_files),
139
- "total_matches_all_files": total_matches_all_files,
140
- "overall_keyword_summary": overall_keyword_summary,
141
- "files_details": all_file_results
142
  }
143
 
144
- report_path = os.path.join(OUTPUT_DIR, "scan_result.json")
145
- with open(report_path, "w", encoding="utf-8") as f:
146
- json.dump(result_data, f, ensure_ascii=False, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
- return {"status": "success", "data": result_data}
 
149
 
150
  @app.get("/download-report")
151
- async def download_report():
152
- report_path = os.path.join(OUTPUT_DIR, "scan_result.json")
153
- if os.path.exists(report_path):
154
- return FileResponse(path=report_path, filename="multi_so_scan_report.json", media_type="application/json")
 
 
155
  return {"error": "الملف غير موجود"}
156
 
157
  if __name__ == "__main__":
 
1
  import os
2
  import re
3
  import json
4
+ import shutil
5
+ import zipfile
6
+ import base64
7
  import asyncio
8
  from datetime import datetime
9
  from typing import List, Dict, Any
10
 
11
+ from fastapi import FastAPI, UploadFile, File, Form, Request, WebSocket, WebSocketDisconnect
12
  from fastapi.responses import HTMLResponse, FileResponse
13
  from fastapi.templating import Jinja2Templates
14
 
15
+ app = FastAPI(title="Decompiled APK ZIP Analyzer")
16
 
17
+ WORKSPACE_DIR = "/tmp/zip_workspace"
18
+ OUTPUT_DIR = "/tmp/zip_output"
19
  os.makedirs(WORKSPACE_DIR, exist_ok=True)
20
  os.makedirs(OUTPUT_DIR, exist_ok=True)
21
 
22
  templates = Jinja2Templates(directory="templates")
23
 
24
+ # تعابير نمطية لفك الأنماط (Regex Patterns)
25
+ URL_PATTERN = re.compile(r'https?://[^\s<>"{}|\\^`]+|www\.[^\s<>"{}|\\^`]+')
26
+ EMAIL_PATTERN = re.compile(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}')
27
+ BASE64_PATTERN = re.compile(r'(?:[A-Za-z0-9+/]{4}){8,}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?')
28
+ HEX_PATTERN = re.compile(r'\b[0-9a-fA-F]{32,}\b')
29
+
30
+ # تحديد الأولوية بناءً على الكلمات المفتاحية
31
+ HIGH_PRIORITY_KEYWORDS = [
32
+ "discord.gg", "discord.com/invite", "stuartmods.xyz", "stuartmods",
33
+ "license", "activation", "verify", "key", "password", "token"
34
  ]
35
 
36
+ MEDIUM_PRIORITY_KEYWORDS = ["api.", "http://", "https://", "secret", "auth"]
37
+
38
  class ConnectionManager:
39
  def __init__(self):
40
  self.active_connections: List[WebSocket] = []
 
47
  if websocket in self.active_connections:
48
  self.active_connections.remove(websocket)
49
 
50
+ async def broadcast_log(self, level: str, message: str, progress: int = 0):
51
  log_entry = {
52
  "timestamp": datetime.now().strftime("%H:%M:%S"),
53
  "level": level,
54
+ "message": message,
55
+ "progress": progress
56
  }
57
  for connection in self.active_connections:
58
  try:
 
62
 
63
  manager = ConnectionManager()
64
 
65
+ def clean_directories():
66
+ """تنظيف المجلدات المؤقتة لمنع الامتلاء"""
67
+ for folder in [WORKSPACE_DIR, OUTPUT_DIR]:
68
+ for filename in os.listdir(folder):
69
+ file_path = os.path.join(folder, filename)
 
 
 
 
70
  try:
71
+ if os.path.isfile(file_path) or os.path.islink(file_path):
72
+ os.unlink(file_path)
73
+ elif os.path.isdir(file_path):
74
+ shutil.rmtree(file_path)
75
+ except Exception as e:
76
+ print(f"Error cleaning {file_path}: {e}")
77
+
78
+ def determine_severity(match_text: str) -> str:
79
+ """تحديد مستوى خطورة/أهمية النتيجة"""
80
+ text_lower = match_text.lower()
81
+ for kw in HIGH_PRIORITY_KEYWORDS:
82
+ if kw in text_lower:
83
+ return "HIGH"
84
+ for kw in MEDIUM_PRIORITY_KEYWORDS:
85
+ if kw in text_lower:
86
+ return "MEDIUM"
87
+ return "LOW"
88
+
89
+ def extract_snippet(text: str, start_idx: int, end_idx: int, margin: int = 30) -> str:
90
+ """استخراج مقتطف نصي حول النتيجة مع سياقها"""
91
+ start = max(0, start_idx - margin)
92
+ end = min(len(text), end_idx + margin)
93
+ snippet = text[start:end].replace('\n', ' ').replace('\r', '')
94
+ return f"...{snippet}..."
95
+
96
+ def decode_base64_strings(text: str) -> List[Dict[str, str]]:
97
+ """البحث عن نصوص Base64 ومحاولة فك تشفيرها"""
98
+ decoded_results = []
99
+ matches = BASE64_PATTERN.findall(text)
100
+ for b64_str in set(matches[:20]): # الحد الأقصى 20 لكل ملف للأداء
101
+ try:
102
+ decoded_bytes = base64.b64decode(b64_str)
103
+ decoded_text = decoded_bytes.decode('utf-8', errors='ignore')
104
+ # التأكد من أن النص المفكوك يحتوي على أحرف قابلة للقراءة
105
+ if len(decoded_text.strip()) > 6 and any(c.isalnum() for c in decoded_text):
106
+ decoded_results.append({
107
+ "original": b64_str,
108
+ "decoded": decoded_text
109
+ })
110
+ except Exception:
111
+ pass
112
+ return decoded_results
113
 
114
  @app.get("/", response_class=HTMLResponse)
115
  async def serve_index(request: Request):
 
124
  except WebSocketDisconnect:
125
  manager.disconnect(websocket)
126
 
127
+ @app.post("/analyze-zip")
128
+ async def analyze_zip(
129
+ file: UploadFile = File(...),
130
+ keywords: str = Form("discord.gg, discord.com/invite, stuartmods.xyz, stuartmods, license, activation, verify, key, password")
131
+ ):
132
+ if not file.filename.endswith(".zip"):
133
+ return {"status": "error", "message": "الرجاء رفع ملف بصيغة .zip فقط"}
134
+
135
+ clean_directories()
136
+
137
+ zip_path = os.path.join(WORKSPACE_DIR, "target.zip")
138
+ extract_folder = os.path.join(WORKSPACE_DIR, "extracted")
139
+ os.makedirs(extract_folder, exist_ok=True)
140
+
141
+ # 1. حفظ ملف الـ ZIP
142
+ await manager.broadcast_log("INFO", f"جاري استقبال الملف: {file.filename}...", 5)
143
+ with open(zip_path, "wb") as buffer:
144
+ while chunk := await file.read(1024 * 1024):
145
+ buffer.write(chunk)
146
+
147
+ # 2. فك الضغط
148
+ await manager.broadcast_log("INFO", "جاري فك ضغط المحتويات...", 15)
149
+ try:
150
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
151
+ zip_file_list = zip_ref.namelist()
152
+ zip_ref.extractall(extract_folder)
153
+ await manager.broadcast_log("SUCCESS", f"تم فك الضغط بنجاح! إجمالي الملفات الداخلية: {len(zip_file_list)}", 25)
154
+ except Exception as e:
155
+ return {"status": "error", "message": f"فشل فك ضغط الملف: {str(e)}"}
156
+
157
+ # 3. معالجة قائمة الكلمات المفتاحية من المستخدم
158
+ user_keywords = [k.strip().lower() for k in keywords.split(",") if k.strip()]
159
+
160
+ # 4. تجميع الملفات المستهدفة
161
+ target_extensions = ('.java', '.smali', '.xml', '.json', '.txt', '.html', '.properties', '.so')
162
+ files_to_scan = []
163
+
164
+ for root, _, files in os.walk(extract_folder):
165
+ for f in files:
166
+ if f.endswith(target_extensions):
167
+ files_to_scan.append(os.path.join(root, f))
168
+
169
+ total_files = len(files_to_scan)
170
+ await manager.broadcast_log("INFO", f"بدء الفحص العميق لـ {total_files} ملف مخصص...", 30)
171
+
172
+ results_by_file = []
173
+ summary_stats = {
174
+ "total_files_scanned": total_files,
175
+ "total_matches": 0,
176
+ "high_severity_count": 0,
177
+ "medium_severity_count": 0,
178
+ "low_severity_count": 0,
179
+ "keywords_found": {kw: 0 for kw in user_keywords}
 
 
 
180
  }
181
 
182
+ # 5. الفحص المباشر والسريع للملفات
183
+ for idx, file_full_path in enumerate(files_to_scan):
184
+ relative_path = os.path.relpath(file_full_path, extract_folder)
185
+
186
+ # بث التقدم كل 20 ملف أو عند آخر ملف
187
+ if idx % 25 == 0 or idx == total_files - 1:
188
+ progress_pct = 30 + int((idx / total_files) * 60)
189
+ await manager.broadcast_log(
190
+ "INFO",
191
+ f"جاري فحص الملف ({idx + 1}/{total_files}): {relative_path}",
192
+ progress_pct
193
+ )
194
+ await asyncio.sleep(0.001) # للسماح برفع السجلات عبر Event Loop
195
+
196
+ try:
197
+ with open(file_full_path, 'r', encoding='utf-8', errors='ignore') as f:
198
+ content = f.read()
199
+
200
+ file_matches = []
201
+
202
+ # أ) البحث عن الكلمات المفتاحية
203
+ content_lower = content.lower()
204
+ for kw in user_keywords:
205
+ if kw in content_lower:
206
+ for match in re.finditer(re.escape(kw), content_lower):
207
+ start, end = match.span()
208
+ snippet = extract_snippet(content, start, end)
209
+ severity = determine_severity(kw)
210
+
211
+ file_matches.append({
212
+ "type": "KEYWORD",
213
+ "match": kw,
214
+ "snippet": snippet,
215
+ "severity": severity
216
+ })
217
+ summary_stats["keywords_found"][kw] += 1
218
+ summary_stats[f"{severity.lower()}_severity_count"] += 1
219
+
220
+ # ب) استخراج الروابط URLs
221
+ urls = URL_PATTERN.findall(content)
222
+ for url in set(urls[:15]): # تجنب التكرار المفرط
223
+ severity = "HIGH" if "discord" in url.lower() or "stuartmods" in url.lower() else "MEDIUM"
224
+ file_matches.append({
225
+ "type": "URL",
226
+ "match": url,
227
+ "snippet": url,
228
+ "severity": severity
229
+ })
230
+ summary_stats[f"{severity.lower()}_severity_count"] += 1
231
+
232
+ # ج) استخراج البريد الإلكتروني Emails
233
+ emails = EMAIL_PATTERN.findall(content)
234
+ for email in set(emails[:10]):
235
+ file_matches.append({
236
+ "type": "EMAIL",
237
+ "match": email,
238
+ "snippet": email,
239
+ "severity": "MEDIUM"
240
+ })
241
+ summary_stats["medium_severity_count"] += 1
242
+
243
+ # د) فك نصوص Base64
244
+ decoded_b64 = decode_base64_strings(content)
245
+ for b64 in decoded_b64:
246
+ file_matches.append({
247
+ "type": "BASE64_DECODED",
248
+ "match": f"{b64['original']} -> {b64['decoded']}",
249
+ "snippet": b64['decoded'],
250
+ "severity": "MEDIUM"
251
+ })
252
+ summary_stats["medium_severity_count"] += 1
253
+
254
+ if file_matches:
255
+ summary_stats["total_matches"] += len(file_matches)
256
+ results_by_file.append({
257
+ "file": relative_path,
258
+ "matches_count": len(file_matches),
259
+ "matches": file_matches
260
+ })
261
+
262
+ except Exception as e:
263
+ pass
264
+
265
+ await manager.broadcast_log("SUCCESS", f"اكتمل الفحص! تم العثور على {summary_stats['total_matches']} نتيجة.", 95)
266
+
267
+ # 6. حفظ التقارير (JSON و HTML)
268
+ report_data = {
269
+ "summary": summary_stats,
270
+ "details": results_by_file
271
+ }
272
+
273
+ json_report_path = os.path.join(OUTPUT_DIR, "analysis_report.json")
274
+ with open(json_report_path, "w", encoding="utf-8") as f:
275
+ json.dump(report_data, f, ensure_ascii=False, indent=2)
276
+
277
+ # توليد التقرير الـ HTML المستقل
278
+ generate_html_report(report_data, os.path.join(OUTPUT_DIR, "analysis_report.html"))
279
+
280
+ await manager.broadcast_log("SUCCESS", "تم إنشاء تقارير JSON و HTML جاهزة للتحميل.", 100)
281
+
282
+ return {"status": "success", "data": report_data}
283
+
284
+ def generate_html_report(data: Dict[str, Any], save_path: str):
285
+ """إنشاء ملف HTML تفاعلي مستقل كتقرير قابل للتحميل"""
286
+ html_content = f"""<!DOCTYPE html>
287
+ <html lang="ar" dir="rtl">
288
+ <head>
289
+ <meta charset="UTF-8">
290
+ <title>APK Analysis Report</title>
291
+ <style>
292
+ body {{ font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 20px; }}
293
+ .card {{ background: #1e293b; padding: 15px; border-radius: 8px; margin-bottom: 15px; border: 1px solid #334155; }}
294
+ h1 {{ color: #38bdf8; font-size: 1.4rem; }}
295
+ .badge {{ padding: 3px 8px; border-radius: 4px; font-weight: bold; font-size: 0.8rem; }}
296
+ .HIGH {{ background: #ef4444; color: white; }}
297
+ .MEDIUM {{ background: #f59e0b; color: black; }}
298
+ .LOW {{ background: #10b981; color: white; }}
299
+ .match-item {{ background: #020617; padding: 8px; border-radius: 4px; margin-top: 6px; font-family: monospace; font-size: 0.85rem; word-break: break-all; }}
300
+ </style>
301
+ </head>
302
+ <body>
303
+ <h1>تقرير فحص ملف APK المفكك</h1>
304
+ <div class="card">
305
+ <h3>الملفات المفحوصة: {data['summary']['total_files_scanned']} | إجمالي النتائج: {data['summary']['total_matches']}</h3>
306
+ <p>عالية الأهمية (HIGH): <span class="badge HIGH">{data['summary']['high_severity_count']}</span> | متوسطة (MEDIUM): <span class="badge MEDIUM">{data['summary']['medium_severity_count']}</span></p>
307
+ </div>
308
+ """
309
+ for file_res in data['details']:
310
+ html_content += f"""<div class="card">
311
+ <h4>📁 {file_res['file']} ({file_res['matches_count']} نتائج)</h4>"""
312
+ for m in file_res['matches']:
313
+ html_content += f"""<div class="match-item">
314
+ <span class="badge {m['severity']}">{m['severity']}</span> <b>[{m['type']}]</b>: {m['snippet']}
315
+ </div>"""
316
+ html_content += "</div>"
317
+
318
+ html_content += "</body></html>"
319
 
320
+ with open(save_path, "w", encoding="utf-8") as f:
321
+ f.write(html_content)
322
 
323
  @app.get("/download-report")
324
+ async def download_report(fmt: str = "json"):
325
+ filename = "analysis_report.json" if fmt == "json" else "analysis_report.html"
326
+ file_path = os.path.join(OUTPUT_DIR, filename)
327
+ if os.path.exists(file_path):
328
+ media_type = "application/json" if fmt == "json" else "text/html"
329
+ return FileResponse(path=file_path, filename=filename, media_type=media_type)
330
  return {"error": "الملف غير موجود"}
331
 
332
  if __name__ == "__main__":