Flutter 应用完整性校验:检测 Engine.so 或 App.so 被篡改的方案
大家好!今天我们来深入探讨 Flutter 应用的完整性校验,特别是如何检测 Engine.so 和 App.so 这两个关键文件是否被篡改。这对于保护 Flutter 应用的安全至关重要,防止恶意代码注入和未经授权的修改。
1. 为什么需要完整性校验?
在移动应用开发中,尤其是像 Flutter 这种跨平台框架,应用的安全性是一个不可忽视的重要方面。恶意攻击者可能会通过篡改应用的关键组件,比如 Engine.so 和 App.so,来达到以下目的:
- 注入恶意代码: 在应用中插入恶意代码,窃取用户数据、进行广告欺诈或其他非法活动。
- 破解应用: 绕过应用的授权机制,例如付费功能,实现免费使用。
- 篡改应用逻辑: 修改应用的正常功能,例如改变支付流程或显示虚假信息。
- 植入后门: 在应用中植入后门,方便日后进行远程控制或数据窃取。
Engine.so 是 Flutter Engine 的共享库,负责渲染 UI、处理输入事件等核心功能。App.so 包含了 Dart 代码编译后的机器码,是应用的核心逻辑。如果这两个文件被篡改,应用的安全性将受到严重威胁。
2. 完整性校验的核心原理:哈希算法
完整性校验的核心原理是使用哈希算法。哈希算法是一种单向函数,它将任意长度的输入数据转换为固定长度的哈希值(也称为摘要或指纹)。哈希算法具有以下特点:
- 唯一性: 不同的输入数据产生相同哈希值的概率极低(理想情况下为零)。
- 确定性: 相同的输入数据总是产生相同的哈希值。
- 不可逆性: 无法通过哈希值反推出原始输入数据。
基于这些特点,我们可以使用哈希算法来校验文件的完整性。具体步骤如下:
- 生成基准哈希值: 在应用发布之前,计算
Engine.so和App.so文件的哈希值,并将这些哈希值作为基准值存储在安全的地方(例如,应用的代码中或者服务器端)。 - 运行时计算哈希值: 在应用运行时,重新计算
Engine.so和App.so文件的哈希值。 - 比较哈希值: 将运行时计算的哈希值与存储的基准哈希值进行比较。如果两个哈希值相同,则说明文件未被篡改;如果两个哈希值不同,则说明文件已被篡改。
3. 常用的哈希算法
常用的哈希算法包括 MD5、SHA-1、SHA-256 和 SHA-512 等。由于 MD5 和 SHA-1 算法存在安全漏洞,容易受到碰撞攻击,因此建议使用 SHA-256 或 SHA-512 算法。
| 算法 | 哈希值长度 (bits) | 安全性 |
|---|---|---|
| MD5 | 128 | 已被破解,不建议使用 |
| SHA-1 | 160 | 已被破解,不建议使用 |
| SHA-256 | 256 | 推荐使用,安全性较高 |
| SHA-512 | 512 | 推荐使用,安全性更高,但计算开销也更大 |
4. Flutter 中实现完整性校验的代码示例
下面是一个在 Flutter 中使用 SHA-256 算法进行完整性校验的示例代码。
import 'dart:io';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:flutter/services.dart';
Future<String> calculateSHA256Hash(String filePath) async {
try {
File file = File(filePath);
if (!await file.exists()) {
print('文件不存在: $filePath');
return null;
}
var bytes = await file.readAsBytes();
var digest = sha256.convert(bytes);
return digest.toString();
} catch (e) {
print('计算哈希值时出错: $e');
return null;
}
}
Future<bool> verifyFileIntegrity(String filePath, String expectedHash) async {
String calculatedHash = await calculateSHA256Hash(filePath);
if (calculatedHash == null) {
return false; // 无法计算哈希值,校验失败
}
return calculatedHash == expectedHash;
}
void main() async {
// 确保 Flutter 引擎已初始化
WidgetsFlutterBinding.ensureInitialized();
// 获取 Engine.so 和 App.so 的路径 (需要平台特定的代码)
String engineSoPath = await getEngineSoPath();
String appSoPath = await getAppSoPath();
// 替换为你在发布应用前计算的基准哈希值
String expectedEngineSoHash = "你的 Engine.so 基准哈希值";
String expectedAppSoHash = "你的 App.so 基准哈希值";
// 校验 Engine.so 的完整性
bool engineSoIntegrity = await verifyFileIntegrity(engineSoPath, expectedEngineSoHash);
print('Engine.so 完整性校验结果: $engineSoIntegrity');
// 校验 App.so 的完整性
bool appSoIntegrity = await verifyFileIntegrity(appSoPath, expectedAppSoHash);
print('App.so 完整性校验结果: $appSoIntegrity');
if (!engineSoIntegrity || !appSoIntegrity) {
// 如果校验失败,可以采取一些措施,例如:
// 1. 弹出警告对话框,提示用户应用可能已被篡改
// 2. 退出应用
// 3. 向服务器报告异常
print('应用完整性校验失败,请重新安装应用!');
// exit(0); // 退出应用
} else {
print('应用完整性校验通过!');
// 继续应用的正常流程
}
}
// 获取 Engine.so 的路径 (平台特定代码,以下是 Android 示例)
Future<String> getEngineSoPath() async {
try {
const platform = const MethodChannel('app_integrity_check/platform');
final String path = await platform.invokeMethod('getEngineSoPath');
return path;
} on PlatformException catch (e) {
print("获取 Engine.so 路径失败: '${e.message}'");
return null;
}
}
// 获取 App.so 的路径 (平台特定代码,以下是 Android 示例)
Future<String> getAppSoPath() async {
try {
const platform = const MethodChannel('app_integrity_check/platform');
final String path = await platform.invokeMethod('getAppSoPath');
return path;
} on PlatformException catch (e) {
print("获取 App.so 路径失败: '${e.message}'");
return null;
}
}
代码解释:
calculateSHA256Hash(String filePath)函数:- 接收文件路径作为参数。
- 读取文件内容为字节数组。
- 使用
crypto包的sha256.convert()方法计算 SHA-256 哈希值。 - 将哈希值转换为字符串并返回。
verifyFileIntegrity(String filePath, String expectedHash)函数:- 接收文件路径和期望的哈希值作为参数。
- 调用
calculateSHA256Hash()函数计算文件的哈希值。 - 将计算出的哈希值与期望的哈希值进行比较。
- 返回
true如果哈希值匹配,否则返回false。
main()函数:- 调用
getEngineSoPath()和getAppSoPath()函数获取Engine.so和App.so文件的路径。(注意:这两个函数需要平台特定的代码,稍后会详细介绍) - 将期望的哈希值(在发布应用前计算的基准值)赋值给
expectedEngineSoHash和expectedAppSoHash变量。 - 调用
verifyFileIntegrity()函数校验Engine.so和App.so文件的完整性。 - 根据校验结果,采取相应的措施,例如弹出警告对话框或退出应用。
- 调用
5. 获取 Engine.so 和 App.so 的路径(平台特定代码)
由于 Flutter 是跨平台框架,获取 Engine.so 和 App.so 文件的路径需要平台特定的代码。下面分别给出 Android 和 iOS 平台的示例代码。
5.1 Android 平台
在 Android 平台上,可以通过 MethodChannel 调用 Java 代码来获取文件的路径。
Flutter 代码:
// 获取 Engine.so 的路径 (Android)
Future<String> getEngineSoPath() async {
try {
const platform = const MethodChannel('app_integrity_check/platform');
final String path = await platform.invokeMethod('getEngineSoPath');
return path;
} on PlatformException catch (e) {
print("获取 Engine.so 路径失败: '${e.message}'");
return null;
}
}
// 获取 App.so 的路径 (Android)
Future<String> getAppSoPath() async {
try {
const platform = const MethodChannel('app_integrity_check/platform');
final String path = await platform.invokeMethod('getAppSoPath');
return path;
} on PlatformException catch (e) {
print("获取 App.so 路径失败: '${e.message}'");
return null;
}
}
Android (Java) 代码:
在 MainActivity.java 文件中添加以下代码:
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.GeneratedPluginRegistrant;
import java.io.File;
public class MainActivity extends io.flutter.embedding.android.FlutterActivity {
private static final String CHANNEL = "app_integrity_check/platform";
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setMethodCallHandler(
(call, result) -> {
if (call.method.equals("getEngineSoPath")) {
String engineSoPath = getEngineSoPath();
result.success(engineSoPath);
} else if (call.method.equals("getAppSoPath")) {
String appSoPath = getAppSoPath();
result.success(appSoPath);
} else {
result.notImplemented();
}
});
}
private String getEngineSoPath() {
try {
ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(), 0);
File libDir = new File(appInfo.nativeLibraryDir);
File engineSo = new File(libDir, "libflutter.so"); // Engine.so 在 Android 中是 libflutter.so
return engineSo.getAbsolutePath();
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return null;
}
}
private String getAppSoPath() {
try {
ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(), 0);
File libDir = new File(appInfo.nativeLibraryDir);
File appSo = new File(libDir, "libapp.so"); // App.so 在 Android 中是 libapp.so
return appSo.getAbsolutePath();
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return null;
}
}
}
代码解释:
MethodChannel: 用于 Flutter 和 Android 平台之间的通信。getEngineSoPath()和getAppSoPath(): Java 代码用于获取Engine.so和App.so文件的绝对路径。在 Android 中,Engine.so实际上是libflutter.so,App.so是libapp.so。appInfo.nativeLibraryDir: 获取应用 Native 库的目录,so 文件通常位于此目录下。
5.2 iOS 平台
在 iOS 平台上,同样可以通过 MethodChannel 调用 Objective-C 或 Swift 代码来获取文件的路径。
Flutter 代码:
// 获取 Engine.so 的路径 (iOS)
Future<String> getEngineSoPath() async {
try {
const platform = const MethodChannel('app_integrity_check/platform');
final String path = await platform.invokeMethod('getEngineSoPath');
return path;
} on PlatformException catch (e) {
print("获取 Engine.so 路径失败: '${e.message}'");
return null;
}
}
// 获取 App.so 的路径 (iOS)
Future<String> getAppSoPath() async {
try {
const platform = const MethodChannel('app_integrity_check/platform');
final String path = await platform.invokeMethod('getAppSoPath');
return path;
} on PlatformException catch (e) {
print("获取 App.so 路径失败: '${e.message}'");
return null;
}
}
iOS (Objective-C) 代码:
在 AppDelegate.m 文件中添加以下代码:
#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
#import <Flutter/Flutter.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
FlutterMethodChannel* methodChannel = [FlutterMethodChannel
methodChannelWithName:@"app_integrity_check/platform"
binaryMessenger:controller.binaryMessenger];
[methodChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
if ([@"getEngineSoPath" isEqualToString:call.method]) {
NSString *engineSoPath = [self getEngineSoPath];
result(engineSoPath);
} else if ([@"getAppSoPath" isEqualToString:call.method]) {
NSString *appSoPath = [self getAppSoPath];
result(appSoPath);
} else {
result(FlutterMethodNotImplemented);
}
}];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- (NSString *)getEngineSoPath {
// 在 iOS 中, Engine.framework 包含了 Engine 的代码。
// 获取 Engine.framework 的路径,然后拼接 Engine.so 的路径。
NSString *engineFrameworkPath = [[NSBundle mainBundle] pathForResource:@"Flutter" ofType:@"framework"];
NSString *engineSoPath = [engineFrameworkPath stringByAppendingPathComponent:@"Flutter"]; // Engine.so 文件名是 Flutter
return engineSoPath;
}
- (NSString *)getAppSoPath {
// 在 iOS 中,App.framework 包含了 App 的代码。
// 获取 App.framework 的路径,然后拼接 App.so 的路径。
NSString *appFrameworkPath = [[NSBundle mainBundle] pathForResource:@"App" ofType:@"framework"];
NSString *appSoPath = [appFrameworkPath stringByAppendingPathComponent:@"App"]; // App.so 文件名是 App
return appSoPath;
}
@end
代码解释:
FlutterMethodChannel: 用于 Flutter 和 iOS 平台之间的通信。getEngineSoPath()和getAppSoPath(): Objective-C 代码用于获取Engine.so和App.so文件的绝对路径。在 iOS 中,Engine 和 App 代码分别位于Flutter.framework和App.framework中,文件名为Flutter和App,没有.so后缀。
6. 安全性考虑
- 基准哈希值的存储: 不要将基准哈希值直接硬编码在代码中,这很容易被攻击者找到并修改。可以将基准哈希值存储在服务器端,或者使用加密算法对哈希值进行加密后再存储在本地。
- 代码混淆: 使用代码混淆工具对 Flutter 代码进行混淆,增加攻击者分析代码的难度。
- 反调试: 实现反调试功能,防止攻击者通过调试工具来分析和修改应用。
- 运行时校验: 不要只在应用启动时进行一次完整性校验,而应该在应用的整个生命周期中定期进行校验。
- 防止篡改校验代码: 攻击者可能会尝试修改校验代码本身,使其失效。因此,需要对校验代码进行保护,例如使用代码混淆、加密等技术。
- Root/越狱检测: 检测设备是否Root/越狱,因为在这些设备上,篡改文件更容易。
7. 其他完整性校验方法
除了哈希算法,还有一些其他的完整性校验方法,例如:
- 代码签名: 使用代码签名技术对应用进行签名,确保应用是由可信的开发者发布的,并且没有被篡改。
- Root/越狱检测: 检测设备是否 Root 或越狱,如果设备已 Root 或越狱,则认为应用可能存在安全风险。
- 动态代码分析: 在应用运行时,对代码进行动态分析,检测是否存在恶意代码。
总结
Flutter 应用的完整性校验对于保护应用的安全至关重要。通过使用哈希算法、代码签名等技术,可以有效地检测 Engine.so 和 App.so 文件是否被篡改,从而防止恶意代码注入和未经授权的修改。同时,需要注意安全性考虑,例如基准哈希值的存储、代码混淆和反调试等。
确保应用安全,需要持续的努力和关注
通过本讲座,我们学习了如何使用哈希算法来校验 Flutter 应用的关键文件的完整性。希望这些知识能够帮助大家更好地保护自己的应用,防止恶意攻击。
记住,安全是一个持续的过程,需要不断学习和改进。