Android JNI接口混淆
作者:访客发布时间:2023-12-25分类:程序开发学习浏览:111
JNI混淆的问题
首先演示混淆存在的问题, 使用Android Studio新建一个模板为Native C++ 的app项目, 名字为JNIDemo
修改app/build.gradle, 使用官方默认的minifyEnabled
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
proguardFiles 'proguard-rules.pro'
}
}
然后运行, 使用jadx-gui看看反编译的结果, native方法并为被混淆
jadx-gui ./app/build/outputs/apk/release/app-release-unsigned.apk
再来看看动态库的情况, 方法名也清晰可见
# 使用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稍微修改一下, 注释系统规则
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, 并且使用独立的混淆规则
# 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的实现
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手动创建需要的工程文件
# 进入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一下
// ...
rootProject.name = "JNIDemo"
include ':app'
includeBuild("./plugins")
这里也可以使用buildSrc的方式, 或是include(":plugins:messplugin")导入, 但是这两种方法会导致, 每次只要修改插件的代码, 都会使得所有代码完全编译一遍, 时间很慢, 小项目还好, 大项目是很蛋疼的. 所以这里采用了gradle的复合编译, 当然速度快只是其中一个优点, 官方说明文档 和 Demo
plugins/settings.gradle.kts
rootProject.name = "plugins"
include(":messplugin")
plugins/messplugin/build.gradle.kts
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
package com.test.plugin
class MessExtension {
var classAndNative: Map<String, String> ? = null
}
plugins/messplugin/src/main/kotlin/com/test/plugin/MessPlugin.kt
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
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窗口能看到下面的输出
> 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
代码只说明流程, 有些异常和判断需要另外处理
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
#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"
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方法注释了两个修饰符
// 让符号保留, 这个肯定要去掉
#define JNIEXPORT __attribute__ ((visibility ("default")))
// 没有定义内容
#define JNICALL
关于这个
{"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI}
" ()Ljava/lang/String; "是方法的签名, 如果不知道在怎么写, 可以通过命令获取
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
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看看结果
jadx-gui ./app/build/outputs/apk/release/app-release-unsigned.apk
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…
[混淆的另一重境界] www.jianshu.com/p/799e5bc62…
相关推荐
- 轻松上手:
(三)笔记可再编辑 - 如何在iPhone,iPad和Android上使用WordPress应用程序
- 一款简单高效的Android异步框架
- [Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- Android---View中的setMinWidth与setMinimumWidth的踩坑记录
- Android广播如何解决Sending non-protected broadcast问题
- 有关Android Binder面试,你未知的9个秘密
- 开启Android学习之旅-2-架构组件实现数据列表及添加(kotlin)
- Android低功耗蓝牙开发总结
- Android 通知文本颜色获取
- 程序开发学习排行
-
- 1鸿蒙HarmonyOS:Web组件网页白屏检测
- 2HTTPS协议是安全传输,为啥还要再加密?
- 3HarmonyOS鸿蒙应用开发——数据持久化Preferences
- 4记解决MaterialButton背景颜色与设置值不同
- 5鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
- 6鸿蒙HarmonyOS实战-ArkUI组件(Stack)
- 7[Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- 8鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
- 9鸿蒙HarmonyOS实战-ArkUI组件(GridRow/GridCol)
- 最近发表
-
- WooCommerce最好的WordPress常用插件下载博客插件模块的相关产品
- 羊驼机器人最好的WordPress常用插件下载博客插件模块
- IP信息记录器最好的WordPress常用插件下载博客插件模块
- Linkly for WooCommerce最好的WordPress常用插件下载博客插件模块
- 元素聚合器Forms最好的WordPress常用插件下载博客插件模块
- Promaker Chat 最好的WordPress通用插件下载 博客插件模块
- 自动更新发布日期最好的WordPress常用插件下载博客插件模块
- WordPress官方最好的获取回复WordPress常用插件下载博客插件模块
- Img to rss最好的wordpress常用插件下载博客插件模块
- WPMozo为Elementor最好的WordPress常用插件下载博客插件模块添加精简版