IPA/APK 签名校验:在 Dart 层通过 FFI 验证自身签名的哈希值
大家好,今天我们要探讨的是一个重要的安全问题:如何在 Dart 层通过 FFI (Foreign Function Interface) 验证 iOS (IPA) 和 Android (APK) 应用自身的签名哈希值。 这对于确保应用完整性和防止篡改至关重要。
一、签名校验的必要性
移动应用的签名是验证应用来源和完整性的关键机制。攻击者可能会尝试篡改应用,例如插入恶意代码、替换资源文件等。如果应用没有进行有效的签名校验,就无法检测到这些篡改行为,从而导致安全风险。
- 防止恶意篡改: 签名校验可以确保应用在安装或运行时没有被未经授权的第三方修改。
- 验证应用来源: 签名可以确认应用是由可信的开发者发布的。
- 增强应用安全性: 签名校验是应用安全防御体系的重要组成部分。
二、传统签名校验的局限性
传统的签名校验通常在应用安装时由操作系统进行。然而,这种校验只能保证安装包的完整性,无法防止应用在运行时被篡改。此外,某些攻击者可能会绕过操作系统的签名校验机制。
三、Dart 层 FFI 签名校验的优势
在 Dart 层通过 FFI 进行签名校验具有以下优势:
- 运行时校验: 可以在应用运行时动态地校验签名,及时发现篡改行为。
- 增强安全性: 可以作为操作系统签名校验的补充,提高应用的安全性。
- 平台无关性: 通过 FFI 可以调用底层平台的 API,实现跨平台的签名校验。
四、实现方案概述
我们的目标是在 Dart 层通过 FFI 调用原生代码,获取应用自身的签名哈希值,并将其与预先存储的哈希值进行比较。具体的实现方案如下:
- 获取签名信息: 使用原生代码(Swift/Kotlin)获取应用的签名证书信息。
- 计算哈希值: 使用原生代码计算签名证书的哈希值(例如 SHA-256)。
- 导出哈希值: 将哈希值通过 FFI 传递给 Dart 层。
- 校验哈希值: 在 Dart 层将获取到的哈希值与预先存储的哈希值进行比较。
五、iOS (IPA) 签名校验实现
5.1. Swift 代码 (iOS)
首先,我们需要创建一个 Swift 文件(例如 ios_signature.swift)来获取应用的签名哈希值。
import Foundation
import Security
@_cdecl("get_signature_hash")
public func get_signature_hash() -> UnsafePointer<CChar>? {
guard let appBundleId = Bundle.main.bundleIdentifier else {
print("Failed to get app bundle ID")
return nil
}
guard let infoPlistPath = Bundle.main.path(forResource: "Info", ofType: "plist") else {
print("Failed to find Info.plist")
return nil
}
guard let infoPlistURL = URL(fileURLWithPath: infoPlistPath) as CFURL? else {
print("Failed to create URL for Info.plist")
return nil
}
var staticCode: SecStaticCode?
let status = SecStaticCodeCreateWithPath(infoPlistURL, SecCSFlags(rawValue: 0), &staticCode)
guard status == errSecSuccess, let code = staticCode else {
print("Failed to create SecStaticCode: (status)")
return nil
}
var requirements: SecRequirement?
let requirementStatus = SecRequirementCreateWithString("identifier = "(appBundleId)"" as CFString, SecCSFlags(rawValue: 0), &requirements)
guard requirementStatus == errSecSuccess, let req = requirements else {
print("Failed to create SecRequirement: (requirementStatus)")
return nil
}
let verifyStatus = SecStaticCodeCheckValidity(code, SecCSFlags(rawValue: 0), req)
guard verifyStatus == errSecSuccess else {
print("Signature verification failed: (verifyStatus)")
return nil
}
var codeSigningInformation: CFDictionary?
let infoStatus = SecCodeCopySigningInformation(code, SecCSFlags(rawValue: [.infoFormatX509]), &codeSigningInformation)
guard infoStatus == errSecSuccess, let signingInfo = codeSigningInformation as? [String: Any] else {
print("Failed to copy signing information: (infoStatus)")
return nil
}
guard let certificates = signingInfo[kSecCodeInfoCertificates as String] as? [Data], !certificates.isEmpty else {
print("No certificates found in signing information")
return nil
}
guard let certificateData = certificates.first else {
print("Failed to get first certificate")
return nil
}
var certificate: SecCertificate?
certificate = SecCertificateCreateWithData(nil, certificateData as CFData)
guard let cert = certificate else {
print("Failed to create SecCertificate")
return nil
}
var cfData: CFData?
SecCertificateCopyData(cert, &cfData)
guard let data = cfData as Data? else {
print("Failed to copy certificate data")
return nil
}
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
CC_SHA256(bytes.baseAddress, CC_LONG(data.count), &hash)
}
let hashString = hash.map { String(format: "%02x", $0) }.joined()
let cHashString = hashString.cString(using: .utf8)!
let cHashStringCopy = strdup(cHashString)
return cHashStringCopy
}
代码解释:
@_cdecl("get_signature_hash"): 声明一个 C 函数,以便 FFI 可以调用它。Bundle.main.bundleIdentifier: 获取应用的 Bundle Identifier,用于后续的签名校验。SecStaticCodeCreateWithPath: 通过 Info.plist 文件的 URL 创建一个SecStaticCode对象,用于表示应用的代码。SecRequirementCreateWithString: 创建一个SecRequirement对象,用于指定签名要求,这里要求应用的 Bundle Identifier 必须匹配。SecStaticCodeCheckValidity: 检查应用的代码是否满足指定的签名要求。SecCodeCopySigningInformation: 获取应用的签名信息,包括证书链。SecCertificateCreateWithData: 从证书数据创建SecCertificate对象。SecCertificateCopyData: 复制证书数据。CC_SHA256: 使用 CommonCrypto 库计算证书数据的 SHA-256 哈希值。strdup: 复制 C 字符串,因为 Dart 需要拥有字符串的所有权。
5.2. 创建 Dart FFI 绑定 (iOS)
接下来,我们需要在 Dart 代码中创建 FFI 绑定,以便调用 Swift 函数。
import 'dart:ffi' as ffi;
import 'dart:io' show Platform;
final DynamicLibrary nativeLib = Platform.isIOS
? DynamicLibrary.process()
: DynamicLibrary.open("libios_signature.dylib"); // For simulator
final _getSignatureHash = nativeLib.lookupFunction<
ffi.Pointer<ffi.Char> Function(),
ffi.Pointer<ffi.Char> Function()>('get_signature_hash');
String getSignatureHash() {
final pointer = _getSignatureHash();
if (pointer == ffi.nullptr) {
return '';
}
final hash = pointer.cast<ffi.Utf8>().toDartString();
// Free the memory allocated in Swift
free(pointer);
return hash;
}
// Function to free memory allocated in Swift
final _free = nativeLib.lookupFunction<
ffi.Void Function(ffi.Pointer<ffi.Char>),
void Function(ffi.Pointer<ffi.Char>),
>('free');
void free(ffi.Pointer<ffi.Char> pointer) {
_free(pointer);
}
代码解释:
DynamicLibrary.process(): 在 iOS 上,使用DynamicLibrary.process()加载当前进程的动态库,因为 Swift 代码会被编译到应用的 Mach-O 可执行文件中。DynamicLibrary.open("libios_signature.dylib"): 在模拟器上,可能需要使用DynamicLibrary.open()加载编译后的动态库。lookupFunction: 查找 Swift 函数get_signature_hash的地址。cast<ffi.Utf8>().toDartString(): 将 C 字符串转换为 Dart 字符串。strdup在 Swift 中分配的内存需要手动释放,使用free函数释放内存。
5.3. 配置 iOS 项目 (iOS)
- 创建 Bridging Header: 如果你的项目是 Objective-C 项目,需要创建一个 Bridging Header 文件,以便在 Objective-C 代码中调用 Swift 代码。在 Bridging Header 文件中添加
#import "YourProjectName-Swift.h"。 - 添加 Swift 文件到项目: 将
ios_signature.swift文件添加到你的 Xcode 项目中。 - 配置 Build Settings: 在 Xcode 的 Build Settings 中,确保 "Objective-C Bridging Header" 设置正确指向你的 Bridging Header 文件(如果使用了 Bridging Header)。
- 编译 Swift 代码为动态库 (Simulator): 如果需要在模拟器上运行,需要将 Swift 代码编译成动态库 (
.dylib),并在 Dart 中使用DynamicLibrary.open()加载该动态库。
5.4. Dart 代码校验签名 (iOS)
void verifySignature() {
final expectedHash = 'your_expected_signature_hash'; // 替换为你的预期哈希值
final actualHash = getSignatureHash();
if (actualHash == expectedHash) {
print('签名校验成功!');
} else {
print('签名校验失败!');
print('预期哈希值: $expectedHash');
print('实际哈希值: $actualHash');
}
}
六、Android (APK) 签名校验实现
6.1. Kotlin 代码 (Android)
我们需要创建一个 Kotlin 文件(例如 android_signature.kt)来获取应用的签名哈希值。
package com.example.signature
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import java.security.MessageDigest
import android.content.pm.PackageInfo
import android.content.pm.Signature
import android.util.Base64
@SuppressWarnings("deprecation")
fun getSignatureHash(context: Context): String? {
try {
val packageManager = context.packageManager
val packageName = context.packageName
val packageInfo: PackageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES)
} else {
packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
}
val signatures: Array<Signature> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.signingInfo.apkContentsSigners
} else {
packageInfo.signatures
}
if (signatures.isEmpty()) {
return null
}
val signature = signatures[0]
val md = MessageDigest.getInstance("SHA-256")
md.update(signature.toByteArray())
val digest = md.digest()
return digest.joinToString("") { "%02x".format(it) }
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
代码解释:
@SuppressWarnings("deprecation"): 压制了getPackageInfo(packageName, PackageManager.GET_SIGNATURES)的弃用警告。packageManager.getPackageInfo: 获取应用的PackageInfo,其中包含签名信息。packageInfo.signatures: 获取应用的签名数组。在 Android P (API 28) 及以上版本,应该使用packageInfo.signingInfo.apkContentsSigners。MessageDigest.getInstance("SHA-256"): 获取 SHA-256 消息摘要算法的实例。signature.toByteArray(): 将签名转换为字节数组。md.update(): 更新消息摘要。md.digest(): 计算消息摘要。digest.joinToString("") { "%02x".format(it) }: 将字节数组转换为十六进制字符串。
6.2. 创建 Dart FFI 绑定 (Android)
我们需要在 Dart 代码中创建 FFI 绑定,以便调用 Kotlin 函数。首先需要在 Android 项目中创建一个方法通道。
// MainActivity.kt 或者 FlutterActivity.kt
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() {
private val CHANNEL = "signature_channel"
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "getSignatureHash") {
val hash = getSignatureHash(this)
if (hash != null) {
result.success(hash)
} else {
result.error("SIGNATURE_ERROR", "Failed to get signature hash", null)
}
} else {
result.notImplemented()
}
}
}
}
然后在 Dart 代码中调用该方法通道:
import 'package:flutter/services.dart';
const MethodChannel _channel = MethodChannel('signature_channel');
Future<String?> getSignatureHash() async {
try {
final String? signatureHash = await _channel.invokeMethod('getSignatureHash');
return signatureHash;
} on PlatformException catch (e) {
print("Failed to get signature hash: '${e.message}'.");
return null;
}
}
代码解释:
MethodChannel: 用于 Dart 和 Android 代码之间的通信。CHANNEL: 定义方法通道的名称。setMethodCallHandler: 设置方法调用处理程序,用于处理来自 Dart 的方法调用。invokeMethod: 在 Dart 代码中调用 Android 方法。
6.3. 配置 Android 项目 (Android)
- 添加 Kotlin 支持: 如果你的项目是 Java 项目,需要添加 Kotlin 支持。
- 添加 Kotlin 文件到项目: 将
android_signature.kt文件添加到你的 Android 项目中。 - 配置权限: 确保你的应用具有
android.permission.GET_SIGNATURES权限。但是请注意,从 Android API 28 (Pie) 开始,需要使用android.permission.REQUEST_INSTALL_PACKAGES权限才能获取其他应用的签名信息。获取自身应用签名信息不需要额外权限. - 更新 Gradle 配置: 确保你的 Gradle 配置文件中包含了 Kotlin 依赖项。
6.4. Dart 代码校验签名 (Android)
void verifySignature() async {
final expectedHash = 'your_expected_signature_hash'; // 替换为你的预期哈希值
final actualHash = await getSignatureHash();
if (actualHash == expectedHash) {
print('签名校验成功!');
} else {
print('签名校验失败!');
print('预期哈希值: $expectedHash');
print('实际哈希值: $actualHash');
}
}
七、安全性考虑
- 哈希值存储: 将预期的签名哈希值存储在安全的地方,例如使用 KeyStore 或 SharedPreferences 进行加密存储。不要将哈希值硬编码在代码中,因为这很容易被攻击者发现和修改。
- 混淆代码: 使用代码混淆工具来增加攻击者逆向工程的难度。
- Root 检测: 在 Android 平台上,可以检测设备是否被 Root,如果设备被 Root,则可以采取额外的安全措施。
- 定期更新: 定期更新应用的签名校验逻辑,以应对新的攻击手段。
八、平台差异性处理
在实际开发中,需要注意 iOS 和 Android 平台之间的差异性:
| 特性 | iOS | Android |
|---|---|---|
| 签名信息获取方式 | 使用 Security 框架 | 使用 PackageManager |
| FFI 调用方式 | DynamicLibrary.process() |
MethodChannel |
| 权限要求 | 无 | 不需要额外权限获取自身应用签名信息 |
九、一些关键问题
- 哈希算法的选择: SHA-256 是一个常用的哈希算法,但也可以根据实际情况选择其他的哈希算法,例如 SHA-512。
- 证书链的处理: 在 iOS 上,签名信息包含证书链。可以选择只校验根证书的哈希值,或者校验整个证书链的哈希值。
- 性能影响: 签名校验会带来一定的性能开销,需要根据实际情况进行优化。
校验应用签名是提高安全性的重要一步
通过以上步骤,我们可以在 Dart 层通过 FFI 实现 iOS 和 Android 应用的签名校验。这种方式可以增强应用的安全性,防止恶意篡改。请记住,安全是一个持续的过程,需要不断地更新和改进安全策略。
一些经验总结
- 在 iOS 上,使用
SecStaticCode和SecRequirement可以进行更严格的签名校验。 - 在 Android 上,需要处理不同 Android 版本之间的 API 差异。
- 在实际开发中,需要充分考虑安全性、性能和平台差异性等因素。