diff --git a/mobsf_ext/static/analyze_apk.py b/mobsf_ext/static/analyze_apk.py new file mode 100644 index 0000000000..902086ead3 --- /dev/null +++ b/mobsf_ext/static/analyze_apk.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +import sys, json +from pathlib import Path + +from detectors.dex_hidden import find_hidden_dex +from detectors.dynamic_loading import detect_dynamic_loading_markers +from detectors.manifest_check import manifest_findings +from yara_scan import run_yara_scan + +def main(): + if len(sys.argv) < 2: + print("Usage: analyze_apk.py ") + sys.exit(1) + + apk_path = Path(sys.argv[1]).resolve() + if not apk_path.exists(): + print(json.dumps({"error": "apk not found", "path": str(apk_path)})) + sys.exit(2) + + result = { + "apk": str(apk_path), + "checks": { + "hidden_dex_paths": [], + "dynamic_loading_markers": [], + "manifest": {}, + "yara_matches": [] + }, + "score": 0, + "summary": "" + } + + # 1) 숨겨진/은닉 DEX + result["checks"]["hidden_dex_paths"] = find_hidden_dex(apk_path) + + # 2) DexClassLoader 등 런타임 로딩 흔적 + result["checks"]["dynamic_loading_markers"] = detect_dynamic_loading_markers(apk_path) + + # 3) Manifest 위험 권한 / SDK 정보 (Androguard 사용) + result["checks"]["manifest"] = manifest_findings(apk_path) + + # 4) YARA 매치 + rules_dir = Path(__file__).resolve().parent / "yara" / "rules" + result["checks"]["yara_matches"] = run_yara_scan(apk_path, rules_dir) + + # 5) 아주 러프한 점수화 + score = 0 + if result["checks"]["hidden_dex_paths"]: + score += 40 + if result["checks"]["dynamic_loading_markers"]: + score += 30 + if result["checks"]["manifest"].get("dangerous_permissions"): + score += min(30, 5 * len(result["checks"]["manifest"]["dangerous_permissions"])) + if result["checks"]["yara_matches"]: + score += 30 + result["score"] = min(100, score) + + flags = [] + if result["checks"]["hidden_dex_paths"]: + flags.append("은닉/암호화 DEX 의심") + if result["checks"]["dynamic_loading_markers"]: + flags.append("런타임 DEX 로딩 흔적") + if result["checks"]["manifest"].get("dangerous_permissions"): + flags.append("위험 권한 요청") + if result["checks"]["yara_matches"]: + flags.append("YARA 매치 있음") + + result["summary"] = " / ".join(flags) if flags else "특이점 미검출(정적 1차 패스)" + out_file = Path("outputs") / (apk_path.stem + ".json") + out_file.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + print(f"[+] 결과 저장 완료: {out_file}") + print(json.dumps(result, ensure_ascii=False, indent=2)) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/mobsf_ext/static/detectors/__init__.py b/mobsf_ext/static/detectors/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mobsf_ext/static/detectors/dex_hidden.py b/mobsf_ext/static/detectors/dex_hidden.py new file mode 100644 index 0000000000..8029a8de03 --- /dev/null +++ b/mobsf_ext/static/detectors/dex_hidden.py @@ -0,0 +1,24 @@ +import zipfile +from pathlib import Path + +DEX_HEADER = b"dex\n035" + +COMMON_HIDE_PREFIX = ("assets/", "res/raw/", "lib/", "assets/bin/", "assets/obj/") + +def find_hidden_dex(apk_path: Path): + """APK 내부에서 classes.dex 외의 DEX 헤더를 탐색""" + hits = [] + with zipfile.ZipFile(apk_path, "r") as z: + for name in z.namelist(): + lower = name.lower() + if lower.endswith(".dex") and lower != "classes.dex": + hits.append(name) + continue + if lower.startswith(COMMON_HIDE_PREFIX) or lower.endswith((".dat", ".bin", ".jar", ".zip")): + try: + data = z.read(name) + except KeyError: + continue + if DEX_HEADER in data: + hits.append(name) + return sorted(set(hits)) \ No newline at end of file diff --git a/mobsf_ext/static/detectors/dynamic_loading.py b/mobsf_ext/static/detectors/dynamic_loading.py new file mode 100644 index 0000000000..b19df3cfa5 --- /dev/null +++ b/mobsf_ext/static/detectors/dynamic_loading.py @@ -0,0 +1,38 @@ +import zipfile +from pathlib import Path + +MARKERS = [ + b"DexClassLoader", + b"PathClassLoader", + b"loadClass(", + b"Class.forName", + b"getMethod(", + b"invoke(", + b"Base64.decode", +] + +TEXT_EXTS = (".xml", ".txt", ".properties", ".json") + +def detect_dynamic_loading_markers(apk_path: Path): + """DexClassLoader 등 문자열 흔적을 APK 내부에서 폭넓게 스캔""" + suspects = [] + with zipfile.ZipFile(apk_path, "r") as z: + for name in z.namelist(): + lname = name.lower() + if lname.endswith(TEXT_EXTS) or lname.startswith(("assets/", "res/raw/")) or lname.endswith((".dex", ".so", ".bin", ".dat")): + try: + data = z.read(name) + except KeyError: + continue + if any(m in data for m in MARKERS): + suspects.append(name) + return sorted(set(suspects)) + +def find_native_libs(apk_path: Path): + import zipfile + libs = [] + with zipfile.ZipFile(apk_path, "r") as z: + for name in z.namelist(): + if name.lower().startswith("lib/") and name.lower().endswith(".so"): + libs.append(name) + return sorted(libs) diff --git a/mobsf_ext/static/detectors/manifest_check.py b/mobsf_ext/static/detectors/manifest_check.py new file mode 100644 index 0000000000..31551790d4 --- /dev/null +++ b/mobsf_ext/static/detectors/manifest_check.py @@ -0,0 +1,51 @@ +from pathlib import Path + +# 호환 import (여러 Androguard 버전 대응) +try: + from androguard.core.bytecodes.apk import APK +except Exception: + try: + from androguard.core.apk import APK # 일부 포크/버전 + except Exception as e: + raise ImportError( + "Androguard APK import 실패. venv에서 `pip install 'androguard==3.3.5'` 후 다시 시도하세요." + ) from e + +from androguard.core.bytecodes.apk import APK + +DANGEROUS = { + "android.permission.READ_SMS", + "android.permission.RECEIVE_SMS", + "android.permission.SEND_SMS", + "android.permission.RECORD_AUDIO", + "android.permission.READ_CALL_LOG", + "android.permission.WRITE_CALL_LOG", + "android.permission.CALL_PHONE", + "android.permission.READ_CONTACTS", + "android.permission.WRITE_CONTACTS", + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE", + "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", + "android.permission.SYSTEM_ALERT_WINDOW", + "android.permission.PACKAGE_USAGE_STATS", + "android.permission.BIND_ACCESSIBILITY_SERVICE", +} + +def manifest_findings(apk_path: Path): + """권한/SDK 정보와 대표 위험 권한 리포트""" + apk = APK(str(apk_path)) + perms = set(apk.get_permissions() or []) + dangerous = sorted(p for p in perms if p in DANGEROUS) + + sdk_info = { + "minSdkVersion": apk.get_min_sdk_version(), + "targetSdkVersion": apk.get_target_sdk_version(), + "maxSdkVersion": apk.get_max_sdk_version(), + } + + return { + "package": apk.get_package(), + "sdk": sdk_info, + "requested_permissions": sorted(perms), + "dangerous_permissions": dangerous, + } \ No newline at end of file diff --git a/mobsf_ext/static/samples/UnCrackable-Level1.apk b/mobsf_ext/static/samples/UnCrackable-Level1.apk new file mode 100644 index 0000000000..9a4f638f1c Binary files /dev/null and b/mobsf_ext/static/samples/UnCrackable-Level1.apk differ diff --git a/mobsf_ext/static/samples/UnCrackable-Level2.apk b/mobsf_ext/static/samples/UnCrackable-Level2.apk new file mode 100644 index 0000000000..13641fea8f Binary files /dev/null and b/mobsf_ext/static/samples/UnCrackable-Level2.apk differ diff --git a/mobsf_ext/static/yara/rules/anti_debug_root.yar b/mobsf_ext/static/yara/rules/anti_debug_root.yar new file mode 100644 index 0000000000..1800dd091b --- /dev/null +++ b/mobsf_ext/static/yara/rules/anti_debug_root.yar @@ -0,0 +1,15 @@ +rule ANDROID_AntiDebug_Root_Strings { + meta: + description = "Root/Anti-debug/frida indicators" + strings: + $su1 = "/system/xbin/su" + $su2 = "/system/bin/su" + $superuser = "Superuser.apk" + $testkeys = "test-keys" + $frida1 = "frida" + $frida2 = "gum-js-loop" + $ptrace = "android.os.Debug" + $dbg1 = "isDebuggerConnected" + condition: + any of ($su*) or $superuser or $testkeys or any of ($frida*) or $ptrace or $dbg1 +} \ No newline at end of file diff --git a/mobsf_ext/static/yara/rules/sample_android_rule.yar b/mobsf_ext/static/yara/rules/sample_android_rule.yar new file mode 100644 index 0000000000..27369556de --- /dev/null +++ b/mobsf_ext/static/yara/rules/sample_android_rule.yar @@ -0,0 +1,11 @@ +rule ANDROID_Generic_Signs { + meta: + author = "team" + description = "Generic suspicious Android strings" + strings: + $a = "DexClassLoader" + $b = "PathClassLoader" + $c = "Base64.decode" + condition: + any of them +} \ No newline at end of file diff --git a/mobsf_ext/static/yara_scan.py b/mobsf_ext/static/yara_scan.py new file mode 100644 index 0000000000..82abafe2c1 --- /dev/null +++ b/mobsf_ext/static/yara_scan.py @@ -0,0 +1,24 @@ +from pathlib import Path +from typing import List +try: + import yara +except Exception: + yara = None + +def run_yara_scan(apk_path: Path, rules_dir: Path) -> List[str]: + """yara-python 기반 스캔. 룰 파일 여러 개를 합쳐 컴파일.""" + if yara is None: + return [] + + rule_files = list(rules_dir.glob("*.yar")) + list(rules_dir.glob("*.yara")) + if not rule_files: + return [] + + # filepaths로 여러 룰을 묶어 컴파일 + filemap = {f"rule_{i}": str(p) for i, p in enumerate(rule_files)} + try: + rules = yara.compile(filepaths=filemap) + matches = rules.match(str(apk_path)) + return sorted({m.rule for m in matches}) + except Exception: + return [] \ No newline at end of file diff --git a/outputs/.gitkeep b/outputs/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/outputs/UnCrackable-Level1.json b/outputs/UnCrackable-Level1.json new file mode 100644 index 0000000000..aa43a0f56f --- /dev/null +++ b/outputs/UnCrackable-Level1.json @@ -0,0 +1,20 @@ +{ + "apk": "D:\\MobSF\\OMT_Semi_project2_MobSF\\mobsf_ext\\static\\samples\\UnCrackable-Level1.apk", + "checks": { + "hidden_dex_paths": [], + "dynamic_loading_markers": [], + "manifest": { + "package": "owasp.mstg.uncrackable1", + "sdk": { + "minSdkVersion": "19", + "targetSdkVersion": "28", + "maxSdkVersion": null + }, + "requested_permissions": [], + "dangerous_permissions": [] + }, + "yara_matches": [] + }, + "score": 0, + "summary": "특이점 미검출(정적 1차 패스)" +} \ No newline at end of file