Android JNI接口混淆

JNI混淆的问题

首先演示混淆存在的问题, 使用Android Studio新建一个模板为Native C++ 的app项目, 名字为JNIDemo

修改app/build.gradle, 使用官方默认的minifyEnabled

java 复制代码
buildTypes {
  release {
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
    proguardFiles 'proguard-rules.pro'
  }
}

然后运行, 使用jadx-gui看看反编译的结果, native方法并为被混淆

arduino 复制代码
jadx-gui ./app/build/outputs/apk/release/app-release-unsigned.apk

再来看看动态库的情况, 方法名也清晰可见

r 复制代码
# 使用find . -name "*.so"搜索动态库的输出目录
# nm -D 是输出动态库的动态符号, 也可使用readel或objdump
nm -D ./app/build/xxxx/libjnidemo.so
0000000000015200 T Java_com_test_jnidemo_MainActivity_stringFromJNI
000000000002ca10 T _ZNKSt10bad_typeid4whatEv
000000000002c8e8 T _ZNKSt13bad_exception4whatEv
000000000002c948 T _ZNKSt20bad_array_new_length4whatEv
000000000002c9b4 T _ZNKSt8bad_cast4whatEv
000000000002c918 T _ZNKSt9bad_alloc4whatEv

是不是一目了然, MainActivity.java和动态库的反编译接口都在裸奔

接下来, 把app/build.gradle稍微修改一下, 注释系统规则

arduino 复制代码
buildTypes {
  release {
    minifyEnabled true
    // proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
    proguardFiles 'proguard-rules.pro'
  }
}

反编译看一下, java层的native方法被混淆

但是app这个时候跑不起来了, 有两个原因

  • 系统自带的库被混淆, 导致运行出错, 解决方式是在Android SDK路径下找到proguard-android-optimize.txt , 然后将内容拷贝到proguard-rules.pro, 并且注释掉native的规则. 但是这种方法不推荐, 注释掉native的规则也会使其它native依赖库出现问题, 所以最好是将native库从app分离出来, 单独创建个native library, 并且使用独立的混淆规则
javascript 复制代码
# proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
find ~/Library/Android/sdk -name "proguard-android-optimize.txt"
​
# ~/Library/Android/sdk/tools/proguard/proguard-android-optimize.txt
-keepclasseswithmembernames class * {
    native <methods>;
}
  • 查看动态库的符号, 符号并没有改变, 还是stringFromJNI , 所以导致虚拟机找不到对应的Java_com_test_jnidemo_MainActivity_o的实现
r 复制代码
nm -D ./app/build/xxxx/libjnidemo.so
0000000000015200 T Java_com_test_jnidemo_MainActivity_stringFromJNI
000000000002ca10 T _ZNKSt10bad_typeid4whatEv
000000000002c8e8 T _ZNKSt13bad_exception4whatEv
000000000002c948 T _ZNKSt20bad_array_new_length4whatEv
000000000002c9b4 T _ZNKSt8bad_cast4whatEv

所以使用系统自带的混淆行不通, 第一个问题可以创建SDK解决, 但第二不行, 反编译动态库, 接口还是暴露的, 接下来我们自己实现插件来混淆

实现一个简单的插件

首先使用kotlin创建一个简单的插件框架, groovy的提示确实不太友好

使用shell手动创建需要的工程文件

bash 复制代码
# 进入Project目录
mkdir -p plugins/messplugin/src/main/kotlin/com/test/plugin
touch plugins/settings.gradle.kts
touch plugins/messplugin/build.gradle.kts
# 实现混淆的主体
touch plugins/messplugin/src/main/kotlin/com/test/plugin/MessPlugin.kt
# 用户的配置
touch plugins/messplugin/src/main/kotlin/com/test/plugin/MessExtension.kt

plugins是插件的工程目录, messplugin就是需要实现的混淆插件

在JNIDemo工程的settings.gradle中添加includeBuild("./plugins") , 后sync一下

php 复制代码
// ...
rootProject.name = "JNIDemo"
include ':app'
includeBuild("./plugins")

这里也可以使用buildSrc的方式, 或是include(":plugins:messplugin")导入, 但是这两种方法会导致, 每次只要修改插件的代码, 都会使得所有代码完全编译一遍, 时间很慢, 小项目还好, 大项目是很蛋疼的. 所以这里采用了gradle的复合编译, 当然速度快只是其中一个优点, 官方说明文档Demo

