八、混淆和加密

记录

Android 中的Log类可用于在 logcat 中创建日志消息(可通过adb logcat命令访问);这些日志有几种不同的级别,它们是

  • log . wtf--“多么可怕的失败”(被视为极端错误)

  • Log.e -错误

  • Log.w -警告

  • Log.i -信息

  • Log.d -调试

  • Log.v -详细

如上所述,这些日志消息可以通过 logcat 读取。Logcat 是 Android 的日志系统,记录从系统消息到堆栈跟踪的所有内容。应用可以通过使用Log类写入 logcat,反过来,这些消息可以通过使用adb logcat命令或在 Android Studio 等程序中查看。

无论您选择哪种级别,所有日志级别都将显示在 logcat 中。例如,以下日志程序代码在指定 debug 的同时,将被记录到 logcat 中,而不考虑构建类型(即,它将被记录在发布构建中)。同样值得记住的是,调试日志消息将被编译到发布应用中。例如,下面我们可以看到 Java 中的日志消息和 Smali(dal vik 字节码的可读表示)中的反汇编发布版本之间的比较。

在 Java 中 :

Log.d(TAG, "I am a normal debug log message");

In Smali:

iget-object p1, p0, Lcom/example/logger/MainActivity;->TAG:Ljava/lang/String;

const-string v0, "A log using is loggable"

invoke-static {p1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

标准测井

开发人员不想使用标准日志记录有几个原因。这些归结为安全性和性能,其中日志应该对恶意参与者隐藏,并且应该在最终用户的设备上避免日志泛滥。

标准日志:

Log.d(TAG, "I am a normal debug log Message");

最终常量变量

限制发布代码中日志语句数量的一种支持方式是使用 Gradle prebuild 生成的 Gradle BuildConfig文件。生成该文件时,如果构建调试版本,将下面一行设置为true,如果构建发布版本,将下面一行设置为false

build config 文件中的调试值:

public static final boolean DEBUG = Boolean.parseBoolean("true");

实现调试常量:

if (BuildConfig.DEBUG){
    Log.d(TAG,"This is a log that won't be compiled in a release build.");
}

当为了发布而构建并设置为 false 时,Java 编译器会发现最终变量不可能为 true,因此不会编译 if 语句中的代码。这意味着日志不会显示在 logcat 中,也意味着日志字符串不会像普通的日志消息一样存在于应用的源代码中。

如果不使用 Gradle,也可以实现与使用BuildConfig.DEBUG类似的效果。这可以通过使用一个最终布尔值来完成,在调试时将其设置为true,在发布版本中设置为false

设置一个 自定义调试常数 :

final boolean SHOULD_LOG = false;
if (SHOULD_LOG){
   Log.d(TAG," A log that should never happen...");
}

使用。可记录

检查是否应该显示日志消息的另一种方法是使用内置在Log类中的.isLoggable方法。该方法检查为特定标记设置的日志级别(应用的默认设置是INFO)。日志级别在一个层次结构中工作,如本节顶部所列。这意味着如果日志级别设置为 Verbose,那么它上面的所有级别也将是true。与使用BuildConfig,不同的是,这个值可以通过编程来改变,这个字符串将被编译到应用的代码库中。

log . is logtable 示例:

if (Log.isLoggable(TAG,Log.DEBUG)){
    Log.d(TAG,"A log using is loggable");
}

这个日志级别可以通过 shell 使用来设置

setprop log.tag.<log_tag> <log_level>

动态检查是否可调试

这里讨论的限制写入 logcat 的日志数量的最后一种技术是通过动态检查应用是否处于调试状态。如上所述,由于该值可以改变,日志和字符串将被编译到构建的发布应用中。

动态检查应用是否处于调试状态的示例:

boolean isDebuggable =  ( 0 != ( getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE ) );
if (isDebuggable){
    Log.d(TAG,"This log will check programmatically if the app is debuggable.");
}

阿帕尔德

最常见的 Android 混淆工具是 ProGuard,DexGuard 1 是高级替代工具。ProGuard 分析和优化的是 Java 字节码,而不是直接的 Java/Kotlin 代码库。ProGuard 实现了一组技术 2 ,它们是:

  • 收缩 -识别并删除不可达或未使用的死代码。包括类、字段、方法和属性。

  • 优化器——对代码和代码流进行优化,以改变性能。

  • Obfuscator——将代码库的某些方面(例如,类、字段和方法)重命名为故意模糊和无意义的名称。

  • 预验证器——对字节码执行预验证检查,如果检查成功,类文件会用预验证信息进行注释。这是 Java Micro Edition 和 Java 6 及更高版本所必需的。

启用 ProGuard

gradle.build文件中的buildTypes标签编辑为minifyEnabled true

例如:

buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}

