| |
| """ |
| 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__) |
|
|
| |
| 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) |
| |
| |
| if output_name: |
| output_dir = WORK_DIR / output_name |
| else: |
| output_dir = WORK_DIR / apk_path.stem |
| |
| |
| 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") |
| |
| |
| 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() |
| |
| |
| 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) |
| |
| |
| 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]} |
| }, 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: |
| 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) |
| |
| |
| 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)}) |
|
|
|
|
| |
| 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) |
|
|
|
|
| |
| if __name__ == "__main__": |
| demo.launch(mcp_server=True) |