Flutter 应用的完整性校验:检测 Engine.so 或 App.so 被篡改的方案

Flutter 应用完整性校验:检测 Engine.so 或 App.so 被篡改的方案

大家好!今天我们来深入探讨 Flutter 应用的完整性校验,特别是如何检测 Engine.soApp.so 这两个关键文件是否被篡改。这对于保护 Flutter 应用的安全至关重要,防止恶意代码注入和未经授权的修改。

1. 为什么需要完整性校验?

在移动应用开发中,尤其是像 Flutter 这种跨平台框架,应用的安全性是一个不可忽视的重要方面。恶意攻击者可能会通过篡改应用的关键组件,比如 Engine.soApp.so,来达到以下目的:

  • 注入恶意代码: 在应用中插入恶意代码,窃取用户数据、进行广告欺诈或其他非法活动。
  • 破解应用: 绕过应用的授权机制,例如付费功能,实现免费使用。
  • 篡改应用逻辑: 修改应用的正常功能,例如改变支付流程或显示虚假信息。
  • 植入后门: 在应用中植入后门,方便日后进行远程控制或数据窃取。

Engine.so 是 Flutter Engine 的共享库,负责渲染 UI、处理输入事件等核心功能。App.so 包含了 Dart 代码编译后的机器码,是应用的核心逻辑。如果这两个文件被篡改,应用的安全性将受到严重威胁。

2. 完整性校验的核心原理:哈希算法

完整性校验的核心原理是使用哈希算法。哈希算法是一种单向函数,它将任意长度的输入数据转换为固定长度的哈希值(也称为摘要或指纹)。哈希算法具有以下特点:

  • 唯一性: 不同的输入数据产生相同哈希值的概率极低(理想情况下为零)。
  • 确定性: 相同的输入数据总是产生相同的哈希值。
  • 不可逆性: 无法通过哈希值反推出原始输入数据。

基于这些特点,我们可以使用哈希算法来校验文件的完整性。具体步骤如下:

  1. 生成基准哈希值: 在应用发布之前,计算 Engine.soApp.so 文件的哈希值,并将这些哈希值作为基准值存储在安全的地方(例如,应用的代码中或者服务器端)。
  2. 运行时计算哈希值: 在应用运行时,重新计算 Engine.soApp.so 文件的哈希值。
  3. 比较哈希值: 将运行时计算的哈希值与存储的基准哈希值进行比较。如果两个哈希值相同,则说明文件未被篡改;如果两个哈希值不同,则说明文件已被篡改。

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.soApp.so 文件的路径。(注意:这两个函数需要平台特定的代码,稍后会详细介绍
    • 将期望的哈希值(在发布应用前计算的基准值)赋值给 expectedEngineSoHashexpectedAppSoHash 变量。
    • 调用 verifyFileIntegrity() 函数校验 Engine.soApp.so 文件的完整性。
    • 根据校验结果,采取相应的措施,例如弹出警告对话框或退出应用。

5. 获取 Engine.so 和 App.so 的路径(平台特定代码)

由于 Flutter 是跨平台框架,获取 Engine.soApp.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.soApp.so 文件的绝对路径。在 Android 中,Engine.so 实际上是 libflutter.soApp.solibapp.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.soApp.so 文件的绝对路径。在 iOS 中,Engine 和 App 代码分别位于 Flutter.frameworkApp.framework 中,文件名为 FlutterApp,没有 .so 后缀。

6. 安全性考虑

  • 基准哈希值的存储: 不要将基准哈希值直接硬编码在代码中,这很容易被攻击者找到并修改。可以将基准哈希值存储在服务器端,或者使用加密算法对哈希值进行加密后再存储在本地。
  • 代码混淆: 使用代码混淆工具对 Flutter 代码进行混淆,增加攻击者分析代码的难度。
  • 反调试: 实现反调试功能,防止攻击者通过调试工具来分析和修改应用。
  • 运行时校验: 不要只在应用启动时进行一次完整性校验,而应该在应用的整个生命周期中定期进行校验。
  • 防止篡改校验代码: 攻击者可能会尝试修改校验代码本身,使其失效。因此,需要对校验代码进行保护,例如使用代码混淆、加密等技术。
  • Root/越狱检测: 检测设备是否Root/越狱,因为在这些设备上,篡改文件更容易。

7. 其他完整性校验方法

除了哈希算法,还有一些其他的完整性校验方法,例如:

  • 代码签名: 使用代码签名技术对应用进行签名,确保应用是由可信的开发者发布的,并且没有被篡改。
  • Root/越狱检测: 检测设备是否 Root 或越狱,如果设备已 Root 或越狱,则认为应用可能存在安全风险。
  • 动态代码分析: 在应用运行时,对代码进行动态分析,检测是否存在恶意代码。

总结

Flutter 应用的完整性校验对于保护应用的安全至关重要。通过使用哈希算法、代码签名等技术,可以有效地检测 Engine.soApp.so 文件是否被篡改,从而防止恶意代码注入和未经授权的修改。同时,需要注意安全性考虑,例如基准哈希值的存储、代码混淆和反调试等。

确保应用安全,需要持续的努力和关注

通过本讲座,我们学习了如何使用哈希算法来校验 Flutter 应用的关键文件的完整性。希望这些知识能够帮助大家更好地保护自己的应用,防止恶意攻击。
记住,安全是一个持续的过程,需要不断学习和改进。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注