ProGuard 映射文件

在使用 Gradle 进行发布构建时,遵循上述步骤之后,Java 字节码将已经被 ProGuard 分析过了。ProGuard 将提供所经历阶段的日志文件。它保存在应用项目根目录的以下相对路径中。

映射文件相对位置:

app/build/outputs/mapping/release/mapping.txt

该文件显示了 ProGuard 已经实现的更改。下面是该文件一部分的示例。在这个例子中可以看到,MainActivity中的函数showString已经被允许列出,而 MainActivity 中另一个名为 loadMe 的函数还没有被允许列出,现在被重命名为n

映射文件示例:

com.example.java_dexloadable.MainActivity -> com.example.java_dexloadable.MainActivity:
    java.lang.String loadMe() -> n
    1:1:java.lang.String com.example.java_dexloadable.StringsClass.stringGetter(int):0:0 -> showString
    1:1:void showString(android.content.Context,int):0 -> showString
    2:2:void showString(android.content.Context,int):0:0 -> showString

如果一个函数或类不在这个映射中,那么它已经被缩小了(意味着它没有在代码中被使用),或者它没有被混淆(由于被允许列出)。

ProGuard 允许列表

默认情况下,ProGuard 会收缩、优化和混淆 Java 字节码中的所有内容。这可以通过编辑位于应用目录根目录下app\proguard-rules.pro的 ProGuard 文件来控制。该文件可以重命名和移动,并在gradle.build文件中指定。

下面的示例规则允许-列出类 com.example.java_dexloadable.MainActivity 中的 showString 函数。这里你需要指定类和函数的访问级别(公共的,私有的,包私有的,等等)。)以及函数的参数:

-keep class com.example.java_dexloadable.MainActivity {
   public showString(android.content.Context, int);
}

下面的例子也是一样;然而,在这个例子中, MainActivity 中的所有函数都被允许列出:

-keep class com.example.java_dexloadable.MainActivity {
   public *;
}

不同类型的保留

在前面的两个示例中,使用了 keep 关键字。有几种不同类型的 3 关键字。这些总结在表 8-1 中。

表 8-1

保留的程序类型

|   |

没有规则

|

-保持

|

-保留类成员

|

-保留姓名

| | --- | --- | --- | --- | --- | | 缩班 | -好的 | x | -好的 | -好的 | | 收缩成员 | -好的 | x | x | -好的 | | 混淆类 | -好的 | x | -好的 | x | | 混淆成员 | -好的 | x | x | x |

入口点

ProGuard 自动将允许列表(也称为白名单)入口点指向一个应用(例如,MAINLAUNCHER类别的活动)。重要的是要记住,作为反射的一部分使用的入口点不会自动允许列出,所以如果使用 refection,这些入口点必须手动允许列出。然而,这将最小化混淆的有效性,因为纯文本组件的频率会更高。由 ProGuard 自动添加到允许列表的入口点通常包括具有 main 方法、applets、MIDlets、activities 等的类。这也包括调用本机 C 代码的类。

示例规则

在下面的例子中,Java 包名是java_dexloadable,,所有的规则都被添加到了Proguard-rules.pro文件中。

保留(允许列表)MainActivity 类中的所有方法:

-keep class com.example.java_dexloadable.MainActivity {
   public *;
}

保留(允许列出)MainActivity 类中的 showString 函数以及 MainActivity 类本身:

-keep class com.example.java_dexloadable.MainActivity {
   public showString(android.content.Context, int);
}

保留(允许列表)顶层包下的所有内容(不应使用):

-keep class com.example.java_dexloadable.** { *; }

保留(允许列出)函数字符串,但不保留类字符串 class 本身:

-keepclassmembers class com.example.java_dexloadable.StringsClass {
   public stringGetter(int);
}

将整个包重新打包成一个根:

-repackageclasses

不执行程序收缩步骤:

--dontshrink

公钥/证书锁定

