门店:“电脑又双叒叕中病毒了”

作者:周杰

背景

店长:"我电脑右下角总是弹框!"

我:"好的好的,马上处理!"

店长:"我电脑卡死了!"

我:"好的好的,马上处理!"

店长:"我电脑被小游戏占领了!"

远程

好吧,当门店这样反馈的时候,我们还是要处理的。上述情况,很多时候,是病毒、驱动安装程序、杀毒软件等造成的,对于病毒,就用杀毒软件清理,对于驱动安装程序、杀软等,就直接卸载。

最初主要通过teamviewer、向日葵、desktop这几款软件远程处理。步骤大概是:

但这样很麻烦,因为要门店提供远程。有时候门店不知道远程是啥意思,就要向他们解释;甚至有些都没装远程软件,还得告诉他们怎么安装

因此我们需要一种更便捷的远程方式

更便捷的远程

想要实现更便捷的远程,有很多方法,比如常见的远程软件本身都提供了这种能力,只不过需要收费~

我们自己实现了一个小软件:

输入门店编码后就可以直接一键远程过去(具体实现涉及公司信息,不方便说~)

如果有想实现这种一键远程能力的,已知的有几种途径:

  1. 杀毒软件企业版。其实本文内容,除了最后一点桌面管控,常见杀毒软件基本都能提供。因此可以购买他们的解决方案
  2. 远程软件企业版。比如teamviewer,他们提供一种批量部署包,被部署的电脑就可以一键远程。当然,部署数量有限制,要根据部署数量购买相应套餐
  3. 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;
}

接着我们把这些数据上报到云端,就可以知道门店的软件情况了(经测试跟控制面板 - 程序卸载里的程序是一样的)。但这种只能知道安装过的软件,如果是一些不用安装的程序,这里是扫不到的。因为我们目前没这种需求,所以没去看这块,想到的可能的两个方法:

  1. 扫进程 - 不靠谱,因为任务管理器里的进程是可以被程序隐藏的(使用的技术是下文提到的SSDT Hook),另外有些看起来正常的进程是被病毒程序代理的
  2. 扫文件系统,然后根据特征码识别文件(没错,听起来有点像杀毒软件)

好,这里就已经知道门店的软件列表了。但在前期,我们有很多门店,他们都安装了各种各样我们认为不好的软件,那我们要一家家的远程过去手动删除吗?

自动删除异常软件

自动删除软件有两个问题:

  1. 程序启动后占用文件,导致文件无法删除
  2. 杀毒软件对文件都做了权限控制,而且还不允许我们修改权限,导致无法删除(有些病毒也有这个能力),而且拦截了修改注册表操作,我们无法通过改注册表来阻止他开机自启

关于这两个问题,我们找到了一个比较厉害的工具: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,可以打开如下界面:

接着新建规则:

这里有三种策略:

  1. 发布者。可以根据文件的数字签名做拦截(这种只能拦截MSI/MSP文件)。
  1. 路径。可以拦截某个指定路径的程序执行,或者拦截某个文件夹下的程序执行。如果选了文件夹,可以从中排除一些需要支持的软件
  1. 文件hash。系统会根据选择的文件生成hash,从而禁止hash相同的程序执行。比如如果我们要禁止chrome运行,可以配置拒绝Google文件夹内文件执行,此时运行chrome就会有报错提示:

通过这种方式,我们可以配置文件夹拒绝访问,然后把我们常用的几个软件排除掉,这样可以让电脑尽可能干净。

这里也可以通过Powershell命令行去设置,先打开Powershell,执行命令:Import-Module AppLocker,这样就引入了一些AppLocker的cmdlet,包括:Get-AppLockerPolicySet-AppLockerPolicy,看名字就知道,这是可以获取和设置AppLocker的。具体使用可以去microsoft官网搜一下这两个命令

不过这种方式有两个问题:

  1. 提示内容固定,这是系统自带的,改不了,从体验角度来说,可能是一个不友好的提示。假设门店在电脑上一顿点,然后一顿弹框,门店又看不懂,那必然又是一个工单上来了
  2. 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的安装包

然后执行:

可以看到会有报错,实际上是创建进程的时候被拦了

拦截浏览器 + 提供程序安装界面

各种垃圾软件被安装,起初都是因为门店想要安装一个正常的软件,比如音乐软件,然后去浏览器搜索,接着搜索引擎就推荐了一个😒,你懂的~

因此可以有个想法:

  1. 拦截浏览器进程 (门店用浏览器基本都是下载软件,所以可以拦截)
  2. 提供一个程序安装界面,引导门店去这里下载

我们判断如果门店要打开浏览器,直接拦截,拦截方式可以用上面拦截软件安装的方式拦截,也可以监听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语句过滤出进程创建事件。

这种方式可以拦截绝大部分手动打开浏览器的行为,不过这种方式有两个地方有延迟,因此不太适合作为通用的软件拦截手段。两个延迟的地方是:

  1. 监听语句里的WITHIN 5,代表判断最近5s有无该类事件,本质就相当于5s轮询,那这就是一个5s的延迟
  2. 进程信息初始化有段时间,上面有个循环代码等待进程信息初始化。这里也是个延迟

延迟导致一个场景会有问题:如果是通过进程唤起程序静默安装,如果安装过程比较快,跳过了这个延迟,或者在延迟期间已经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前端团队」,获取更多干货实践,欢迎交流分享。

相关推荐
hairenjing11237 小时前
使用 Mac 数据恢复从 iPhoto 图库中恢复照片
windows·stm32·嵌入式硬件·macos·word
九鼎科技-Leo9 小时前
了解 .NET 运行时与 .NET 框架:基础概念与相互关系
windows·c#·.net
九鼎科技-Leo11 小时前
什么是 ASP.NET Core?与 ASP.NET MVC 有什么区别?
windows·后端·c#·asp.net·mvc·.net
黎明晓月15 小时前
Java之字符串分割转换List
java·windows·list
九鼎科技-Leo15 小时前
在 C# 中,ICollection 和 IList 接口有什么区别?
windows·c#·.net
顾辰呀16 小时前
实现uniapp-微信小程序 搜索框+上拉加载+下拉刷新
前端·windows
Bunny Chen19 小时前
如何缩小PPT演示文稿的大小?
windows·microsoft·powerpoint
如光照19 小时前
Linux与Windows中的流量抓取工具:wireshark与tcpdump
linux·windows·测试工具·网络安全
wwc_boke20 小时前
Linux查看端口占用及Windows查看端口占用
linux·运维·windows
WangMing_X21 小时前
C# 一个工具类让winform自动根据窗体大小缩放所有控件
开发语言·windows·c#