Files
PYAS/PYAS.py
T
2026-04-29 17:41:00 +08:00

2633 lines
115 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os, re, sys, time, json, uuid, stat, queue, msvcrt, winreg
import shutil, hashlib, platform, threading, subprocess, pefile, pystray
import socket, requests, webview, webbrowser, ctypes, ctypes.wintypes
from PIL import Image, ImageDraw
from concurrent.futures import ThreadPoolExecutor
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from PYAS_Engine import sign_scanner, rule_scanner, pe_scanner, cloud_scanner
####################################################################################################
class PROCESSENTRY32W(ctypes.Structure):
_fields_ = [
("dwSize", ctypes.wintypes.DWORD),
("cntUsage", ctypes.wintypes.DWORD),
("th32ProcessID", ctypes.wintypes.DWORD),
("th32DefaultHeapID", ctypes.wintypes.LPVOID),
("th32ModuleID", ctypes.wintypes.DWORD),
("cntThreads", ctypes.wintypes.DWORD),
("th32ParentProcessID", ctypes.wintypes.DWORD),
("pcPriClassBase", ctypes.wintypes.LONG),
("dwFlags", ctypes.wintypes.DWORD),
("szExeFile", ctypes.wintypes.WCHAR * 260)
]
class MIB_TCPROW_OWNER_PID(ctypes.Structure):
_fields_ = [
("dwState", ctypes.wintypes.DWORD),
("dwLocalAddr", ctypes.wintypes.DWORD),
("dwLocalPort", ctypes.wintypes.DWORD),
("dwRemoteAddr", ctypes.wintypes.DWORD),
("dwRemotePort", ctypes.wintypes.DWORD),
("dwOwningPid", ctypes.wintypes.DWORD)
]
class MIB_TCPTABLE_OWNER_PID(ctypes.Structure):
_fields_ = [
("dwNumEntries", ctypes.wintypes.DWORD),
("table", MIB_TCPROW_OWNER_PID * 1)
]
class FILE_NOTIFY_INFORMATION(ctypes.Structure):
_fields_ = [
("NextEntryOffset", ctypes.wintypes.DWORD),
("Action", ctypes.wintypes.DWORD),
("FileNameLength", ctypes.wintypes.DWORD),
("FileName", ctypes.wintypes.WCHAR * 1024)
]
class SERVICE_STATUS(ctypes.Structure):
_fields_ = [
("dwServiceType", ctypes.wintypes.DWORD),
("dwCurrentState", ctypes.wintypes.DWORD),
("dwControlsAccepted", ctypes.wintypes.DWORD),
("dwWin32ExitCode", ctypes.wintypes.DWORD),
("dwServiceSpecificExitCode", ctypes.wintypes.DWORD),
("dwCheckPoint", ctypes.wintypes.DWORD),
("dwWaitHint", ctypes.wintypes.DWORD)
]
class PROCESS_BASIC_INFORMATION(ctypes.Structure):
_fields_ = [
("Reserved1", ctypes.wintypes.LPVOID),
("PebBaseAddress", ctypes.wintypes.LPVOID),
("Reserved2", ctypes.wintypes.LPVOID * 2),
("UniqueProcessId", ctypes.wintypes.LPVOID),
("Reserved3", ctypes.wintypes.LPVOID)
]
class UNICODE_STRING(ctypes.Structure):
_fields_ = [
("Length", ctypes.wintypes.USHORT),
("MaximumLength", ctypes.wintypes.USHORT),
("Buffer", ctypes.c_void_p)
]
class FILTER_MESSAGE_HEADER(ctypes.Structure):
_fields_ = [
("ReplyLength", ctypes.wintypes.ULONG),
("MessageId", ctypes.c_uint64)
]
class PYAS_MESSAGE(ctypes.Structure):
_fields_ = [
("MessageCode", ctypes.wintypes.ULONG),
("ProcessId", ctypes.wintypes.ULONG),
("Path", ctypes.wintypes.WCHAR * 1024)
]
class PYAS_FULL_MESSAGE(ctypes.Structure):
_fields_ = [
("Header", FILTER_MESSAGE_HEADER),
("Data", PYAS_MESSAGE)
]
class PYAS_USER_MESSAGE(ctypes.Structure):
_fields_ = [
("Command", ctypes.wintypes.ULONG),
("Path", ctypes.wintypes.WCHAR * 1024)
]
class COPYDATASTRUCT(ctypes.Structure):
_fields_ = [
("dwData", ctypes.c_size_t),
("cbData", ctypes.wintypes.DWORD),
("lpData", ctypes.c_void_p)
]
####################################################################################################
class WindowAPI:
def __init__(self):
self._window = None
self.lock_config = threading.RLock()
self.lock_logs = threading.RLock()
self.lock_virus = threading.RLock()
self.lock_file_ops = threading.RLock()
self.init_environ()
self.init_windll()
if not self.check_singleton("PYAS_Security_Mutex"):
hwnd = self.user32.FindWindowW(None, "PYAS Security")
if hwnd:
if "-quit" in self.args_pyas:
try:
cds = COPYDATASTRUCT()
cds.dwData = 3
cds.cbData = 0
cds.lpData = None
self.user32.SendMessageTimeoutW(hwnd, 0x004A, 0, ctypes.byref(cds), 0x0002, 3000, None)
except Exception:
pass
elif "-scan" in self.args_pyas:
try:
idx = self.args_pyas.index("-scan")
target = self.args_pyas[idx+1]
cds = COPYDATASTRUCT()
cds.dwData = 1
encoded = target.encode('utf-8')
cds.cbData = len(encoded) + 1
buffer = ctypes.create_string_buffer(encoded)
cds.lpData = ctypes.cast(buffer, ctypes.c_void_p)
self.user32.SendMessageTimeoutW(hwnd, 0x004A, 0, ctypes.byref(cds), 0x0002, 3000, None)
self.user32.SetForegroundWindow(hwnd)
except Exception:
pass
else:
try:
cds = COPYDATASTRUCT()
cds.dwData = 2
cds.cbData = 0
cds.lpData = None
self.user32.SendMessageTimeoutW(hwnd, 0x004A, 0, ctypes.byref(cds), 0x0002, 3000, None)
self.user32.SetForegroundWindow(hwnd)
except Exception:
pass
os._exit(0)
if "-quit" in self.args_pyas:
os._exit(0)
self.init_variables()
self.load_config()
self.load_logs()
def check_singleton(self, name):
try:
self.h_mutex = self.kernel32.CreateMutexW(None, False, name)
if ctypes.get_last_error() == 183:
return False
return True
except Exception:
return False
def init_ui_ready(self):
with self.lock_config:
if self.engine_initialized:
return
self.engine_initialized = True
self.start_daemon_thread(self.init_engine_thread)
def set_window(self, window):
self._window = window
def minimize(self):
hwnd = self.user32.FindWindowW(None, "PYAS Security")
if hwnd:
self.user32.ShowWindow(hwnd, 6)
elif self._window:
self._window.minimize()
def hide_window(self):
if self._window:
self._window.hide()
def get_app_icon(self):
icon_path = os.path.join(self.path_pyas, "Interface", "static", "img", "icon.ico")
if os.path.exists(icon_path):
try:
return Image.open(icon_path)
except Exception:
pass
def get_tray_text(self, key):
with self.lock_config:
lang = self.pyas_config.get("language", "traditional_switch")
texts = {
"open_ui": {
"traditional_switch": "開啟介面",
"simplified_switch": "打开界面",
"english_switch": "Open PYAS",
"japanese_switch": "PYAS を開く",
"korean_switch": "PYAS 열기",
"french_switch": "Ouvrir PYAS",
"spanish_switch": "Abrir PYAS",
"hindi_switch": "PYAS खोलें",
"arabic_switch": "فتح PYAS",
"russian_switch": "Открыть PYAS",
"slovenian_switch": "Odpri PYAS"
},
"exit_app": {
"traditional_switch": "退出防護",
"simplified_switch": "退出防护",
"english_switch": "Exit Security",
"japanese_switch": "保護を終了",
"korean_switch": "보호 종료",
"french_switch": "Quitter la sécurité",
"spanish_switch": "Salir de la seguridad",
"hindi_switch": "सुरक्षा से बाहर निकलें",
"arabic_switch": "خروج من الحماية",
"russian_switch": "Выйти из защиты",
"slovenian_switch": "Izhod iz zaščite"
}
}
return texts.get(key, {}).get(lang, texts[key]["traditional_switch"])
def show_tray(self):
if self.tray_icon is not None:
return
menu = pystray.Menu(
pystray.MenuItem(lambda item: self.get_tray_text("open_ui"), self.restore_from_tray, default=True),
pystray.MenuItem(lambda item: self.get_tray_text("exit_app"), self.close)
)
self.tray_icon = pystray.Icon("PYAS", self.get_app_icon(), "PYAS Security", menu)
self.tray_icon.run_detached()
def restore_from_tray(self, icon=None, item=None):
if self._window:
self._window.restore()
self._window.show()
hwnd = self.user32.FindWindowW(None, "PYAS Security")
if hwnd:
self.user32.SetForegroundWindow(hwnd)
def close(self, *args, **kwargs):
if self.tray_icon:
try:
self.tray_icon.stop()
except Exception:
pass
self.tray_icon = None
if self._window:
try:
self._window.hide()
except Exception:
pass
def _cleanup_and_exit():
with self.lock_config:
self.pyas_config["process_switch"] = False
self.pyas_config["document_switch"] = False
self.pyas_config["system_switch"] = False
self.pyas_config["driver_switch"] = False
self.pyas_config["network_switch"] = False
self.stop_system_driver()
if self._window:
self._window.destroy()
os._exit(0)
threading.Thread(target=_cleanup_and_exit, daemon=True).start()
def get_file_version(self, file_path):
try:
pe = pefile.PE(file_path, fast_load=True)
pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE']])
if hasattr(pe, 'FileInfo'):
for fileinfo_list in pe.FileInfo:
for info in fileinfo_list:
if getattr(info, 'name', '') in ('StringFileInfo', b'StringFileInfo'):
for st in getattr(info, 'StringTable', []):
for key, val in st.entries.items():
k = key.decode('utf-8', 'ignore') if isinstance(key, bytes) else str(key)
v = val.decode('utf-8', 'ignore') if isinstance(val, bytes) else str(val)
if k == 'FileVersion':
pe.close()
return v.strip()
pe.close()
except Exception:
pass
return "0.0.0.0"
def compare_versions(self, v1, v2):
try:
t1 = tuple(int(x) for x in re.findall(r"\d+", v1))
t2 = tuple(int(x) for x in re.findall(r"\d+", v2))
return t1 >= t2
except Exception:
return False
####################################################################################################
def init_environ(self):
self.python = sys.executable
if getattr(sys, 'frozen', False):
self.file_pyas = self.norm_path(sys.executable)
else:
self.file_pyas = self.norm_path(os.path.abspath(sys.argv[0]))
self.args_pyas = sys.argv[1:]
self.path_pyas = os.path.dirname(self.file_pyas)
self.pid_pyas = int(os.getpid())
self.path_appdata = os.environ.get("APPDATA")
self.path_name = os.environ.get("USERNAME")
self.path_temp = os.environ.get("TEMP", f"C:\\Users\\{self.path_name}\\AppData\\Local\\Temp")
self.path_config = os.environ.get("ALLUSERSPROFILE", "C:\\ProgramData")
self.path_system = os.environ.get("SYSTEMROOT", "C:\\Windows")
self.path_user = os.environ.get("USERPROFILE", f"C:\\Users\\{self.path_name}")
self.path_systemp = os.path.join(self.path_system, "Temp")
self.file_config = os.path.join(self.path_config, "PYAS", "Config.json")
self.file_log = os.path.join(self.path_config, "PYAS", "Report.json")
self.path_properties = os.path.join(self.path_pyas, "Engine", "Properties")
self.path_pattern = os.path.join(self.path_pyas, "Engine", "Pattern")
self.path_heuristic = os.path.join(self.path_pyas, "Engine", "Heuristic")
self.path_protect = os.path.join(self.path_pyas, "Plugins", "Filter")
self.path_drivers = os.path.join(self.path_protect, "PYAS_Driver.sys")
####################################################################################################
def init_windll(self):
for name in ["ntdll", "Psapi", "user32", "kernel32", "advapi32", "iphlpapi", "shell32", "fltlib"]:
try:
setattr(self, name.lower(), ctypes.WinDLL(name, use_last_error=True))
except Exception as e:
self.write_log("WARN", "init_windll", detail=str(e), success=False)
self.advapi32.OpenSCManagerW.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.wintypes.DWORD]
self.advapi32.OpenSCManagerW.restype = ctypes.wintypes.HANDLE
self.advapi32.CreateServiceW.argtypes = [
ctypes.wintypes.HANDLE, ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.wintypes.DWORD,
ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_wchar_p,
ctypes.c_wchar_p, ctypes.POINTER(ctypes.wintypes.DWORD), ctypes.c_wchar_p, ctypes.c_wchar_p,
ctypes.c_wchar_p]
self.advapi32.CreateServiceW.restype = ctypes.wintypes.HANDLE
self.advapi32.OpenServiceW.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_wchar_p, ctypes.wintypes.DWORD]
self.advapi32.OpenServiceW.restype = ctypes.wintypes.HANDLE
self.advapi32.StartServiceW.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.c_wchar_p)]
self.advapi32.StartServiceW.restype = ctypes.wintypes.BOOL
self.advapi32.ControlService.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, ctypes.POINTER(SERVICE_STATUS)]
self.advapi32.ControlService.restype = ctypes.wintypes.BOOL
self.advapi32.DeleteService.argtypes = [ctypes.wintypes.HANDLE]
self.advapi32.DeleteService.restype = ctypes.wintypes.BOOL
self.advapi32.CloseServiceHandle.argtypes = [ctypes.wintypes.HANDLE]
self.advapi32.CloseServiceHandle.restype = ctypes.wintypes.BOOL
self.ntdll.NtQueryInformationProcess.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.ULONG, ctypes.c_void_p, ctypes.wintypes.ULONG, ctypes.POINTER(ctypes.wintypes.ULONG)]
self.ntdll.NtQueryInformationProcess.restype = ctypes.wintypes.ULONG
self.ntdll.NtSuspendProcess.argtypes = [ctypes.wintypes.HANDLE]
self.ntdll.NtSuspendProcess.restype = ctypes.c_ulong
self.ntdll.NtResumeProcess.argtypes = [ctypes.wintypes.HANDLE]
self.ntdll.NtResumeProcess.restype = ctypes.c_ulong
self.shell32.CommandLineToArgvW.argtypes = [ctypes.wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)]
self.shell32.CommandLineToArgvW.restype = ctypes.POINTER(ctypes.wintypes.LPWSTR)
self.fltlib.FilterConnectCommunicationPort.argtypes = [ctypes.wintypes.LPCWSTR, ctypes.wintypes.DWORD, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.c_void_p, ctypes.POINTER(ctypes.wintypes.HANDLE)]
self.fltlib.FilterConnectCommunicationPort.restype = ctypes.c_long
self.fltlib.FilterGetMessage.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.c_void_p]
self.fltlib.FilterGetMessage.restype = ctypes.c_long
self.fltlib.FilterSendMessage.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD)]
self.fltlib.FilterSendMessage.restype = ctypes.c_long
self.kernel32.CreateToolhelp32Snapshot.restype = ctypes.wintypes.HANDLE
self.kernel32.CreateToolhelp32Snapshot.argtypes = [ctypes.wintypes.DWORD, ctypes.wintypes.DWORD]
self.kernel32.Process32FirstW.restype = ctypes.wintypes.BOOL
self.kernel32.Process32FirstW.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_void_p]
self.kernel32.Process32NextW.restype = ctypes.wintypes.BOOL
self.kernel32.Process32NextW.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_void_p]
self.kernel32.OpenProcess.restype = ctypes.wintypes.HANDLE
self.kernel32.OpenProcess.argtypes = [ctypes.wintypes.DWORD, ctypes.wintypes.BOOL, ctypes.wintypes.DWORD]
self.kernel32.CloseHandle.restype = ctypes.wintypes.BOOL
self.kernel32.CloseHandle.argtypes = [ctypes.wintypes.HANDLE]
self.kernel32.TerminateProcess.restype = ctypes.wintypes.BOOL
self.kernel32.TerminateProcess.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_uint]
self.kernel32.CreateMutexW.restype = ctypes.wintypes.HANDLE
self.kernel32.CreateMutexW.argtypes = [ctypes.c_void_p, ctypes.wintypes.BOOL, ctypes.c_wchar_p]
self.kernel32.CreateFileW.restype = ctypes.wintypes.HANDLE
self.kernel32.CreateFileW.argtypes = [ctypes.c_wchar_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.HANDLE]
self.kernel32.ReadProcessMemory.restype = ctypes.wintypes.BOOL
self.kernel32.ReadProcessMemory.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
self.kernel32.QueryFullProcessImageNameW.restype = ctypes.wintypes.BOOL
self.kernel32.QueryFullProcessImageNameW.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, ctypes.c_wchar_p, ctypes.POINTER(ctypes.wintypes.DWORD)]
####################################################################################################
def init_variables(self):
self.python = sys.executable
self.sign = sign_scanner()
self.sign.init_windll(["wintrust"])
self.heuristic = rule_scanner()
self.properties = pe_scanner()
self.cloud = cloud_scanner()
self.cloud_queue = queue.Queue()
self.lock_driver = threading.RLock()
self.lock_update = threading.RLock()
self.driver_port = None
self.scan_running = False
self.scan_finished = False
self.virus_lock = {}
self.virus_results = []
self.scan_count = 0
self.mbr_backup = {}
self.logs_data = []
self.tray_icon = None
self.engine_initialized = False
self.lock_proc = threading.RLock()
self.lock_net = threading.RLock()
self.pyas_default = {
"version": "3.5.1",
"api_host": "https://pyas-security.com/",
"api_key": "fBRZxYS1UxykM-qzNOlKOEl63WILzlvgNMn6QfsG6FXCAAIktCrOPTAfY5_hEyuZ",
"suffix": [".exe", ".dll", ".sys", ".ocx", ".scr", ".efi", ".acm", ".ax", ".cpl", ".drv", ".com", ".mui", ".pyd"],
"block": [2001, 3001, 5001, 6001],
"size": 100 * 1024 * 1024,
"language": "english_switch",
"theme": "white_switch",
"process_switch": True,
"document_switch": True,
"system_switch": True,
"driver_switch": True,
"network_switch": True,
"extension_switch": False,
"sensitive_switch": False,
"cloud_switch": True,
"context_switch": False,
"custom_rule": [],
"white_list": [],
"quarantine": [],
"block_list": []
}
self.pass_windows = [
{"exe": "System Idle Process", "class": "", "title": ""},
{"exe": "", "class": "Windows.UI.Core.CoreWindow", "title": ""},
{"exe": "explorer.exe", "class": "", "title": ""}
]
self.thread_pool = ThreadPoolExecutor(max_workers=8)
for _ in range(2):
self.start_daemon_thread(self.cloud_worker)
####################################################################################################
def load_config(self):
with self.lock_config:
if not os.path.exists(self.file_config):
self.pyas_config = self.pyas_default.copy()
self.write_log("INFO", "Config Update", detail="Create default config")
self.save_config()
else:
try:
with open(self.file_config, "r", encoding="utf-8") as f:
data = json.load(f)
self.pyas_config = {**self.pyas_default, **data}
self.pyas_config["version"] = self.pyas_default["version"]
except Exception as e:
self.pyas_config = self.pyas_default.copy()
self.write_log("WARN", "load_config", detail=str(e), success=False)
def save_config(self):
with self.lock_config:
try:
os.makedirs(os.path.dirname(self.file_config), exist_ok=True)
with open(self.file_config, "w", encoding="utf-8") as f:
json.dump(self.pyas_config, f, indent=4, ensure_ascii=False)
except Exception as e:
self.write_log("WARN", "save_config", detail=str(e), success=False)
def update_config(self, key, value):
with self.lock_update:
with self.lock_config:
old_value = self.pyas_config.get(key)
if old_value == value:
return value
self.pyas_config[key] = value
self.write_log("INFO", "Config Update", detail=f"[{key}] {old_value} -> {value}")
self.save_config()
if key == "process_switch" and value:
self.start_daemon_thread(self.protect_proc_thread)
elif key == "document_switch" and value:
self.start_daemon_thread(self.protect_file_thread)
elif key == "system_switch" and value:
self.start_daemon_thread(self.protect_system_thread)
elif key == "network_switch" and value:
self.start_daemon_thread(self.protect_net_thread)
elif key == "context_switch":
self.register_context_menu(value)
elif key == "driver_switch":
if value:
if self.install_system_driver():
self.start_daemon_thread(self.pipe_server_thread)
else:
with self.lock_config:
self.pyas_config[key] = False
self.write_log("INFO", "Config Update", detail=f"[{key}] True -> False")
self.save_config()
if self._window:
self._window.evaluate_js(f"if(window.revertSwitch) window.revertSwitch('{key}');")
return False
else:
self.stop_system_driver()
return value
def get_config(self):
with self.lock_config:
return self.pyas_config.copy()
####################################################################################################
def register_context_menu(self, enable):
paths = [
r"Software\Classes\*\shell\PYAS_Scan",
r"Software\Classes\Directory\shell\PYAS_Scan"
]
if getattr(sys, 'frozen', False):
cmd_path = f'"{self.file_pyas}"'
else:
cmd_path = f'"{self.python}" "{self.file_pyas}"'
try:
for path in paths:
if enable:
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) as key:
winreg.SetValue(key, "", winreg.REG_SZ, "PYAS Security Scan")
winreg.SetValueEx(key, "Icon", 0, winreg.REG_SZ, f'{cmd_path},0')
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, rf"{path}\command") as key:
winreg.SetValue(key, "", winreg.REG_SZ, f'{cmd_path} -scan "%1"')
else:
try:
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, rf"{path}\command")
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, path)
except FileNotFoundError:
pass
except Exception as e:
self.write_log("WARN", "register_context_menu", detail=str(e), success=False)
def trigger_context_scan(self, target):
if self._window:
self._window.evaluate_js(f"if(window.triggerContextScan) window.triggerContextScan({json.dumps(target.replace(os.sep, '/'))});")
####################################################################################################
def show_notification(self, title, message):
try:
if self.tray_icon:
self.tray_icon.notify(message, title)
except Exception as e:
self.write_log("WARN", "show_notification", detail=str(e), success=False)
####################################################################################################
def load_logs(self):
with self.lock_logs:
if os.path.exists(self.file_log):
try:
with open(self.file_log, "r", encoding="utf-8") as f:
self.logs_data = json.load(f)
except Exception:
self.logs_data = []
def write_log(self, level, action, detail=None, code=None, pid=None, file_hash=None, source=None, target=None, operate=None, success=True):
with self.lock_logs:
entry = {
"id": str(uuid.uuid4()),
"timestamp": time.time(),
"time_str": time.strftime("%Y-%m-%d %H:%M:%S"),
"level": level,
"action": action,
"detail": detail,
"code": code,
"pid": pid,
"hash": file_hash,
"source": source,
"target": target,
"operate": operate,
"success": success
}
self.logs_data.append(entry)
if len(self.logs_data) > 10000:
self.logs_data = self.logs_data[-10000:]
try:
os.makedirs(os.path.dirname(self.file_log), exist_ok=True)
with open(self.file_log, "w", encoding="utf-8") as f:
json.dump(self.logs_data, f, indent=4, ensure_ascii=False)
except Exception:
pass
if self._window:
self._window.evaluate_js(f"if(window.updateLogs) window.updateLogs({json.dumps(entry)});")
if level == "BLOCK" and self.tray_icon:
self.trigger_block_notification(action, source, target, code)
def trigger_block_notification(self, action, source, target, code):
if action not in ["Process Block", "File Block", "Network Block", "Driver Block"]:
return
with self.lock_config:
lang = self.pyas_config.get("language", "traditional_switch")
path = source or ""
titles = {
"Process Block": {
"traditional_switch": "進程防護", "simplified_switch": "进程防护", "english_switch": "Process Protection",
"japanese_switch": "プロセス保護", "korean_switch": "프로세스 보호", "french_switch": "Protection des Processus",
"spanish_switch": "Protección de Procesos", "hindi_switch": "प्रक्रिया सुरक्षा", "arabic_switch": "حماية العمليات",
"russian_switch": "Защита процессов", "slovenian_switch": "Zaščita procesov"
},
"File Block": {
"traditional_switch": "檔案防護", "simplified_switch": "文件防护", "english_switch": "File Protection",
"japanese_switch": "ファイル保護", "korean_switch": "파일 보호", "french_switch": "Protection des Fichiers",
"spanish_switch": "Protección de Archivos", "hindi_switch": "फ़ाइल सुरक्षा", "arabic_switch": "حماية الملفات",
"russian_switch": "Защита файлов", "slovenian_switch": "Zaščita datotek"
},
"Network Block": {
"traditional_switch": "網路防護", "simplified_switch": "网络防护", "english_switch": "Network Protection",
"japanese_switch": "ネットワーク保護", "korean_switch": "네트워크 보호", "french_switch": "Protection Réseau",
"spanish_switch": "Protección de Red", "hindi_switch": "नेटवर्क सुरक्षा", "arabic_switch": "حماية الشبكة",
"russian_switch": "Сетевая защита", "slovenian_switch": "Omrežna zaščita"
},
"Driver Block": {
"traditional_switch": "驅動防護", "simplified_switch": "驱动防护", "english_switch": "Driver Protection",
"japanese_switch": "ドライバー保護", "korean_switch": "드라이버 보호", "french_switch": "Protection des Pilotes",
"spanish_switch": "Protección de Controladores", "hindi_switch": "ड्राइवर सुरक्षा", "arabic_switch": "حماية برامج التشغيل",
"russian_switch": "Защита драйверов", "slovenian_switch": "Zaščita gonilnikov"
}
}
messages = {
"traditional_switch": f"威脅已終止: {path}",
"simplified_switch": f"威胁已终止: {path}",
"english_switch": f"Threat terminated: {path}",
"japanese_switch": f"脅威が終了しました: {path}",
"korean_switch": f"위협이 종료되었습니다: {path}",
"french_switch": f"Menace terminée : {path}",
"spanish_switch": f"Amenaza terminada: {path}",
"hindi_switch": f"खतरा समाप्त: {path}",
"arabic_switch": f"تم إنهاء التهديد: {path}",
"russian_switch": f"Угроза устранена: {path}",
"slovenian_switch": f"Grožnja odpravljena: {path}"
}
title = titles.get(action, {}).get(lang, titles.get(action, {})["traditional_switch"])
message = messages.get(lang, messages["traditional_switch"])
try:
self.tray_icon.notify(message, title)
except Exception:
pass
def get_logs(self):
with self.lock_logs:
return self.logs_data.copy()
def clear_logs(self, log_ids=None):
with self.lock_logs:
if log_ids is None:
self.logs_data = []
else:
self.logs_data = [log for log in self.logs_data if log['id'] not in log_ids]
try:
if not self.logs_data:
if os.path.exists(self.file_log):
os.remove(self.file_log)
else:
os.makedirs(os.path.dirname(self.file_log), exist_ok=True)
with open(self.file_log, "w", encoding="utf-8") as f:
json.dump(self.logs_data, f, indent=4, ensure_ascii=False)
except Exception:
pass
def export_logs(self, log_ids=None):
if self._window:
path = self._window.create_file_dialog(webview.FileDialog.SAVE, directory='', save_filename='PYAS_Logs.json')
if path:
target_path = path[0] if isinstance(path, (tuple, list)) else path
with self.lock_logs:
export_data = self.logs_data
if log_ids is not None:
export_data = [log for log in self.logs_data if log['id'] in log_ids]
try:
with open(target_path, 'w', encoding='utf-8') as f:
json.dump(export_data, f, indent=4, ensure_ascii=False)
return True
except Exception:
pass
return False
####################################################################################################
def sync_driver_whitelist(self, file_path, is_add=True):
with self.lock_driver:
if not self.driver_port:
return False
driver_path = str(file_path)
drive_letter = os.path.splitdrive(driver_path)[0]
if drive_letter:
driver_path = "*" + driver_path[len(drive_letter):]
msg = PYAS_USER_MESSAGE()
msg.Command = 1 if is_add else 2
msg.Path = driver_path
bytes_returned = ctypes.wintypes.DWORD(0)
try:
hr = self.fltlib.FilterSendMessage(self.driver_port, ctypes.byref(msg), ctypes.sizeof(msg), None, 0, ctypes.byref(bytes_returned))
return hr == 0
except Exception:
return False
def show_alert(self, title, message, style="info"):
MB_OK = 0x00000000
MB_ICONINFORMATION = 0x00000040
MB_ICONWARNING = 0x00000030
MB_ICONERROR = 0x00000010
flags = MB_OK
if style == "error":
flags |= MB_ICONERROR
elif style == "warning":
flags |= MB_ICONWARNING
else:
flags |= MB_ICONINFORMATION
self.user32.MessageBoxW(0, message, title, flags)
return True
def show_confirm(self, title, message):
MB_YESNO = 0x00000004
MB_ICONQUESTION = 0x00000020
IDYES = 6
return self.user32.MessageBoxW(0, message, title, MB_YESNO | MB_ICONQUESTION) == IDYES
def prompt_virus_action(self, path):
with self.lock_config:
lang = self.pyas_config.get("language", "traditional_switch")
messages = {
"traditional_switch": (f"對 {path} 執行動作:\n\n[是] 加入白名單\n[否] 加入隔離區\n[取消] 不做任何事", "操作選擇"),
"simplified_switch": (f"对 {path} 执行动作:\n\n[是] 加入白名单\n[否] 加入隔离区\n[取消] 不做任何事", "操作选择"),
"english_switch": (f"Action for {path}:\n\n[Yes] Add to Whitelist\n[No] Add to Quarantine\n[Cancel] Do nothing", "Operation Selection"),
"japanese_switch": (f"{path} に対するアクション:\n\n[はい] ホワイトリストに追加\n[いいえ] 隔離に追加\n[キャンセル] 何もしない", "操作の選択"),
"korean_switch": (f"{path}에 대한 작업:\n\n[예] 화이트리스트에 추가\n[아니요] 격리소에 추가\n[취소] 아무 작업도 하지 않음", "작업 선택"),
"french_switch": (f"Action pour {path} :\n\n[Oui] Ajouter à la liste blanche\n[Non] Ajouter à la quarantaine\n[Annuler] Ne rien faire", "Sélection d'opération"),
"spanish_switch": (f"Acción para {path}:\n\n[Sí] Añadir a la lista blanca\n[No] Añadir a la cuarentena\n[Cancelar] No hacer nada", "Selección de operación"),
"hindi_switch": (f"{path} के लिए कार्रवाई:\n\n[हाँ] श्वेतसूची में जोड़ें\n[नहीं] संगरोध में जोड़ें\n[रद्द करें] कुछ न करें", "ऑपरेशन चयन"),
"arabic_switch": (f"إجراء لـ {path}:\n\n[نعم] إضافة إلى القائمة البيضاء\n[لا] إضافة إلى الحجر الصحي\n[إلغاء] لا تفعل شيئًا", "تحديد العملية"),
"russian_switch": (f"Действие для {path}:\n\n[Да] Добавить в белый список\n[Нет] Добавить в карантин\n[Отмена] Ничего не делать", "Выбор операции"),
"slovenian_switch": (f"Dejanje za {path}:\n\n[Da] Dodaj na seznam dovoljenih\n[Ne] Dodaj v karanteno\n[Prekliči] Ne naredi ničesar", "Izbira operacije")
}
msg, title = messages.get(lang, messages["traditional_switch"])
MB_YESNOCANCEL = 0x00000003
MB_ICONQUESTION = 0x00000020
IDYES = 6
IDNO = 7
res = self.user32.MessageBoxW(0, msg, title, MB_YESNOCANCEL | MB_ICONQUESTION)
if res == IDYES:
return "1"
elif res == IDNO:
return "2"
return "0"
####################################################################################################
def start_daemon_thread(self, target, *args, **kwargs):
t = threading.Thread(target=target, args=args, kwargs=kwargs, daemon=True)
t.start()
return t
def init_engine_thread(self):
try:
self.backup_mbr()
self.relock_file()
self.init_whitelist()
self.start_daemon_thread(self.popup_intercept_thread)
def log_callback(x):
self.write_log("INFO", "Load Engine", source=os.path.basename(x))
self.heuristic.load_path(self.path_heuristic, callback=log_callback)
self.properties.load_path(self.path_properties, callback=log_callback)
self.write_log("INFO", "System", detail="Engine Initialization Complete")
if "-scan" in self.args_pyas:
try:
idx = self.args_pyas.index("-scan")
target = self.args_pyas[idx+1]
self.trigger_context_scan(target)
except Exception:
pass
with self.lock_config:
if self.pyas_config.get("process_switch"): self.start_daemon_thread(self.protect_proc_thread)
if self.pyas_config.get("document_switch"): self.start_daemon_thread(self.protect_file_thread)
if self.pyas_config.get("system_switch"): self.start_daemon_thread(self.protect_system_thread)
if self.pyas_config.get("network_switch"): self.start_daemon_thread(self.protect_net_thread)
if self.pyas_config.get("driver_switch"):
if self.install_system_driver():
self.start_daemon_thread(self.pipe_server_thread)
else:
self.update_config("driver_switch", False)
except Exception as e:
self.write_log("WARN", "init_engine_thread", detail=str(e), success=False)
def backup_mbr(self, max_drives=26):
self.mbr_backup = {}
mbr_signature = b"\x55\xAA"
for drive in range(max_drives):
drive_path = rf"\\.\PhysicalDrive{drive}"
try:
with open(drive_path, "rb") as f:
mbr = f.read(512)
if len(mbr) == 512 and mbr[510:512] == mbr_signature:
self.mbr_backup[drive] = mbr
except Exception:
continue
def norm_path(self, path, must_exist=True):
if isinstance(path, list):
return [p for p in (self.norm_path(x, must_exist) for x in path) if p]
if isinstance(path, str):
ap = os.path.normpath(os.path.abspath(path))
return ap if (not must_exist or os.path.exists(ap)) else None
return path
def path_equal(self, a, b):
pa = self.norm_path(a, must_exist=False)
pb = self.norm_path(b, must_exist=False)
if not pa or not pb:
return False
return os.path.normcase(pa) == os.path.normcase(pb)
def start_scan(self, targets):
with self.lock_virus:
self.scan_running = True
self.scan_finished = False
self.virus_results = []
self.scan_count = 0
self.scan_start = time.time()
self.thread_pool.submit(self.scan_worker, targets)
def stop_scan(self):
with self.lock_virus:
self.scan_running = False
####################################################################################################
def scan_worker(self, targets):
try:
for file_path in self.yield_files(targets):
with self.lock_virus:
if not self.scan_running:
break
norm_path = self.norm_path(file_path)
if not norm_path:
continue
was_locked = False
try:
with self.lock_file_ops:
if norm_path in self.virus_lock:
self.lock_file(norm_path, False)
was_locked = True
if self.is_in_whitelist(norm_path):
continue
with self.lock_virus:
self.scan_count += 1
with self.lock_config:
suffix = self.pyas_config.get("suffix", [])
ext = os.path.splitext(norm_path)[-1].lower()
if ext not in suffix:
continue
result = self.scan_engine(norm_path)
if result:
with self.lock_virus:
self.virus_results.append(norm_path)
if self._window:
self._window.evaluate_js(f"if(window.addVirusResult) window.addVirusResult({json.dumps(result)}, {json.dumps(norm_path.replace(os.sep, '/'))});")
self.cloud_check(norm_path)
self.write_log("SCAN", "Virus Detected", source=norm_path, file_hash=self.calc_file_hash(norm_path))
if self._window:
self._window.evaluate_js(f"if(window.updateScanProgress) window.updateScanProgress({json.dumps(norm_path.replace(os.sep, '/'))});")
except Exception as e:
self.write_log("WARN", "Scan Engine", source=norm_path, detail=str(e), success=False)
finally:
if was_locked:
self.lock_file(norm_path, True)
finally:
with self.lock_virus:
self.scan_finished = True
count = len(self.virus_results)
elapsed = int(time.time() - self.scan_start)
scanned = self.scan_count
with self.lock_config:
lang = self.pyas_config.get("language", "traditional_switch")
messages = {
"traditional_switch": f"發現 {count} 個病毒,掃描 {scanned} 個檔案,耗時 {elapsed} 秒",
"simplified_switch": f"发现 {count} 个病毒,扫描 {scanned} 个文件,耗时 {elapsed} 秒",
"english_switch": f"Found {count} viruses, scanned {scanned} files, time {elapsed}s",
"japanese_switch": f"{count} 個のウイルスを発見し、{scanned} 個のファイルをスキャンしました(所要時間 {elapsed} 秒)",
"korean_switch": f"바이러스 {count}개 발견, 파일 {scanned}개 검사, 소요 시간 {elapsed}초",
"french_switch": f"{count} virus trouvés, {scanned} fichiers analysés, temps {elapsed}s",
"spanish_switch": f"Se encontraron {count} virus, {scanned} archivos escaneados, tiempo {elapsed}s",
"hindi_switch": f"{count} वायरस मिले, {scanned} फ़ाइलें स्कैन की गईं, समय {elapsed}s",
"arabic_switch": f"تم العثور على {count} فيروسات، تم فحص {scanned} ملفات، الوقت {elapsed} ثانية",
"russian_switch": f"Найдено {count} вирусов, проверено {scanned} файлов, время {elapsed}с",
"slovenian_switch": f"Najdenih {count} virusov, skeniranih {scanned} datotek, čas {elapsed}s"
}
result_msg = messages.get(lang, messages["traditional_switch"])
if self._window:
self._window.evaluate_js(f"if(window.finishScan) window.finishScan({json.dumps(result_msg)}, {count});")
self.write_log("INFO", "Scan Completed", detail=f"Found {count} viruses, scanned {scanned} files, time {elapsed}s")
####################################################################################################
def yield_files(self, targets):
if isinstance(targets, str):
if os.path.isdir(targets):
for root, _, files in os.walk(targets):
for f in files:
yield self.norm_path(os.path.join(root, f))
elif os.path.isfile(targets):
yield self.norm_path(targets)
elif isinstance(targets, (list, tuple, set)):
for t in targets:
yield from self.yield_files(t)
def scan_engine(self, file_path):
with self.lock_config:
ext_switch = self.pyas_config.get("extension_switch", False)
sen_switch = self.pyas_config.get("sensitive_switch", False)
try:
pe_label, _ = self.properties.pe_scan(file_path, enhanced_mode=sen_switch)
if pe_label:
return pe_label
except Exception:
pass
try:
if ext_switch:
yara_label, _ = self.heuristic.yara_scan(file_path)
if yara_label:
return yara_label
except Exception:
pass
return False
####################################################################################################
def solve_scan(self, file_paths):
deleted_paths = []
running_procs = self.get_process_list()
proc_map = {}
for proc in running_procs:
if proc["path"] and proc["path"] != "None":
norm_p = self.norm_path(proc["path"], must_exist=False)
if norm_p:
proc_map.setdefault(os.path.normcase(norm_p), []).append(proc["pid"])
with self.lock_virus:
deleted_set = set()
for raw_path in file_paths:
path = self.norm_path(raw_path, must_exist=False)
if not path:
continue
if self._window:
self._window.evaluate_js(f"if(window.updateDeleteProgress) window.updateDeleteProgress({json.dumps(path.replace(os.sep, '/'))});")
try:
if path in self.virus_lock:
self.lock_file(path, False)
path_key = os.path.normcase(path)
if path_key in proc_map:
for pid in proc_map[path_key]:
self.kill_process(pid)
try:
os.chmod(path, stat.S_IWRITE)
except Exception:
pass
self.remove_list_items("quarantine", [path])
os.remove(path)
deleted_paths.append(raw_path)
deleted_set.add(path)
self.write_log("INFO", "Virus Delete", source=path, file_hash=self.calc_file_hash(path), operate=True, success=True)
except Exception as e:
self.write_log("SCAN", "Virus Delete", source=path, file_hash=self.calc_file_hash(path), detail=str(e), operate=True, success=False)
if deleted_set:
self.virus_results = [p for p in self.virus_results if p not in deleted_set]
remaining = len(self.virus_results)
with self.lock_config:
lang = self.pyas_config.get("language", "traditional_switch")
messages = {
"traditional_switch": f"剩餘 {remaining} 個病毒,已刪除 {len(deleted_paths)} 個檔案。",
"simplified_switch": f"剩余 {remaining} 个病毒,已删除 {len(deleted_paths)} 个文件。",
"english_switch": f"Remaining {remaining} viruses, deleted {len(deleted_paths)} files.",
"japanese_switch": f"残りのウイルス {remaining} 個、削除されたファイル {len(deleted_paths)} 個。",
"korean_switch": f"남은 바이러스 {remaining}개, 삭제된 파일 {len(deleted_paths)}개.",
"french_switch": f"Virus restants : {remaining}, fichiers supprimés : {len(deleted_paths)}.",
"spanish_switch": f"Virus restantes: {remaining}, archivos eliminados: {len(deleted_paths)}.",
"hindi_switch": f"शेष वायरस {remaining}, हटाए गए फ़ाइलें {len(deleted_paths)}.",
"arabic_switch": f"الفيروسات المتبقية {remaining}، تم حذف {len(deleted_paths)} ملفات.",
"russian_switch": f"Осталось вирусов: {remaining}, удалено файлов: {len(deleted_paths)}.",
"slovenian_switch": f"Preostalih virusov: {remaining}, izbrisanih datotek: {len(deleted_paths)}."
}
result_msg = messages.get(lang, messages["traditional_switch"])
if self._window:
self._window.evaluate_js(f"if(window.finishScan) window.finishScan({json.dumps(result_msg)}, {remaining});")
return deleted_paths
def remove_virus_result(self, paths):
with self.lock_virus:
norm_paths = set(self.norm_path(p, must_exist=False) for p in paths)
self.virus_results = [p for p in self.virus_results if p not in norm_paths]
return len(self.virus_results)
####################################################################################################
def trigger_scan(self, method):
with self.lock_config:
lang = self.pyas_config.get("language", "traditional_switch")
messages = {
"traditional_switch": "已取消掃描",
"simplified_switch": "已取消扫描",
"english_switch": "Scan cancelled",
"japanese_switch": "スキャンがキャンセルされました",
"korean_switch": "스캔이 취소되었습니다",
"french_switch": "Analyse annulée",
"spanish_switch": "Escaneo cancelado",
"hindi_switch": "स्कैन रद्द कर दिया गया",
"arabic_switch": "تم إلغاء الفحص",
"russian_switch": "Сканирование отменено",
"slovenian_switch": "Skeniranje preklicano"
}
cancel_msg = messages.get(lang, messages["traditional_switch"])
targets = []
if method == "smart":
for folder in ["Desktop", "Downloads", "AppData"]:
fp = os.path.join(self.path_user, folder)
if os.path.exists(fp):
targets.append(fp)
if os.path.exists(self.path_config):
targets.append(self.path_config)
for proc in self.get_process_list():
if proc["path"] and proc["path"] != "None":
targets.append(proc["path"])
self.start_scan(list(set(targets)))
elif method == "file":
targets = self.select_files()
if targets:
self.start_scan(targets)
else:
if self._window:
self._window.evaluate_js(f"if(window.finishScan) window.finishScan('{cancel_msg}', 0);")
elif method == "path":
targets = self.select_folder()
if targets:
self.start_scan(targets)
else:
if self._window:
self._window.evaluate_js(f"if(window.finishScan) window.finishScan('{cancel_msg}', 0);")
elif method == "full":
targets = [f"{chr(d)}:/" for d in range(65, 91) if os.path.exists(f"{chr(d)}:/")]
self.start_scan(targets)
####################################################################################################
def open_file_location(self, file_path):
if file_path and os.path.exists(file_path):
try:
subprocess.Popen(f'explorer /select,"{file_path}"')
return True
except Exception:
pass
return False
####################################################################################################
def cloud_worker(self):
while True:
try:
file_path = self.cloud_queue.get()
self.perform_cloud_scan(file_path)
self.cloud_queue.task_done()
except Exception:
pass
def cloud_check(self, file_path):
self.cloud_queue.put(file_path)
def perform_cloud_scan(self, file_path):
was_locked = False
try:
with self.lock_config:
if not self.pyas_config.get("cloud_switch", True):
return False
api_host = self.pyas_config.get("api_host")
api_key = self.pyas_config.get("api_key")
max_size = self.pyas_config.get("size", 100 * 1024 * 1024)
if not os.path.exists(file_path) or not os.path.isfile(file_path):
return False
with self.lock_file_ops:
if file_path in self.virus_lock:
self.lock_file(file_path, False)
was_locked = True
size = os.path.getsize(file_path)
if size > max_size:
if was_locked:
self.lock_file(file_path, True)
return False
success, sha256 = self.cloud.upload_file(file_path, api_host, api_key)
if was_locked:
self.lock_file(file_path, True)
was_locked = False
if not success:
self.write_log("WARN", "Cloud API", source=file_path, detail="Failed", success=False)
except Exception as e:
self.write_log("WARN", "perform_cloud_scan", detail=str(e), success=False)
finally:
if was_locked:
try:
self.lock_file(file_path, True)
except Exception:
pass
return False
####################################################################################################
def scan_system_junk(self):
junk_list = []
try:
for path in [self.path_temp, self.path_systemp]:
if os.path.exists(path):
for root, dirs, files in os.walk(path):
for file in files:
full_path = os.path.join(root, file)
try:
size = os.path.getsize(full_path)
junk_list.append({"path": full_path, "size": size})
except Exception:
pass
return junk_list
except Exception as e:
self.write_log("WARN", "scan_system_junk", detail=str(e), success=False)
return []
def clean_system_junk(self, paths_to_delete=None):
total_deleted = 0
try:
if paths_to_delete is not None:
for path in paths_to_delete:
try:
size = os.path.getsize(path)
os.remove(path)
total_deleted += size
except Exception:
pass
else:
for path in [self.path_temp, self.path_systemp]:
total_deleted += self._traverse_delete(path)
self.write_log("INFO", "Clean Junk", detail=f"Deleted {total_deleted // 1024} KB", operate=True)
return total_deleted
except Exception as e:
self.write_log("WARN", "clean_system_junk", detail=str(e), operate=True, success=False)
return 0
def remove_list_items(self, list_key, paths_to_remove):
with self.lock_config:
target_list = self.pyas_config.get(list_key, [])
original_len = len(target_list)
if list_key == "quarantine":
for item in target_list:
if isinstance(item, dict) and item.get("file") in paths_to_remove:
self.lock_file(item["file"], False)
new_list = []
removed_items = []
for item in target_list:
if isinstance(item, dict):
val = item.get("file") or item.get("exe") or item.get("title")
if val not in paths_to_remove:
new_list.append(item)
else:
removed_items.append(val)
if list_key == "white_list":
self.sync_driver_whitelist(val, False)
else:
if item not in paths_to_remove:
new_list.append(item)
else:
removed_items.append(item)
if list_key == "white_list":
self.sync_driver_whitelist(item, False)
self.pyas_config[list_key] = new_list
if len(self.pyas_config[list_key]) < original_len:
self.write_log("INFO", "Config Update", detail=f"List [{list_key}] remove: {removed_items}")
self.save_config()
return True
return False
def _traverse_delete(self, path):
deleted = 0
if not os.path.exists(path):
return deleted
for fd in os.listdir(path):
file = os.path.join(path, fd)
try:
if os.path.isdir(file):
deleted += self._traverse_delete(file)
try:
os.rmdir(file)
except Exception:
pass
else:
size = os.path.getsize(file)
os.remove(file)
deleted += size
except Exception:
continue
return deleted
####################################################################################################
def scan_system_repair(self):
items = []
if self.check_system_mbr():
items.append({"display": "修復系統 MBR", "value": "mbr"})
if self.check_system_restrict():
items.append({"display": "修復系統限制", "value": "restrict"})
if self.check_system_file_type():
items.append({"display": "修復檔案關聯", "value": "file_type"})
if self.check_system_file_icon():
items.append({"display": "修復檔案圖標", "value": "file_icon"})
if self.check_system_image():
items.append({"display": "修復映像劫持", "value": "image"})
if self.check_system_wallpaper():
items.append({"display": "修復桌面壁紙", "value": "wallpaper"})
return items
def check_system_mbr(self):
if not self.mbr_backup:
return False
for drive, mbr_value in self.mbr_backup.items():
drive_path = rf"\\.\PhysicalDrive{drive}"
try:
with open(drive_path, "rb") as f:
if f.read(512) != mbr_value:
return True
except Exception:
pass
return False
def _get_restrict_lists(self):
permissions = [
"NoControlPanel", "NoDrives", "NoFileMenu", "NoFind", "NoStartMenuPinnedList",
"NoSetFolders", "NoSetFolderOptions", "NoViewOnDrive", "NoClose", "NoDesktop",
"NoLogoff", "NoFolderOptions", "RestrictRun", "NoViewContextMenu", "HideClock",
"NoStartMenuMyGames", "NoStartMenuMyMusic", "DisableCMD", "NoAddingComponents",
"NoWinKeys", "NoStartMenuLogOff", "NoSimpleNetIDList", "NoLowDiskSpaceChecks",
"DisableLockWorkstation","Restrict_Run", "DisableTaskMgr", "DisableRegistryTools",
"DisableChangePassword", "Wallpaper", "NoComponents", "NoStartMenuMorePrograms",
"NoActiveDesktop", "NoSetActiveDesktop", "NoRecentDocsMenu", "NoWindowsUpdate",
"NoChangeStartMenu", "NoFavoritesMenu", "NoRecentDocsHistory", "NoSetTaskbar",
"NoSMHelp", "NoTrayContextMenu", "NoManageMyComputerVerb", "NoRealMode", "NoRun",
"ClearRecentDocsOnExit", "NoActiveDesktopChanges", "NoStartMenuNetworkPlaces"
]
paths = [
(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Policies\Microsoft\MMC"),
(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Policies\Microsoft\Windows\System"),
(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer"),
(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"),
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer"),
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"),
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"),
(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Policies\Microsoft\Windows\System")
]
return permissions, paths
def check_system_restrict(self):
permissions, restrict_paths = self._get_restrict_lists()
for hkey, path in restrict_paths:
try:
with winreg.OpenKey(hkey, path, 0, winreg.KEY_READ) as reg:
for val in permissions:
try:
winreg.QueryValueEx(reg, val)
return True
except FileNotFoundError:
pass
except Exception:
pass
return False
def check_system_file_type(self):
for root in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]:
for ext in [".exe", ".bat", ".cmd", ".com"]:
try:
with winreg.OpenKey(root, rf"SOFTWARE\Classes\{ext}", 0, winreg.KEY_READ) as reg:
val, _ = winreg.QueryValueEx(reg, "")
if val != ("exefile" if ext == ".exe" else ext[1:]+"file"):
return True
except Exception:
pass
for shell_cmd in ["open", "runas"]:
try:
with winreg.OpenKey(root, rf"SOFTWARE\Classes\exefile\shell\{shell_cmd}\command", 0, winreg.KEY_READ) as reg:
val, _ = winreg.QueryValueEx(reg, "")
if val != '"%1" %*':
return True
except Exception:
pass
return False
def check_system_file_icon(self):
for root in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]:
try:
with winreg.OpenKey(root, r"SOFTWARE\Classes\exefile\DefaultIcon", 0, winreg.KEY_READ) as reg:
val, _ = winreg.QueryValueEx(reg, "")
if val != "%1":
return True
except Exception:
pass
return False
def check_system_image(self):
key_path = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options"
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path, 0, winreg.KEY_READ) as reg:
i = 0
while True:
try:
subkey = winreg.EnumKey(reg, i)
with winreg.OpenKey(reg, subkey, 0, winreg.KEY_READ) as sub_reg:
for value in ["Debugger", "UseFilter", "GlobalFlag", "MitigationOptions"]:
try:
winreg.QueryValueEx(sub_reg, value)
return True
except FileNotFoundError:
pass
i += 1
except OSError:
break
except Exception:
pass
return False
def check_system_wallpaper(self):
try:
wallpaper = os.path.join(self.path_system, "web", "wallpaper", "Windows", "img0.jpg")
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Control Panel\Desktop", 0, winreg.KEY_READ) as reg:
val, _ = winreg.QueryValueEx(reg, "Wallpaper")
if val != wallpaper:
return True
except Exception:
pass
return False
def execute_system_repair(self, items):
try:
if "mbr" in items:
self.repair_system_mbr()
if "restrict" in items:
self.repair_system_restrict()
if "file_type" in items:
self.repair_system_file_type()
if "file_icon" in items:
self.repair_system_file_icon()
if "image" in items:
self.repair_system_image()
if "wallpaper" in items:
self.repair_system_wallpaper()
self.write_log("INFO", "System Repair", detail=f"Repaired {len(items)} items", operate=True)
return True
except Exception as e:
self.write_log("WARN", "execute_system_repair", detail=str(e), operate=True, success=False)
return False
####################################################################################################
def reset_config(self):
with self.lock_config:
self.pyas_config = self.pyas_default.copy()
self.write_log("INFO", "Config Update", detail="Reset to default")
self.save_config()
return True
def open_website(self):
try:
webbrowser.open("https://pyas-security.com/antivirus")
return True
except Exception:
return False
def open_url(self, url):
try:
webbrowser.open(url)
return True
except Exception:
return False
def check_update(self):
try:
current = self.pyas_config.get("version", "0.0.0")
j = requests.get("https://api.github.com/repos/87owo/PYAS/releases/latest", headers={"Accept": "application/vnd.github+json", "User-Agent": "PYAS"}, timeout=10).json()
latest = str(j.get("tag_name") or j.get("name") or "").strip()
page = j.get("html_url") or "https://github.com/87owo/PYAS/releases"
if latest:
rl = re.sub(r"^[vV]\s*", "", latest)
cl = re.sub(r"^[vV]\s*", "", current)
tr = tuple(int(x) for x in re.findall(r"\d+", rl))
tl = tuple(int(x) for x in re.findall(r"\d+", cl))
if tr > tl:
return {"has_update": True, "latest": latest, "current": current, "url": page}
return {"has_update": False, "latest": latest, "current": current, "url": page}
except Exception:
pass
return {"error": True}
####################################################################################################
def list_process(self):
pe = PROCESSENTRY32W()
pe.dwSize = ctypes.sizeof(PROCESSENTRY32W)
snapshot = self.kernel32.CreateToolhelp32Snapshot(0x00000002, 0)
result = []
if snapshot in (-1, 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF):
return result
success = self.kernel32.Process32FirstW(snapshot, ctypes.byref(pe))
while success:
pid = pe.th32ProcessID
name, file_path = None, None
if pid > 4:
name, file_path = self.get_exe_info(pid)
if not name:
try:
name = pe.szExeFile
except Exception:
name = "Unknown"
result.append({"pid": pid, "name": name, "path": file_path or "None"})
success = self.kernel32.Process32NextW(snapshot, ctypes.byref(pe))
self.kernel32.CloseHandle(snapshot)
return result
def kill_process(self, pid):
try:
h = self.kernel32.OpenProcess(0x1F0FFF, False, pid)
if h:
try:
file_path = self.norm_path(self.get_process_file(h))
if self.path_equal(file_path, self.file_pyas):
return False
else:
self.kernel32.TerminateProcess(h, 0)
return True
finally:
self.kernel32.CloseHandle(h)
except Exception as e:
self.write_log("WARN", "kill_process", pid=pid, detail=str(e), operate=True, success=False)
return False
####################################################################################################
def get_process_file(self, h_process):
buf = ctypes.create_unicode_buffer(1024)
if self.psapi.GetProcessImageFileNameW(h_process, buf, 1024):
return self.norm_path(self.device_path_to_drive(buf.value))
return ""
def get_exe_info(self, pid):
name, file_path = None, None
h = self.kernel32.OpenProcess(0x1000, False, pid)
if h:
try:
buf = ctypes.create_unicode_buffer(1024)
size = ctypes.wintypes.DWORD(1024)
if self.kernel32.QueryFullProcessImageNameW(h, 0, buf, ctypes.byref(size)):
file_path = self.norm_path(buf.value)
if file_path:
name = os.path.basename(file_path)
else:
if self.psapi.GetProcessImageFileNameW(h, buf, 1024):
path = self.norm_path(self.device_path_to_drive(buf.value))
if path:
file_path = path
name = os.path.basename(path)
finally:
self.kernel32.CloseHandle(h)
return name, file_path
def device_path_to_drive(self, path):
if not path:
return ""
for d in range(65, 91):
drive = f"{chr(d)}:"
buf = ctypes.create_unicode_buffer(1024)
if self.kernel32.QueryDosDeviceW(drive, buf, 1024):
dev = buf.value
if path.startswith(dev):
return path.replace(dev, drive, 1)
return path
####################################################################################################
def lock_file(self, file, lock):
with self.lock_file_ops:
try:
if lock:
if file not in self.virus_lock:
fd = os.open(file, os.O_RDWR | os.O_BINARY)
try:
try:
size = os.path.getsize(file)
except Exception:
size = 1
lock_size = size if size > 0 else 1
msvcrt.locking(fd, msvcrt.LK_NBRLCK, lock_size)
self.virus_lock[file] = (fd, lock_size)
except Exception:
os.close(fd)
raise
else:
if file in self.virus_lock:
fd, lock_size = self.virus_lock[file]
try:
msvcrt.locking(fd, msvcrt.LK_UNLCK, lock_size)
finally:
os.close(fd)
del self.virus_lock[file]
except Exception as e:
self.write_log("WARN", "lock_file", source=file, detail=str(e), success=False)
def relock_file(self):
with self.lock_config:
quarantine_list = self.pyas_config.get("quarantine", [])
for item in quarantine_list:
file = item["file"]
if os.path.exists(file):
self.lock_file(file, True)
####################################################################################################
def calc_file_hash(self, file_path, block_size=65536):
try:
h = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(block_size), b""):
h.update(chunk)
return h.hexdigest()
except Exception:
return None
def manage_named_list(self, list_key, files, action="add", lock_func=None):
if list_key == "quarantine" and lock_func is None:
lock_func = self.lock_file
norm_paths = self.norm_path(files or [], must_exist=True)
if isinstance(norm_paths, str):
norm_paths = [norm_paths] if norm_paths else []
if not norm_paths:
return 0
if action == "add":
if list_key == "white_list":
self.remove_list_items("quarantine", norm_paths)
elif list_key == "quarantine":
self.remove_list_items("white_list", norm_paths)
acted_items = []
with self.lock_config:
target_list = self.pyas_config.setdefault(list_key, [])
if action == "add":
for path in norm_paths:
exists = any(self.norm_path(item.get("file", "")) == path for item in target_list if isinstance(item, dict))
if not exists:
if lock_func:
lock_func(path, True)
target_list.append({"file": path})
acted_items.append(path)
if list_key == "white_list":
self.sync_driver_whitelist(path, True)
elif action == "remove":
for item in list(target_list):
file_path = self.norm_path(item.get("file", ""))
if isinstance(item, dict) and file_path in norm_paths:
if lock_func:
lock_func(file_path, False)
target_list.remove(item)
acted_items.append(file_path)
if list_key == "white_list":
self.sync_driver_whitelist(file_path, False)
if acted_items:
self.write_log("INFO", "Config Update", detail=f"List [{list_key}] {action}: {acted_items}")
self.save_config()
return len(acted_items)
def is_in_whitelist(self, file_path):
p = self.norm_path(file_path)
if not p:
return False
with self.lock_config:
whitelist = self.pyas_config.get("white_list", [])
p_norm = os.path.normcase(p)
return any(os.path.normcase(item.get("file", "")) == p_norm for item in whitelist if isinstance(item, dict))
def init_whitelist(self):
self.manage_named_list("white_list", [self.file_pyas], action="add")
####################################################################################################
def protect_proc_thread(self):
with self.lock_proc:
self.exist_process = self.get_process_list_pids()
while True:
with self.lock_config:
if not self.pyas_config.get("process_switch", False):
break
try:
time.sleep(0.1)
cur = self.get_process_list_pids()
with self.lock_proc:
new_pids = cur - self.exist_process
self.exist_process = cur
for pid in new_pids:
self.thread_pool.submit(self.handle_new_process, pid)
except Exception as e:
self.write_log("WARN", "protect_proc_thread", detail=str(e), success=False)
def get_process_list(self):
pe = PROCESSENTRY32W()
pe.dwSize = ctypes.sizeof(PROCESSENTRY32W)
snapshot = self.kernel32.CreateToolhelp32Snapshot(0x00000002, 0)
result = []
if snapshot in (-1, 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF):
return result
try:
success = self.kernel32.Process32FirstW(snapshot, ctypes.byref(pe))
while success:
pid = pe.th32ProcessID
name, file_path = None, None
if pid > 4:
name, file_path = self.get_exe_info(pid)
if not name:
try:
name = pe.szExeFile
except Exception:
name = "Unknown"
result.append({"pid": pid, "name": name, "path": file_path or "None"})
success = self.kernel32.Process32NextW(snapshot, ctypes.byref(pe))
finally:
self.kernel32.CloseHandle(snapshot)
return result
def get_process_list_pids(self):
exist_process = set()
pe = PROCESSENTRY32W()
pe.dwSize = ctypes.sizeof(PROCESSENTRY32W)
hSnapshot = self.kernel32.CreateToolhelp32Snapshot(0x00000002, 0)
if hSnapshot in (-1, 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF):
return exist_process
try:
if self.kernel32.Process32FirstW(hSnapshot, ctypes.byref(pe)):
while True:
exist_process.add(pe.th32ProcessID)
if not self.kernel32.Process32NextW(hSnapshot, ctypes.byref(pe)):
break
finally:
self.kernel32.CloseHandle(hSnapshot)
return exist_process
####################################################################################################
def handle_new_process(self, pid):
h = self.kernel32.OpenProcess(0x1F0FFF, False, pid)
if not h:
return
self.ntdll.NtSuspendProcess(h)
try:
cmdline = self.get_process_cmdline(h)
if "-scan" in cmdline and self.path_equal(self.get_process_file(h), self.file_pyas):
return
paths = self.extract_paths_from_cmdline(cmdline)
with self.lock_config:
suffix = self.pyas_config.get("suffix", [])
for p in paths:
file_path = self.norm_path(self.device_path_to_drive(p))
if not file_path or not os.path.isfile(file_path):
continue
ext = os.path.splitext(file_path)[-1].lower()
if ext not in suffix:
continue
if self.is_in_whitelist(file_path):
continue
self.cloud_check(file_path)
if self.scan_engine(file_path):
self.kernel32.TerminateProcess(h, 0)
self.write_log("BLOCK", "Process Block", pid=pid, source=file_path, file_hash=self.calc_file_hash(file_path))
break
finally:
self.ntdll.NtResumeProcess(h)
self.kernel32.CloseHandle(h)
####################################################################################################
def get_process_cmdline(self, h):
pbi = PROCESS_BASIC_INFORMATION()
retlen = ctypes.wintypes.ULONG(0)
status = self.ntdll.NtQueryInformationProcess(h, 0, ctypes.byref(pbi), ctypes.sizeof(pbi), ctypes.byref(retlen))
if status != 0:
return ""
pointer_size = ctypes.sizeof(ctypes.c_void_p)
peb_address = int(pbi.PebBaseAddress) if pbi.PebBaseAddress else 0
read_buf = (ctypes.c_ubyte * pointer_size)()
lpNumberOfBytesRead = ctypes.c_size_t(0)
offset_process_parameters = 0x20 if pointer_size == 8 else 0x10
addr_pp = peb_address + offset_process_parameters
if not addr_pp:
return ""
if not self.kernel32.ReadProcessMemory(h, ctypes.c_void_p(addr_pp), read_buf, pointer_size, ctypes.byref(lpNumberOfBytesRead)):
return ""
proc_params_address = ctypes.c_void_p.from_buffer_copy(read_buf).value
if not proc_params_address:
return ""
offset_command_line = 0x70 if pointer_size == 8 else 0x40
us = UNICODE_STRING()
addr_cmd = int(proc_params_address) + offset_command_line
if not self.kernel32.ReadProcessMemory(h, ctypes.c_void_p(addr_cmd), ctypes.byref(us), ctypes.sizeof(us), ctypes.byref(lpNumberOfBytesRead)):
return ""
if not us.Buffer or us.Length == 0:
return ""
buf_len = int(us.Length // 2)
buf = (ctypes.c_wchar * buf_len)()
if not self.kernel32.ReadProcessMemory(h, ctypes.c_void_p(int(us.Buffer)), buf, us.Length, ctypes.byref(lpNumberOfBytesRead)):
return ""
return "".join(buf)
def extract_paths_from_cmdline(self, cmdline):
if not cmdline:
return []
argc = ctypes.c_int(0)
argv = self.shell32.CommandLineToArgvW(cmdline, ctypes.byref(argc))
if not argv:
return []
args = [argv[i] for i in range(argc.value)]
self.kernel32.LocalFree(ctypes.cast(argv, ctypes.c_void_p))
patterns = [r'([A-Za-z]:\\[^"\']+)', r'(\\\\[^"\']+)', r'(\.\\[^"\']+)', r'(\./[^"\']+)', r'([A-Za-z]:/[^"\']+)', r'([^\s]*\\[^\s]+)']
found = []
for arg in args:
for p in patterns:
for m in re.finditer(p, arg):
found.append(m.group(1).strip('"').strip("'"))
return list(dict.fromkeys(found))
####################################################################################################
def protect_file_thread(self):
hDir = self.kernel32.CreateFileW(self.path_user, 0x0001, 0x00000007, None, 3, 0x02000000, None)
if not hDir or hDir == -1:
return
try:
buffer = ctypes.create_string_buffer(65536)
temp_prefix = os.path.normcase(self.path_temp)
if not temp_prefix.endswith(os.sep):
temp_prefix += os.sep
while True:
with self.lock_config:
if not self.pyas_config.get("document_switch", False):
break
try:
bytes_returned = ctypes.wintypes.DWORD()
res = self.kernel32.ReadDirectoryChangesW(hDir, buffer, ctypes.sizeof(buffer), True, 0x0000001F, ctypes.byref(bytes_returned), None, None)
if not res or bytes_returned.value == 0:
continue
offset = 0
while True:
notify = FILE_NOTIFY_INFORMATION.from_buffer(buffer, offset)
raw_filename = notify.FileName[:notify.FileNameLength // 2]
if raw_filename and notify.Action in [2, 3, 4]:
file_path = self.norm_path(os.path.join(self.path_user, raw_filename), must_exist=True)
if file_path and not self.is_in_whitelist(file_path):
norm_path = os.path.normcase(file_path)
is_temp_mei = False
if norm_path.startswith(temp_prefix):
sub_path = norm_path[len(temp_prefix):]
if sub_path.startswith("_mei"):
is_temp_mei = True
if not is_temp_mei:
with self.lock_config:
suffix = self.pyas_config.get("suffix", [])
ext = os.path.splitext(file_path)[-1].lower()
if ext in suffix:
self.thread_pool.submit(self.handle_new_file, file_path)
if notify.NextEntryOffset == 0:
break
offset += notify.NextEntryOffset
except Exception as e:
self.write_log("WARN", "protect_file_thread", detail=str(e), success=False)
finally:
self.kernel32.CloseHandle(hDir)
def handle_new_file(self, file_path):
try:
for _ in range(3):
size1 = os.path.getsize(file_path)
time.sleep(0.5)
size2 = os.path.getsize(file_path)
if size1 == size2 and size1 > 0:
break
else:
return
with self.lock_file_ops:
if file_path in self.virus_lock:
return
if self.scan_engine(file_path):
if self.manage_named_list("quarantine", [file_path], action="add", lock_func=self.lock_file) > 0:
self.write_log("BLOCK", "File Block", source=file_path, file_hash=self.calc_file_hash(file_path))
self.cloud_check(file_path)
except Exception:
pass
####################################################################################################
def protect_net_thread(self):
with self.lock_net:
self.exist_connections = set()
while True:
with self.lock_config:
if not self.pyas_config.get("network_switch", False):
break
try:
time.sleep(0.5)
conns = self.get_connections_list()
with self.lock_net:
new_conns = conns - self.exist_connections
self.exist_connections = conns
for key in new_conns:
self.thread_pool.submit(self.handle_new_connection, key)
except Exception as e:
self.write_log("WARN", "protect_net_thread", detail=str(e), success=False)
with self.lock_net:
self.exist_connections = set()
def get_connections_list(self):
connections = set()
try:
size = ctypes.wintypes.DWORD()
ret = self.iphlpapi.GetExtendedTcpTable(None, ctypes.byref(size), True, 2, 5, 0)
if ret != 122:
return connections
buf = ctypes.create_string_buffer(size.value)
ret = self.iphlpapi.GetExtendedTcpTable(buf, ctypes.byref(size), True, 2, 5, 0)
if ret != 0:
return connections
num_entries = ctypes.cast(buf, ctypes.POINTER(ctypes.wintypes.DWORD)).contents.value
row_size = ctypes.sizeof(MIB_TCPROW_OWNER_PID)
offset = ctypes.sizeof(ctypes.wintypes.DWORD)
for i in range(num_entries):
entry_addr = ctypes.addressof(buf) + offset + i * row_size
row = MIB_TCPROW_OWNER_PID.from_address(entry_addr)
connections.add((row.dwOwningPid, row.dwRemoteAddr, row.dwRemotePort))
except Exception:
pass
return connections
def handle_new_connection(self, key):
pid, remote_addr, remote_port = key
try:
h = self.kernel32.OpenProcess(0x1F0FFF, False, pid)
if not h:
return
try:
remote_ip = f"{remote_addr & 0xFF}.{(remote_addr >> 8) & 0xFF}.{(remote_addr >> 16) & 0xFF}.{(remote_addr >> 24) & 0xFF}"
file_path = self.norm_path(self.get_process_file(h))
if file_path and not self.is_in_whitelist(file_path):
if hasattr(self.heuristic, "network") and remote_ip in self.heuristic.network:
self.kernel32.TerminateProcess(h, 0)
self.write_log("BLOCK", "Network Block", pid=pid, source=file_path, target=remote_ip, file_hash=self.calc_file_hash(file_path))
finally:
self.kernel32.CloseHandle(h)
except Exception as e:
self.write_log("WARN", "handle_new_connection", pid=key[0], detail=str(e), success=False)
####################################################################################################
def protect_system_thread(self):
while True:
with self.lock_config:
if not self.pyas_config.get("system_switch", False):
break
try:
self.repair_system_mbr()
self.repair_system_restrict()
self.repair_system_file_type()
self.repair_system_file_icon()
self.repair_system_image()
self.check_process_survival()
time.sleep(0.5)
except Exception as e:
self.write_log("WARN", "protect_system_thread", detail=str(e), success=False)
####################################################################################################
def capture_popup_window(self):
try:
start_time = time.time()
while time.time() - start_time < 5:
hwnd = self.user32.GetForegroundWindow()
if not hwnd:
time.sleep(0.5)
continue
pid = ctypes.c_ulong(0)
self.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
if pid.value != self.pid_pyas and pid.value > 4:
length = self.user32.GetWindowTextLengthW(hwnd)
title = ctypes.create_unicode_buffer(length + 1)
self.user32.GetWindowTextW(hwnd, title, length + 1)
class_name = ctypes.create_unicode_buffer(256)
self.user32.GetClassNameW(hwnd, class_name, 256)
proc_name, _ = self.get_exe_info(pid.value)
t_str, c_str = str(title.value), str(class_name.value)
if proc_name and not any(item.get("exe") == proc_name or item.get("class") == c_str for item in self.pass_windows):
return {"exe": proc_name, "class": c_str, "title": t_str}
time.sleep(0.5)
except Exception as e:
self.write_log("WARN", "capture_popup_window", detail=str(e), success=False)
return None
def add_popup_rule(self, rule):
if not rule:
return False
with self.lock_config:
target_list = self.pyas_config.setdefault("block_list", [])
for item in target_list:
if item.get("exe") == rule.get("exe") and item.get("class") == rule.get("class") and item.get("title") == rule.get("title"):
return False
target_list.append(rule)
self.write_log("INFO", "Config Update", detail=f"List [block_list] add: {rule}")
self.save_config()
return True
####################################################################################################
def repair_system_restrict(self):
try:
permissions, restrict_paths = self._get_restrict_lists()
for hkey, path in restrict_paths:
try:
with winreg.OpenKey(hkey, path, 0, winreg.KEY_SET_VALUE | winreg.KEY_WRITE) as reg:
for val in permissions:
try:
winreg.DeleteValue(reg, val)
except FileNotFoundError:
pass
except Exception:
pass
except Exception as e:
self.write_log("WARN", "repair_system_restrict", detail=str(e), success=False)
def repair_system_file_type(self):
try:
for root in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]:
for ext in [".exe", ".bat", ".cmd", ".com"]:
try:
with winreg.CreateKey(root, rf"SOFTWARE\Classes\{ext}") as reg:
winreg.SetValue(reg, "", winreg.REG_SZ, "exefile" if ext == ".exe" else ext[1:]+"file")
except Exception:
pass
for shell_cmd in ["open", "runas"]:
try:
with winreg.CreateKey(root, rf"SOFTWARE\Classes\exefile\shell\{shell_cmd}\command") as reg:
winreg.SetValue(reg, "", winreg.REG_SZ, '"%1" %*')
except Exception:
pass
except Exception as e:
self.write_log("WARN", "repair_system_file_type", detail=str(e), success=False)
def repair_system_file_icon(self):
try:
for root in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]:
try:
with winreg.CreateKey(root, r"SOFTWARE\Classes\exefile\DefaultIcon") as reg:
winreg.SetValue(reg, "", winreg.REG_SZ, "%1")
except Exception:
pass
except Exception as e:
self.write_log("WARN", "repair_system_file_icon", detail=str(e), success=False)
def repair_system_image(self):
key_path = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options"
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path, 0, winreg.KEY_ALL_ACCESS) as reg:
i = 0
while True:
try:
subkey = winreg.EnumKey(reg, i)
with winreg.OpenKey(reg, subkey, 0, winreg.KEY_ALL_ACCESS) as sub_reg:
for value in ["Debugger", "UseFilter", "GlobalFlag", "MitigationOptions"]:
try:
winreg.DeleteValue(sub_reg, value)
except FileNotFoundError:
pass
i += 1
except OSError:
break
except Exception as e:
self.write_log("WARN", "repair_system_image", detail=str(e), success=False)
def repair_system_wallpaper(self):
try:
wallpaper = os.path.join(self.path_system, "web", "wallpaper", "Windows", "img0.jpg")
if not os.path.exists(wallpaper):
return
key = r"Control Panel\Desktop"
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key, 0, winreg.KEY_SET_VALUE) as reg:
winreg.SetValueEx(reg, "Wallpaper", 0, winreg.REG_SZ, wallpaper)
theme_dir = os.path.join(self.path_appdata, "Microsoft", "Windows", "Themes")
for fname in ["TranscodedWallpaper", "TranscodedWallpaper.tmp"]:
fpath = os.path.join(theme_dir, fname)
if os.path.exists(fpath):
try:
os.remove(fpath)
except Exception:
pass
cache_dir = os.path.join(theme_dir, "CachedFiles")
if os.path.exists(cache_dir):
try:
shutil.rmtree(cache_dir, ignore_errors=True)
except Exception:
pass
self.user32.SystemParametersInfoW(20, 0, wallpaper, 3)
except Exception as e:
self.write_log("WARN", "repair_system_wallpaper", detail=str(e), success=False)
def repair_system_mbr(self):
if not self.mbr_backup:
return
for drive, mbr_value in self.mbr_backup.items():
drive_path = rf"\\.\PhysicalDrive{drive}"
try:
with open(drive_path, "rb+") as f:
current = f.read(512)
if current != mbr_value:
f.seek(0)
f.write(mbr_value)
self.write_log("INFO", "MBR Repaired", source=drive_path, operate=True)
except Exception:
pass
####################################################################################################
def check_process_survival(self):
target_map = {"explorer.exe": "explorer.exe"}
try:
running = set()
pe = PROCESSENTRY32W()
pe.dwSize = ctypes.sizeof(PROCESSENTRY32W)
snapshot = self.kernel32.CreateToolhelp32Snapshot(0x00000002, 0)
if snapshot not in (-1, 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF):
try:
if self.kernel32.Process32FirstW(snapshot, ctypes.byref(pe)):
while True:
try:
running.add(pe.szExeFile.lower())
except Exception:
pass
if not self.kernel32.Process32NextW(snapshot, ctypes.byref(pe)):
break
finally:
self.kernel32.CloseHandle(snapshot)
for name, cmd in target_map.items():
if name.lower() not in running:
subprocess.Popen(cmd, shell=True)
self.write_log("INFO", "System Restart", source=name)
except Exception:
pass
####################################################################################################
def install_system_driver(self):
try:
service_name = "PYAS_Driver"
cmd = f'sc create {service_name} binPath="{self.path_drivers}" type=kernel start=demand error=normal depend=FltMgr group="FSFilter Activity Monitor"'
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True)
key_path = r"SYSTEM\CurrentControlSet\Services\PYAS_Driver\Instances"
with winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, key_path) as key:
winreg.SetValueEx(key, "DefaultInstance", 0, winreg.REG_SZ, "PYAS Instance")
with winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, rf"{key_path}\PYAS Instance") as key:
winreg.SetValueEx(key, "Altitude", 0, winreg.REG_SZ, "320000")
winreg.SetValueEx(key, "Flags", 0, winreg.REG_DWORD, 0)
subprocess.run(["sc", "start", service_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True)
return self.check_system_driver()
except Exception:
return False
def stop_system_driver(self):
try:
with self.lock_driver:
if self.driver_port:
try:
self.kernel32.CloseHandle(self.driver_port)
except Exception:
pass
self.driver_port = None
time.sleep(0.5)
subprocess.run(["sc", "stop", "PYAS_Driver"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True)
subprocess.run(["sc", "delete", "PYAS_Driver"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True)
return True
except Exception:
return False
def check_system_driver(self):
try:
result = subprocess.run(["sc", "query", "PYAS_Driver"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True)
return "RUNNING" in result.stdout
except Exception:
return False
####################################################################################################
def pipe_server_thread(self):
try:
port_name = "\\PYAS_Output_Pipe"
while True:
with self.lock_config:
if not self.pyas_config.get("driver_switch", False): break
temp_port = ctypes.wintypes.HANDLE()
hr = self.fltlib.FilterConnectCommunicationPort(port_name, 0, None, 0, None, ctypes.byref(temp_port))
if hr == 0:
with self.lock_driver:
self.driver_port = temp_port
with self.lock_config:
whitelist = self.pyas_config.get("white_list", [])
for item in whitelist:
if isinstance(item, dict) and item.get("file"):
self.sync_driver_whitelist(item["file"], True)
message = PYAS_FULL_MESSAGE()
while True:
with self.lock_config:
if not self.pyas_config.get("driver_switch", False): break
with self.lock_driver:
current_port = self.driver_port
if not current_port:
break
try:
hr_get = self.fltlib.FilterGetMessage(current_port, ctypes.byref(message), ctypes.sizeof(PYAS_FULL_MESSAGE), None)
except OSError:
break
if hr_get == 0:
code = message.Data.MessageCode
pid = message.Data.ProcessId
raw_path = self.get_exe_info(pid)[1]
target = message.Data.Path
with self.lock_config:
block_codes = self.pyas_config.get("block", [])
if code in block_codes and not self.is_in_whitelist(raw_path):
self.kill_process(pid)
self.write_log("BLOCK", "Driver Block", pid=pid, source=raw_path, target=target, code=code, file_hash=self.calc_file_hash(raw_path))
else:
break
with self.lock_driver:
if self.driver_port:
self.kernel32.CloseHandle(self.driver_port)
self.driver_port = None
time.sleep(0.2)
except Exception as e:
self.write_log("WARN", "pipe_server_thread", detail=str(e), success=False)
####################################################################################################
def popup_intercept_thread(self):
WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_int, ctypes.c_int)
hwnd_list = []
def enum_windows_callback(hWnd, lParam):
if self.user32.IsWindowVisible(hWnd):
hwnd_list.append(hWnd)
return True
enum_cb = WNDENUMPROC(enum_windows_callback)
while True:
try:
time.sleep(0.5)
with self.lock_config:
rules = self.pyas_config.get("block_list", [])
if not rules:
continue
hwnd_list.clear()
self.user32.EnumWindows(enum_cb, 0)
for hWnd in list(hwnd_list):
pid = ctypes.c_ulong(0)
self.user32.GetWindowThreadProcessId(hWnd, ctypes.byref(pid))
if pid.value <= 4:
continue
proc_name, _ = self.get_exe_info(pid.value)
length = self.user32.GetWindowTextLengthW(hWnd)
title = ctypes.create_unicode_buffer(length + 1)
self.user32.GetWindowTextW(hWnd, title, length + 1)
class_name = ctypes.create_unicode_buffer(256)
self.user32.GetClassNameW(hWnd, class_name, 256)
t_str = str(title.value)
c_str = str(class_name.value)
is_pass = any(item.get("exe") == proc_name or item.get("class") == c_str for item in self.pass_windows)
if is_pass:
continue
is_block = False
for item in rules:
match_exe = item.get("exe") and item.get("exe") == proc_name
match_class = item.get("class") and item.get("class") == c_str
match_title = item.get("title") and item.get("title") == t_str
if match_exe and (match_class or match_title):
is_block = True
break
if is_block:
for msg in [0x0010, 0x0002, 0x0012, 0x0112]:
self.user32.SendMessageW(hWnd, msg, 0xF060, 0)
if proc_name:
self.kill_process(pid.value)
except Exception:
pass
####################################################################################################
def select_files(self):
if self._window:
result = self._window.create_file_dialog(webview.FileDialog.OPEN, allow_multiple=True)
return result or []
return []
def select_folder(self):
if self._window:
result = self._window.create_file_dialog(webview.FileDialog.FOLDER)
return result or []
return []
####################################################################################################
def get_base_path():
if getattr(sys, 'frozen', False):
return os.path.dirname(sys.executable)
return os.path.dirname(os.path.abspath(__file__))
####################################################################################################
class NoCacheRequestHandler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
web_dir = os.path.join(get_base_path(), "Interface")
super().__init__(*args, directory=web_dir, **kwargs)
def do_GET(self):
if self.path == '/':
self.path = '/templates/index.html'
return super().do_GET()
def end_headers(self):
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
self.send_header("Expires", "0")
self.send_header("Pragma", "no-cache")
super().end_headers()
####################################################################################################
class WindowHook:
def __init__(self, title, api_ref=None):
self.title = title
self.api_ref = api_ref
self.old_wndproc = None
self.WM_DPICHANGED = 0x02E0
self.WM_NCHITTEST = 0x0084
self.WM_COPYDATA = 0x004A
self.HTCAPTION = 2
self.GWLP_WNDPROC = -4
class RECT(ctypes.Structure):
_fields_ = [("left", ctypes.c_long), ("top", ctypes.c_long),
("right", ctypes.c_long), ("bottom", ctypes.c_long)]
self.RECT = RECT
self.WNDPROC = ctypes.WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p)
self.new_wndproc_cb = self.WNDPROC(self.wndproc)
self.user32 = ctypes.windll.user32
self.user32.CallWindowProcW.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p]
self.user32.CallWindowProcW.restype = ctypes.c_void_p
if ctypes.sizeof(ctypes.c_void_p) == 8:
self.SetWindowLong = self.user32.SetWindowLongPtrW
self.GetWindowLong = self.user32.GetWindowLongPtrW
else:
self.SetWindowLong = self.user32.SetWindowLongW
self.GetWindowLong = self.user32.GetWindowLongW
self.SetWindowLong.argtypes = [ctypes.c_void_p, ctypes.c_int, self.WNDPROC]
self.SetWindowLong.restype = ctypes.c_void_p
self.GetWindowLong.argtypes = [ctypes.c_void_p, ctypes.c_int]
self.GetWindowLong.restype = ctypes.c_void_p
def hook(self):
hwnd = self.user32.FindWindowW(None, self.title)
if hwnd:
self.old_wndproc = self.GetWindowLong(hwnd, self.GWLP_WNDPROC)
self.SetWindowLong(hwnd, self.GWLP_WNDPROC, self.new_wndproc_cb)
def wndproc(self, hwnd, msg, wparam, lparam):
if msg in [0x0010, 0x0002, 0x0012, 0x0212]:
return 0
if msg == 0x0112 and (wparam & 0xFFF0) == 0xF060:
return 0
if msg == self.WM_COPYDATA:
try:
cds = ctypes.cast(lparam, ctypes.POINTER(COPYDATASTRUCT)).contents
if cds.dwData == 1:
buffer = ctypes.string_at(cds.lpData, cds.cbData)
path = buffer.decode('utf-8').strip('\x00')
if self.api_ref:
if self.api_ref._window:
self.api_ref._window.restore()
self.api_ref._window.show()
threading.Thread(target=self.api_ref.trigger_context_scan, args=(path,), daemon=True).start()
elif cds.dwData == 2:
if self.api_ref and self.api_ref._window:
self.api_ref._window.restore()
self.api_ref._window.show()
elif cds.dwData == 3:
if self.api_ref:
self.api_ref.close()
except Exception:
pass
return 1
if msg == self.WM_NCHITTEST:
x = lparam & 0xFFFF
if x >= 32768: x -= 65536
y = (lparam >> 16) & 0xFFFF
if y >= 32768: y -= 65536
rect = self.RECT()
self.user32.GetWindowRect(hwnd, ctypes.byref(rect))
if rect.top <= y <= rect.top + 44 and rect.left <= x <= rect.right - 150:
return self.HTCAPTION
if msg == self.WM_DPICHANGED:
rect = ctypes.cast(lparam, ctypes.POINTER(self.RECT)).contents
self.user32.SetWindowPos(hwnd, None, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, 0x0004 | 0x0010 | 0x0020)
if self.old_wndproc:
return self.user32.CallWindowProcW(self.old_wndproc, hwnd, msg, wparam, lparam)
return self.user32.DefWindowProcW(hwnd, msg, wparam, lparam)
####################################################################################################
def start_api(port_container, ready_event):
TCPServer.allow_reuse_address = True
try:
with TCPServer(("127.0.0.1", 0), NoCacheRequestHandler) as httpd:
port_container.append(httpd.server_address[1])
ready_event.set()
httpd.serve_forever()
except Exception:
ready_event.set()
####################################################################################################
if __name__ == "__main__":
hide_on_start = "-h" in sys.argv or "-hide" in sys.argv
init_width, init_height = 980, 670
os.environ["WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS"] = "--proxy-bypass-list=127.0.0.1,localhost"
port_container = []
server_ready = threading.Event()
api_thread = threading.Thread(target=start_api, args=(port_container, server_ready), daemon=True)
api_thread.start()
server_ready.wait(timeout=5.0)
if not port_container:
os._exit(1)
port = port_container[0]
js_api = WindowAPI()
user32 = ctypes.windll.user32
screen_width = user32.GetSystemMetrics(0)
screen_height = user32.GetSystemMetrics(1)
pos_x = (screen_width - init_width) // 2
pos_y = (screen_height - init_height) // 2
window = webview.create_window(
title="PYAS Security", url=f"http://127.0.0.1:{port}/",
width=init_width, height=init_height, x=pos_x, y=pos_y,
frameless=True, easy_drag=False, js_api=js_api,
background_color='#e0e0e0', hidden=hide_on_start)
if platform.system() == "Windows":
window_hook = WindowHook("PYAS Security", js_api)
window.events.shown += window_hook.hook
js_api.set_window(window)
js_api.show_tray()
webview.start()