plugins/settings.gradle.kts

ini 复制代码
rootProject.name = "plugins"
include(":messplugin")

plugins/messplugin/build.gradle.kts

scss 复制代码
buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20")
        classpath("com.android.tools.build:gradle:8.2.0")
    }
}
​
plugins {
    `kotlin-dsl`
    `java-gradle-plugin`
}
​
repositories {
    google()
    mavenCentral()
}
​
dependencies {
    implementation("com.android.tools.build:gradle:8.2.0")
    // 解析mapping.txt需要的库
    implementation("com.guardsquare:proguard-gradle:7.4.1")
}
​
sourceSets {
    main {
        kotlin {
            srcDirs("src/main/kotlin")  //插件的源码目录
        }
    }
}
​
// 注册插件, 使得其它工程可以导入
gradlePlugin {
    plugins {
        create("messplugin") {
            id = "messplugin"
            implementationClass = "com.dc.plugin.MessPlugin"
        }
    }
}

plugins/messplugin/src/main/kotlin/com/test/plugin/MessExtension.kt

javascript 复制代码
package com.test.plugin
​
class MessExtension {
    var classAndNative: Map<String, String> ? = null
}

plugins/messplugin/src/main/kotlin/com/test/plugin/MessPlugin.kt

kotlin 复制代码
package com.test.plugin
​
import org.gradle.api.Plugin
import org.gradle.api.Project
​
class MessPlugin: Plugin<Project> {
    override fun apply(project: Project) {
        println("enter mess plugin")
​
        // 创建用户配置
        val messExtension = project.extensions
            .create("messConfig", MessExtension::class.java)
    }
}

好了, 插件的结构完成, 接下来看看如何使用

app/build.gradle

arduino 复制代码
plugins {
    id 'com.android.application'
    id "messplugin" //添加这一行
}
messConfig {
    // 配置native注册类和实现的c文件
    classAndNative = ["com.test.jnidemo.MainActivity": "src/main/cpp/native-lib.cpp"]
}
...

使用Android Studio sync一下, 在Build窗口能看到下面的输出

ruby 复制代码
> Task :plugins:messplugin:pluginDescriptors UP-TO-DATE
> Task :plugins:messplugin:processResources UP-TO-DATE
> Task :plugins:messplugin:compileKotlin
> Task :plugins:messplugin:compileJava NO-SOURCE
> Task :plugins:messplugin:classes UP-TO-DATE
> Task :plugins:messplugin:jar
> Task :plugins:messplugin:inspectClassesForKotlinIC
​
> Configure project :app
enter mess plugin

简单的插件就完成了~~~

实现插件混淆

plugins/messplugin/src/main/kotlin/com/test/plugin/MessPlugin.kt

代码只说明流程, 有些异常和判断需要另外处理

