Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| """ | |
| APktool MCP Server - Gradio Space | |
| Expõe funcionalidades do Apktool como MCP server para HuggingChat | |
| """ | |
| import gradio as gr | |
| import subprocess | |
| import tempfile | |
| import shutil | |
| import os | |
| from pathlib import Path | |
| import json | |
| import logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # Diretório de trabalho persistente | |
| WORK_DIR = Path("/tmp/apktool_workspace") | |
| WORK_DIR.mkdir(exist_ok=True) | |
| def decode_apk(apk_file, output_name: str = "", force: bool = False, no_res: bool = False, no_src: bool = False) -> str: | |
| """ | |
| Decompile an APK file to extract resources, manifest, and smali code. | |
| Args: | |
| apk_file: The APK file to decompile | |
| output_name: Output directory name (optional, defaults to APK name) | |
| force: Force overwrite existing directory | |
| no_res: Do not decode resources | |
| no_src: Do not decode sources (smali) | |
| Returns: | |
| JSON string with decompiled directory path and status | |
| """ | |
| if apk_file is None: | |
| return json.dumps({"error": "No APK file provided"}) | |
| try: | |
| apk_path = Path(apk_file) | |
| # Determinar diretório de saída | |
| if output_name: | |
| output_dir = WORK_DIR / output_name | |
| else: | |
| output_dir = WORK_DIR / apk_path.stem | |
| # Construir comando | |
| cmd = ["apktool", "d", str(apk_path), "-o", str(output_dir)] | |
| if force: | |
| cmd.append("-f") | |
| if no_res: | |
| cmd.append("-r") | |
| if no_src: | |
| cmd.append("-s") | |
| # Executar | |
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) | |
| if result.returncode == 0: | |
| return json.dumps({ | |
| "success": True, | |
| "output_dir": str(output_dir), | |
| "message": f"APK decompiled successfully to {output_dir}", | |
| "stdout": result.stdout | |
| }) | |
| else: | |
| return json.dumps({ | |
| "success": False, | |
| "error": result.stderr, | |
| "message": "Failed to decompile APK" | |
| }) | |
| except subprocess.TimeoutExpired: | |
| return json.dumps({"success": False, "error": "Operation timed out"}) | |
| except Exception as e: | |
| return json.dumps({"success": False, "error": str(e)}) | |
| def build_apk(source_dir: str, output_path: str = "") -> str: | |
| """ | |
| Recompile/build an APK from decompiled source directory. | |
| Args: | |
| source_dir: Path to the decompiled source directory | |
| output_path: Output APK path (optional) | |
| Returns: | |
| JSON string with built APK path and status | |
| """ | |
| try: | |
| src_path = WORK_DIR / source_dir if not source_dir.startswith("/") else Path(source_dir) | |
| if not src_path.exists(): | |
| return json.dumps({"success": False, "error": f"Source directory not found: {src_path}"}) | |
| cmd = ["apktool", "b", str(src_path)] | |
| if output_path: | |
| cmd.extend(["-o", output_path]) | |
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) | |
| if result.returncode == 0: | |
| built_apk = src_path / "dist" / f"{src_path.name}.apk" | |
| return json.dumps({ | |
| "success": True, | |
| "apk_path": str(built_apk) if built_apk.exists() else "Unknown", | |
| "message": "APK built successfully", | |
| "stdout": result.stdout | |
| }) | |
| else: | |
| return json.dumps({ | |
| "success": False, | |
| "error": result.stderr, | |
| "message": "Failed to build APK" | |
| }) | |
| except Exception as e: | |
| return json.dumps({"success": False, "error": str(e)}) | |
| def analyze_manifest(apk_name: str) -> str: | |
| """ | |
| Parse AndroidManifest.xml for permissions and components. | |
| Args: | |
| apk_name: Name of the decompiled APK directory | |
| Returns: | |
| JSON string with manifest analysis | |
| """ | |
| try: | |
| manifest_path = WORK_DIR / apk_name / "AndroidManifest.xml" | |
| if not manifest_path.exists(): | |
| return json.dumps({"success": False, "error": f"Manifest not found. Decompile APK first."}) | |
| with open(manifest_path, 'r') as f: | |
| content = f.read() | |
| # Extrair informações básicas | |
| import re | |
| package = re.search(r'package="([^"]+)"', content) | |
| permissions = re.findall(r'<uses-permission[^>]*android:name="([^"]+)"', content) | |
| activities = re.findall(r'<activity[^>]*android:name="([^"]+)"', content) | |
| services = re.findall(r'<service[^>]*android:name="([^"]+)"', content) | |
| receivers = re.findall(r'<receiver[^>]*android:name="([^"]+)"', content) | |
| return json.dumps({ | |
| "success": True, | |
| "package": package.group(1) if package else "Unknown", | |
| "permissions": permissions, | |
| "activities": activities, | |
| "services": services, | |
| "receivers": receivers, | |
| "permission_count": len(permissions), | |
| "component_count": len(activities) + len(services) + len(receivers) | |
| }, indent=2) | |
| except Exception as e: | |
| return json.dumps({"success": False, "error": str(e)}) | |
| def list_permissions(apk_name: str) -> str: | |
| """ | |
| List all permissions requested by an APK. | |
| Args: | |
| apk_name: Name of the decompiled APK directory | |
| Returns: | |
| JSON string with permissions list | |
| """ | |
| try: | |
| manifest_path = WORK_DIR / apk_name / "AndroidManifest.xml" | |
| if not manifest_path.exists(): | |
| return json.dumps({"success": False, "error": "Manifest not found. Decompile APK first."}) | |
| with open(manifest_path, 'r') as f: | |
| content = f.read() | |
| import re | |
| permissions = re.findall(r'<uses-permission[^>]*android:name="([^"]+)"', content) | |
| # Categorizar permissões perigosas | |
| dangerous_permissions = [ | |
| "READ_CONTACTS", "WRITE_CONTACTS", "READ_CALENDAR", "WRITE_CALENDAR", | |
| "CAMERA", "READ_CALL_LOG", "WRITE_CALL_LOG", "PROCESS_OUTGOING_CALLS", | |
| "ACCESS_FINE_LOCATION", "ACCESS_COARSE_LOCATION", "RECORD_AUDIO", | |
| "READ_PHONE_STATE", "CALL_PHONE", "ANSWER_PHONE_CALLS", | |
| "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE", "SEND_SMS", "RECEIVE_SMS" | |
| ] | |
| dangerous = [] | |
| normal = [] | |
| for p in permissions: | |
| is_dangerous = any(d in p.upper() for d in dangerous_permissions) | |
| if is_dangerous: | |
| dangerous.append(p) | |
| else: | |
| normal.append(p) | |
| return json.dumps({ | |
| "success": True, | |
| "total_permissions": len(permissions), | |
| "dangerous_permissions": dangerous, | |
| "normal_permissions": normal, | |
| "dangerous_count": len(dangerous) | |
| }, indent=2) | |
| except Exception as e: | |
| return json.dumps({"success": False, "error": str(e)}) | |
| def extract_strings(apk_name: str, locale: str = "") -> str: | |
| """ | |
| Extract string resources from a decompiled APK. | |
| Args: | |
| apk_name: Name of the decompiled APK directory | |
| locale: Locale to extract (e.g., 'en', 'pt'). Empty for default. | |
| Returns: | |
| JSON string with extracted strings | |
| """ | |
| try: | |
| if locale: | |
| strings_path = WORK_DIR / apk_name / "res" / f"values-{locale}" / "strings.xml" | |
| else: | |
| strings_path = WORK_DIR / apk_name / "res" / "values" / "strings.xml" | |
| if not strings_path.exists(): | |
| return json.dumps({"success": False, "error": f"Strings file not found at {strings_path}"}) | |
| with open(strings_path, 'r') as f: | |
| content = f.read() | |
| import re | |
| strings = re.findall(r'<string name="([^"]+)"[^>]*>([^<]*)</string>', content) | |
| return json.dumps({ | |
| "success": True, | |
| "file_path": str(strings_path), | |
| "string_count": len(strings), | |
| "strings": {name: value for name, value in strings[:50]} # Limitar a 50 | |
| }, indent=2) | |
| except Exception as e: | |
| return json.dumps({"success": False, "error": str(e)}) | |
| def find_smali_references(apk_name: str, pattern: str) -> str: | |
| """ | |
| Search for patterns in decompiled smali code. | |
| Args: | |
| apk_name: Name of the decompiled APK directory | |
| pattern: Search pattern (regex supported) | |
| Returns: | |
| JSON string with search results | |
| """ | |
| try: | |
| smali_dir = WORK_DIR / apk_name / "smali" | |
| if not smali_dir.exists(): | |
| return json.dumps({"success": False, "error": "Smali directory not found. Decompile APK first."}) | |
| import re | |
| results = [] | |
| regex = re.compile(pattern, re.IGNORECASE) | |
| for smali_file in smali_dir.rglob("*.smali"): | |
| try: | |
| with open(smali_file, 'r', errors='ignore') as f: | |
| for i, line in enumerate(f, 1): | |
| if regex.search(line): | |
| results.append({ | |
| "file": str(smali_file.relative_to(smali_dir)), | |
| "line": i, | |
| "content": line.strip()[:200] | |
| }) | |
| if len(results) >= 100: # Limitar resultados | |
| break | |
| except: | |
| continue | |
| if len(results) >= 100: | |
| break | |
| return json.dumps({ | |
| "success": True, | |
| "pattern": pattern, | |
| "matches": len(results), | |
| "results": results | |
| }, indent=2) | |
| except Exception as e: | |
| return json.dumps({"success": False, "error": str(e)}) | |
| def get_apk_info(apk_file) -> str: | |
| """ | |
| Get basic APK metadata and information. | |
| Args: | |
| apk_file: The APK file to analyze | |
| Returns: | |
| JSON string with APK information | |
| """ | |
| if apk_file is None: | |
| return json.dumps({"error": "No APK file provided"}) | |
| try: | |
| apk_path = Path(apk_file) | |
| # Usar aapt para info básica | |
| result = subprocess.run( | |
| ["aapt", "dump", "badging", str(apk_path)], | |
| capture_output=True, text=True, timeout=60 | |
| ) | |
| info = { | |
| "success": True, | |
| "file_name": apk_path.name, | |
| "file_size_mb": round(apk_path.stat().st_size / (1024 * 1024), 2), | |
| } | |
| if result.returncode == 0: | |
| import re | |
| package = re.search(r"package: name='([^']+)'", result.stdout) | |
| version = re.search(r"versionName='([^']+)'", result.stdout) | |
| sdk = re.search(r"targetSdkVersion:'([^']+)'", result.stdout) | |
| if package: | |
| info["package_name"] = package.group(1) | |
| if version: | |
| info["version"] = version.group(1) | |
| if sdk: | |
| info["target_sdk"] = sdk.group(1) | |
| return json.dumps(info, indent=2) | |
| except Exception as e: | |
| return json.dumps({"success": False, "error": str(e)}) | |
| def list_decompiled() -> str: | |
| """ | |
| List all decompiled APKs in the workspace. | |
| Returns: | |
| JSON string with list of decompiled APKs | |
| """ | |
| try: | |
| decompiled = [] | |
| for item in WORK_DIR.iterdir(): | |
| if item.is_dir() and (item / "AndroidManifest.xml").exists(): | |
| decompiled.append(item.name) | |
| return json.dumps({ | |
| "success": True, | |
| "workspace": str(WORK_DIR), | |
| "decompiled_apks": decompiled, | |
| "count": len(decompiled) | |
| }, indent=2) | |
| except Exception as e: | |
| return json.dumps({"success": False, "error": str(e)}) | |
| # Criar a interface Gradio | |
| with gr.Blocks(title="APktool MCP Server") as demo: | |
| gr.Markdown("# 🔧 APktool MCP Server") | |
| gr.Markdown("Android APK analysis tools exposed as MCP for HuggingChat") | |
| with gr.Tab("Decode APK"): | |
| decode_input = gr.File(label="APK File", file_types=[".apk"]) | |
| decode_name = gr.Textbox(label="Output Directory Name (optional)") | |
| with gr.Row(): | |
| decode_force = gr.Checkbox(label="Force Overwrite", value=False) | |
| decode_no_res = gr.Checkbox(label="Skip Resources", value=False) | |
| decode_no_src = gr.Checkbox(label="Skip Sources", value=False) | |
| decode_btn = gr.Button("Decode", variant="primary") | |
| decode_output = gr.Textbox(label="Result", lines=10) | |
| decode_btn.click(decode_apk, [decode_input, decode_name, decode_force, decode_no_res, decode_no_src], decode_output) | |
| with gr.Tab("Build APK"): | |
| build_dir = gr.Textbox(label="Source Directory Name") | |
| build_output = gr.Textbox(label="Output APK Path (optional)") | |
| build_btn = gr.Button("Build", variant="primary") | |
| build_result = gr.Textbox(label="Result", lines=10) | |
| build_btn.click(build_apk, [build_dir, build_output], build_result) | |
| with gr.Tab("Analyze"): | |
| analyze_apk = gr.Textbox(label="Decompiled APK Name") | |
| analyze_btn = gr.Button("Analyze Manifest", variant="primary") | |
| analyze_output = gr.Textbox(label="Result", lines=15) | |
| analyze_btn.click(analyze_manifest, [analyze_apk], analyze_output) | |
| with gr.Tab("Permissions"): | |
| perm_apk = gr.Textbox(label="Decompiled APK Name") | |
| perm_btn = gr.Button("List Permissions", variant="primary") | |
| perm_output = gr.Textbox(label="Result", lines=15) | |
| perm_btn.click(list_permissions, [perm_apk], perm_output) | |
| with gr.Tab("Strings"): | |
| strings_apk = gr.Textbox(label="Decompiled APK Name") | |
| strings_locale = gr.Textbox(label="Locale (e.g., 'en', 'pt')", value="") | |
| strings_btn = gr.Button("Extract Strings", variant="primary") | |
| strings_output = gr.Textbox(label="Result", lines=15) | |
| strings_btn.click(extract_strings, [strings_apk, strings_locale], strings_output) | |
| with gr.Tab("Search Smali"): | |
| search_apk = gr.Textbox(label="Decompiled APK Name") | |
| search_pattern = gr.Textbox(label="Search Pattern (regex)", placeholder="e.g., api_key, password, http") | |
| search_btn = gr.Button("Search", variant="primary") | |
| search_output = gr.Textbox(label="Result", lines=15) | |
| search_btn.click(find_smali_references, [search_apk, search_pattern], search_output) | |
| with gr.Tab("APK Info"): | |
| info_file = gr.File(label="APK File", file_types=[".apk"]) | |
| info_btn = gr.Button("Get Info", variant="primary") | |
| info_output = gr.Textbox(label="Result", lines=10) | |
| info_btn.click(get_apk_info, [info_file], info_output) | |
| with gr.Tab("Workspace"): | |
| list_btn = gr.Button("List Decompiled APKs", variant="primary") | |
| list_output = gr.Textbox(label="Workspace Contents", lines=10) | |
| list_btn.click(list_decompiled, [], list_output) | |
| # Lançar como MCP server! | |
| if __name__ == "__main__": | |
| demo.launch(mcp_server=True) |