tkmaster1 commited on
Commit
9cf9341
·
verified ·
1 Parent(s): 0b9b626

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +432 -0
app.py ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ APktool MCP Server - Gradio Space
4
+ Expõe funcionalidades do Apktool como MCP server para HuggingChat
5
+ """
6
+
7
+ import gradio as gr
8
+ import subprocess
9
+ import tempfile
10
+ import shutil
11
+ import os
12
+ from pathlib import Path
13
+ import json
14
+ import logging
15
+
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Diretório de trabalho persistente
20
+ WORK_DIR = Path("/tmp/apktool_workspace")
21
+ WORK_DIR.mkdir(exist_ok=True)
22
+
23
+
24
+ def decode_apk(apk_file, output_name: str = "", force: bool = False, no_res: bool = False, no_src: bool = False) -> str:
25
+ """
26
+ Decompile an APK file to extract resources, manifest, and smali code.
27
+
28
+ Args:
29
+ apk_file: The APK file to decompile
30
+ output_name: Output directory name (optional, defaults to APK name)
31
+ force: Force overwrite existing directory
32
+ no_res: Do not decode resources
33
+ no_src: Do not decode sources (smali)
34
+
35
+ Returns:
36
+ JSON string with decompiled directory path and status
37
+ """
38
+ if apk_file is None:
39
+ return json.dumps({"error": "No APK file provided"})
40
+
41
+ try:
42
+ apk_path = Path(apk_file)
43
+
44
+ # Determinar diretório de saída
45
+ if output_name:
46
+ output_dir = WORK_DIR / output_name
47
+ else:
48
+ output_dir = WORK_DIR / apk_path.stem
49
+
50
+ # Construir comando
51
+ cmd = ["apktool", "d", str(apk_path), "-o", str(output_dir)]
52
+ if force:
53
+ cmd.append("-f")
54
+ if no_res:
55
+ cmd.append("-r")
56
+ if no_src:
57
+ cmd.append("-s")
58
+
59
+ # Executar
60
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
61
+
62
+ if result.returncode == 0:
63
+ return json.dumps({
64
+ "success": True,
65
+ "output_dir": str(output_dir),
66
+ "message": f"APK decompiled successfully to {output_dir}",
67
+ "stdout": result.stdout
68
+ })
69
+ else:
70
+ return json.dumps({
71
+ "success": False,
72
+ "error": result.stderr,
73
+ "message": "Failed to decompile APK"
74
+ })
75
+ except subprocess.TimeoutExpired:
76
+ return json.dumps({"success": False, "error": "Operation timed out"})
77
+ except Exception as e:
78
+ return json.dumps({"success": False, "error": str(e)})
79
+
80
+
81
+ def build_apk(source_dir: str, output_path: str = "") -> str:
82
+ """
83
+ Recompile/build an APK from decompiled source directory.
84
+
85
+ Args:
86
+ source_dir: Path to the decompiled source directory
87
+ output_path: Output APK path (optional)
88
+
89
+ Returns:
90
+ JSON string with built APK path and status
91
+ """
92
+ try:
93
+ src_path = WORK_DIR / source_dir if not source_dir.startswith("/") else Path(source_dir)
94
+
95
+ if not src_path.exists():
96
+ return json.dumps({"success": False, "error": f"Source directory not found: {src_path}"})
97
+
98
+ cmd = ["apktool", "b", str(src_path)]
99
+ if output_path:
100
+ cmd.extend(["-o", output_path])
101
+
102
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
103
+
104
+ if result.returncode == 0:
105
+ built_apk = src_path / "dist" / f"{src_path.name}.apk"
106
+ return json.dumps({
107
+ "success": True,
108
+ "apk_path": str(built_apk) if built_apk.exists() else "Unknown",
109
+ "message": "APK built successfully",
110
+ "stdout": result.stdout
111
+ })
112
+ else:
113
+ return json.dumps({
114
+ "success": False,
115
+ "error": result.stderr,
116
+ "message": "Failed to build APK"
117
+ })
118
+ except Exception as e:
119
+ return json.dumps({"success": False, "error": str(e)})
120
+
121
+
122
+ def analyze_manifest(apk_name: str) -> str:
123
+ """
124
+ Parse AndroidManifest.xml for permissions and components.
125
+
126
+ Args:
127
+ apk_name: Name of the decompiled APK directory
128
+
129
+ Returns:
130
+ JSON string with manifest analysis
131
+ """
132
+ try:
133
+ manifest_path = WORK_DIR / apk_name / "AndroidManifest.xml"
134
+
135
+ if not manifest_path.exists():
136
+ return json.dumps({"success": False, "error": f"Manifest not found. Decompile APK first."})
137
+
138
+ with open(manifest_path, 'r') as f:
139
+ content = f.read()
140
+
141
+ # Extrair informações básicas
142
+ import re
143
+
144
+ package = re.search(r'package="([^"]+)"', content)
145
+ permissions = re.findall(r'<uses-permission[^>]*android:name="([^"]+)"', content)
146
+ activities = re.findall(r'<activity[^>]*android:name="([^"]+)"', content)
147
+ services = re.findall(r'<service[^>]*android:name="([^"]+)"', content)
148
+ receivers = re.findall(r'<receiver[^>]*android:name="([^"]+)"', content)
149
+
150
+ return json.dumps({
151
+ "success": True,
152
+ "package": package.group(1) if package else "Unknown",
153
+ "permissions": permissions,
154
+ "activities": activities,
155
+ "services": services,
156
+ "receivers": receivers,
157
+ "permission_count": len(permissions),
158
+ "component_count": len(activities) + len(services) + len(receivers)
159
+ }, indent=2)
160
+ except Exception as e:
161
+ return json.dumps({"success": False, "error": str(e)})
162
+
163
+
164
+ def list_permissions(apk_name: str) -> str:
165
+ """
166
+ List all permissions requested by an APK.
167
+
168
+ Args:
169
+ apk_name: Name of the decompiled APK directory
170
+
171
+ Returns:
172
+ JSON string with permissions list
173
+ """
174
+ try:
175
+ manifest_path = WORK_DIR / apk_name / "AndroidManifest.xml"
176
+
177
+ if not manifest_path.exists():
178
+ return json.dumps({"success": False, "error": "Manifest not found. Decompile APK first."})
179
+
180
+ with open(manifest_path, 'r') as f:
181
+ content = f.read()
182
+
183
+ import re
184
+ permissions = re.findall(r'<uses-permission[^>]*android:name="([^"]+)"', content)
185
+
186
+ # Categorizar permissões perigosas
187
+ dangerous_permissions = [
188
+ "READ_CONTACTS", "WRITE_CONTACTS", "READ_CALENDAR", "WRITE_CALENDAR",
189
+ "CAMERA", "READ_CALL_LOG", "WRITE_CALL_LOG", "PROCESS_OUTGOING_CALLS",
190
+ "ACCESS_FINE_LOCATION", "ACCESS_COARSE_LOCATION", "RECORD_AUDIO",
191
+ "READ_PHONE_STATE", "CALL_PHONE", "ANSWER_PHONE_CALLS",
192
+ "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE", "SEND_SMS", "RECEIVE_SMS"
193
+ ]
194
+
195
+ dangerous = []
196
+ normal = []
197
+
198
+ for p in permissions:
199
+ is_dangerous = any(d in p.upper() for d in dangerous_permissions)
200
+ if is_dangerous:
201
+ dangerous.append(p)
202
+ else:
203
+ normal.append(p)
204
+
205
+ return json.dumps({
206
+ "success": True,
207
+ "total_permissions": len(permissions),
208
+ "dangerous_permissions": dangerous,
209
+ "normal_permissions": normal,
210
+ "dangerous_count": len(dangerous)
211
+ }, indent=2)
212
+ except Exception as e:
213
+ return json.dumps({"success": False, "error": str(e)})
214
+
215
+
216
+ def extract_strings(apk_name: str, locale: str = "") -> str:
217
+ """
218
+ Extract string resources from a decompiled APK.
219
+
220
+ Args:
221
+ apk_name: Name of the decompiled APK directory
222
+ locale: Locale to extract (e.g., 'en', 'pt'). Empty for default.
223
+
224
+ Returns:
225
+ JSON string with extracted strings
226
+ """
227
+ try:
228
+ if locale:
229
+ strings_path = WORK_DIR / apk_name / "res" / f"values-{locale}" / "strings.xml"
230
+ else:
231
+ strings_path = WORK_DIR / apk_name / "res" / "values" / "strings.xml"
232
+
233
+ if not strings_path.exists():
234
+ return json.dumps({"success": False, "error": f"Strings file not found at {strings_path}"})
235
+
236
+ with open(strings_path, 'r') as f:
237
+ content = f.read()
238
+
239
+ import re
240
+ strings = re.findall(r'<string name="([^"]+)"[^>]*>([^<]*)</string>', content)
241
+
242
+ return json.dumps({
243
+ "success": True,
244
+ "file_path": str(strings_path),
245
+ "string_count": len(strings),
246
+ "strings": {name: value for name, value in strings[:50]} # Limitar a 50
247
+ }, indent=2)
248
+ except Exception as e:
249
+ return json.dumps({"success": False, "error": str(e)})
250
+
251
+
252
+ def find_smali_references(apk_name: str, pattern: str) -> str:
253
+ """
254
+ Search for patterns in decompiled smali code.
255
+
256
+ Args:
257
+ apk_name: Name of the decompiled APK directory
258
+ pattern: Search pattern (regex supported)
259
+
260
+ Returns:
261
+ JSON string with search results
262
+ """
263
+ try:
264
+ smali_dir = WORK_DIR / apk_name / "smali"
265
+
266
+ if not smali_dir.exists():
267
+ return json.dumps({"success": False, "error": "Smali directory not found. Decompile APK first."})
268
+
269
+ import re
270
+ results = []
271
+ regex = re.compile(pattern, re.IGNORECASE)
272
+
273
+ for smali_file in smali_dir.rglob("*.smali"):
274
+ try:
275
+ with open(smali_file, 'r', errors='ignore') as f:
276
+ for i, line in enumerate(f, 1):
277
+ if regex.search(line):
278
+ results.append({
279
+ "file": str(smali_file.relative_to(smali_dir)),
280
+ "line": i,
281
+ "content": line.strip()[:200]
282
+ })
283
+ if len(results) >= 100: # Limitar resultados
284
+ break
285
+ except:
286
+ continue
287
+ if len(results) >= 100:
288
+ break
289
+
290
+ return json.dumps({
291
+ "success": True,
292
+ "pattern": pattern,
293
+ "matches": len(results),
294
+ "results": results
295
+ }, indent=2)
296
+ except Exception as e:
297
+ return json.dumps({"success": False, "error": str(e)})
298
+
299
+
300
+ def get_apk_info(apk_file) -> str:
301
+ """
302
+ Get basic APK metadata and information.
303
+
304
+ Args:
305
+ apk_file: The APK file to analyze
306
+
307
+ Returns:
308
+ JSON string with APK information
309
+ """
310
+ if apk_file is None:
311
+ return json.dumps({"error": "No APK file provided"})
312
+
313
+ try:
314
+ apk_path = Path(apk_file)
315
+
316
+ # Usar aapt para info básica
317
+ result = subprocess.run(
318
+ ["aapt", "dump", "badging", str(apk_path)],
319
+ capture_output=True, text=True, timeout=60
320
+ )
321
+
322
+ info = {
323
+ "success": True,
324
+ "file_name": apk_path.name,
325
+ "file_size_mb": round(apk_path.stat().st_size / (1024 * 1024), 2),
326
+ }
327
+
328
+ if result.returncode == 0:
329
+ import re
330
+ package = re.search(r"package: name='([^']+)'", result.stdout)
331
+ version = re.search(r"versionName='([^']+)'", result.stdout)
332
+ sdk = re.search(r"targetSdkVersion:'([^']+)'", result.stdout)
333
+
334
+ if package:
335
+ info["package_name"] = package.group(1)
336
+ if version:
337
+ info["version"] = version.group(1)
338
+ if sdk:
339
+ info["target_sdk"] = sdk.group(1)
340
+
341
+ return json.dumps(info, indent=2)
342
+ except Exception as e:
343
+ return json.dumps({"success": False, "error": str(e)})
344
+
345
+
346
+ def list_decompiled() -> str:
347
+ """
348
+ List all decompiled APKs in the workspace.
349
+
350
+ Returns:
351
+ JSON string with list of decompiled APKs
352
+ """
353
+ try:
354
+ decompiled = []
355
+ for item in WORK_DIR.iterdir():
356
+ if item.is_dir() and (item / "AndroidManifest.xml").exists():
357
+ decompiled.append(item.name)
358
+
359
+ return json.dumps({
360
+ "success": True,
361
+ "workspace": str(WORK_DIR),
362
+ "decompiled_apks": decompiled,
363
+ "count": len(decompiled)
364
+ }, indent=2)
365
+ except Exception as e:
366
+ return json.dumps({"success": False, "error": str(e)})
367
+
368
+
369
+ # Criar a interface Gradio
370
+ with gr.Blocks(title="APktool MCP Server") as demo:
371
+ gr.Markdown("# 🔧 APktool MCP Server")
372
+ gr.Markdown("Android APK analysis tools exposed as MCP for HuggingChat")
373
+
374
+ with gr.Tab("Decode APK"):
375
+ decode_input = gr.File(label="APK File", file_types=[".apk"])
376
+ decode_name = gr.Textbox(label="Output Directory Name (optional)")
377
+ with gr.Row():
378
+ decode_force = gr.Checkbox(label="Force Overwrite", value=False)
379
+ decode_no_res = gr.Checkbox(label="Skip Resources", value=False)
380
+ decode_no_src = gr.Checkbox(label="Skip Sources", value=False)
381
+ decode_btn = gr.Button("Decode", variant="primary")
382
+ decode_output = gr.Textbox(label="Result", lines=10)
383
+ decode_btn.click(decode_apk, [decode_input, decode_name, decode_force, decode_no_res, decode_no_src], decode_output)
384
+
385
+ with gr.Tab("Build APK"):
386
+ build_dir = gr.Textbox(label="Source Directory Name")
387
+ build_output = gr.Textbox(label="Output APK Path (optional)")
388
+ build_btn = gr.Button("Build", variant="primary")
389
+ build_result = gr.Textbox(label="Result", lines=10)
390
+ build_btn.click(build_apk, [build_dir, build_output], build_result)
391
+
392
+ with gr.Tab("Analyze"):
393
+ analyze_apk = gr.Textbox(label="Decompiled APK Name")
394
+ analyze_btn = gr.Button("Analyze Manifest", variant="primary")
395
+ analyze_output = gr.Textbox(label="Result", lines=15)
396
+ analyze_btn.click(analyze_manifest, [analyze_apk], analyze_output)
397
+
398
+ with gr.Tab("Permissions"):
399
+ perm_apk = gr.Textbox(label="Decompiled APK Name")
400
+ perm_btn = gr.Button("List Permissions", variant="primary")
401
+ perm_output = gr.Textbox(label="Result", lines=15)
402
+ perm_btn.click(list_permissions, [perm_apk], perm_output)
403
+
404
+ with gr.Tab("Strings"):
405
+ strings_apk = gr.Textbox(label="Decompiled APK Name")
406
+ strings_locale = gr.Textbox(label="Locale (e.g., 'en', 'pt')", value="")
407
+ strings_btn = gr.Button("Extract Strings", variant="primary")
408
+ strings_output = gr.Textbox(label="Result", lines=15)
409
+ strings_btn.click(extract_strings, [strings_apk, strings_locale], strings_output)
410
+
411
+ with gr.Tab("Search Smali"):
412
+ search_apk = gr.Textbox(label="Decompiled APK Name")
413
+ search_pattern = gr.Textbox(label="Search Pattern (regex)", placeholder="e.g., api_key, password, http")
414
+ search_btn = gr.Button("Search", variant="primary")
415
+ search_output = gr.Textbox(label="Result", lines=15)
416
+ search_btn.click(find_smali_references, [search_apk, search_pattern], search_output)
417
+
418
+ with gr.Tab("APK Info"):
419
+ info_file = gr.File(label="APK File", file_types=[".apk"])
420
+ info_btn = gr.Button("Get Info", variant="primary")
421
+ info_output = gr.Textbox(label="Result", lines=10)
422
+ info_btn.click(get_apk_info, [info_file], info_output)
423
+
424
+ with gr.Tab("Workspace"):
425
+ list_btn = gr.Button("List Decompiled APKs", variant="primary")
426
+ list_output = gr.Textbox(label="Workspace Contents", lines=10)
427
+ list_btn.click(list_decompiled, [], list_output)
428
+
429
+
430
+ # Lançar como MCP server!
431
+ if __name__ == "__main__":
432
+ demo.launch(mcp_server=True)