随着计算机与互联网相关技术的蓬勃高速发展,计算机已不再是“专业人员”的独有工具软件应用也逐渐与人们的日常生活深度绑定。如何妥善的保护软件,让软件安全运行在用户/客户的设备上,更好的服务大众助力业务稳定发展,软件保护自始至终都是一个重要的问题。
从软件保护的视角来看,可以笼统的分为应用保护、代码保护、算法保护,本文也将从这 3 个纬度进行阐述软件保护相关的问题 应用保护应用加壳:压缩壳、加密壳、抽取壳、保护壳 应用保护(也称应用加壳),主要是为了保护软件的著作信息、关键资源和关键实现等资产的一种有效措施。“加壳”与自然界和生物界中动植物为了保护关键“种子”非常相似,当然这壳有先天的(譬如蛋壳,天生自带保护壳),也有后天的动物的羽毛、古时的铠甲盔甲
应用保护历史悠远,从 DOS 时代起就出现了反编译相关技术,而应用保护在那个时期已初见雏形。 壳的初始作用是保护软件,但后来发展的方向不一就出现了各种各样的壳,一般有压缩壳、加密壳、抽取壳、保护壳、虚拟执行壳几个阶段,具体如下所示 - 压缩壳:着重于减少应用体积,之所以称之为压缩壳,是由于其核心的思想是压缩应用。单论压缩壳其本身并不采用应用保护和调试器检测相关的技术
- 加密壳:着重于应用加密编码,在运行时解密解码成正式的代码。加密壳除了保护应用本身之外,还会应用在注册机制、使用次数、时间限制、木马免杀等常见常见,在实际的应用场景中加密壳会结合压缩壳,进行多次加密编码,达到了较高强度的保护效果。
- 抽取壳:着重于软件代码抽取,先将对对应的代码指令抽取出来,在运行时将抽取的代码或指令回填到对应代码中。在实际的代码或指令抽取前一般会将代码或指令进行归一化处理,另外对于将要回填的代码或指令可能还会使用懒/热加载的形式。
- 保护壳:着重于壳的保护(注意是壳的保护,而非是应用)。保护壳不仅会采用压缩、加密、编码、转换来保护对应的应用,还会对调试器,分析工具、应用指纹进行检测,包含不限于多进程保护、内存校验、反调试器和系统注入。
- 虚拟执行壳:着重于将代码或指令替换掉原有的代码或指令并构造正式指令与自定义的指令之间映射关系,在壳中先使用自身的解释器(也称 虚拟执行引擎),将自定义的指令解释执行成真实的代码与指令。以及结合压缩、加密、编码、混淆和转换,可能原本一条指令就变成了成千上万条指令。
保护原理软件加壳除了保护软件的著作信息、关键资源和关键实现外,还为了隐藏了程序真实的 程序入口点(OEP,Original Entry Point。或者用了假的 OEP ), 对于隐藏了 OEP 的保护,需要先寻找应用真正的 OEP,才可以完成 OEP 脱壳。
应用加壳和脱(解)壳彼此互为逆运算(类比于密码学中的加密与解密的思路与流程)。无非是在具体过程中采用的措施和思路并不相同,大致流程分为如下三个步骤 - 对未加壳的应用的信息进行提取
- 将抽取出来的信息(头信息、数据、映射关系),进行压缩、加密、抽取、虚拟化(当然在这个过程中会,做一些“辅助”的操作,譬如头信息擦除、数据保护(编码转换/算式拆分)、映射关系隐藏、载入方式的变化,应用的签名)
- 在运行时先运行壳中的逻辑(此时“壳”就获取到了执行权)在正式的应用运行起来之前,先执行了壳中的解壳逻辑,解壳逻辑运行完成后,再运行实际的应用程序。
从而达到应用保护的目的,但又不干扰实际的逻辑 脱壳方式一般来说获取到抽取后的信息、解壳的逻辑后,然后由逆向工程师去“模拟”壳中脱壳逻辑即可完成脱壳(参考上述步骤 3)。【当然壳类型不同,模拟的过程也不相同。 譬如在虚拟执行壳中需要定位壳的解释器和自定义字节码的映射关系,才可还原】。
上述,“模拟”是个较为笼统的讲述。而实际的脱壳方式有内存dump、缓存dump、文件监听、内存重组、主动调用
代码保护静态保护:布局混淆、数值混淆、控制流混淆、预防混淆 动态保护:VMP、自修改代码 其他保护:多态保护、代码变形、代码加壳 如果说应用保护是给应用程序的大门加上了一把“大锁”,那么代码保护就是针对组成应用程序的源码加了多个“小锁”。在实际的代码保护中由于代码的粒度更细,组成更为分散,那么保护措施相对来说更多。
在实际应用中,代码保护通常无法完整且长期的保护,也仅只能给对应的逆向人员延长分析与调试的时间。在实际的应用中引入代码保护会需要消耗更多的计算资源和存储资源的消耗,所以对于性能损耗和阻碍逆向人员的资源投入,需要在实际场景中进行相关的权衡取舍。
对于代码保护有如下几种分类方式 - 应用程序组成:从应用程序组成的视角来看应用程序由代码(逻辑代码和控制代码)和信息数据组成。那么从组成方式,可以将应用程序的代码保护和数据保护。
- 代码保护:除了整体的应用保护外,还可以对代码进行保护。而保护方式可以分为静态混淆(阻碍逆向人员进行静态分析),和动态保护(防止逆向人员进行动态调试)
- 数据保护:数据保护即可以根据认证鉴权的方式,对可以访问到数据的人员进行分层,也可以对于数据敏感程度进行脱敏(如手机号、密码、身份证等信息),还可以在传输过程中对数据进行压缩、编码、加密,甚至是在数据中插入水印
- 源码编译打包:从源码编译打包的视角来看,代码保护可分为编码时的代码保护,编译期的代码保护,构建完成后的指令混淆
静态混淆所谓的静态分析是在不运行代码的情况下,对代码的静态特征和功能模块进行分析的方法。静态分析代码的优势在于它能描绘程序的轮廓,包括控制流和数据结构。 静态混淆是指保留代码原有功能上的代码等价转换,使其难以阅读、分析和理解。难以阅读,分析是混淆的目的,“等价转换”是确保混淆后代码和源代码的功能保持一致。
对于代码混淆的分类,通常以 Collberg 的理论为基础,细分为布局混淆、数据混淆、控制混淆和预防混淆。 布局混淆布局混淆原是指删除或混淆与执行无关的辅助文本信息,增加攻击者阅读和理解代码的难度,具体到高级语言中就是指源代码中的注释文本、调试信息、代码命名和格式等。布局混淆能够在引入更多计算的同时进行代码,虽然单独布局混淆保护强度并不强,但通常在实际应用中都会用到布局混淆。
布局混淆也包括采用技术手段处理代码中的常量名、变量名、函数名等标识符,增加逆向工作者对代码理解的难度。具体有如下 2 种体现 - 删除无效代码:通过将注释信息、未调用的代码和数据、代码缩进与换行符(也称之为代码压缩,通常同时还会有代码格式化检测,以避免被格式化)等进行删除。
- 标识符重命名:通过将文件名、常量名、变量名、类名、函数(方法)名、参数名进行替换。从而达到去除代码语义等效果
数据混淆数据是构成语言代码的基本元素(注意:此数据并非应用程序的数据,而是代码中原有的数据),同时也是语义分析的重要依据。在数据混淆的过程中通常会引入更多的计算,不过好在数据混淆较于布局混淆会有更好保护的效果。 数据混淆较于布局混淆是个更大的话题,一方面是数据类型种类更多,另一方面类型不同可引入的保护措施也更多。所以将在下述中对于基础数据类型进行分别讨论其混淆方式。 基础类型包含:字符(串)类型、数值类型、布尔类型、对象类型等 字符混淆字符是最常见的常量数据(在此,将字符、字符串和文本统一称之为字符)。而字符中通常包含重要信息,如类名、方法名、变量名、异常,错误甚至是关键文本。而字符混淆便是将有“业务语义”的代码通过编码、加密的方式,将包含关键语义的信息进行保护。 - 编码混淆:通过对关键的字符进行编码转换,在运行时动态解码从而达到对抗静态分析的目的。譬如将字符串拆分成单个字符,单个字符基于 ASCII 码转化为数字,
- 加密保护:同上,将关键字符进行加密,在运行时动态解密
数字混淆数字混淆即通过数字计算拆分、进制转换、公式拆解的方式,对代码中的数字进行保护。虽然数字本身包含的业务语义,并非像字符中体现的那么敏感。但数字又是在代码中随处可见,譬如循环迭代的索引、数组取值。对数字进行混淆保护有助于代码防护强度更上一层楼。 - 计算拆分:即将对应的数字看作结果(通常在代码中是直接使用对应的数字),而计算拆分即将该“结果”进行拆分,需要先进行计算才可得到该结果。譬如在源码中为 1,那么混淆过后的数字 1 可表示为 3+7-9+2-1 。(只需要保证最终计算的值与混淆前的值一致即可)
- 进制转换:即将源码中常见的十进制,转换成二进制、八进制、十六进制、三十二进制等即可,这与上述字符混淆中的编码混淆有异曲同工之妙,值得一提的是无需在运行时动态转换(因为计算机中可以直接识别)
布尔混淆布尔类型在底层直接由 0、1 构成,所以使得既可以使用数字混淆的技巧,也可通过类型转换等方式完成布尔混淆。 控制混淆代码块都是按照逻辑顺序有序划分与组合,并且将相关的代码放在一起。在不改变程序功能的前提下,可以通过拆分重组代码等方式打破这种常规逻辑,使代码间的关系变得模糊,以此来保护程序的源码。
控制混淆即改变控制流或将原有控制流复杂化,通常的方式有不透明谓词、插入冗余代码和控制流平坦化 - 不透明谓词:通过严格的逻辑推理证明某些复杂的表达式成立,而这些成立的表达式称为不透明谓词表达式,表达式成立的结果是已知的,而表达式结果表面上是不明显的,称为不透明
- 冗余代码:程序中的其他代码没有任何调用关系的代码,死代码是指在程序中永远不会被执行到的代码。将其插入程序中并不会对其造成任何影响,同时还可以增加破解者的阅读难度。插入代码的方法可以借助不透明谓词。
- 控制流平坦化:将源码条件分支和循环语句组成的控制分支结构转化为单一的分发器结构,可以使用这种方法对代码中原有的控制流进行混淆,增加控制流的复杂度。
预防混淆预防混淆的目的不是通过混淆代码增加分析调试代码的复杂度,而是提高现有的反混淆技术破解代码的难度或检测现有的反混淆器中存在的问题,并针对现有的反混淆器中的漏洞设计混淆算法,增加其破解代码的难度。譬如花指令、调试器检测 - 头信息擦除:通过头信息的擦除和切断链接关系,构造损坏的代码文件。从而对抗分析工具
- 花指令:用于对抗和干扰逆向分析的技巧,它通过在真实代码中插入一些无用的、看似随机或者无害的代码,且同时还保证原有程序的正确执行的代码。
- 调试器检测:用于检测调试工具(如浏览器中的控制台检测)、设备运行环境(如 Android 中的 root 检测、Hook 检测、Unidbg 检测等)
动态混淆动态混淆,主要因对动态分析。技术在实现上主要包括自修改代码技术和虚拟机保护技术。 VM:代码虚拟机VM 是一种通过将源代码按照自定义的编译器变成对应的字节码,然后在运行时在代码中运行自定义的 VM 解释器。可类比于 JVM VMP:代码虚拟化保护VMP 是一种通过增加了一层自定义指令集、解释器的一种保护方式。其主要特性是自定义的指令集(也称虚拟指令集),由于实际的代码均是 VMP 动态运行后得到的。故无法直接有效的分析 SMC:自修改代码自修改代码(SMC,Self-Modified Code)是一类特殊的代码技术,即在运行时修改自身代码,从而使得程序实际行为与反汇编结果不符,同时修改前的代码段数据也可能非合法指令,从而无法被反汇编器识别,这加大了软件逆向工程的难度。 其主要利用了冯罗伊曼体系结构的存储程序的特点,即指令和数据存储在同一个内存空间中,因此指令可以被视作数据被其他指令读取和修改。程序在运行时向代码段中写数据,并且写入的数据被作为指令执行,达到自我修改的效果。自修改保护机制可以有效抵御静态逆向分析而且由于代码仅在需要时才以明文的形式出现,可以在一定程度上阻碍逆向工具获取程序所有的明文代码,从而抵抗动态分析。 其他混淆虽然代码混淆、加密、编码能够有效的保护代码,但也存在一定的问题。因为保护一成不变的话,被破解终究仅是时间问题。 多态保护多态保护的特点是随机性和变化性。多态保护即对代码本身进行变种替换,从而产生大量不同形式但功能相同的代码,甚至还可以采用多种不同的实现方法。 代码变形可以将代码变形看作多态保护的衍生。代码变形在多态技术的基础上更进一步,在每次应用过程中不仅对解密代码,还对代码主体也进行变换,使不同代码实例的代码完全不同。 算法保护算法保护,也称加密算法保护。由于在软件保护中通常伴随着编码、加密和摘要等行为。其用途和必要性在此不再过多赘述,但目前所使用的标准的密码学算法或多或少均有相关明显特征,譬如 base64 的码表,又譬如 md5 的魔数。一旦识别到关键点,即可直接通过动态调试到方式,直捣黄龙。直接定位到附近。那么上述的应用保护,还是代码保护如形同虚设无异。
那么基于此,可以对算法进行相关保护,从而保证无法直接从密码学的角度直接切入。最常见的保护方式有 3 种,即算法魔改,TEE 算法魔改算法魔改,即对标准的算法进行魔改或二开,从而隐藏掉相关特征。通常可以从如下几个部分进行魔改(可任选其一,也可综合使用): - 输入值处理
- 加密算法初始变量修改(包含码表、密钥等)
- 计算过程修改
- 返回值处理
当然也可以链式调用多个算法,共同实现保护。 白盒化传统的加密算法中算法和密钥是完全独立的,也就是说算法相同密钥不同则可以得到不同的加密结果 白盒加密将算法和密钥紧密捆绑在了一起,由算法和密钥生成一个加密表和一个解密表,然后可以独立用查找加密表来加密,用解密表解密,不再依赖于原来的加解密算法和密钥。 TEETEE 是一种具有运算和储存功能,能提供安全性和完整性保护的独立处理环境。其基本思想是:在硬件中为敏感数据单独分配一块隔离的内存,所有敏感数据的计算均在这块内存中进行,并且除了经过授权的接口外,硬件中的其他部分不能访问这块隔离的内存中的信息。以此来实现敏感数据的隐私计算。
小结本文,分别从应用保护、代码保护、算法保护三个纬度,对软件保护进行阐述。 在应用保护中,不仅介绍了保护原理也介绍了脱壳思路,即为了帮助攻击者提供脱壳思路,也帮助防守者进行针对性对抗保护 在代码保护中,分别从静态混淆和动态混淆的方式来对代码进行保护,当然还引申出了其他的混淆方式 在算法保护中,分别从算法魔改、白盒化以及 TEE 三种方式进行密码学算法保护
|