原文链接:
平台概述https://mobile-security.gitbook.io/mobile-security-testing-guide/android-testing-guide/0x05a-platform-overview
Android 基础安全测试https://mobile-security.gitbook.io/mobile-security-testing-guide/android-testing-guide/0x05b-basic-security_testing
Android 反逆向防御https://mobile-security.gitbook.io/mobile-security-testing-guide/android-testing-guide/0x05j-testing-resiliency-against-reverse-engineering
作者: 默默
Android Keystore对称-非对称加密防止关键数据被克隆
背景
最近需要解决用户使用手机克隆进行手机备份,某些设备相关的数据,比如设备ID的的存储文件也被克隆,导致用户更换设备之后,从本地读取到的设备ID信息依旧是上个手机的(Android Q以及以上的设备,有时候没办法获取设备ID,只能给一个随机数,因此只能存储在应用本地),这样会导致安全问题。
KeyStore简介
利用 Android KeyStore System,您可以在容器中存储加密密钥,从而提高从设备中提取密钥的难度。在密钥进入密钥库后,可以将它们用于加密操作,而密钥材料仍不可导出。此外,它提供了密钥使用的时间和方式限制措施,例如要求进行用户身份验证才能使用密钥,或者限制为只能在某些加密模式中使用。
密钥库系统由 KeyChain API 以及在 Android 4.3(API 级别 18)中引入的 Android 密钥库提供程序功能使用。本文说明了何时以及如何使用 Android 密钥库提供程序。
注意:Android Keystore的API非线程安全,对于API的调用需要进行互斥操作。线程不安全的原因很简单,那就是不管上层创建多少个对象,底层都只对应同一个加解密硬件,硬件没有完成操作之前给出另一个操作命令会导致硬件工作异常。这也是官方文档 EncryptedFile 类和 EncryptedSharedPreferences 类中的方法不是线程安全的根本原因。
我们从 Google Tink 项目的讨论 Bug: using EncryptedSharedPreferences, it can cause crashes to users right when initializing it #535中可以了解到,目前三星Samsung的部分机型的底层实现就是线程不安全的。
更多的机型信息参考 Bug: using EncryptedSharedPreferences, it can cause crashes to users right when initializing it 崩溃率排行中,三星,小米遥遥领先!
应用场景
1、 存储密匙:Android提供的这个KeyStore最大的作用就是不需要开发者去维护这个密匙的存储问题,相比起存储在用户的数据空间或者是外部存储器都更加安全。需要注意:这个密匙随着用户清除数据或者卸载应用都会被清除掉。
2、得益于Android独立的一套密匙库系统,可以提高安全性
安全功能
Android 密钥库系统可以保护密钥材料免遭未经授权的使用。首先,Android 密钥库可以防止从应用进程和 Android 设备中整体提取密钥材料,从而避免了在 Android 设备之外以未经授权的方式使用密钥材料。其次,Android 密钥库可以让应用指定密钥的授权使用方式,并在应用进程之外强制实施这些限制,从而避免了在 Android 设备上以未经授权的方式使用密钥材料。
提取防范
Android 密钥库密钥使用两项安全措施来避免密钥材料被提取:
- 密钥材料永不进入应用进程。通过 Android 密钥库密钥执行加密操作时,应用会将待签署或验证的明文、密文和消息馈送到执行加密操作的系统进程。如果应用进程受攻击,攻击者也许能使用应用密钥,但无法提取密钥材料(例如,在 Android 设备以外使用)。
- 您可以将密钥材料绑定至 Android 设备的安全硬件,例如可信执行环境 (TEE) 和安全元素 (SE)。为密钥启用此功能时,其密钥材料永远不会暴露于安全硬件之外。如果 Android 操作系统受到攻击或者攻击者可以读取设备内部存储空间,攻击者也许能在 Android 设备上使用应用的 Android 密钥库,但无法从设备上提取这些数据。只有设备的安全硬件支持密钥算法、区块模式、填充方案和密钥有权使用的摘要的特定组合时,才可启用此功能。要检查是否为密钥启用了此功能,请获取密钥的 KeyInfo 并检查 KeyInfo.isInsideSecurityHardware() 的返回值。
密钥使用授权
为了避免在 Android 设备上以未经授权的方式使用密钥材料,在生成或导入密钥时 Android 密钥库会让应用指定密钥的授权使用方式。一旦生成或导入密钥,其授权将无法更改。然后,每次使用密钥时,都会由 Android 密钥库强制执行授权。这是一项高级安全功能,通常仅用于有以下要求的情形:在生成/导入密钥后(而不是之前或当中),应用进程受到攻击不会导致密钥以未经授权的方式使用。
支持的密钥使用授权可归为以下几个类别:
- 加密:授权密钥算法、运算或目的(加密、解密、签署、验证)、填充方案、区块模式以及可与密钥搭配使用的摘要;
- 时间有效性间隔:密钥获得使用授权的时间间隔;
- 用户身份验证:密钥只能在用户最近进行身份验证时使用。请参阅要求进行用户身份验证才能使用密钥。
作为一项额外的安全措施,对于密钥材料位于安全硬件内部的密钥(请参阅 KeyInfo.isInsideSecurityHardware()),某些密钥使用授权可能由安全硬件实施,具体取决于 Android 设备。加密和用户身份验证授权可能由安全硬件实施。由于安全硬件一般不具备独立的安全实时时钟,时间有效性间隔授权不可能由其实施。
您可以使用 KeyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware() 查询密钥的用户身份验证授权是否由安全硬件实施。
选择密钥链或 Android 密钥库提供程序
在需要系统级凭据时请使用 KeyChain API。在应用通过 KeyChain API 请求使用任何凭据时,用户需要通过系统提供的 UI 选择应用可以访问已安装的哪些凭据。因此,在用户同意的情况下多个应用可以使用同一套凭据。
使用 Android 密钥库提供程序让各个应用存储自己的凭据,并且只允许应用自身访问。这样,应用可以管理仅能由自己使用的凭据,同时又可以提供等同于 KeyChain API 为系统级凭据提供的安全优势。这一方法不需要用户选择凭据。
使用 Android 密钥库提供程序
要使用此功能,请使用标准的 KeyStore 和 KeyPairGenerator 或 KeyGenerator 类,以及在 Android 4.3(API 级别 18)中引入的 AndroidKeyStore 提供程序。
AndroidKeyStore 注册为 KeyStore 类型以用于 KeyStore.getInstance(type) 方法,而在用于 KeyPairGenerator.getInstance(algorithm, provider) 和 KeyGenerator.getInstance(algorithm, provider) 方法时则注册为提供程序。
生成新私钥
生成新的 PrivateKey 要求您同时指定自签署证书具备的初始 X.509 属性。之后,您可以使用 KeyStore.setKeyEntry 将证书替换为由证书颁发机构 (CA) 签署的证书。
要生成密钥,则使用 KeyPairGenerator 和 KeyPairGeneratorSpec:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* * Generate a new EC key pair entry in the Android Keystore by * using the KeyPairGenerator API. The private key can only be * used for signing or verification and only with SHA-256 or * SHA-512 as the message digest. */ KeyPairGenerator kpg = KeyPairGenerator.getInstance( KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); kpg.initialize(new KeyGenParameterSpec.Builder( alias, KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY) .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) .build()); KeyPair kp = kpg.generateKeyPair(); |
生成新密钥
要生成密钥,请使用 KeyGenerator 和 KeyGenParameterSpec。
使用密钥库条目
AndroidKeyStore 提供程序的使用通过所有的标准 KeyStore API 加以实现。
列出条目
通过调用 aliases() 方法列出密钥库中的条目:
|
1 2 3 4 5 6 7 8 |
/* * Load the Android KeyStore instance using the the * "AndroidKeyStore" provider to list out what entries are * currently stored. */ KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); ks.load(null); Enumeration<String> aliases = ks.aliases(); |
签署和验证数据
通过从密钥库提取 KeyStore.Entry 并使用 Signature API(例如 sign())签署数据:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* * Use a PrivateKey in the KeyStore to create a signature over * some data. */ KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); ks.load(null); KeyStore.Entry entry = ks.getEntry(alias, null); if (!(entry instanceof PrivateKeyEntry)) { Log.w(TAG, "Not an instance of a PrivateKeyEntry"); return null; } Signature s = Signature.getInstance("SHA256withECDSA"); s.initSign(((PrivateKeyEntry) entry).getPrivateKey()); s.update(data); byte[] signature = s.sign(); |
类似地,请使用 verify(byte[]) 方法验证数据:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* * Verify a signature previously made by a PrivateKey in our * KeyStore. This uses the X.509 certificate attached to our * private key in the KeyStore to validate a previously * generated signature. */ KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); ks.load(null); KeyStore.Entry entry = ks.getEntry(alias, null); if (!(entry instanceof PrivateKeyEntry)) { Log.w(TAG, "Not an instance of a PrivateKeyEntry"); return false; } Signature s = Signature.getInstance("SHA256withECDSA"); s.initVerify(((PrivateKeyEntry) entry).getCertificate()); s.update(data); boolean valid = s.verify(signature); |
要求进行用户身份验证才能使用密钥
生成密钥或将密钥导入到 AndroidKeyStore 时,您可以指定密钥仅授权给经过身份验证的用户使用。用户使用安全锁定屏幕凭据(模式/PIN/密码、指纹)的子集进行身份验证。
这是一项高级安全功能,通常仅用于有以下要求的情形:在生成/导入密钥后(而不是之前或当中),应用进程受到攻击不会导致密钥被未经身份验证的用户使用。
如果密钥仅授权给经过身份验证的用户使用,可以将其配置为以下列两种模式之一运行:
- 经过身份验证的用户可以在一段时间内使用密钥。在用户解锁安全锁定屏幕或使用 KeyguardManager.createConfirmDeviceCredentialIntent 流程确认其安全锁定屏幕凭据后,即可使用此模式中的所有密钥。每个密钥的授权持续时间各不相同,并由 setUserAuthenticationValidityDurationSeconds 在密钥生成或导入时指定。此类密钥只能在启用安全锁定屏幕时生成或导入(请参阅 KeyguardManager.isDeviceSecure())。在安全锁定屏幕停用(重新配置为“无”、“滑动”或不验证用户身份的其他模式)或被强制重置(例如由设备管理员执行)时,这些密钥将永久失效。
- 用户身份验证会授权与某一密钥关联的特定加密操作。在此模式中,涉及此类密钥的每个操作都需要用户单独授权。目前,此类授权的唯一方式是指纹身份验证:FingerprintManager.authenticate。此类密钥只能在至少注册一个指纹时生成或导入(请参阅 FingerprintManager.hasEnrolledFingerprints)。一旦注册新指纹或取消注册所有指纹,这些密钥将永久失效。
Android数据加密:
Android 提供了 KeyStore 等可以长期存储和检索加密密钥的机制,Android KeyStore 系统特别适合于存储加密密钥。
“AndroidKeyStore” 是 KeyStore 的一个子集,存进 AndroidKeyStore 的 key 将受到签名保护,并且这些 key 是存在系统里的,而不是在 App 的 data 目录下,依托于硬件的 KeyChain 存储,可以做到 private key 一旦存入就无法取出,
每个 App 自己创建的 key,别的应用是访问不到的。
它提供了限制何时以何种方式使用密钥的方法,比如使用密钥时需要用户认证或限制密钥只能在加密模式下使用。
一个应用程式只能编辑、保存、取出自己的密钥。
App可以生成或者接收一个公私密钥对,并存储在Android的Keystore系统中。公钥可以用于在应用数据放置到特定文件夹前对数据进行加密,私钥可以在需要的时候解密相应的数据。
作用:
KeyStore 适用于生成和存储密钥,这些密钥可以用来加密运行时获取到的数据,比如运行时,用户输入的密码,或者服务端传下来的 token。
操作方式
- 存:使用 Keystore 加密信息后存入 SharedPreferences
- 取:从 SharedPreferences 取出信息並使用 Keystore 解密
建议做法
1. 使用对称式加解密,但只能在Api Level 23+使用
对称式加解密(AES)速度较快,但是对称式的Key若要存在 KeyStore 裡,Api Level一定要在23以上才支持,23以下是无法存入 KeyStore 的,非对称式的Key則不在此限。
2. 想兼容各Api版本(23以下也能用)
- 若要存取的東西不多、字串長度也不長:直接使用非对称式加解密即可
- 若要存取的東西很多或字串長度很長:由於非对称式加解密速度较慢,使用非对称式+对称式加解密可以解決此問題。
考慮到加解密效能、版本兼容,下面會介紹用非对称式+对称式來加解密。
KeyStore非对称+对称式加解密流程
- 使用 KeyStore 产生随机的 RSA Key;
- 产生 AES Key,并用 RSA Public Key 加密后存入 SharedPrefs;
- 从 SharedPrefs 取出 AES Key,並用 RSA Private Key 解密,用这把 AES Key 来加解密信息;
主流的加密方式有:(对称加密)AES、DES 、EC (非对称加密)RSA、DSA
工作模式:
DES一共有:
电子密码本模式(ECB)、加密分组链接模式(CBC)、加密反馈模式(CFB)、输出反馈模式(OFB);
AES一共有:
电子密码本模式(ECB)、加密分组链接模式(CBC)、加密反馈模式(CFB)、输出反馈模式(OFB)、计数器模式(CTR),伽罗瓦计数器模式(GCM)
PKCS5Padding是填充模式,还有其它的填充模式;
对于初始化向量 iv: 初始化向量参数,AES 为16bytes. DES 为 8bytes
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
private static final String KEYSTORE_PROVIDER = "AndroidKeyStore"; // 算法/模式/补码方式 不使用CBC模式,注意Padding Oracle攻击 private static final String AES_MODE = "AES/GCM/NoPadding"; /* * RSA ENCRYPTION_PADDING_RSA_PKCS1 存在安全漏洞,高于API 23的版本使用 ENCRYPTION_PADDING_RSA_OAEP * 漏洞关键字: * "Chosen ciphertext attacks against protocols based on the RSA encryption standard PKCS #1" * 目前测试来看,很多设备暂时不支持 ENCRYPTION_PADDING_RSA_OAEP 模式,比较可惜 */ private static final String RSA_MODE = "RSA/ECB/PKCS1Padding"; private static final String KEYSTORE_ALIAS = "KEYSTORE_DEMO"; /** * Returns whether the device has a StrongBox backed KeyStore. */ public static boolean hasStrongBox(@NonNull final Context context) { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { /* * 针对小米厂商,全系列不启用 StrongBox 支持 * 目前已知的问题升级,在 小米 14 系列上,使用 StrongBox 生成的密钥,已经有客户反馈周期性的密钥错误,导致无法解密出数据 * 典型的现象就是调用硬件密钥的时候抛出异常,无法读取出来,应该是密钥在硬件中丢失了 * * 在小米 15 / 红米 K80 系列上,使用 "AES/GCM/NoPadding" 生成的密钥如果关联生物识别,比如指纹,则可以加密数据,但是解密加密后的数据 * 会失败,经过与小米厂商的邮件沟通,这部分功能在整个系列上存在问题,无法正常工作 * * 因此,全系的小米手机/红米手机都默认不启用 StrongBox * */ if("Xiaomi".equalsIgnoreCase(android.os.Build.BRAND) || "Redmi".equalsIgnoreCase(android.os.Build.BRAND)) { return false; } return context.getPackageManager() .hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE); } return false; } KeyStore mKeyStore = KeyStore.getInstance(KEYSTORE_PROVIDER); mKeyStore.load(null); |
(1)产生随机的 RSA Key
产生 RSA Key 会使用到 KeyPairGenerator:
其中 KeyPairGeneratorSpec 在Api 23以上已经 Deprecated 了;
Api level 23以上改使用 KeyGenParameterSpec
|
1 2 3 4 5 6 7 8 9 10 11 12 |
/* * 注意KeyStore的API线程不安全,不建议主线程执行,主线程可能导致界面卡顿 * 建议全部调度到一个统一的HanderThread子线程进行处理 **/ @WorkerThread private void genKeyStoreKey(@NonNull final Context context) throws Exception { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { generateRSAKey_AboveApi23(context); } else { generateRSAKey_BelowApi23(context); } } |
Api 23 以上使用 KeyGenParameterSpec
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
/* * 注意KeyStore的API线程不安全,不建议主线程执行,主线程可能导致界面卡顿 * 建议全部调度到一个统一的HanderThread子线程进行处理 **/ @WorkerThread @RequiresApi(api = Build.VERSION_CODES.M) private void generateHwAesKey_AboveApi23(@NonNull final String keystoreAlias, @NonNull final String provider) throws Exception { // https://developer.android.com/training/articles/keystore.html#SupportedCiphers // https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder.html#setRandomizedEncryptionRequired(boolean)) final KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec .Builder(keystoreAlias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setCertificateSubject(new X500Principal("CN=" + getKeyStoreAlias(context))) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) /* 使用相同的IV多次加密不同的数据,会存在被恶意穷举的风险,我们要求硬件每次加密返回不同的加密向量IV * 这就要求我们需要存储加密数据的同时,存储返回的随机向量,然后在解密的时候提供加密时候的随机向量 */ .setRandomizedEncryptionRequired(true); //要求硬件生成随机向量 if (hasStrongBox(context) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)) { try { /* 如果系统支持硬件StrongBox保存密钥,则尝试交给StrongBox保护 */ builder.setIsStrongBoxBacked(true); generateHwAesKey_AboveApi23(builder, provider); } catch (Throwable t) { Logs.e(TAG, t.getMessage(), t); /* * 2024/06 * 已知在小米 14/15 Xiaomi HyperOS 系统上,部分手机在启用 StrongBox 的情况下,会发生如下异常: * <p> * java.security.ProviderException: Keystore key generation failed * at android.security.keystore2.AndroidKeyStoreKeyGeneratorSpi.engineGenerateKey(AndroidKeyStoreKeyGeneratorSpi.java:413) * at javax.crypto.KeyGenerator.generateKey(KeyGenerator.java:612) * at com.xxxx.xxxx.xxxx.xxxx.xxxx.xxxx.x(SourceFile:28) * <p> * Caused by: android.security.KeyStoreException: Not implemented (internal Keystore code: -100 message: system/security/keystore2/src/security_level.rs:622 * <p> * Caused by: * 0: system/security/keystore2/src/security_level.rs:620: While generating Key without explicit attestation key. * 1: Error::Km(r.... * * 我们需要先尝试使用 StrongBox 生成密钥,如果生成失败,则尝试不使用 StrongBox 生成密钥 **/ builder.setIsStrongBoxBacked(false); generateHwAesKey_AboveApi23(builder, provider); } } else { generateHwAesKey_AboveApi23(builder, provider); } } @WorkerThread @RequiresApi(api = Build.VERSION_CODES.M) private void generateHwAesKey_AboveApi23(@NonNull final KeyGenParameterSpec.Builder builder, @NonNull final String provider) throws Exception { final KeyGenerator keyGenerator = KeyGenerator .getInstance(KeyProperties.KEY_ALGORITHM_AES, provider); final KeyGenParameterSpec spec = builder.build(); keyGenerator.init(spec); keyGenerator.generateKey(); } |
Api 23 以下使用 KeyPairGeneratorSpec
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
/* * 注意KeyStore的API线程不安全,不建议主线程执行,主线程可能导致界面卡顿 * 建议全部调度到一个统一的HanderThread子线程进行处理 **/ @WorkerThread private void generateRSAKey_BelowApi23(@NonNull final Context context) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException { final Calendar end = Calendar.getInstance(); end.add(Calendar.YEAR, 99); final KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context) .setAlias(KEYSTORE_ALIAS) .setSubject(new X500Principal("CN=" + KEYSTORE_ALIAS)) .setSerialNumber(BigInteger.TEN) /* 开始时间设置为1970/1/1原因在于 * 防止系统由于时间无效或者电池没电导致的时间只能从最早时间开始的问题, * 如果时间不正确会导致在调用的时候抛出 * android.security.keystore.KeyNotYetValidException: Key not yet valid */ .setStartDate(new Date(0)) .setEndDate(end.getTime()) .build(); final KeyPairGenerator keyPairGenerator = KeyPairGenerator .getInstance("RSA", KEYSTORE_PROVIDER); keyPairGenerator.initialize(spec); keyPairGenerator.generateKeyPair(); } |
注意,已知,在某些低端设备上,RSA密钥对生成的时间可能超过1S以上。尝试过异步子线程初始化,但是由KeyStore内部的函数在实现的时候,线程不安全,导致子线程初始化的时候诱发了异常行为,因此只能在主线程中进行操作。
(2)产生AES Key后,并用RSA Public Key加密后存入SharedPrefs
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
private void genAESKey() throws Exception { // Generate AES-Key final byte[] aesKey = new byte[16]; final SecureRandom secureRandom = new SecureRandom(); secureRandom.nextBytes(aesKey); // Generate 12 bytes iv then save to SharedPrefs final byte[] generated = secureRandom.generateSeed(12); final String iv = Base64.encodeToString(generated, Base64.DEFAULT); final String encryptIV = encryptRSA(iv.getBytes()); prefsHelper.setIV(encryptIV); // Encrypt AES-Key with RSA Public Key then save to SharedPrefs final String encryptAESKey = encryptRSA(aesKey); prefsHelper.setAESKey(encryptAESKey); } |
1]加密存储:使用RSA Public Key 加密 AES Key,存入缓存中。
2] 解密使用:使用RSA Private Key 解密 得到 AES Key。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
private String encryptRSA(@NonNull final byte[] plainText) throws Exception { final PublicKey publicKey = keyStore.getCertificate(KEYSTORE_ALIAS).getPublicKey(); final Cipher cipher = Cipher.getInstance(RSA_MODE); cipher.init(Cipher.ENCRYPT_MODE, publicKey); final byte[] encryptedByte = cipher.doFinal(plainText); return Base64.encodeToString(encryptedByte, Base64.DEFAULT); } private byte[] decryptRSA(@NonNull final String encryptedText) throws Exception { final PrivateKey privateKey = (PrivateKey) keyStore.getKey(KEYSTORE_ALIAS, null); final Cipher cipher = Cipher.getInstance(RSA_MODE); cipher.init(Cipher.DECRYPT_MODE, privateKey); final byte[] encryptedBytes = Base64.decode(encryptedText, Base64.DEFAULT); final byte[] decryptedBytes = cipher.doFinal(encryptedBytes); return decryptedBytes; } |
获取AES :
|
1 2 3 4 5 6 |
private SecretKeySpec getAESKey() throws Exception { final String encryptedKey = prefsHelper.getAESKey(); final byte[] aesKey = decryptRSA(encryptedKey); return new SecretKeySpec(aesKey, AES_MODE); } |
再使用AES 加解密内容:
对于:Cipher 初始化
|
1 2 3 4 5 6 7 8 9 10 |
//实例化加密类,参数为加密方式,要写全 // 算法/模式/补码方式 不使用CBC模式,注意Padding Oracle攻击 Cipher cipher = Cipher.getIntance("AES/CBC/PKCS5Padding"); //初始化,此方法可以采用三种方式,按服务器要求来添加。 //(1)无第三个参数 //(2)第三个参数为SecureRandom random = new SecureRandom(); // 中random对象,随机数。(AES不可采用这种方法) //(3)第三个参数:IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec/random); |
具体使用:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/** * AES Encryption * @param plainText: A string which needs to be encrypted. * @return A base64's string after encrypting. */ private String encryptAES(@NonNull final String plainText) throws Exception { final Cipher cipher = Cipher.getInstance(AES_MODE); cipher.init(Cipher.ENCRYPT_MODE, getAESKey(), new IvParameterSpec(getIV())); // 加密過後的byte final byte[] encryptedBytes = cipher.doFinal(plainText.getBytes()); // 將byte轉為base64的string編碼 return Base64.encodeToString(encryptedBytes, Base64.DEFAULT); } private String decryptAES(@NonNull final String encryptedText) throws Exception { // 將加密過後的Base64編碼格式 解碼成 byte final byte[] decodedBytes = Base64.decode(encryptedText.getBytes(), Base64.DEFAULT); // 將解碼過後的byte 使用AES解密 final Cipher cipher = Cipher.getInstance(AES_MODE); cipher.init(Cipher.DECRYPT_MODE, getAESKey(), new IvParameterSpec(getIV())); return new String(cipher.doFinal(decodedBytes)); } |
iv 初始化向量
|
1 2 3 4 5 |
private byte[] getIV() throws Exception { final String prefIV = prefsHelper.getIV(); final byte[] iv = decryptRSA(prefIV); return Base64.decode(iv, Base64.DEFAULT); } |
关于RSA:
使用RSA加解密时,在较低版本的手机上可能无法选择OAEP(最优非对称加密填充,RSA的加密解密是基于OAEP的)這個模式;
因此可以改使用RSA_PKCS1_PADDING模式,使用这个模式的話,输入必须比RSA的Key最大长度少11個字节,如果需要被加密的字串过长的话,可以在产生Key时指定Key Size长度,或是将字串分段加密。
以预设Key Size = 2048bit(256byte)來说,输入最长只能到256–11=245byte,我們可以透过setKeySize(int keySize)指定Key的长度,但是Key Size越大,加解密时速度就越慢。
|
1 2 3 4 5 6 |
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec .Builder(KEYSTORE_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) .setKeySize(4096) .build(); |
需要注意,由于设备可能存储密钥到硬件设备(KeyInfo.isInsideSecurityHardware()),然而硬件设备不一定能保存我们手工指定的某些长度的密钥。导致如果我们设置了指定长度,可能由于硬件设备不支持,反而只能存储到系统中,造成密钥存储的安全性反而下降了。
我们希望厂家设置的默认值是硬件能支持的最大安全性,并且尽量存储到硬件中。尽管密钥安全性可能下降了,但是存储安全性反而上升了。
或者从高到低重试,选择当前设备支持的最高的硬件存储的长度。究竟是存储位置重要还是加密级别更重要,需要权衡。
判断生成的密钥是否由硬件存储,参考如下代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
/*API 23 以下的硬件,我们默认就无法存储密钥到硬件设备*/ public boolean isInsideSecurityHardware() throws Exception { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final PrivateKey key = (PrivateKey) keyStore.getKey(KEYSTORE_ALIAS, null); final KeyFactory factory = KeyFactory.getInstance(key.getAlgorithm()); final KeyInfo info = factory.getKeySpec(key, KeyInfo.class); //RSA密钥的长度也可以通过KeyInfo进行查询 return info.isInsideSecurityHardware(); } return false; } |
获取设备生成的密钥的长度,参考如下代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public int getSecureKeySize() throws Exception { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final PrivateKey key = (PrivateKey) keyStore.getKey(KEYSTORE_ALIAS, null); final KeyFactory factory = KeyFactory.getInstance(key.getAlgorithm()); final KeyInfo info = factory.getKeySpec(key, KeyInfo.class); if (null != info) { return info.getKeySize(); } } else { final Certificate certificate = keyStore.getCertificate(KEYSTORE_ALIAS); if (certificate instanceof X509Certificate) { final X509Certificate cert = (X509Certificate) certificate; final PublicKey publicKey = cert.getPublicKey(); if (publicKey instanceof RSAPublicKey) { final RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey; final BigInteger bigInteger = rsaPublicKey.getModulus(); if (null != bigInteger) { return bigInteger.bitLength(); } } } } return 0; } |
需要解决的一个疑惑就是,既然可以通过
|
1 |
final PrivateKey privateKey = (PrivateKey) keyStore.getKey(KEYSTORE_ALIAS, null); |
的方式获得RSA私钥,那么,我们能不导出这个私钥呢?
答案显然是不能的,原因在于系统给出的私钥只是一个代理,并没有实际的私钥数据,私钥数据被存储在相关的硬件或者系统内核中,主要证据就是privateKey.getEncoded()返回了null,这样就实现了关键的私钥数据都无法获得。
参考如下测试代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
final PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, null); assertNotNull(privateKey); // 默认的AndroidKeyStore应该不能读出私钥数据 assertNull(privateKey.getEncoded()); assertNull(privateKey.getFormat()); /* RSA 密钥重新生产的方法参考下面的代码,从获取到的 PrivateKey中我们只能得到系数(modulus),其他参数都得不到 因此保证了密钥的安全性 RSAPrivateCrtKeySpec spec = new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent, primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient); KeyFactory keyFactory = KeyFactory.getInstance("RSA","BC"); PrivateKey privateKey = keyFactory.generatePrivate(spec); */ //公钥要可以被导出 final Certificate cert = keyStore.getCertificate(alias); assertNotNull(cert.getEncoded()); |
需要注意的一个问题在于,由于RSA的大素数搜索机制,导致每次生成密钥的时间可能会超过预期,最长的可能会耗时1-2S以上。因此,如果设备支持,我们直接申请硬件AES密钥的方式来进行数据的加解密操作,达到更高的安全程度。
可用的参考代码如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyInfo; import android.security.keystore.KeyProperties; import android.util.Base64; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; import org.json.JSONObject; import java.security.KeyStore; import java.security.spec.KeySpec; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.GCMParameterSpec; import javax.security.auth.x500.X500Principal; /** * 通过AES加密方式,用KeyGenerator生成秘钥,保存在Android Keystore中 * 对数据进行加解密 * <p> * 1、创建秘钥,保存在AndroidKeystore里面,秘钥别名为alias * 2、创建并初始化cipher对象,获取秘钥,对数据进行加解密 */ class HwAesSecProvider { private static final String TAG = "HwAesSecProvider"; private static final String KEYSTORE_AES_ALIAS = "Alias_AES_"; // 算法/模式/补码方式 不使用CBC模式,注意Padding Oracle攻击 private static final String AES_MODE = "AES/GCM/NoPadding"; private final static String KEY_IV = "iv"; private final static String KEY_DATA = "data"; private final Context context; private final KeyStore keyStore; public HwAesSecProvider(@NonNull final Context context, @NonNull final KeyStore keyStore) { this.context = context; this.keyStore = keyStore; } @NonNull private static String getKeyStoreAlias(@NonNull final Context context) { return KEYSTORE_AES_ALIAS + PrefsHelper.getPackageNameHash(context); } @NonNull public String decryptAES(@NonNull final String encryptedText) { try { final JSONObject json = new JSONObject(encryptedText); final byte[] iv = Base64.decode(json.optString(KEY_IV, ""), Base64.DEFAULT); final byte[] data = Base64.decode(json.optString(KEY_DATA, ""), Base64.DEFAULT); if (iv.length <= 0 || data.length <= 0) { return ""; } final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(getKeyStoreAlias(context), null); if (null != secretKeyEntry) { final SecretKey secretKey = secretKeyEntry.getSecretKey(); // KeyGenParameterSpecs中设置的block模式是KeyProperties.BLOCK_MODE_GCM,所以这里只能使用这个模式解密数据。 final Cipher cipher = Cipher.getInstance(AES_MODE); // 需要为GCMParameterSpec 指定一个加密的block的长度(可以是128、120、112、104、96),从密钥信息中读取后填入 // 并且用到之前的加密过程中用到的IV。 final GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(getSecureKeySize(secretKey), iv, 0, iv.length); cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); return new String(cipher.doFinal(data)); } } catch (Throwable t) { Logs.e(TAG, t.getMessage(), t); } return ""; } @NonNull public String encryptAES(@NonNull final String plainText) { try { final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(getKeyStoreAlias(context), null); if (null != secretKeyEntry) { final SecretKey secretKey = secretKeyEntry.getSecretKey(); // KeyGenParameterSpecs中设置的block模式是KeyProperties.BLOCK_MODE_GCM,所以这里只能使用这个模式解密数据。 final Cipher cipher = Cipher.getInstance(AES_MODE); cipher.init(Cipher.ENCRYPT_MODE, secretKey); final JSONObject json = new JSONObject(); json.put(KEY_DATA, Base64.encodeToString(cipher.doFinal(plainText.getBytes()), Base64.DEFAULT)); /* 使用相同的IV多次加密不同的数据,会存在被恶意穷举的风险,我们要求硬件每次加密返回不同的加密向量IV * 这就要求我们需要存储加密数据的同时,存储返回的随机向量,然后在解密的时候提供加密时候的随机向量 */ json.put(KEY_IV, Base64.encodeToString(cipher.getIV(), Base64.DEFAULT)); return json.toString(); } } catch (Throwable t) { Logs.e(TAG, t.getMessage(), t); } return ""; } /* * 注意KeyStore的API线程不安全,不建议主线程执行,主线程可能导致界面卡顿 * 建议全部调度到一个统一的HanderThread子线程进行处理 **/ @WorkerThread @RequiresApi(api = Build.VERSION_CODES.M) private void generateHwAesKey_AboveApi23(@NonNull final String keystoreAlias, @NonNull final String provider) throws Exception { // https://developer.android.com/training/articles/keystore.html#SupportedCiphers // https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder.html#setRandomizedEncryptionRequired(boolean)) final KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec .Builder(keystoreAlias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setCertificateSubject(new X500Principal("CN=" + getKeyStoreAlias(context))) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) /* 使用相同的IV多次加密不同的数据,会存在被恶意穷举的风险,我们要求硬件每次加密返回不同的加密向量IV * 这就要求我们需要存储加密数据的同时,存储返回的随机向量,然后在解密的时候提供加密时候的随机向量 */ .setRandomizedEncryptionRequired(true); //要求硬件生成随机向量 if (hasStrongBox(context) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)) { try { /* 如果系统支持硬件StrongBox保存密钥,则尝试交给StrongBox保护 */ builder.setIsStrongBoxBacked(true); generateHwAesKey_AboveApi23(builder, provider); } catch (Throwable t) { Logs.e(TAG, t.getMessage(), t); /* * 2024/06 * 已知在小米 14/15 Xiaomi HyperOS 系统上,部分手机在启用 StrongBox 的情况下,会发生如下异常: * <p> * java.security.ProviderException: Keystore key generation failed * at android.security.keystore2.AndroidKeyStoreKeyGeneratorSpi.engineGenerateKey(AndroidKeyStoreKeyGeneratorSpi.java:413) * at javax.crypto.KeyGenerator.generateKey(KeyGenerator.java:612) * at com.xxxx.xxxx.xxxx.xxxx.xxxx.xxxx.x(SourceFile:28) * <p> * Caused by: android.security.KeyStoreException: Not implemented (internal Keystore code: -100 message: system/security/keystore2/src/security_level.rs:622 * <p> * Caused by: * 0: system/security/keystore2/src/security_level.rs:620: While generating Key without explicit attestation key. * 1: Error::Km(r.... * * 我们需要先尝试使用 StrongBox 生成密钥,如果生成失败,则尝试不使用 StrongBox 生成密钥 **/ builder.setIsStrongBoxBacked(false); generateHwAesKey_AboveApi23(builder, provider); } } else { generateHwAesKey_AboveApi23(builder, provider); } } @WorkerThread @RequiresApi(api = Build.VERSION_CODES.M) private void generateHwAesKey_AboveApi23(@NonNull final KeyGenParameterSpec.Builder builder, @NonNull final String provider) throws Exception { final KeyGenerator keyGenerator = KeyGenerator .getInstance(KeyProperties.KEY_ALGORITHM_AES, provider); final KeyGenParameterSpec spec = builder.build(); keyGenerator.init(spec); keyGenerator.generateKey(); } /* * 注意KeyStore的API线程不安全,不建议主线程执行,主线程可能导致界面卡顿 * 建议全部调度到一个统一的HanderThread子线程进行处理 **/ @WorkerThread public boolean generateSecStoreKeys() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { try { generateHwAesKey_AboveApi23(getKeyStoreAlias(context), SecConst.AND_KEYSTORE_PROVIDER); return isInsideSecureHardware(); } catch (Throwable t) { Logs.e(TAG, t.getMessage(), t); } } return false; } public boolean isInsideSecureHardware() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final KeyInfo info = getKeyInfo(); if (null != info) { return info.isInsideSecureHardware(); } } return false; } public int getSecureKeySize() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final KeyInfo info = getKeyInfo(); if (null != info) { return info.getKeySize(); } } return 0; } @RequiresApi(api = Build.VERSION_CODES.M) @Nullable private KeyInfo getKeyInfo(@NonNull final SecretKey secretKey) throws Exception { final SecretKeyFactory factory = SecretKeyFactory.getInstance(secretKey.getAlgorithm(), SecConst.AND_KEYSTORE_PROVIDER); if (null != factory) { final KeySpec spec = factory.getKeySpec(secretKey, KeyInfo.class); if (null != spec) { return (KeyInfo) spec; } } return null; } private int getSecureKeySize(@NonNull final SecretKey secretKey) throws Exception { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final KeyInfo info = getKeyInfo(secretKey); if (null != info) { return info.getKeySize(); } } return 0; } @RequiresApi(api = Build.VERSION_CODES.M) @Nullable private KeyInfo getKeyInfo(@NonNull final String alias) { try { final SecretKey key = (SecretKey) keyStore.getKey(alias, null); if (null != key) { return getKeyInfo(key); } } catch (Throwable t) { Logs.e(TAG, "Exception getting key info", t); } return null; } @RequiresApi(api = Build.VERSION_CODES.M) @Nullable private KeyInfo getKeyInfo() { return getKeyInfo(getKeyStoreAlias(context)); } /** * Returns whether the device has a StrongBox backed KeyStore. */ public static boolean hasStrongBox(@NonNull final Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return context.getPackageManager() .hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE); } return false; } } |
参考链接
- Android Keystore 对称-非对称加密
- AndroidKeyStore的使用
- Android KeyStore密钥存储
- Android 密钥库系统
- BouncyCastle 密钥转换 - Java pkcs1格式,pkcs8格式互转
- C#与Java的RSA中的X509EncodedKeySpec、PKCS8EncodedKeySpec
- android安全存储,使用AndroidKeyStore的问题
- OPENSSL RSA加密与解密
- How to store and retrieve an RSA public key in Android keystore which is generated from server side application?
- 如何用已知的参数生成一个RSAPrivateKey
- 公钥,私钥和数字签名这样最好理解 (转载)
- MIUI安全与隐私白皮书
- android.security.KeyStoreException: Signature/MAC verification failed When trying to decrypt in a React Native Module
- 分组密码加密操作模式
- List of Android smart phones that support Android StrongBox on API 28(Android Pie)
- Android 纯本地安全存储方案
- 从AndroidKeystore加载私钥时出现问题
- Using AES with AndroidKeyStore
- Key attestation的几个关键点!
- Verifying hardware-backed key pairs with Key Attestation
- GrapheneOS/AttestationSamples
- google/android-key-attestation
- App security improvement program
- aes iv值_AES加密: GCM和CBC模式的区别
- 我对Padding Oracle攻击的分析和思考(详细)
- 使用 SafetyNet 抵御安全威胁
- Android Pie 引入 Keystore 新特性,安全防护再升级
- Import encrypted AES key into Android Keystore and store it under new alias
- Android Cryptographic APIs
- 聊聊密码学中的Padding
- Chosen ciphertext attacks against protocols based on the RSA encryption standard PKCS #1
- Import encrypted AES key into Android Keystore and store it under new alias
- 密钥认证和 ID 认证
- alphamu/KeyStoreHelper.java
- How to write thread-safe code for Android Keystore
- Bug: using EncryptedSharedPreferences, it can cause crashes to users right when initializing it #535
- [原创]Key Attestation原理理解
- Android Developers/Develop/Reference/Cipher
- Android Developers/Develop/Reference/AlgorithmParameters
- Android Developers/Security/加密
- Android Developers/App quality/安全准则
- Android Local Authentication
如何检查Linux中是否启用了USB3.0 UASP(USB附加SCSI协议)模式?
USB 3.0 在数据量较多且零碎传输时,效能的表现会较差,主要原因是在于传输技术所造成的状况。
USB 采用半双工传输模式与 BOT (Bluk Only Transfer) 传输协议,半双工传输模式就像是再讲对讲机一样,一方说完另一方才能继续对话,也就是说数据的传输方向仅有单向传输功能。
BOT 传输协议则是一种单线程的传输架构,这个架构就像马路上只能行使一台车子,不管路有多宽,都必须等待这量车子行驶过后下一部车子才能继续通行。
USB 的加速方式
提升为全双工模式
当 USB 升级到 USB 3.0 时,将原来的半双工传输模式,改为全双工模式,并且提升十倍的带宽,不过其架构还是在 BOT 之下。
BOT 加速模式
这个方式虽然还是在 BOT 的架构之下,但是是将原来在路上行使的轿车,换一台更大一点的公交车,将一次传送的数据量加大,就可以减少来回的次数,来达到加速的目的。
UASP 加速模式
BOT加速模式虽然可以有效的增加传输速度,不过还是没有在根本上解决无法多任务处理的问题,所以 USB-IF (USB Implementers Forum) 一个为开发 USB 规格的企业共同成立的非营利性机构,为了让传输效能更好,制定了UASP(USB Attached SCSI Protocol)传输架构,让 USB3.0 也能像 SCSI 传输协议的方式传输数据,不需等待上一笔数据传输完成后再进行传输数据的动作,并改善了在 CPU 的使用率、数据延迟及等待时间。
要达到 UASP 加速功能,请先确认您计算机端是否同样有支持 UASP 功能。
目前支持 UASP 的系统有 WIN 8 、Mac OS 8 & 9 ,除了以上系统,您也可以询问您购买的计算机公司,是否有提供相关的驱动程序。
ubuntu 20.04系统AOSP(Android 11)集成Frida
参考 ubuntu 20.04编译Android 11源代码&模拟器 完成Android 11源代码的编译工作,保证能编译通过。
想自己手工编译Frida源代码的话,请参照下面:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
$ git clone https://github.com/frida/frida.git $ cd frida $ make $ export ANDROID_NDK_ROOT=/data/Android/android-sdk-linux/ndk/22.0.6917172 $ sudo apt-get install npm $ sudo apt install python3-pip $ pip3 install colorama prompt-toolkit pygments $ rm -rf build $ make core-android-x86_64 $ make core-android-x86 # 最后生成的文件在 build/frida-android-x86 build/frida-android-x86_64 # 如果需要调整编译参数,在releng/setup-env.sh中进行调整 比如: meson_common_flags="['-g']" |
如果想直接下载对应版本的Frida库并存放到已经编译过的库位置,由于64位系统需要兼容32位应用,因此需要安装两个版本的动态库:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
$ cd ~/AndSrc/aosp/ $ cd out/target/product/generic_x86_64/system/lib64 $ wget https://github.com/frida/frida/releases/download/14.2.2/frida-gadget-14.2.2-android-x86_64.so.xz $ xz -d frida-gadget-14.2.2-android-x86_64.so.xz # Android要求动态库必须是lib开头 $ mv frida-gadget-14.2.2-android-x86_64.so libfrida-gadget-14.2.2-android-x86_64.so $ cd ~/AndSrc/aosp/ $ cd out/target/product/generic_x86_64/system/lib $ wget https://github.com/frida/frida/releases/download/14.2.2/frida-gadget-14.2.2-android-x86.so.xz $ xz -d frida-gadget-14.2.2-android-x86.so.xz # Android要求动态库必须是lib开头 $ mv frida-gadget-14.2.2-android-x86.so libfrida-gadget-14.2.2-android-x86.so |
创建Frida Gadget库的配置文件
|
1 2 3 4 5 6 7 8 9 10 11 |
$ cd ~/AndSrc/aosp/ $ cd out/target/product/generic_x86_64/system/lib64 $ touch libfrida-gadget-14.2.2-android-x86_64.config.so $ cd ~/AndSrc/aosp/ $ cd out/target/product/generic_x86_64/system/lib $ touch libfrida-gadget-14.2.2-android-x86.config.so |
里面的配置内容如下:
|
1 2 3 4 5 6 7 8 |
{ "interaction": { "type": "listen", "address": "127.0.0.1", "port": 27042, "on_load": "resume" } } |
观察Frida源代码,发现在 frida-core/lib/gadget/gadget-glue.c中配置了lib库的入口函数
|
1 2 3 4 5 |
__attribute__ ((constructor)) static void frida_on_load (void) { frida_gadget_load (NULL, NULL, NULL); } |
这就意味着只要使用dlopen加载frida-gadget,我们就能实现对于指定应用的Hook。
我们只需要监听子进程,不需要在Zygote中加载,因此只需要在源代码 frameworks/base/core/jni/com_android_internal_os_Zygote.cpp 的com_android_internal_os_Zygote_nativeForkAndSpecialize函数中增加加载代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* * 在虚拟机,系统框架初始化完成之后,加载frida框架 * 需要增加头文件的引用 #include <dlfcn.h> */ #if defined(__x86_64__) || defined(__i386__) { #if defined(__x86_64__) #define FRIDA_LIB "/system/lib64/libfrida-gadget-14.2.2-android-x86_64.so" #else #define FRIDA_LIB "/system/lib/libfrida-gadget-14.2.2-android-x86.so" #endif const char *name = env->GetStringUTFChars(nice_name, 0); void* frida = dlopen(FRIDA_LIB, RTLD_NOW); if(NULL == frida) { ALOGE("(%s) load frida-gadget(%s) failed, err= %d\n", name, FRIDA_LIB, errno); } else { ALOGI("(%s) load frida-gadget(%s) success\n", name, FRIDA_LIB); } env->ReleaseStringUTFChars(nice_name, name); } #endif |
具体添加位置如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
static jint com_android_internal_os_Zygote_nativeForkAndSpecialize( JNIEnv* env, jclass, jint uid, jint gid, jintArray gids, jint runtime_flags, jobjectArray rlimits, jint mount_external, jstring se_info, jstring nice_name, jintArray managed_fds_to_close, jintArray managed_fds_to_ignore, jboolean is_child_zygote, jstring instruction_set, jstring app_data_dir, jboolean is_top_app, jobjectArray pkg_data_info_list, jobjectArray whitelisted_data_info_list, jboolean mount_data_dirs, jboolean mount_storage_dirs) { jlong capabilities = CalculateCapabilities(env, uid, gid, gids, is_child_zygote); if (UNLIKELY(managed_fds_to_close == nullptr)) { ZygoteFailure(env, "zygote", nice_name, "Zygote received a null fds_to_close vector."); } std::vector<int> fds_to_close = ExtractJIntArray(env, "zygote", nice_name, managed_fds_to_close).value(); std::vector<int> fds_to_ignore = ExtractJIntArray(env, "zygote", nice_name, managed_fds_to_ignore) .value_or(std::vector<int>()); std::vector<int> usap_pipes = MakeUsapPipeReadFDVector(); fds_to_close.insert(fds_to_close.end(), usap_pipes.begin(), usap_pipes.end()); fds_to_ignore.insert(fds_to_ignore.end(), usap_pipes.begin(), usap_pipes.end()); fds_to_close.push_back(gUsapPoolSocketFD); if (gUsapPoolEventFD != -1) { fds_to_close.push_back(gUsapPoolEventFD); fds_to_ignore.push_back(gUsapPoolEventFD); } if (gSystemServerSocketFd != -1) { fds_to_close.push_back(gSystemServerSocketFd); fds_to_ignore.push_back(gSystemServerSocketFd); } pid_t pid = ForkCommon(env, false, fds_to_close, fds_to_ignore, true); if (pid == 0) { SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities, mount_external, se_info, nice_name, false, is_child_zygote == JNI_TRUE, instruction_set, app_data_dir, is_top_app == JNI_TRUE, pkg_data_info_list, whitelisted_data_info_list, mount_data_dirs == JNI_TRUE, mount_storage_dirs == JNI_TRUE); /* * 在虚拟机,系统框架初始化完成之后,加载frida框架 * 需要增加头文件的引用 #include <dlfcn.h> */ #if defined(__x86_64__) || defined(__i386__) { #if defined(__x86_64__) #define FRIDA_LIB "/system/lib64/libfrida-gadget-14.2.2-android-x86_64.so" #else #define FRIDA_LIB "/system/lib/libfrida-gadget-14.2.2-android-x86.so" #endif const char *name = env->GetStringUTFChars(nice_name, 0); void* frida = dlopen(FRIDA_LIB, RTLD_NOW); if(NULL == frida) { ALOGE("(%s) load frida-gadget(%s) failed, err= %d\n", name, FRIDA_LIB, errno); } else { ALOGI("(%s) load frida-gadget(%s) success\n", name, FRIDA_LIB); } env->ReleaseStringUTFChars(nice_name, name); } #endif } return pid; } |
编译并重新生成系统镜像:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ cd ~/AndSrc/aosp/ $ export USE_CCACHE=1 $ source build/envsetup.sh $ lunch aosp_x86_64-eng $ cd frameworks/base $ mmm # 如果只是替换镜像里面的so文件,不修改源代码,则需要执行 make snod 才能重新生成镜像 # cd ~/AndSrc/aosp/ # make snod |
运行镜像
选择system-qemu.img和vendor-qemu.img,这两个镜像是专门为qemu运行制作的,如果选择system.img 和vendor.img,则avd运行失败。
|
1 2 3 4 5 6 7 8 9 10 11 |
$ cd ~/AndSrc/aosp $ export ANDROID_BUILD_TOP=~/AndSrc/aosp $ export PATH=$PATH:$ANDROID_BUILD_TOP/out/host/linux-x86/bin $ export ANDROID_SWT=$ANDROID_BUILD_TOP/out/host/linux-x86/framework $ export ANDROID_PRODUCT_OUT=$ANDROID_BUILD_TOP/out/target/product/generic_x86_64 $ ./prebuilts/android-emulator/linux-x86_64/emulator -system ./out/target/product/generic_x86_64/system-qemu.img -data ./out/target/product/generic_x86_64/data.img -ramdisk ./out/target/product/generic_x86_64/ramdisk-qemu.img -vendor ./out/target/product/generic_x86_64/vendor-qemu.img -verbose -show-kernel |
上面运行起来的镜像是从~/AndSrc/aosp/out/debug/target/product/generic/hardware-qemu.ini即可读取配置信息的,但是这个文件直接修改无效,我们如果需要修改参数,只能从启动参数中设置。
比如我们如果需要增大内存,开启GPU的支持,则执行如下命令:
|
1 |
$ ./prebuilts/android-emulator/linux-x86_64/emulator -system ./out/target/product/generic_x86_64/system-qemu.img -data ./out/target/product/generic_x86_64/data.img -ramdisk ./out/target/product/generic_x86_64/ramdisk-qemu.img -vendor ./out/target/product/generic_x86_64/vendor-qemu.img -gpu on -memory 4096 -verbose -show-kernel |
参考链接
- xposed源码编译与集成
- ubuntu 20.04编译Android 11源代码&模拟器
- Magisk与EdXposed框架安装实践(Android P及以上)
- YAHFA
- YAHFA--ART环境下的Hook框架
- YAHFA--ART环境下的Hook框架
- android N : UnsatisfiedLinkError 只能访问设置为公用库的so库
- linux下SO中INIT函数中添加自己的代码
- Framework基础:Android N 公共so库怎么定义呢?
- 在Android N上对Java方法做hook遇到的坑
- EdXposed & ART Hook 细节分享
- linkedin / dexmaker
- Android动态类生成预加载-dexmaker使用
- Android Frida 框架
- Frida源码分析
- hook工具frida原理及使用
- Global hook for customized AOSP
- Frida官方文档-Gadget
- Android ART VM的启动流程(五)
- Frida 安装和使用
- Frida releases
- Frida's Gadget Injection on Android: No Root, 2 Methods
- Android8.0 系统启动之孵化
- Android runtime机制(二)zygote进程的启动流程
- Gadget
- build python-macos on Apple M1 failed #1565
- Understanding Android: Zygote and DalvikVM
- 这恐怕是学习Frida最详细的笔记了(3)
How to mount Virtualbox drive image(vdi) on Ubuntu 16.10/20.04
This tutorial will explain How to mount Virtualbox drive image(.vdi) on Ubuntu 16.10.First we need to install QEMU tools using the following command
sudo apt-get install qemu-kvm
Now we need to load the network block device (nbd) module using the following command
sudo modprobe nbd
run qemu-nbd, which is a user space loopback block device server for QEMU-supported disk images.
qemu-nbd -c /dev/nbd0 [vdi-file]
Note:- vdi-file :- Your vdi file location for example /home/test/ubuntu.vdi
Example
qemu-nbd -c /dev/nbd0 /home/test/ubuntu.vdi
You can check the partitions in the image using the following command
sudo fdisk -l /dev/nbd0
The above command will check the entire image as a block device named /dev/nbd0, and the partitions within it as subdevices. For example, the first partition in the image will appear as /dev/nbd0p1.
If you want to mount the first partition use the following command
sudo mount /dev/nbd0p1 /mnt
Now you can check all the contents of /mnt partition and copy all the required files.
After completing your work you can unmount the partition using the following command
sudo umount /mnt
Shutdown nbd services using the following command
sudo qemu-nbd -d /dev/nbd0
参考链接
Flutter五种工程类型Application/Module/Plugin/Package/Skeleton
- Flutter Application: Flutter应用标准模版
- Flutter Module :Flutter与原生混合开发
- Flutter Plugin:Flutter插件
- Flutter Package:纯Dart组件
- Flutter Skeleton:Flutter应用建议实践例程模版
flutter开发:sqlflite的使用
引入插件
|
1 2 3 4 5 |
dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.0 sqflite: ^1.3.2+1 |
封装公共方法
这里的两个公共类是封装的比较好的,只要有sqlflite数据库操作就可以拿过来直接用的
- 数据库初始化,获取数据库对象以及关闭数据库。在sqflite中的数据库操作,首先得获取到数据库对象,通过数据库对象才可以对相应的表进行增删查改。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; class SqlManager { static const _VERSION = 1; static const _NAME = "my.db"; static Database _database; ///初始化 static init() async { var databasesPath = await getDatabasesPath(); String path = join(databasesPath, _NAME); _database = await openDatabase(path, version: _VERSION, onCreate: (Database db, int version) async {}); } ///判断表是否存在 static isTableExits(String tableName) async { await getCurrentDatabase(); var res = await _database.rawQuery( "select * from Sqlite_master where type = 'table' and name = '$tableName'"); return res != null && res.length > 0; } ///获取当前数据库对象 static Future<Database> getCurrentDatabase() async { if (_database == null) { await init(); } return _database; } ///关闭 static close() { _database?.close(); _database = null; } } |
- 在对某一张表进行操作时,先得判断数据库对象是否存在,不存在就得创建。再判断表是否在该数据库中存在,不存在就创建。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import 'package:meta/meta.dart'; import 'package:sqflite/sqflite.dart'; import 'SqlManager.dart'; abstract class BaseDbProvider { bool isTableExits = false; createTableString(); tableName(); ///创建表sql语句 tableBaseString(String sql) { return sql; } Future<Database> getDataBase() async { return await open(); } ///super 函数对父类进行初始化 @mustCallSuper prepare(name, String createSql) async { isTableExits = await SqlManager.isTableExits(name); if (!isTableExits) { Database db = await SqlManager.getCurrentDatabase(); return await db.execute(createSql); } } @mustCallSuper open() async { if (!isTableExits) { await prepare(tableName(), createTableString()); } return await SqlManager.getCurrentDatabase(); } } |
具体操作
- 新建bean类,这里除了基本的get和set方法之外还有User对象转map,map转User的两个方法。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
class User { User(); int _id; String _name; String _desc; int get id => _id; String get name => _name; String get desc => _desc; set desc(String value) { _desc = value; } set name(String value) { _name = value; } set id(int value) { _id = value; } Map<String, dynamic> toMap() { var map = Map<String, dynamic>(); map['id'] = _id; map['name'] = _name; map['desc'] = _desc; return map; } User.fromMapObject(Map<String, dynamic> map) { this._id = map['id']; this._name = map['name']; this._desc = map['desc']; } } |
- User表的增删查改方法
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
import 'package:sqflite/sqlite_api.dart'; import 'BaseDbProvider.dart'; import 'User.dart'; class UserDbProvider extends BaseDbProvider { ///表名 final String name = 'UserInfo'; final String columnId = "id"; final String columnName = "name"; final String columnDesc = "desc"; UserDbProvider(); //获取表名称 @override tableName() { return name; } //创建表操作 @override createTableString() { return ''' create table $name ( $columnId integer primary key,$columnName text not null, $columnDesc text not null) '''; } ///查询数据 Future selectUser(int id) async { Database db = await getDataBase(); return await db.rawQuery("select * from $name where $columnId = $id"); } //查询数据库所有 Future<List<Map<String, dynamic>>> selectMapList() async { var db = await getDataBase(); var result = await db.query(name); return result; } //获取数据库里所有user Future<List<User>> getAllUser() async { var userMapList = await selectMapList(); var count = userMapList.length; List<User> userList = List<User>(); for (int i = 0; i < count; i++) { userList.add(User.fromMapObject(userMapList[i])); } return userList; } //根据id查询user Future<User> getUser(int id) async { var noteMapList = await selectUser(id); // Get 'Map List' from database var user = User.fromMapObject(noteMapList[id]); return user; } //增加数据 Future<int> insertUser(User user) async { var db = await getDataBase(); var result = await db.insert(name, user.toMap()); return result; } //更新数据 Future<int> update(User user) async { var database = await getDataBase(); var result = await database.rawUpdate( "update $name set $columnName = ?,$columnDesc = ? where $columnId= ?", [user.name, user.desc, user.id]); return result; } //删除数据 Future<int> deleteUser(int id) async { var db = await getDataBase(); var result = await db.rawDelete('DELETE FROM $name WHERE $columnId = $id'); return result; } } |
外部使用
- 创建Provider对象。
|
1 |
UserDbProvider provider = UserDbProvider(); |
2.通过对象调用User的增删查改方法
|
1 2 3 4 5 6 |
if (user.id == null) { user.id = new DateTime.now().millisecondsSinceEpoch; //id 为当前时间戳 result = await provider.insertUser(user); } else { result = await provider.update(user); } |
完整demo
参考链接
PlantUML高效绘制流程图
背景
最近需要绘制较多的流程图,结果发现原始的绘图方式不便于维护调整,后来发现PlantUML可以代码的方式实现,感觉不错。目前比较方便的工具是IntelliJ IDEA提供的PlantUML插件了。其他的比如vscode提供的插件,目前测试不能正常绘制流程图。
IntelliJ IDEA新建一个.md扩展名的文件,就可以直接预览并且导出PlantUML绘制的流程图了。
活动图(新语法)
当前活动图(activity diagram)的语法有诸多限制和缺点,比如代码难以维护。
所以从V7947开始提出一种全新的、更好的语法格式和软件实现供用户使用(beta版)。
就像序列图一样,新的软件实现的另一个优点是它不再依赖与Graphviz。
新的语法将会替换旧的语法。然而考虑到兼容性,旧的语法仍被能够使用以确保向前兼容。
但是我们鼓励用户使用新的语法格式。
简单活动图
活动标签(activity label)以冒号开始,以分号结束。
文本格式支持creole wiki语法。
活动默认安装它们定义的顺序就行连接。
|
1 2 3 4 5 |
@startuml :Hello world; :This is on defined on several **lines**; @enduml |
Clang的线程安全分析模块Thread Safety Analysis
背景
最近在浏览 Android 11 源代码的时候,发现在ART虚拟机头文件 art_method.h 中,存在大量的类似REQUIRES_SHARED(Locks::mutator_lock_); 的代码。
Brotli压缩
Brotli是一种全新的数据格式,可以提供比Zopfli高20-26%的压缩比。据谷歌研究,Brotli压缩速度同zlib的Deflate实现大致相同,而在Canterbury语料库上的压缩密度比LZMA和bzip2略大。
链接:Google开源Brotli压缩算法 。
微软使用了一种基于谷歌提供的C代码的实现,向.NET Core 2.1添加了Brotli压缩支持。由于Brotli得到了许多Web浏览器和Web服务器的广泛支持,所以.NET Core提供对这项技术的支持是非常有用的。
什么是 Brotli 压缩算法
Brotli最初发布于2015年,用于网络字体的离线压缩。Google软件工程师在2015年9月发布了包含通用无损数据压缩的Brotli增强版本,特别侧重于HTTP压缩。其中的编码器被部分改写以提高压缩比,编码器和解码器都提高了速度,流式API已被改进,增加更多压缩质量级别。新版本还展现了跨平台的性能改进,以及减少解码所需的内存。
与常见的通用压缩算法不同,Brotli使用一个预定义的120千字节字典。该字典包含超过13000个常用单词、短语和其他子字符串,这些来自一个文本和HTML文档的大型语料库。预定义的算法可以提升较小文件的压缩密度。
使用brotli替换deflate来对文本文件压缩通常可以增加20%的压缩密度,而压缩与解压缩速度则大致不变。使用Brotli进行流压缩的内容编码类型已被提议使用“br”。
Brotli 的实际压缩效果,需要根据需要来具体分析,目前并不是每个网站都有较好的加速效果,另外,目前仅仅被限制在HTTPS上,HTTP是不支持的。
在 ubuntu 20.04 系统上自带的 apache 2.4.41 上启用 brotli 压缩算法的方式如下:
|
1 2 3 |
$ sudo apt-get install brotli $ sudo a2enmod brotli |
在 Apache 2 的配置文件增加 brolti 的配置信息,如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<VirtualHost 1.2.3.4:443> DocumentRoot /var/www/ ServerName example.com SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem Include /etc/letsencrypt/options-ssl-apache.conf CustomLog /var/log/apache2/example.com.access.log common ErrorLog /var/log/apache2/example.com.error.log <IfModule mod_brotli.c> SetOutputFilter BROTLI_COMPRESS;DEFLATE BrotliCompressionQuality 6 BrotliCompressionWindow 18 AddOutputFilterByType BROTLI_COMPRESS text/html AddOutputFilterByType BROTLI_COMPRESS text/plain AddOutputFilterByType BROTLI_COMPRESS text/xml AddOutputFilterByType BROTLI_COMPRESS text/css AddOutputFilterByType BROTLI_COMPRESS text/javascript AddOutputFilterByType BROTLI_COMPRESS application/x-javascript AddOutputFilterByType BROTLI_COMPRESS application/javascript AddOutputFilterByType BROTLI_COMPRESS application/json AddOutputFilterByType BROTLI_COMPRESS application/x-font-ttf AddOutputFilterByType BROTLI_COMPRESS application/vnd.ms-fontobject AddOutputFilterByType BROTLI_COMPRESS image/x-icon </IfModule> </VirtualHost> |
重启服务,如下:
|
1 |
$ sudo systemctl restart apache2.service |
测试结果
|
1 2 3 4 5 6 7 8 9 10 11 12 |
$ curl -I -H 'Accept-Encoding: br' https://www.mobibrw.com HTTP/1.1 200 OK Date: Wed, 10 Feb 2021 01:17:49 GMT Server: Apache Vary: Accept-Encoding,Cookie Cache-Control: max-age=3, must-revalidate Upgrade: h2 Connection: Upgrade Last-Modified: Wed, 10 Feb 2021 01:16:24 GMT Content-Encoding: br Content-Length: 1 Content-Type: text/html; charset=UTF-8 |