kotlin 复制代码
package com.test.plugin
​
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.configurationcache.extensions.capitalized
import proguard.obfuscate.MappingProcessor
import proguard.obfuscate.MappingReader
import java.io.File
​
class MessPlugin : Plugin<Project> {
​
    class Config(
        val className: String, // 被混淆的类
        val nativePath: String // 对应的c文件路径
    ) 
    {
        // className被混淆的新类
        var newClassName: String? = null
​
        // 存储了原方法与混淆方法的对应关系
        var methods: MutableMap<String, String> = mutableMapOf()
​
        // 源码的备份路径, 比如 native-lib.cpp~
        var backupPath: String? = null
        override fun toString() = "$className, $nativePath, $newClassName, $methods"
    }
​
    // 存储了所有混淆类的配置
    // [com.test.jnidemo.MainActivity, /xxx/src/main/cpp/native-lib.cpp,
    // com.test.jnidemo.MainActivity, {stringFromJNI=o}]
    private val configs = mutableListOf<Config>()
​
    override fun apply(project: Project) {
        println("enter mess plugin")
​
        // 创建用户配置
        val messExtension = project.extensions
            .create("messConfig", MessExtension::class.java)
​
        project.afterEvaluate {
            // 将app/build.gradle messConfig配置存储起来
            messExtension.classAndNative!!
                .forEach { (className, nativePath) ->
                    configs.add(
                        Config(className, "${projectDir}/${nativePath}")
                    )
                }
​
            // 获取当前的构建信息
            val releaseVariant = extensions
                .getByType(AppExtension::class.java)
                .applicationVariants.firstOrNull {
                    it.buildType.name.capitalized() == "Release"
                }!!
            // 开启了minifyEnabled后, 会生成mapping.txt
            val mappingFile = releaseVariant.mappingFile
            // 这是编译c代码的task, 不同的gradle版本, 可能不一样, debug模式也不一样
            val nativeBuildTask = tasks
                .findByName("buildCMakeRelWithDebInfo[arm64-v8a]")!!
            // 这是系统混淆的task
            val proguardTask = tasks
                .findByName("minifyReleaseWithR8")!!
​
            // 使native编译在java类混淆之后运行, 应该需要解析mapping后替换
            nativeBuildTask.dependsOn(proguardTask)
​
            nativeBuildTask.doFirst {
                // 编译前解析mapping文件, 和替换c源码
                parseMapping(mappingFile)
                replaceNativeSource()
            }
            nativeBuildTask.doLast {
                // 编译完c文件后, 恢复替换的代码
                restoreNativeSource()
            }
        }
    }
​
    // 解析mapping文件
    private fun parseMapping(mappingFile: File) {
        MappingReader(mappingFile).pump(
            object : MappingProcessor {
                override fun processClassMapping(
                    className: String,
                    newClassName: String
                ): Boolean {
                    // 如果发现配置的类, 则返回true
                    // 如果返回false, processMethodMapping就不会运行
                    return configs.firstOrNull {
                        it.className == className
                    }?.let {
                        it.newClassName = newClassName
                    } != null
                }
​
                override fun processFieldMapping(
                    className: String,
                    fieldType: String,
                    fieldName: String,
                    newClassName: String,
                    newFieldName: String
                ) {
                }
​
                override fun processMethodMapping(
                    className: String,
                    firstLineNumber: Int,
                    lastLineNumber: Int,
                    methodReturnType: String,
                    methodName: String,
                    methodArguments: String,
                    newClassName: String,
                    newFirstLineNumber: Int,
                    newLastLineNumber: Int,
                    newMethodName: String
                ) {
                    // 如果混淆前和混淆后一样, 跳过, 比如构造方法
                    if (methodName == newMethodName) return
                    // 记录类的混淆方法对应关系
                    configs.firstOrNull {
                        it.className == className
                    }?.apply {
                        methods[methodName] = newMethodName
                    }
                }
            })
        println("configs: $configs")
    }
​
    // 在编译c文件前备份和替换
    private fun replaceNativeSource() {
        configs.forEach {
            val nativeFile = File(it.nativePath).apply {
                // 备份文件添加~
                // native-lib.cpp -> native-lib.cpp~
                it.backupPath = "${absolutePath}~"
                copyTo(File(it.backupPath!!), true)
            }
​
            var source = nativeFile.readText()
            if (it.newClassName != null) {
                // 动态注册的类是"com/test/tokenlib/NativeLib"
                // 这里是放类换成混淆后的字符串
                val realClassName = it.className
                    .replace(".", "/")
                val realNewClassName = it.newClassName!!
                    .replace(".", "/")
                source = source.replace(
                    ""$realClassName"",
                    ""$realNewClassName""
                )
            }
​
            it.methods.forEach { (oldMethod, newMethod) ->
                // 这个是替换混淆方法
                source = source.replace(
                    ""$oldMethod"",
                    ""$newMethod""
                )
            }
            nativeFile.writeText(source)
        }
    }
​
    // 编译完成后恢复原来的c文件
    private fun restoreNativeSource() {
        configs.filter {
            it.backupPath != null
        }.forEach {
            File(it.backupPath!!).apply {
                // 恢复并删除备份文件
                copyTo(File(it.nativePath), true)
                delete()
            }
        }
    }
}

说明下流程

  • 首先获取用户的配置messConfig, 并存储到configs
  • 获取native编译和混淆的task, 并且使native编译在混淆之后运行
  • 在native编译之前, 通过mapping.txt解析混淆的类和方法, 并替换native代码
  • native编译之后还原代码

src/main/cpp/native-lib.cpp

