commit f89107e11fc7b8ac75f6e840deded5a56343adbc Author: DigiJ Date: Fri Mar 13 12:59:54 2026 -0700 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Tools/linux/NOTICE.txt b/Tools/linux/NOTICE.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tools/linux/adb b/Tools/linux/adb new file mode 100644 index 0000000..e69de29 diff --git a/Tools/linux/etc1tool b/Tools/linux/etc1tool new file mode 100644 index 0000000..e69de29 diff --git a/Tools/linux/fastboot b/Tools/linux/fastboot new file mode 100644 index 0000000..e69de29 diff --git a/Tools/linux/hprof-conv b/Tools/linux/hprof-conv new file mode 100644 index 0000000..e69de29 diff --git a/Tools/linux/lib64/libc++.so b/Tools/linux/lib64/libc++.so new file mode 100644 index 0000000..454cc10 Binary files /dev/null and b/Tools/linux/lib64/libc++.so differ diff --git a/Tools/linux/make_f2fs b/Tools/linux/make_f2fs new file mode 100644 index 0000000..e69de29 diff --git a/Tools/linux/make_f2fs_casefold b/Tools/linux/make_f2fs_casefold new file mode 100644 index 0000000..e69de29 diff --git a/Tools/linux/mke2fs b/Tools/linux/mke2fs new file mode 100644 index 0000000..e69de29 diff --git a/Tools/linux/mke2fs.conf b/Tools/linux/mke2fs.conf new file mode 100644 index 0000000..8ea960d --- /dev/null +++ b/Tools/linux/mke2fs.conf @@ -0,0 +1,53 @@ +[defaults] + base_features = sparse_super,large_file,filetype,dir_index,ext_attr + default_mntopts = acl,user_xattr + enable_periodic_fsck = 0 + blocksize = 4096 + inode_size = 256 + inode_ratio = 16384 + reserved_ratio = 1.0 + +[fs_types] + ext3 = { + features = has_journal + } + ext4 = { + features = has_journal,extent,huge_file,dir_nlink,extra_isize,uninit_bg + inode_size = 256 + } + ext4dev = { + features = has_journal,extent,huge_file,flex_bg,inline_data,64bit,dir_nlink,extra_isize + inode_size = 256 + options = test_fs=1 + } + small = { + blocksize = 1024 + inode_size = 128 + inode_ratio = 4096 + } + floppy = { + blocksize = 1024 + inode_size = 128 + inode_ratio = 8192 + } + big = { + inode_ratio = 32768 + } + huge = { + inode_ratio = 65536 + } + news = { + inode_ratio = 4096 + } + largefile = { + inode_ratio = 1048576 + blocksize = -1 + } + largefile4 = { + inode_ratio = 4194304 + blocksize = -1 + } + hurd = { + blocksize = 4096 + inode_size = 128 + } diff --git a/Tools/linux/source.properties b/Tools/linux/source.properties new file mode 100644 index 0000000..dc3a2fe --- /dev/null +++ b/Tools/linux/source.properties @@ -0,0 +1,2 @@ +Pkg.UserSrc=false +Pkg.Revision=36.0.0 diff --git a/Tools/linux/sqlite3 b/Tools/linux/sqlite3 new file mode 100644 index 0000000..e69de29 diff --git a/Tools/mac/NOTICE.txt b/Tools/mac/NOTICE.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tools/mac/adb b/Tools/mac/adb new file mode 100644 index 0000000..e69de29 diff --git a/Tools/mac/etc1tool b/Tools/mac/etc1tool new file mode 100644 index 0000000..e69de29 diff --git a/Tools/mac/fastboot b/Tools/mac/fastboot new file mode 100644 index 0000000..e69de29 diff --git a/Tools/mac/hprof-conv b/Tools/mac/hprof-conv new file mode 100644 index 0000000..e69de29 diff --git a/Tools/mac/lib64/libc++.dylib b/Tools/mac/lib64/libc++.dylib new file mode 100644 index 0000000..95f6b9d Binary files /dev/null and b/Tools/mac/lib64/libc++.dylib differ diff --git a/Tools/mac/make_f2fs b/Tools/mac/make_f2fs new file mode 100644 index 0000000..e69de29 diff --git a/Tools/mac/make_f2fs_casefold b/Tools/mac/make_f2fs_casefold new file mode 100644 index 0000000..e69de29 diff --git a/Tools/mac/mke2fs b/Tools/mac/mke2fs new file mode 100644 index 0000000..e69de29 diff --git a/Tools/mac/mke2fs.conf b/Tools/mac/mke2fs.conf new file mode 100644 index 0000000..8ea960d --- /dev/null +++ b/Tools/mac/mke2fs.conf @@ -0,0 +1,53 @@ +[defaults] + base_features = sparse_super,large_file,filetype,dir_index,ext_attr + default_mntopts = acl,user_xattr + enable_periodic_fsck = 0 + blocksize = 4096 + inode_size = 256 + inode_ratio = 16384 + reserved_ratio = 1.0 + +[fs_types] + ext3 = { + features = has_journal + } + ext4 = { + features = has_journal,extent,huge_file,dir_nlink,extra_isize,uninit_bg + inode_size = 256 + } + ext4dev = { + features = has_journal,extent,huge_file,flex_bg,inline_data,64bit,dir_nlink,extra_isize + inode_size = 256 + options = test_fs=1 + } + small = { + blocksize = 1024 + inode_size = 128 + inode_ratio = 4096 + } + floppy = { + blocksize = 1024 + inode_size = 128 + inode_ratio = 8192 + } + big = { + inode_ratio = 32768 + } + huge = { + inode_ratio = 65536 + } + news = { + inode_ratio = 4096 + } + largefile = { + inode_ratio = 1048576 + blocksize = -1 + } + largefile4 = { + inode_ratio = 4194304 + blocksize = -1 + } + hurd = { + blocksize = 4096 + inode_size = 128 + } diff --git a/Tools/mac/source.properties b/Tools/mac/source.properties new file mode 100644 index 0000000..dc3a2fe --- /dev/null +++ b/Tools/mac/source.properties @@ -0,0 +1,2 @@ +Pkg.UserSrc=false +Pkg.Revision=36.0.0 diff --git a/Tools/mac/sqlite3 b/Tools/mac/sqlite3 new file mode 100644 index 0000000..e69de29 diff --git a/Tools/windows/AdbWinApi.dll b/Tools/windows/AdbWinApi.dll new file mode 100644 index 0000000..e69de29 diff --git a/Tools/windows/AdbWinUsbApi.dll b/Tools/windows/AdbWinUsbApi.dll new file mode 100644 index 0000000..e69de29 diff --git a/Tools/windows/NOTICE.txt b/Tools/windows/NOTICE.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tools/windows/adb.exe b/Tools/windows/adb.exe new file mode 100644 index 0000000..e69de29 diff --git a/Tools/windows/etc1tool.exe b/Tools/windows/etc1tool.exe new file mode 100644 index 0000000..e69de29 diff --git a/Tools/windows/fastboot.exe b/Tools/windows/fastboot.exe new file mode 100644 index 0000000..e69de29 diff --git a/Tools/windows/hprof-conv.exe b/Tools/windows/hprof-conv.exe new file mode 100644 index 0000000..e69de29 diff --git a/Tools/windows/libwinpthread-1.dll b/Tools/windows/libwinpthread-1.dll new file mode 100644 index 0000000..e69de29 diff --git a/Tools/windows/make_f2fs.exe b/Tools/windows/make_f2fs.exe new file mode 100644 index 0000000..e69de29 diff --git a/Tools/windows/make_f2fs_casefold.exe b/Tools/windows/make_f2fs_casefold.exe new file mode 100644 index 0000000..e69de29 diff --git a/Tools/windows/mke2fs.conf b/Tools/windows/mke2fs.conf new file mode 100644 index 0000000..8ea960d --- /dev/null +++ b/Tools/windows/mke2fs.conf @@ -0,0 +1,53 @@ +[defaults] + base_features = sparse_super,large_file,filetype,dir_index,ext_attr + default_mntopts = acl,user_xattr + enable_periodic_fsck = 0 + blocksize = 4096 + inode_size = 256 + inode_ratio = 16384 + reserved_ratio = 1.0 + +[fs_types] + ext3 = { + features = has_journal + } + ext4 = { + features = has_journal,extent,huge_file,dir_nlink,extra_isize,uninit_bg + inode_size = 256 + } + ext4dev = { + features = has_journal,extent,huge_file,flex_bg,inline_data,64bit,dir_nlink,extra_isize + inode_size = 256 + options = test_fs=1 + } + small = { + blocksize = 1024 + inode_size = 128 + inode_ratio = 4096 + } + floppy = { + blocksize = 1024 + inode_size = 128 + inode_ratio = 8192 + } + big = { + inode_ratio = 32768 + } + huge = { + inode_ratio = 65536 + } + news = { + inode_ratio = 4096 + } + largefile = { + inode_ratio = 1048576 + blocksize = -1 + } + largefile4 = { + inode_ratio = 4194304 + blocksize = -1 + } + hurd = { + blocksize = 4096 + inode_size = 128 + } diff --git a/Tools/windows/mke2fs.exe b/Tools/windows/mke2fs.exe new file mode 100644 index 0000000..e69de29 diff --git a/Tools/windows/source.properties b/Tools/windows/source.properties new file mode 100644 index 0000000..dc3a2fe --- /dev/null +++ b/Tools/windows/source.properties @@ -0,0 +1,2 @@ +Pkg.UserSrc=false +Pkg.Revision=36.0.0 diff --git a/Tools/windows/sqlite3.exe b/Tools/windows/sqlite3.exe new file mode 100644 index 0000000..e69de29 diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..e9e7901 --- /dev/null +++ b/modules/__init__.py @@ -0,0 +1,2 @@ +# path: modules/__init__.py +# Makes 'modules' a package diff --git a/modules/__pycache__/__init__.cpython-313.pyc b/modules/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..8ac8f8a Binary files /dev/null and b/modules/__pycache__/__init__.cpython-313.pyc differ diff --git a/modules/__pycache__/deep_scan.cpython-313.pyc b/modules/__pycache__/deep_scan.cpython-313.pyc new file mode 100644 index 0000000..6ac0343 Binary files /dev/null and b/modules/__pycache__/deep_scan.cpython-313.pyc differ diff --git a/modules/__pycache__/quick_start.cpython-313.pyc b/modules/__pycache__/quick_start.cpython-313.pyc new file mode 100644 index 0000000..edbc4b6 Binary files /dev/null and b/modules/__pycache__/quick_start.cpython-313.pyc differ diff --git a/modules/__pycache__/scan.cpython-313.pyc b/modules/__pycache__/scan.cpython-313.pyc new file mode 100644 index 0000000..c39eede Binary files /dev/null and b/modules/__pycache__/scan.cpython-313.pyc differ diff --git a/modules/deep_scan.py b/modules/deep_scan.py new file mode 100644 index 0000000..5c03841 --- /dev/null +++ b/modules/deep_scan.py @@ -0,0 +1,414 @@ +# -*- coding: utf-8 -*- +# path: C:\Users\mdavi\PycharmProjects\SpyHunter\modules\deep_scan.py + +import os +import subprocess +import time +import requests +import hashlib +from datetime import datetime + +# --------------------------------------------------------------------------- +# Optional Androguard Imports +# --------------------------------------------------------------------------- +try: + from androguard.core.bytecodes.apk import APK + from androguard.core.bytecodes.dvm import DalvikVMFormat + from androguard.core.analysis.analysis import Analysis + ANDROGUARD_AVAILABLE = True +except (ImportError, ModuleNotFoundError): + print("[WARN] Androguard not available; heuristic scans will be skipped.") + ANDROGUARD_AVAILABLE = False + +# --------------------------------------------------------------------------- +# Optional YARA Imports +# --------------------------------------------------------------------------- +try: + import yara + YARA_AVAILABLE = True +except ImportError: + print("[WARN] YARA not available; signature scanning will be skipped.") + YARA_AVAILABLE = False + +# --------------------------------------------------------------------------- +# Constants: Directories for pulled APKs and signature rules +# --------------------------------------------------------------------------- +APK_PULL_DIR = os.path.join(os.getcwd(), "pulled_apks") +SIGNATURES_DIR = os.path.join(os.getcwd(), "signatures") +LOG_DIR = os.path.join(os.getcwd(), "logs") +LOG_FILE = os.path.join(LOG_DIR, "action_log.txt") + +# --------------------------------------------------------------------------- +# Dynamic Discovery via ADB +# --------------------------------------------------------------------------- +def get_installed_apks(): + """ + Discover installed APKs via ADB and pull them locally for analysis. + Returns a list of filepaths to the pulled APKs. + """ + os.makedirs(APK_PULL_DIR, exist_ok=True) + try: + proc = subprocess.run( + ["adb", "shell", "pm", "list", "packages", "-f"], + capture_output=True, text=True, check=True + ) + except subprocess.CalledProcessError as e: + print(f"[ERROR] Failed to list packages: {e}") + return [] + + apk_paths = [] + for line in proc.stdout.splitlines(): + if not line.startswith("package:"): + continue + # format: package:/data/app/.../base.apk=com.example.app + try: + left, pkg = line[len("package:"):].split("=", 1) + remote_path = left.strip() + package_name = pkg.strip() + local_name = package_name.replace(".", "_") + ".apk" + local_path = os.path.join(APK_PULL_DIR, local_name) + + print(f"[INFO] Pulling {package_name} from {remote_path}...") + subprocess.run(["adb", "pull", remote_path, local_path], check=True) + apk_paths.append(local_path) + except Exception as e: + print(f"[WARN] Could not pull '{line}': {e}") + + return apk_paths + +# --------------------------------------------------------------------------- +# Signature Scanning (YARA/CSV) +# --------------------------------------------------------------------------- +def signature_scan(apk_paths): + """ + Scan each APK with optional YARA rules. + Returns list of dicts: + { + app_name: str, + package: str or None, + sha256: str or None, + apk_path: str, + yara_matches: List[str] + } + """ + # compile YARA rules if available + rules = None + if YARA_AVAILABLE and os.path.isdir(SIGNATURES_DIR): + files = [ + os.path.join(SIGNATURES_DIR, f) + for f in os.listdir(SIGNATURES_DIR) + if f.endswith((".yar", ".yara")) + ] + if files: + try: + rules = yara.compile(filepaths={os.path.basename(f): f for f in files}) + except Exception as e: + print(f"[ERROR] Failed to compile YARA rules: {e}") + + results = [] + for path in apk_paths: + app_name = os.path.basename(path) + # compute SHA256 + try: + with open(path, "rb") as f: + sha256 = hashlib.sha256(f.read()).hexdigest() + except Exception as e: + print(f"[WARN] Could not hash {path}: {e}") + sha256 = None + + # extract package via Androguard if available + package = None + if ANDROGUARD_AVAILABLE: + try: + a = APK(path) + package = a.get_package() + except Exception: + pass + + # run YARA + yara_matches = [] + if rules: + try: + matches = rules.match(path) + yara_matches = [m.rule for m in matches] if matches else [] + except Exception as e: + print(f"[WARN] YARA scan error for {path}: {e}") + + results.append({ + "app_name": app_name, + "package": package, + "sha256": sha256, + "apk_path": path, + "yara_matches": yara_matches, + }) + + return results + +# --------------------------------------------------------------------------- +# Logging Setup +# --------------------------------------------------------------------------- +def ensure_log_dir(): + os.makedirs(LOG_DIR, exist_ok=True) + +def log_action(action, package, status, message=""): + """ + Append a timestamped action entry to the log file. + """ + ensure_log_dir() + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(f"[{ts}] {action.upper()} | {package} | {status} | {message}\n") + +# --------------------------------------------------------------------------- +# abuse.ch Integration +# --------------------------------------------------------------------------- +def query_abuse_ch(sha256: str) -> bool: + """ + Return True if abuse.ch confirms this hash, False otherwise. + """ + try: + resp = requests.post( + "https://mb-api.abuse.ch/api/v1/", + data={"query": "get_info", "hash": sha256}, + timeout=10 + ) + return resp.ok and bool(resp.json().get("data")) + except Exception as e: + print(f"[ERROR] abuse.ch query failed: {e}") + return False + +# --------------------------------------------------------------------------- +# Heuristics / Behavioral Detection +# --------------------------------------------------------------------------- +SUSPICIOUS_PERMS = { + "android.permission.READ_SMS", + "android.permission.READ_CONTACTS", + "android.permission.RECORD_AUDIO", + "android.permission.CAMERA", + "android.permission.ACCESS_FINE_LOCATION", +} +SUSPICIOUS_COMPONENT_KEYWORDS = ("tracker", "spy", "monitor", "surveil") + +def heuristics_scan(apk_paths): + """ + Perform heuristic analysis on each APK. + Returns list of dicts: { apk_path, reason }. + Skipped entirely if Androguard is unavailable. + """ + if not ANDROGUARD_AVAILABLE: + return [] + + hits = [] + for path in apk_paths: + try: + a = APK(path) + perms = set(a.get_permissions()) + bad = perms & SUSPICIOUS_PERMS + if bad: + hits.append({ + "apk_path": path, + "reason": f"Suspicious perms: {', '.join(bad)}" + }) + continue + + for comp in a.get_services() + a.get_receivers(): + if any(k in comp.lower() for k in SUSPICIOUS_COMPONENT_KEYWORDS): + hits.append({ + "apk_path": path, + "reason": f"Suspicious component: {comp}" + }) + raise StopIteration + + dex = DalvikVMFormat(a.get_dex()) + for method in dex.get_methods(): + src = method.get_source() + if "forName(" in src or "loadLibrary(" in src: + hits.append({ + "apk_path": path, + "reason": "Uses reflection/dynamic load" + }) + raise StopIteration + + except StopIteration: + continue + except Exception as e: + print(f"[WARN] Heuristic error on {path}: {e}") + + return hits + +# --------------------------------------------------------------------------- +# Freeze / Uninstall with Retry + Logging +# --------------------------------------------------------------------------- +def prompt_reboot_and_retry(pkg, action): + print(f"\n[NOTE] {action.title()} failed for {pkg}.") + if input("Reboot device and retry? [y/N]: ").strip().lower() != "y": + return + + try: + subprocess.run(["adb", "reboot"], check=True) + print("[INFO] Rebooting... press Enter once device is back online.") + input() + cmd = ( + ["adb","shell","pm","disable-user","--user","0",pkg] + if action == "freeze" + else ["adb","uninstall",pkg] + ) + subprocess.run(cmd, check=True) + print(f"[SUCCESS] Retry {action} succeeded for {pkg}.") + log_action(action, pkg, "success_after_reboot") + except subprocess.CalledProcessError: + print(f"[FAIL] Still unable to {action} {pkg}.") + log_action(action, pkg, "final_fail_after_reboot") + +def freeze_single_apk(info): + pkg = info.get("package") + if not pkg: + print("[ERROR] No package name.") + return + + print(f"[INFO] Freezing {pkg}...") + for attempt in range(3): + try: + subprocess.run( + ["adb","shell","pm","disable-user","--user","0",pkg], + check=True + ) + print(f"[SUCCESS] {pkg} frozen.") + log_action("freeze", pkg, "success") + return + except subprocess.CalledProcessError: + print(f"[WARN] Attempt {attempt+1} failed.") + time.sleep(1) + + print(f"[FAIL] Could not freeze {pkg}.") + log_action("freeze", pkg, "fail", "3 attempts") + prompt_reboot_and_retry(pkg, "freeze") + +def uninstall_single_apk(info): + pkg = info.get("package") + if not pkg: + print("[ERROR] No package name.") + return + + print(f"[INFO] Uninstalling {pkg}...") + for attempt in range(3): + try: + subprocess.run(["adb","uninstall",pkg], check=True) + print(f"[SUCCESS] {pkg} uninstalled.") + log_action("uninstall", pkg, "success") + return + except subprocess.CalledProcessError: + print(f"[WARN] Attempt {attempt+1} failed.") + time.sleep(1) + + print(f"[FAIL] Could not uninstall {pkg}.") + log_action("uninstall", pkg, "fail", "3 attempts") + prompt_reboot_and_retry(pkg, "uninstall") + +def freeze_apks(results): + for app in results: + freeze_single_apk(app) + +def uninstall_apks(results): + for app in results: + uninstall_single_apk(app) + +# --------------------------------------------------------------------------- +# Results Display +# --------------------------------------------------------------------------- +def show_result_detail(r): + print("\n--- App Details ---") + for k, v in r.items(): + print(f"{k}: {v}") + sha = r.get("sha256") + if sha: + print("\nQuerying abuse.ch…") + print("[ALERT] Confirmed" if query_abuse_ch(sha) + else "[WARNING] Not found") + while True: + cmd = input("[a] Freeze | [u] Uninstall | [b] Back: ").strip().lower() + if cmd == "a": + freeze_single_apk(r) + elif cmd == "u": + uninstall_single_apk(r) + elif cmd == "b": + break + +def show_results_info(results): + per_page, page = 10, 0 + total = len(results) + while True: + start, end = page * per_page, (page + 1) * per_page + print(f"\n--- Page {page+1}/{(total-1)//per_page+1} ---") + print("{:<3}{:<30}{:<20}{:<8}".format("#", "App", "Package", "Heur")) + for idx, item in enumerate(results[start:end], start+1): + h = "Y" if item.get("heuristic") else "" + print( + f"{idx:<3}" + f"{item.get('app_name','-')[:28]:<30}" + f"{item.get('package','-'):<20}" + f"{h:<8}" + ) + cmd = input("[n]Next [p]Prev [#]Detail [b]Back: ").strip().lower() + if cmd == "n" and end < total: + page += 1 + elif cmd == "p" and page > 0: + page -= 1 + elif cmd.isdigit() and 1 <= int(cmd) <= total: + show_result_detail(results[int(cmd) - 1]) + elif cmd == "b": + break + +# --------------------------------------------------------------------------- +# End-of-Scan Menu +# --------------------------------------------------------------------------- +def show_post_scan_menu(static_results, apk_paths): + """ + Merge static (YARA/CSV) hits with heuristics and launch the menu. + static_results: list of dicts from signature_scan() + apk_paths: list of pulled APK file paths + """ + heur_hits = heuristics_scan(apk_paths) + combined = [] + for r in static_results: + r["heuristic"] = False + combined.append(r) + for h in heur_hits: + combined.append({ + "app_name": os.path.basename(h["apk_path"]), + "package": None, + "sha256": None, + "heuristic": True, + "reason": h["reason"], + "apk_path": h["apk_path"], + }) + + while True: + print("\n--- End of Scan Menu ---") + print("1) Show Results and Information") + print("2) Freeze Possible Infected APK") + print("3) Uninstall Possible Infected APKs") + print("4) Exit") + choice = input("Select: ").strip() + if choice == "1": + show_results_info(combined) + elif choice == "2": + freeze_apks(combined) + elif choice == "3": + uninstall_apks(combined) + elif choice == "4": + break + else: + print("Invalid selection.") + +# --------------------------------------------------------------------------- +# Entry Point +# --------------------------------------------------------------------------- +def main(): + """ + Invoked by spyhunter.py as modules.scan.main() + """ + apk_paths = get_installed_apks() + static_results = signature_scan(apk_paths) + show_post_scan_menu(static_results, apk_paths) + diff --git a/modules/quick_start.py b/modules/quick_start.py new file mode 100644 index 0000000..c363821 --- /dev/null +++ b/modules/quick_start.py @@ -0,0 +1,125 @@ +# path: modules/quick_start.py +import subprocess +import sys +import os +import platform +import time +from shutil import get_terminal_size + +MENU_NAME = "Quick Start" + +GOV_SPYWARE_PACKAGES = { + "com.nsogroup.pega": "Pegasus - NSO Group", + "com.nsogroup.pegasus": "Pegasus - NSO Group", + "com.finfisher.mobile": "FinFisher / FinSpy", + "com.italtel.hermit": "Hermit - RCS Lab", + "com.cy4root.predator": "Predator - Cytrox", + "com.hackingteam.htagent": "Hacking Team RCS" +} + +def get_adb_path(): + os_map = {"Windows": "windows", "Linux": "linux", "Darwin": "mac"} + system = platform.system() + subdir = os_map.get(system) + if not subdir: + return None + root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + adb_filename = "adb.exe" if system == "Windows" else "adb" + adb_path = os.path.join(root_dir, "tools", subdir, adb_filename) + return adb_path if os.path.exists(adb_path) else None + +def start_log(): + timestamp = time.strftime("%Y%m%d-%H%M%S") + log_path = f"spyhunter_log_{timestamp}.txt" + return open(log_path, "w") + +def get_phone_info(adb_path): + info = {} + fields = ["ro.product.manufacturer", "ro.product.model", "ro.build.version.release", + "ro.serialno", "ro.build.version.sdk", "ro.build.display.id"] + for field in fields: + result = subprocess.run([adb_path, "shell", "getprop", field], capture_output=True, text=True) + info[field] = result.stdout.strip() + return info + +def get_installed_packages(adb_path): + result = subprocess.run([adb_path, "shell", "pm", "list", "packages"], capture_output=True, text=True) + return [line.replace("package:", "").strip() for line in result.stdout.splitlines()] + +def progress_bar(seconds=10): + width = get_terminal_size((80, 20)).columns - 20 + for i in range(seconds + 1): + bar = "#" * int((i / seconds) * width) + sys.stdout.write(f"\rStarting scan in [{i:2}/{seconds}] seconds: [{bar:<{width}}]") + sys.stdout.flush() + time.sleep(1) + print("\n") + +def scan(adb_path, log): + print("\n[SCAN RESULTS]") + log.write("[SCAN RESULTS]\n") + packages = get_installed_packages(adb_path) + found = [] + + for pkg, description in GOV_SPYWARE_PACKAGES.items(): + if pkg in packages: + print(f"[❌] {description} ({pkg})") + log.write(f"[DETECTED] {description} ({pkg})\n") + found.append((pkg, description)) + else: + print(f"[βœ…] {description} ({pkg})") + log.write(f"[OK] {description} ({pkg})\n") + + if found: + print("\n[ALERT] Spyware detected!") + for pkg, desc in found: + print(f"\nPackage: {pkg}\nDescription: {desc}\nRemoval: Use 'adb uninstall {pkg}' or perform a factory reset if pre-installed.\n") + log.write(f"\nALERT: {pkg} - {desc}\n") + else: + print("\n[INFO] No government spyware detected.") + log.write("\n[INFO] No spyware found.\n") + +def main(): + adb_path = get_adb_path() + if not adb_path: + print("[ERROR] ADB not found. Make sure it is in tools/{windows,linux,mac}/") + return + + log = start_log() + log.write("SpyHunter Quick Start Log\n") + + print("\n[Quick Start] Android Government Spyware Scanner\n") + print("Make sure USB Debugging is enabled on your Android device.") + print("1. Go to 'Settings' > 'About Phone' > tap 'Build Number' 7 times to enable Developer Options.") + print("2. Return to 'Settings' > 'Developer Options'.") + print("3. Enable 'USB Debugging'.\n") + + choice = input("Press Enter when ready, or type 'esc' to return to the main menu: ").strip().lower() + if choice == 'esc': + log.close() + return + + print("\n[INFO] Gathering device information...\n") + phone_info = get_phone_info(adb_path) + print("[DEVICE INFORMATION]\n") + + readable = { + "ro.product.manufacturer": "Manufacturer", + "ro.product.model": "Model", + "ro.build.version.release": "Android Version", + "ro.serialno": "Serial Number", + "ro.build.version.sdk": "SDK Level", + "ro.build.display.id": "Build ID" + } + + for prop, label in readable.items(): + value = phone_info.get(prop, "Unknown") + print(f"{label:<15}: {value}") + log.write(f"{label}: {value}\n") + + print("\n[INFO] Initializing scan...") + progress_bar(10) + + scan(adb_path, log) + log.close() + input("\nPress Enter to return to main menu...") diff --git a/modules/scan.py b/modules/scan.py new file mode 100644 index 0000000..69aedab --- /dev/null +++ b/modules/scan.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from .deep_scan import main + +__all__ = ["main"] diff --git a/spyhunter.py b/spyhunter.py new file mode 100644 index 0000000..cea8baf --- /dev/null +++ b/spyhunter.py @@ -0,0 +1,93 @@ +# path: spyhunter.py +import os +import importlib +import sys +import traceback + +STATIC_MENU = { + "1": "Quick Start", + "2": "ADB/Device Settings", + "3": "Detection Setup", + "4": "Scan" +} + +MODULE_MAPPING = { + "1": "modules.quick_start", + "2": "modules.adb_settings", + "3": "modules.detection_setup", + "4": "modules.scan" +} + +BANNER = r""" + _________ ___ ___ __ + / _____/_____ ___.__./ | \ __ __ _____/ |_ ___________ + \_____ \\____ < | / ~ \ | \/ \ __\/ __ \_ __ \ + / \ |_> >___ \ Y / | / | \ | \ ___/| | \/ +/_______ / __// ____|\___|_ /|____/|___| /__| \___ >__| + |__| + Version 0.0.1 by SsSnake β€” Grand Butcher of The DCR +""" + +def print_banner(): + os.system('cls' if os.name == 'nt' else 'clear') + print(BANNER) + +def load_custom_modules(): + menu = {} + custom_path = os.path.join(os.path.dirname(__file__), "modules") + if not os.path.exists(custom_path): + os.makedirs(custom_path) + for fname in os.listdir(custom_path): + if fname.endswith(".py") and fname not in ["__init__.py"]: + try: + modname = f"modules.{fname[:-3]}" + mod = importlib.import_module(modname) + if hasattr(mod, "MENU_NAME") and hasattr(mod, "main"): + key = str(len(STATIC_MENU) + len(menu) + 1) + menu[key] = (mod.MENU_NAME, modname) + except Exception: + print(f"[WARN] Failed to load module {fname}: {traceback.format_exc(limit=1)}") + return menu + +def show_menu(static_menu, custom_menu): + for key, name in static_menu.items(): + print(f" {key}) {name}") + for key, (name, _) in custom_menu.items(): + print(f" {key}) {name}") + print("\n[INFO] Press 'q' at any time to quit.\n") + +def main(): + while True: + try: + print_banner() + custom_menu = load_custom_modules() + show_menu(STATIC_MENU, custom_menu) + choice = input("Enter your selection: ").strip().lower() + + if choice == 'q': + print("[INFO] Exiting SpyHunter...") + sys.exit(0) + + if choice in STATIC_MENU: + module_path = MODULE_MAPPING[choice] + elif choice in custom_menu: + module_path = custom_menu[choice][1] + else: + print("[ERROR] Invalid choice. Try again.") + input("Press Enter to continue...") + continue + + # Load and run the module + module = importlib.import_module(module_path) + module.main() + + except KeyboardInterrupt: + print("\n[INFO] User aborted with Ctrl+C.") + sys.exit(0) + except Exception as e: + print(f"[FATAL] An error occurred: {e}") + traceback.print_exc() + input("Press Enter to return to main menu...") + +if __name__ == "__main__": + main()