目前加固对于App开发人员来说,不管是App Store的审核4.3问题,还是为了防止逆向工程、篡改、反编译等问题,加固都算是一个必备的选择了。
但是加固技术在一步步升级的同时,其固有的安全缺陷和兼容性问题却始终未能得到解决。
目前,加固技术已经发展到第五代--虚机源码保护,使用代码类型更广泛,App保护级别更高,兼容性更强。那么,这一波技术升级,能解决上面的安全和兼容性问题吗?我们先挨个看看各代技术的优势和缺陷吧。
第一代加固技术中的动态加载功能通常被用于保护应用程序中的核心代码,以及防止恶意攻击者对代码进行反编译、逆向工程等操作。动态加载功能通常包括以下几个步骤:
1.加密:将应用程序中的核心代码进行加密处理,以防止恶意攻击者对代码进行解密操作。
2.动态加载:在应用程序启动时,动态加载器会将加密后的代码加载到内存中,并对其进行解密操作。
3.代码完整性检查:在加载代码后,动态加载器会对代码进行完整性检查,以确保代码没有被篡改或修改。
4.运行时保护:在应用程序运行期间,动态加载器会对代码进行保护,以防止攻击者利用漏洞对代码进行攻击或修改。
用Java代码简单写了一个动态加载的核心代码:
import java.io.File;
import java.io.FileInputStream;
import java.security.MessageDigest;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class DynamicLoader {
private static final String KEY = "mysecretkey"; // 加密密钥
private static final String FILE_PATH = "/path/to/encrypted/code"; // 加密后的代码文件路径
private static final String MD5_CHECKSUM = "1234567890abcdef1234567890abcdef"; // 加密前的代码的MD5校验和
public static void main(String[] args) throws Exception {
byte[] encryptedCode = loadEncryptedCode(); // 加载加密后的代码
byte[] decryptedCode = decrypt(encryptedCode); // 解密代码
checkCodeIntegrity(decryptedCode); // 检查代码完整性
runCode(decryptedCode); // 运行代码
}
// 加载加密后的代码
private static byte[] loadEncryptedCode() throws Exception {
File file = new File(FILE_PATH);
FileInputStream fis = new FileInputStream(file);
byte[] buffer = new byte[(int) file.length()];
fis.read(buffer);
fis.close();
return buffer;
}
// 解密代码
private static byte[] decrypt(byte[] encryptedData) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
return cipher.doFinal(encryptedData);
}
// 检查代码完整性
private static void checkCodeIntegrity(byte[] code) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] checksum = md.digest(code);
String hex = bytesToHex(checksum);
if (!hex.equals(MD5_CHECKSUM)) {
throw new Exception("Code has been modified");
}
}
// 运行代码
private static void runCode(byte[] code) throws Exception {
// 通过反射执行代码
Class> clazz = new MyClassLoader().defineClass(null, code, 0, code.length);
clazz.getDeclaredMethod("run").invoke(null);
}
// 将字节数组转换为十六进制字符串
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
// 自定义ClassLoader,用于加载解密后的代码
private static class MyClassLoader extends ClassLoader {
public Class> defineClass(String name, byte[] code, int off, int len) {
return super.defineClass(name, code, off, len);
}
}
}
PS0:由于动态加载的实现方式可能因操作系统等因素而异,以上代码示例仅供参考,不能直接用于实际应用。(自己根据需求更改)
PS1:【multidex组件的加固原理】:Android的DEX文件在设计之初程序普遍较小,所以在DEX文件设计时,只允许包含65535个函数引用。而随着Android应用的发展,大量的应用的代码已经超过了65535的限制,为了解决这个问题,Android5.0之后原生支持加载多个dex,而为了对旧版本的兼容,Android提供了multidex组件。该组件的实现原理与上面介绍的是一致的。
第一代加固技术的缺陷是依赖Java的动态加载机制,而这个机制要求关键逻辑(Payload)部分必须解压,并且释放到文件系统,这就给了攻击机会去获取对应的文件。虽然可以通过关键逻辑(Payload)被加载后,被从文件系统删除,用于防止被复制,但是攻击者可以拦截对应的删除函数,阻止删除。
而关键逻辑(Payload)会被加密后保存,可用于对抗静态分析,但是攻击者可以通过自定义虚拟机,拦截动态加载机制所使用的关键函数,在这个函数内部,复制文件系统中的关键逻辑(Payload)文件。
第二代加固技术——不落地加载(Non-Resident Load,简称NRL)技术是一种加固技术,用于保护软件代码免受反向工程和恶意代码注入等攻击。它的核心思想是将软件代码分为两部分:一个不需要保护的加载器和一个需要保护的主程序,加载器负责将主程序加载到内存中并执行,主程序则通过加密和混淆等方式保护代码的安全性。
相比于动态加载,不落地加载在APK修改方面已经完善,能做到对开发的零干扰。
NRL技术的主要流程如下:
1.加载器加载:加载器先将主程序的加密部分解密,然后将解密后的主程序加载到内存中。这个过程中,加载器需要读取主程序文件并将其解密。
2.反调试:为了防止被调试,主程序需要执行一些反调试的操作。例如,它可以检测调试器是否存在,或者检测是否有其他程序在对其进行调试。
3.解密和解压:主程序在内存中运行之前,需要进行解密和解压缩。这个过程中,主程序会调用相应的解密和解压缩函数,对自身进行解密和解压缩。
4.恢复IAT:IAT(Import Address Table)是用于动态链接库中的函数导入的数据结构,它记录了函数在内存中的地址。为了保护代码,主程序需要恢复IAT,并将函数地址解密和更新到IAT中。
5.启动主程序:主程序经过以上的准备工作之后,可以开始执行了。
以下是一个不落地加载的核心代码示例,使用AWS Lambda作为后端处理函数。
在本示例中,使用AWS Lambda处理来自API网关的HTTP请求。API网关是一个完全托管的服务,可以将HTTP请求路由到不同的AWS Lambda函数。
要使用这个示例,需要一个AWS账户和AWS CLI(命令行界面)。
首先,创建一个新的Lambda函数:
接下来,创建一个API网关:
现在,将API网关与Lambda函数关联:
ok,我们已经将API网关与Lambda函数关联,并准备好处理来自API网关的HTTP请求。
下面是一个示例代码,用来处理Lambda函数中的请求,计算两个数字的和:
import json
def lambda_handler(event, context):
# 解析请求正文中的JSON数据
body = json.loads(event['body'])
num1 = int(body['num1'])
num2 = int(body['num2'])
# 计算两个数字的和
result = num1 + num2
# 返回结果
response = {
"statusCode": 200,
"body": json.dumps({
"result": result
})
}
return response
不落地加载技术并不是完美的,存在一些缺陷和对抗方法:
由于第二代加固技术仅仅对文件级别进行加密,其带来的问题是内存中的Payload是连续的,可以被攻击者轻易获取。第三代加固技术对这部分进行了改进,将保护级别降到了函数级别。
指令抽离技术是一种安全性较高的代码加固技术,它将敏感指令从程序代码中提取出来,形成一个独立的指令库,程序运行时通过调用这个指令库来完成敏感操作,从而避免了指令在程序中明文存在的风险。
展示一个简单的示例代码:
// 原始程序代码
void encrypt(char* data, int len, char* key) {
// 执行加密操作
}
// 指令抽离后的程序代码
#include "encrypt_lib.h"
void encrypt(char* data, int len, char* key) {
encrypt_lib(data, len, key);
}
缺陷:
对抗:
第三代加固技术在函数级别的保护,使用Android虚拟机内的解释器执行代码,带来可能被记录的缺陷,第四代加固技术使用自己的解释器来避免第三代的缺陷。而自定义的解释器无法对Android系统内的其他函数进行直接调用,必须使用JAVA的JNI接口进行调用。其主要实现由两种:
A、DEX文件内的函数被标记为native,内容被抽离并转换成一个符合JNI要求的动态库。 动态库内通过JNI和Android系统进行交互。
B、DEX文件内的函数被标记为native,内容被抽离并转换成自定义的指令格式,该格式使用自定义接收器执行,和A一样需要使用JNI和Android系统进行调用。
兼容性第四代VMP加固技术一般配合第三代加固技术使用,所以第三代的所有兼容性问题,指令转换/VMP加固也存在。缺陷与对抗不论使用指令转换/VMP加固的A方案或者B方案,其必须通过虚拟机提供的JNI接口与虚拟机进行交互,攻击者可以直接将指令转换/VMP加固方案当作黑盒,通过自定义的JNI接口对象,对黑盒内部进行探测、记录和分析,进而得到完整DEX程序。
(第四代加固DEX文件恢复)另外,第四代VMP加固技术只实现Java代码保护,没有做到使用VMP技术来保护C/C++等代码,安全保护能力有所欠缺。
跟第四代的VMP加固技术相比,虚机源码保护加固是用虚机技术保护所有的代码,包括Java,Kotlin,C/C++,Objective-C,Swift等多种代码,具备极高的兼容性;使App得到更高安全级别的保护,运行更加稳定。
虚机源码保护为用户提供一套完整的工具链,首先把用户待保护的核心代码编译成中间的二进制文件,随后生成独特的虚机源码保护执行环境和只能在该环境下执行的运行程序。
虚机源码保护会在App内部隔离出独立的执行环境,该核心代码的运行程序在此独立的执行环境里运行。即便App本身被破解,这部分核心代码仍然不可见。
(虚机源码保护加固流程)生成的虚机源码保护拥有独特的可变指令集,极大的提高了指令跟踪、逆向分析的难度。
同时,虚机源码保护还提供了反调试能力和监控能力。虚机源码保护可以通过自身的探针感知到环境的变化,实时探测到外界对本环境的调试、注入等非正常执行流程变化,将调试动作引入程序陷阱,并发出警报,进而进行实时更新,提高安全强度。
加固技术发展及其攻防对抗的更迭,伴随着互联网技术发展不断升级。
作为“正义”的一方,我们深信邪不能胜正,而虚机源码保护加固作为当前领先的加固技术,在未来很长一段时间,能够为App提供足够强度的保护,为企业和开发者的业务发展保驾护航。
免费试用:加固产品