公钥锁定允许应用将特定的加密公钥与给定的 web 服务器相关联。这反过来用于降低中间人攻击的可能性。

执行公钥锁定时,需要所连接的 web 服务器的公钥。

有两种相当简单的方法可以做到这一点——要么使用下面的 openssl 命令,要么使用下面的代码并从错误消息中提取公钥:

openssl x509 -in cert.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

在下面的代码中,使用前面消息中的公钥散列作为您的散列。在 Android 中,联网不能在主线程上执行,因此它需要在长期运行的服务、异步任务或线程中执行(详见第 9 章)。

将以下依赖项添加到 build.gradle 文件中,因为这个示例使用 OkHTTP 库。还要确保应用具有互联网权限,并且没有在主线程上进行联网:

implementation("com.squareup.okhttp3:okhttp:4.9.0")

证书锁定示例:

String hostname =  "google.com";

CertificatePinner certPinner = new CertificatePinner.Builder()
        .add(
                hostname,
                "sha256/MeCugOOsbHh2GNsYG8FO7wO7E4rjtmR7o0LM4iXHJlM="
        )
        .build();

OkHttpClient okHttpClient = new OkHttpClient.Builder()
        .certificatePinner(certPinner)
        .build();

HttpUrl.Builder urlBuilder = HttpUrl.parse("https://"+hostname).newBuilder();
String url = urlBuilder.build().toString();

MediaType JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody body = RequestBody.create(JSON, "{\"test\":\"testvalue\"}");

Request request = new Request.Builder()
        .url(url)
        .post(body)
        .build();

Log.v(TAG,request.toString());

Response response = null;
try {
    response = okHttpClient.newCall(request).execute();
    ResponseBody jsonData = response.body();
    Log.v(TAG, jsonData.toString());

} catch (IOException e) {
    e.printStackTrace();
}

return Result.success();

AES 加密

AES 使用对称算法,这意味着加密和解密使用相同的密钥。下面是一个用 Java 实现 AES-256 加密的轻量级例子。

下面是 Java 中 AES-256 加密方法的一个例子:

try {
    Cipher cipher = null;
    cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");

    KeyGenerator keygen = null;
    keygen = KeyGenerator.getInstance("AES");

    keygen.init(256);
    SecretKey key = keygen.generateKey();

    String plainTextString = "I am a plain text";
    String cipherTextAsString = "N/A";
    String newPlainTextAsString = "N/A";
    byte[] plainText = plainTextString.getBytes();

    cipher.init(Cipher.ENCRYPT_MODE, key);

    byte[] cipherText = new byte[0];

    cipherText = cipher.doFinal(plainText);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        cipherTextAsString = new String(cipherText, StandardCharsets.UTF_8);
    }

    IvParameterSpec iv = new IvParameterSpec(cipher.getIV());
    cipher.init(Cipher.DECRYPT_MODE, key, iv);

    byte[] newPlainText = cipher.doFinal(cipherText);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        newPlainTextAsString = new String(newPlainText, StandardCharsets.UTF_8);
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        Log.v(getApplicationContext().getPackageName(), "The plaintext '" + plainTextString + "' encrypted is " + Base64.getEncoder().encodeToString(cipherText) + " and decrypted is '" + newPlainTextAsString);
    }

}catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
} catch (InvalidKeyException e) {
    e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
    e.printStackTrace();
} catch (NoSuchPaddingException e) {
    e.printStackTrace();
} catch (BadPaddingException e) {
    e.printStackTrace();
} catch (IllegalBlockSizeException e) {
    e.printStackTrace();
}
Footnotes [1](#Fn1_source) " dex guard vs . ProGuard | guard square . "2017 年 4 月 13 日, [`https://www.guardsquare.com/en/blog/dexguard-vs-proguard`](https://www.guardsquare.com/en/blog/dexguard-vs-proguard) 。于 5 月 12 日访问。2020.   [2](#Fn2_source) "保护手册|简介|保护广场." [`https://www.guardsquare.com/en/products/proguard/manual/introduction`](https://www.guardsquare.com/en/products/proguard/manual/introduction) 。5 月 11 日访问。2020.   [3](#Fn3_source) “区分不同的程序...-jebware . com " 2017 年 11 月 14 日, [`https://jebware.com/blog/?p=418`](https://jebware.com/blog/%253Fp%253D418) 。5 月 11 日访问。2020.