作者:周杰
背景
店长:"我电脑右下角总是弹框!"
我:"好的好的,马上处理!"
店长:"我电脑卡死了!"
我:"好的好的,马上处理!"
店长:"我电脑被小游戏占领了!"
远程
好吧,当门店这样反馈的时候,我们还是要处理的。上述情况,很多时候,是病毒、驱动安装程序、杀毒软件等造成的,对于病毒,就用杀毒软件清理,对于驱动安装程序、杀软等,就直接卸载。
最初主要通过teamviewer、向日葵、desktop这几款软件远程处理。步骤大概是:
但这样很麻烦,因为要门店提供远程。有时候门店不知道远程是啥意思,就要向他们解释;甚至有些都没装远程软件,还得告诉他们怎么安装
因此我们需要一种更便捷的远程方式
更便捷的远程
想要实现更便捷的远程,有很多方法,比如常见的远程软件本身都提供了这种能力,只不过需要收费~
我们自己实现了一个小软件:
输入门店编码后就可以直接一键远程过去(具体实现涉及公司信息,不方便说~)
如果有想实现这种一键远程能力的,已知的有几种途径:
- 杀毒软件企业版。其实本文内容,除了最后一点桌面管控,常见杀毒软件基本都能提供。因此可以购买他们的解决方案
- 远程软件企业版。比如teamviewer,他们提供一种批量部署包,被部署的电脑就可以一键远程。当然,部署数量有限制,要根据部署数量购买相应套餐
- github开源软件。github上有些开源软件支持远程能力,可以自己改改来用
删除异常软件
当我们可以一键远程门店电脑后,要删除异常软件就方便很多了
但如果只等门店反馈,再远程过去处理,就很被动。万一哪天门店集中反馈岂不是要完。
因此我们需要一种监控手段,能知道哪些门店有异常软件,一旦发现,我们可以直接远程过去处理掉
监控门店有哪些异常软件
可以从注册表拉取门店安装的软件,比如:
c
public static List<IAppData> getAllSoftWare () {
List<IAppData> appDataList = new List<IAppData>();
RegistryKey Key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall");
if (Key != null)//如果系统禁止访问则 返回null
{
String SoftwareName = SubKey.GetValue("DisplayName", "").ToString();
...
if (SoftwareName != "" && appDataList.Find((it) => it.DisplayName == SoftwareName) == null && SystemComponent != "1")
{
var data = new IAppData();
data.DisplayName = SoftwareName;
...
appDataList.Add(data);
}
}
...
// 这里没列完代码,否则会有点啰嗦,就备注下注意事项
// 1、要用OpenSubKey和OpenBaseKey两种方式结合取注册表
// 2、要读两个路径下的注册表:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall、SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall
// 3、要分别读LocalMachine、CurrentUser两个根路径下的上述两个路径的注册表项
// 如果不这样读,是读不完整的
// 4、可以用注册表项的SystemComponent属性是否为1,来判断是否是系统组件 - 控制面板-程序卸载里没有包括系统组件
return appDataList;
}
接着我们把这些数据上报到云端,就可以知道门店的软件情况了(经测试跟控制面板 - 程序卸载里的程序是一样的)。但这种只能知道安装过的软件,如果是一些不用安装的程序,这里是扫不到的。因为我们目前没这种需求,所以没去看这块,想到的可能的两个方法:
- 扫进程 - 不靠谱,因为任务管理器里的进程是可以被程序隐藏的(使用的技术是下文提到的
SSDT Hook
),另外有些看起来正常的进程是被病毒程序代理的 - 扫文件系统,然后根据特征码识别文件(没错,听起来有点像杀毒软件)
好,这里就已经知道门店的软件列表了。但在前期,我们有很多门店,他们都安装了各种各样我们认为不好的软件,那我们要一家家的远程过去手动删除吗?
自动删除异常软件
自动删除软件有两个问题:
- 程序启动后占用文件,导致文件无法删除
- 杀毒软件对文件都做了权限控制,而且还不允许我们修改权限,导致无法删除(有些病毒也有这个能力),而且拦截了修改注册表操作,我们无法通过改注册表来阻止他开机自启
关于这两个问题,我们找到了一个比较厉害的工具:IObit Unlocker
,这个软件可以单独解除文件占用,也可以直接删除文件,而且支持命令行(大佬们也可以自己写代码做强删,这个复杂点,还在研究,以后有机会再分享~):
c
runCmd('IObitUnlocker.exe /Delete "文件的绝对路径"')
这个执行后会有个弹框:
这是我们不希望用户看到的,但我们可以通过程序把弹框干掉,只要杀掉IObitUnlocker.exe进程就行了
c
// 杀进程之前判断弹框是否存在,可以用FindWindow函数去查找窗口
if (/** 有弹框了 */) {
Process[] processes = Process.GetProcessesByName("IObitUnlocker");
foreach (Process process in processes)
{
process.Kill(false);
}
}
然后还有个问题是,这个程序是有安装界面的,我们也不希望用户看到,我测了下,把安装后的目录压缩传到云端,然后下载解压到用户电脑,也是能正常用的,这样门店就不会感知到软件安装过程(其实这个软件也支持静默安装,加上/silent参数就可以,但安装后会自动唤起程序,程序本身是有界面的)
好!假设现在我们把所有门店的所有异常软件都清掉了,那这就ok了吗?
非也!事实是没过两天,我们就会通过监控发现门店又出现了大量异常软件!原因是我们删归删,但门店可以再装回来啊~因此我们要彻底根绝门店装异常软件的场景!
拦截软件安装
这里列举两种我们调研或使用的方法,供参考
策略组
策略组是windows
自带的一种安全设置,使用Win键+R打开运行窗口并输入gpedit.msc
,可以打开如下界面:
接着新建规则:
这里有三种策略:
- 发布者。可以根据文件的数字签名做拦截(这种只能拦截MSI/MSP文件)。
- 路径。可以拦截某个指定路径的程序执行,或者拦截某个文件夹下的程序执行。如果选了文件夹,可以从中排除一些需要支持的软件
- 文件hash。系统会根据选择的文件生成hash,从而禁止hash相同的程序执行。比如如果我们要禁止chrome运行,可以配置拒绝Google文件夹内文件执行,此时运行chrome就会有报错提示:
通过这种方式,我们可以配置文件夹拒绝访问,然后把我们常用的几个软件排除掉,这样可以让电脑尽可能干净。
这里也可以通过Powershell
命令行去设置,先打开Powershell
,执行命令:Import-Module AppLocker
,这样就引入了一些AppLocker
的cmdlet,包括:Get-AppLockerPolicy
和Set-AppLockerPolicy
,看名字就知道,这是可以获取和设置AppLocker
的。具体使用可以去microsoft官网搜一下这两个命令
不过这种方式有两个问题:
- 提示内容固定,这是系统自带的,改不了,从体验角度来说,可能是一个不友好的提示。假设门店在电脑上一顿点,然后一顿弹框,门店又看不懂,那必然又是一个工单上来了
- win10/11默认是不支持组策略的,要通过手段去开启,这个可以自行百度
SSDT hook
程序本质上都是通过系统api唤起的,比如 CreateProcess
, 就跟浏览器上我们可以代理window
对象上的api
一样,我们也可以代理windows
系统的api
,在里面写上逻辑,判断如果是要拦截的程序,就不走原本的CreateProcess
,直接return就完事,核心代码如下(这里的实现用的是minhook
这个库,有兴趣的可以去了解下):
c
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <stdio.h>
#include <winternl.h>
#include <MinHook.h>
#if defined _M_X64
#pragma comment(lib, "D:\\project\\minhook\\vcpkg\\packages\\minhook_x64-windows\\lib\\minhook.x64.lib")
#elif defined _M_IX86
#pragma comment(lib, "minhook.x86.lib")
#endif
using namespace std;
#pragma comment(lib, "version")
#pragma warning(disable : 4996)
// 定义一个指针函数类型
typedef BOOL(WINAPI* myCreateProcessW)(
_In_opt_ LPCWSTR lpApplicationName,
_Inout_opt_ LPWSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCWSTR lpCurrentDirectory,
_In_ LPSTARTUPINFOW lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
);
// 定义一个存放原函数的指针
myCreateProcessW fpCreateProcessW = NULL;
BOOL WINAPI HookedCreateProcessW(
_In_opt_ LPCWSTR lpApplicationName,
_Inout_opt_ LPWSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCWSTR lpCurrentDirectory,
_In_ LPSTARTUPINFOW lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
) {
if (/** 要拦截 */) { // 这里可以通过判断执行文件属性,比如获取版权所属公司,如果是某司的,就一律拦截
// doLog("dll拦截: " + filePath);
return true;
};
return fpCreateProcessW(lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation);
}
// 封装MinHook的使用
template <typename T>
inline MH_STATUS MH_CreateHookEx(LPVOID pTarget, LPVOID pDetour, T** ppOriginal)
{
return MH_CreateHook(pTarget, pDetour, reinterpret_cast<LPVOID*>(ppOriginal));
}
template <typename T>
inline MH_STATUS MH_CreateHookApiEx(
LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, T** ppOriginal)
{
return MH_CreateHookApi(
pszModule, pszProcName, pDetour, reinterpret_cast<LPVOID*>(ppOriginal));
}
// 封装Hook函数
BOOL Hook() {
// 初始化MinHook
MH_Initialize();
MH_CreateHookApiEx(L"kernel32", "CreateProcessW", HookedCreateProcessW, &fpCreateProcessW);
MH_EnableHook(MH_ALL_HOOKS);
return true;
}
BOOL unHook() {
MH_DisableHook(MH_ALL_HOOKS);
MH_Uninitialize();
return true;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Hook();
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
unHook();
break;
}
return TRUE;
}
上述代码最终会编译成一个dll文件,接着我们要全局注入此dll文件,核心代码如下:
c
SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, hDllModule, 0)
原理是SetWindowsHookEx
这个函数可以在系统中安装全局钩子,我们注册的WH_GETMESSAGE
是个消息钩子,是用于监视消息队列的,而Windows
系统是基于消息驱动的,所以所有进程都会有一个自己的消息队列,都会触发WH_GETMESSAGE
这个钩子,一旦触发,有了上述代码后,就会先加载hDllModule
再走后续的消息流程。
因此这样就可以给所有进程注入这个dll,然后这些进程再创建其他进程的时候,拦截就会生效了。
这里有两种场景: 1、在桌面或文件夹点击xx.exe
,实际上是explore.exe
这个程序的进程调用createProcess
来创建进程,而explore.exe
的进程加载此dll后,再调用createProcess
就会走我们的hook
函数,从而可以触发拦截逻辑。 2、程序A
唤起xx.exe
的进程,拦截流程也跟桌面手点exe是一样的,只是把explore.exe
换成A
现在我们试试用脚本唤起异常软件的安装程序,写个node
脚本
javascript
const { spawnSync } = require('child_process');
spawnSync("C:\\Users\\Administrator\\Desktop\\inst.exe"); // 3xx的安装包
然后执行:
可以看到会有报错,实际上是创建进程的时候被拦了
拦截浏览器 + 提供程序安装界面
各种垃圾软件被安装,起初都是因为门店想要安装一个正常的软件,比如音乐软件,然后去浏览器搜索,接着搜索引擎就推荐了一个😒,你懂的~
因此可以有个想法:
- 拦截浏览器进程 (门店用浏览器基本都是下载软件,所以可以拦截)
- 提供一个程序安装界面,引导门店去这里下载
我们判断如果门店要打开浏览器,直接拦截,拦截方式可以用上面拦截软件安装的方式拦截,也可以监听WMI
事件拦截:
c
public static void preventBrowser()
{
// 创建 WMI 查询语句
var queryString = "SELECT * FROM __InstanceCreationEvent WITHIN 5 WHERE TargetInstance ISA 'Win32_Process'";
// 创建 WMI 查询对象
var query = new EventQuery(queryString);
// 创建 WMI 监视器
using (var watcher = new ManagementEventWatcher(query))
{
// 设置 WMI 监视器句柄
watcher.EventArrived += OnProcessStarted;
// 开始监视
watcher.Start();
}
}
private static void OnProcessStarted(object sender, EventArrivedEventArgs e)
{
// 获取新创建进程的信息
var process = e.NewEvent.Properties["TargetInstance"].Value as ManagementBaseObject;
if (process != null)
{
for (int i = 0; i < 60; i++)
{
if (process.Properties.Count <= 0)
{
Thread.Sleep(50); // 等待50ms
}
else
{
break;
}
}
// 获取进程路径
var processPath = process.Properties["ExecutablePath"].Value?.ToString();
var processName = process["Name"].ToString();
// 这个可以作为文件的执行路径
var CommandLine = process.Properties["CommandLine"].Value?.ToString();
if (processName == null && processPath == null && CommandLine == null) return;
// 检查是否是浏览器进程
if (isBrowser(processName, processPath, CommandLine))
{
// 杀死进程
try
{
var processId = process.Properties["ProcessId"].Value?.ToString();
if (!string.IsNullOrWhiteSpace(processId))
{
var targetProcess = Process.GetProcessById(int.Parse(processId));
targetProcess.Kill();
}
}
catch (Exception ex)
{
Log("无法杀死进程: " + ex.toString());
}
}
}
}
__InstanceCreationEvent
这个代表系统里一些WMI
对象的创建事件,再通过Where
语句过滤出进程创建事件。
这种方式可以拦截绝大部分手动打开浏览器的行为,不过这种方式有两个地方有延迟,因此不太适合作为通用的软件拦截手段。两个延迟的地方是:
- 监听语句里的
WITHIN 5
,代表判断最近5s有无该类事件,本质就相当于5s轮询,那这就是一个5s的延迟 - 进程信息初始化有段时间,上面有个循环代码等待进程信息初始化。这里也是个延迟
延迟导致一个场景会有问题:如果是通过进程唤起程序静默安装,如果安装过程比较快,跳过了这个延迟,或者在延迟期间已经Hook了系统API做了防杀,那就拦截不了了。但简单拦截手点打开浏览器还是可以的
拦截打开浏览器还有个问题是,要判断是手点浏览器程序,还是浏览器的自动更新等进程。这里我们就没去做这种判断了,直接禁用了常见浏览器的自动更新任务,比如搜狗和火狐禁用如下:
c
Global.RunCmd("schtasks /change /tn \"SogouExplorer Updater Task\" /disable");
Global.RunCmd("schtasks /change /tn \"SogouExplorer Updater Task(Core)\" /disable");
Global.RunCmd("reg add \"HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Mozilla\\Firefox\" /v DisableAppUpdate /t REG_DWORD /d 1 /f");
这里我们拦截浏览器后,弹框引导门店去我们的软件管理里安装想要的程序
点击确定后跳转到程序下载页面
这里就是云端配置好应用列表,点安装后,先下载安装包,然后执行即可。接着获取执行进程是否存在,不存在就可以认为执行完了,就可以再次点安装,否则安装按钮置灰,简单执行+判断代码如下:
c
Process process = new();
process.StartInfo.FileName = exePath;
process.StartInfo.UseShellExecute = false;
process.Start();
while (true)
{
Thread.Sleep(3000);
Process[] localByName = Process.GetProcessesByName(exeName);
if (localByName.Length == 0) {
Log("安装完成 " + exeName);
break;
}
}
但这里有个问题:
有些门店会这个操作,我拦他操作,他直接把我程序干没了。。。就这么想要去浏览器下载,看来还是引导做的不够好💀
不过也有个技术手段能处理:像杀毒软件一样做防杀,用的方法也是SSDT hook
,不过hook
的是 openProcess
这个api,判断如果是自己的程序,就return
。然后再结束任务就会出现如下弹框:
但我们没做这个,因为我们发现了一个终极办法,我们希望从根本上引导直至改变门店的认知,把电脑只当作点单机,而非一个正常的电脑。我们想对电脑做更彻底的管控,直接管控整个系统,让他们一看界面就知道,这只是个收银机。这样他们就不会做出一些他们觉得在电脑上能做的,但我们认为危险的操作
桌面管控
界面大概长这样:
功能目前主要是以下4块:
这样等于门店面对的不再是windows系统了,而是一个虚拟界面,在这个界面内模拟一些常规操作,比如唤起程序、网络、关机、音量等,这样门店就只能在点单、收银流程的正常范围进行操作了,门店再也不能愉快打蜘蛛纸牌了
(具体实现,以后有机会再说~)
总结
这篇文章由给门店清理电脑为切入点,讲了一些我们对这种场景的处理办法。
刚开始,由门店反馈过来,给我们提供远程密钥后,我们远程过去清理。
后来我们可以监控门店的软件并直接远程过去清理,甚至可以自动清理,并对门店安装、运行软件的动作进行拦截和管理,以绝后患;
最后对整个桌面做管控,从根本引导并改变门店对点单机的认知。
通过上述手段,我们可以防止门店的误操作导致一些异常软件对系统造成影响。同时大家可以发现,这些手段会大大加强我们对门店收银机的管控能力,这才是最终的目的
小茗推荐
最后
关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享。