c 复制代码
#include <jni.h>
#include <string>
​
extern "C" 
// JNIEXPORT
jstring
// JNICALL
stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
​
const JNINativeMethod gMethods[] = {
        {"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI}
};
​
const char *gClassName = "com/test/jnidemo/MainActivity";
​
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    if ((vm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK)) return -1;
    jclass nativeClass = env->FindClass(gClassName);
    if (nativeClass == NULL) return -1;
    jint count = sizeof(gMethods)/ sizeof(gMethods[0]);
    if ((env->RegisterNatives(nativeClass, gMethods, count) < 0)) return -1;
    return JNI_VERSION_1_6;
}

这里使用了动态注册, 在插件进行了动态替换后, "stringFromJNI"变成了"o"

ini 复制代码
const JNINativeMethod gMethods[] = {
        {"o", "()Ljava/lang/String;", (void *) stringFromJNI}
};
const char *gClassName = "com/test/jnidemo/MainActivity";
// const char *gClassName = "a/c";

com/test/jnidemo/MainActivity 也会被换成类似a/c, 但MainActivity被其它资源文件引用, 所以就没有混淆, 如果把MainActivity类换成NativeLib就会被混淆

stringFromJNI方法注释了两个修饰符

arduino 复制代码
// 让符号保留, 这个肯定要去掉
#define JNIEXPORT  __attribute__ ((visibility ("default")))
// 没有定义内容
#define JNICALL

关于这个

arduino 复制代码
{"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI}

" ()Ljava/lang/String; "是方法的签名, 如果不知道在怎么写, 可以通过命令获取

arduino 复制代码
find . -name "MainActivity.class"
# ./app/build/intermediates/javac/release/classes/com/test/jnidemo/MainActivity.class
javap -s -p ./app/buid/xxxx/MainActivity.class
#  就能拿到所有的方法签名
#  public native java.lang.String stringFromJNI();
#    descriptor: ()Ljava/lang/String;

src/main/cpp/CMakeLists.txt

scss 复制代码
cmake_minimum_required(VERSION 3.22.1)
project("jnidemo")
add_compile_options(-fvisibility=hidden) # 添加隐藏符号配置
add_library(${CMAKE_PROJECT_NAME} SHARED
        native-lib.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME})

结果

通过Android Studio运行app的assembleRelease看看结果

arduino 复制代码
jadx-gui ./app/build/outputs/apk/release/app-release-unsigned.apk
r 复制代码
nm -D ./app/build/intermediates/merged_native_libs/release/out/lib/arm64-v8a/libjnidemo.so
000000000001532c T JNI_OnLoad
000000000002cb44 T _ZNKSt10bad_typeid4whatEv
000000000002ca1c T _ZNKSt13bad_exception4whatEv
000000000002ca7c T _ZNKSt20bad_array_new_length4whatEv
000000000002cae8 T _ZNKSt8bad_cast4whatEv
000000000002ca4c T _ZNKSt9bad_alloc4whatEv

Java_com_test_jnidemo_MainActivity_stringFromJNI也被隐藏, native-lib改为c后, 可以看源码, 反编译后

真实的方法全是sub_xxx

大概就是这么多了

缺点是编译时涉及到源码的修改与还原, 大佬们有更好方案希望提供一下, 谢谢

调试源码: Github 源码

注: 方法并非原创, 只是原作者代码时代比较久远, 就总结了一下使用的流程

Android JNI接口混淆方案\] [github.com/qs00019/Mes...](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fqs00019%2FMessNative "https://github.com/qs00019/MessNative") \[​混淆的另一重境界\] [www.jianshu.com/p/799e5bc62...](https://link.juejin.cn?target=https%3A%2F%2Fwww.jianshu.com%2Fp%2F799e5bc62633 "https://www.jianshu.com/p/799e5bc62633")

相关推荐
安卓理事人1 小时前
安卓LinkedBlockingQueue消息队列
android
万能的小裴同学2 小时前
Android M3U8视频播放器
android·音视频
q***57743 小时前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober3 小时前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿4 小时前
关于ObjectAnimator
android
zhangphil5 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我6 小时前
从头写一个自己的app
android·前端·flutter
lichong9517 小时前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端
用户69371750013847 小时前
14.Kotlin 类:类的形态(一):抽象类 (Abstract Class)
android·后端·kotlin
火柴就是我7 小时前
NekoBoxForAndroid 编译libcore.aar
android