first commit
This commit is contained in:
2
modules/__init__.py
Normal file
2
modules/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# path: modules/__init__.py
|
||||
# Makes 'modules' a package
|
||||
BIN
modules/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
modules/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/__pycache__/deep_scan.cpython-313.pyc
Normal file
BIN
modules/__pycache__/deep_scan.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/__pycache__/quick_start.cpython-313.pyc
Normal file
BIN
modules/__pycache__/quick_start.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/__pycache__/scan.cpython-313.pyc
Normal file
BIN
modules/__pycache__/scan.cpython-313.pyc
Normal file
Binary file not shown.
414
modules/deep_scan.py
Normal file
414
modules/deep_scan.py
Normal file
@@ -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)
|
||||
|
||||
125
modules/quick_start.py
Normal file
125
modules/quick_start.py
Normal file
@@ -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...")
|
||||
5
modules/scan.py
Normal file
5
modules/scan.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .deep_scan import main
|
||||
|
||||
__all__ = ["main"]
|
||||
Reference in New Issue
Block a user