• 搞定加壳恶意进程检测流程!理解和构建复杂业务流程的基本方法


    运用之妙,存乎一心。


    引子

    在 “计算机编程领域的三十六种基本思想概览” 一文中,概览了计算机编程领域的若干重要思想;在““驯服”业务流程:盘点业务开发中的常见流程模式 ”一文中,概览了业务开发中的各种基本流程模式。有童鞋就问了:这些都有点“泛泛而谈“,究竟能派上什么用场呢?其实,用处可大着呢!

    今天来讨论一个比较复杂的业务流程,即最近做的 UPX 加壳恶意进程的检测流程。

    背景知识

    啥叫 UPX 加壳恶意进程呢?

    我们知道,计算机里的所有程序的运行,都是以进程的运行态实现的。许多程序由于自身保护(防破解等)的缘故,会在程序外面加上一层壳,这层壳会改变程序文件的一些元数据信息,并将程序的入口隐藏在某个地方。

    加壳,即是在可执行文件的外层包装了一层(压缩、虚拟化等),形成一个新的可执行文件。加壳可以隐藏程序真正的OEP(入口点),阻止外部程序或软件对加壳程序本身的反汇编分析或者动态分析。

    从 UPX 官网及百度百科可知,UPX 是一款成熟的可执行程序文件压缩器,压缩过的可执行文件体积缩小50%-70% (试验过,2M -> 1.1M, 5M -> 2M),这样减少了磁盘占用空间、网络传输的时间。通过 UPX 压缩过的程序和程序库完全没有功能损失,和压缩之前一样可正常地运行。

    恶意进程(比如病毒程序、挖矿程序、勒索程序)也可以应用“加壳”的思想来保护自己,绕过检测。恶意进程可以使用 UPX 给自己加一层壳。那么要检测这种加壳的恶意进程,就需要先给它”解壳“,然后再做检测。本文不讨论加壳解壳的具体细节,只要记住以下两条命令即可:

    加壳

    upx executable -o executable.out
    

    解壳

    upx -d executable.out -o executable
    

    咱们来看看这个加壳恶意进程的检测流程。

    检测流程

    加壳恶意进程的检测流程如下图所示:

    把(图一)的”通用入侵检测流程“和”过已有引擎库检测“,这几行字分别用(图二)和(图三)替换掉,就得到了完整的检测流程。emmm..., 有木有感觉到脑力有点不够用了? 我也有这个感觉的。

    (图一)

    其中,通用入侵检测流程如下:

    (图二)

    过已有引擎库检测流程如下:

    1.  创建一个总任务;

    1. 根据每个引擎检测库分别创建一个子任务;

    2. 每个引擎检测库子任务相互独立并行执行,定时任务驱动;

    3. 每个引擎检测库子任务的流程是:先查样本库,如果命中样本库,直接更新样本库结果及任务检测状态;如果没有命中样本结果库,则创建一个异步定时任务来执行新检测;

    5. 根据所有子任务的检测状态,更新总任务状态;当所有子任务都检测完成,更新总任务状态为完成;

    1. 若总任务状态为完成,则搜集所有该任务对应的所有子任务的引擎检测结果,如有任一命中,则生成告警。

    (图三) 由于涉及公司技术方案敏感性,就不公开了,读者可通过上述文字来理解下。


    复杂性在哪里

    这个检测流程的复杂性体现在哪里呢? 它的总体流程结构很复杂,既有并发也有多重异步,还必须在所有子流程完成后汇总所有子流程的检测结果。

    • 子任务并发。并发是软件复杂性的来源之一。对于每个恶意进程,都会创建对应的多个检测子任务;每个检测子任务是相互独立执行的,执行时序是不确定的, 谁先执行完成无法确定。加之 CPU 每秒可执行千万条指令,程序时时刻刻在高速运转着,以人脑的推理和理解能力,根本无法推导出里面具体是怎么执行完成的;不像物理学那样,可以精确推导出何时抵达什么位置;

    • 有多重异步。入口检测流程到原进程文件上传的线程执行是第一重异步;原进程文件上传完成后完善文件上传记录,然后提交该进程文件检测任务到线程池是第二重异步;线程池执行根据总检测任务及多个引擎库创建多个引擎库检测子任务是第三重异步;每个引擎库检测子任务都在各自的定时任务里相互独立执行是第四重异步;每个子任务在定时任务里执行,若未命中样本库,则需要新创建新的子任务,新的子任务会在新的定时任务线程里执行是第五重异步;汇集对应所有子任务的检测结果是第六重异步;根据检测命中结果生成告警是第七重异步。每一重异步都意味着并发的复杂度增加;

    • 所有并发异步流程的执行时序都是不确定的。你不知道它们何时会执行完成;

    • 算一算,假设有 4 个引擎库检测,那么一个恶意进程检测就需要 1 + 1 + 1 + 4 + 4 + 1 + 1 = 13 个线程同时在并发执行。 假设有 N 个恶意进程在同时检测呢,就是 N + M + 8 + C + D。 N 是第一重异步的线程最大数;M 是文件回调处理的最大消费线程数;C 是用于汇集检测结果的最大线程数; D 是用于生成告警的最大线程数。

    想象一下,你的团队有 13 个成员在一起协作做一件事,每个成员都在以远超过音速的速度并行前进,你需要在他们都完成的时候汇总他们的结果。你能弄清楚这些是怎么完成的么?

    在无法推导程序如何具体地完成这一切(无法推导其执行顺序,每一次恶意进程检测任务的执行过程都是不一样的!),却能够让这所有一切流程有序地完成,汇聚所有必要的检测结果,并生成唯一的最终告警,简直是一种魔法!

    构建复杂流程的方法

    如何来理解和构建这个检测流程呢? 运用“分解-抽象-封装-复用-组合-关联-重构-函数式编程”的思想和方法来搞定。

    分解

    分解,即是将具有独立语义的子流程和流程逻辑单元分离出来。

    • 将整体流程分解为若干个子流程;
    • 将每个子流程进一步分解为更小粒度的流程逻辑单元;
    • 一直分解,直至每个流程逻辑单元简单而清晰(只做一件事)。

    (图一)的每个框框里的文字,都代表一个子流程或细粒度的流程逻辑单元。这些子流程或流程逻辑单元有:

    • 参数校验;开关检测;白名单过滤;文件大小检验;
    • 创建文件上传记录;上传文件操作; 文件上传完成后的回调,更新文件上传记录;
    • 创建总检测任务;创建多个子任务并与总任务关联;更新总任务状态;
    • 下载进程文件;解壳操作;解壳成功后设置解壳文件信息;
    • 构建多个引擎库检测子任务;用多个定时任务分别驱动多个引擎库检测子任务运行;
    • 查询和匹配检测结果样本库;调用引擎能力来检测进程是否恶意;
    • 子任务检测完成后获取结果;汇总检测子任务的检测结果;分析是否恶意进程;
    • 判断是否已有告警;更新一个已有告警;生成一个新的告警。

    构建流程逻辑单元的模式可以对应到““驯服”业务流程:盘点业务开发中的常见流程模式 ”一文中的基本流程模式。

    分解这一步非常重要:

    • 将整体流程中的子流程分离出来再组合,可使其依赖和组合关系分割得更清晰,使凌乱的整体流程逻辑整通顺,变得整洁有序;
    • 分解整体流程的过程中可以增进对整体流程的理解;
    • 为抽象、封装和复用打下基础。

    分解得足够细,就更容易抽象和封装,更容易复用。

    抽象

    在分解的基础上,将关键信息提炼出来,将流程共性提炼出来,封装成可复用的标准对象和方法。

    • 关键信息提炼;
    • 共性流程提炼。

    标准对象提炼非常重要,体现了对流程中关键信息的认知。流程通常是围绕关键信息而构建起来的。

    流程共性抽象亦非常重要,将子流程的共性抽离出来,更容易复用,减少重复代码及伴随的BUG。

    比如这个恶意进程检测流程的关键信息是什么? MD5, SHA256, filePath。准确地构造这 3 个信息,基本就把握了恶意进程检测流程的关键。

    比如各种检测流程有什么共性流程?获取规则信息、构建告警详情、存储告警详情、白名单检测。

    比如这个加壳恶意进程的检测流程有什么共性流程?其告警生成流程共性是“若有则更新,若没有则创建新的”。

    抽象的结果通常是若干个可复用的独立语义的概念。而封装则需要将概念转换为具体的实现形式。

    封装(抽象的实现)

    抽象好后,就需要设计良好的可复用的封装形式。

    • 关键信息封装成标准对象(可对接不同业务)。
    • 子流程共性封装成流程组件;
    • 流程组件封装成高层次的流程组块。

    良好的封装可以减少重复代码的出现,让流程更清晰而容易理解,减少认知负担。

    封装单元可大可小。封装单元可以是一个对象、方法、组件类,也可以是多个类组成的库或框架,或一个相对完整的流程。

    比如知道恶意进程检测的关键信息 MD5, SHA256, filePath,就可以把这三个信息封装成一个标准对象 VirusCheckTaskInfo。

    比如通用入侵检测流程里的获取规则信息、检测结果填充、告警详情构建、告警存储、发送大数据,都可以封装成流程组件,可以在不同的检测流程里复用。

    复用

    • 提炼流程组件的通用部分进行复用;
    • 提炼流程组块的通用部分进行复用。

    有了封装的基础,复用就比较容易了。只需考虑在具体的业务场景如何去引用,或者对现有封装做一点小的改造。

    流程组块是理解和构建复杂流程的基础。越是能够在高层次流程组块思考和构建流程,就能驾驭越复杂的流程。

    有了可复用的封装的基础,接下来就只需要考虑封装单元的组合和关联。

    组合

    • 在可复用的流程组件和流程组块的级别上进行组合。

    比如上述多个流程组件可组合成通用入侵检测流程的核心部分,而这个通用入侵检测流程的核心部分又在更复杂的检测流程里复用。比如过各种已有引擎检测库,就几个字,却代表着一个包含多个引擎库的复杂的恶意进程检测流程。

    组合通常是串行、并发的各种流程组合模式。这就运用到““驯服”业务流程:盘点业务开发中的常见流程模式 ”一文总结的各种业务流程模式及组合模式。

    关联

    • 将有关联的信息通过唯一标识关联起来;
    • 通过实体表的唯一标识字段的外键关联来建立。

    比如脱壳进程的文件和原进程的文件信息,就可以通过在脱壳进程文件记录里关联原进程文件记录的 MD5 或 SHA256 或 fileId 来实现。

    重构

    • 反复运用上述方法,对已有流程进行改小步重构,是一种很好的理解和梳理复杂流程的方式;
    • 小步重构技巧:每次抽离一部分流程,然后写一个短函数进行调用。

    函数式编程

    函数式编程是一种强大的编程思想和编程技艺,可以很好地将共性和差异抽离,让整个流程更加顺畅。

    比如前面的“命中后,若已有告警则更新已有告警,若没有则生成新的告警”,可用如下实现:

    public void generateOrUpdateVirusDetection(VirusCheckTaskInfoDO virusCheckTaskInfo,
                                                   BiConsumer<VirusCheckTaskInfoDO, VirusDetectResult> virusResultSetterFunc,
                                                   Function<VirusCheckTaskInfoDO, VirusDetectResult> newVirusResultFunc) {
    
            String detectionId = virusCheckTaskInfo.getDetectionId();
            String tenantId = virusCheckTaskInfo.getTenantId();
    
            DetectionDO detectionDO = detectionService.findByDetectionId(tenantId, detectionId);
    
            // 已有告警,则更新已有告警
            if (detectionDO != null && detectionDO.getDetectionMethod() == DetectionMethodEnum.VIRUS) {
    
                LOG.info("exist virus detection: detectionId: {}, uniqueKey: {}", detectionId, virusCheckTaskInfo.getUniqueKey());
    
                DetectResultBase detectResultBase = detectionDO.getDetectResult();
                if (detectResultBase instanceof VirusDetectResult) {
    
                    VirusDetectResult vr = (VirusDetectResult) detectResultBase;
                    virusResultSetterFunc.accept(virusCheckTaskInfo, vr);
    
                    // update detection record
                }
            } else {
    
                LOG.info("new virus detection. detectionId : {}, uniqueKey: {}", detectionId, virusCheckTaskInfo.getUniqueKey());
    
                asyncFlower.asyncCheck(tenantId, detectionId, agentDetection -> {
                    VirusDetectResult virusDetectResult = newVirusResultFunc.apply(virusCheckTaskInfo);
                    // generate and send new detection 
                });
            }
        }
    

    魔法在哪里

    既然程序员无法精确计算出程序的执行时序,那么是如何保证程序的正确执行呢? 有两个核心思想:

    • 严谨推导。与物理学家精确计算出速度和位置不同,程序员不依靠精确计算,而是依靠严谨推导。程序员只要保证流程的秩序组合是正确的即可,至于程序具体是什么时间点完成了什么,是不可控的。

    • 抽象。亦称“高层次语义”,黑盒,白盒。黑盒是指程序员可以在不需要知道事物内部实现原理细节的情况下利用某个事物的能力;白盒是指程序员在知道一件事物的内部结构和实现后,可以在这个基础上构建出更高层次事物。

    严谨推导。比如,虽然有多重异步,但从衔接点的角度来看执行时序组合是可控的。文件上传完成后,发送消息,然后接收消息做文件回调;看上去是异步的,实际上对于单个文件的处理是通过消息串行的。比如多个引擎库子任务检测,尽管我无法知道何时会完成,但只要它完成,标记个完成状态,然后根据完成状态去做后续的事情。这样,就可以把这个流程变成可控的可理解的。不是对每个时刻进行控制,而是对于关键时刻进行控制。

    抽象。比如,我可能并不知道多个引擎库检测的实现细节,但我知道他们是通过定时任务来驱动和完成的,且我知道需要传入的关键信息。只要传入关键信息,就能复用已有的引擎库检测子任务流程。如果出现问题,那可能是我对关键信息理解不正确,需要进一步去了解内部实现,完善关键信息。

    当然,由于程序执行的时序是高度不确定的。很有可能因为环境波动或执行时间差的缘故就出现 BUG 。比如脱壳后的进程文件过 Hash 库命中生成告警,这个地方是通过发送消息来实现的;如果这个消息的接收跟后面脱壳后进程文件过引擎库检测完成后汇集结果生成告警的时机重合,就可能会出 BUG。这种 BUG 正常情形不大可能出现,但如果网络波动,或者两者执行时序正好会合在一个时刻,就会出现。这正应了墨菲定律:会出错的事总会出错。


    小结

    本文介绍了如何运用“分解-抽象-封装-复用-组合-关联-重构-函数式编程”的思想和方法,来理解和驾驭一个复杂的加壳恶意进程的检测流程。

    编程思想虽然看上去“玄虚”,却似有绵绵不绝之内力,能助人降服复杂的问题;而模式套路,则是解决实际问题的有力工具箱。

    PS: 搞定这个检测流程,多少还是有些成就感的。经过这个流程的锻炼,再加上方法论的提炼,感觉自己理解复杂流程的能力又上了一层楼。

    参考文献

  • 相关阅读:
    漫画 | 一台Linux服务器最多能支撑多少个TCP连接(非常重要)
    http请求与http响应
    gin BindJSON
    Joplin开源笔记软件使用入门
    使用pyttsx3实现简单tts服务
    07 | 哨兵机制:主库挂了,如何不间断服务?
    08 | 哨兵集群:哨兵挂了,主从库还能切换吗?
    30 | 如何使用Redis实现分布式锁?
    06 | 数据同步:主从库如何实现数据一致?
    0405 | AOF日志:宕机了,Redis如何避免数据丢失?
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/16726928.html
Copyright © 2020-2023  润新知