前言
上一篇文章: Android字符串安全(一):如何逆向破解纯Kotlin/Java的字符串加密/混淆 - 掘金 (juejin.cn)
版本号:StringFog 4.0.1、Gradle 7.4.2
知名安全大厂的字符串加密效果
通过前文可以看到,StringFog默认的字符串加密效果,只能稍微阻挡不了解smali的普通码农,如果了解过一些JVM基本概念,就可以完美还原。它还把字符串变长了,仿佛白白增大了包体积。
给读者看张图,参考下某安全大厂旗下的某分身产品,他们的Kotlin/Java层字符串加密是如何做的,这也是我们最终要实现的效果。
扩展StringFog实现Kotlin/Java层字符串加密到Native
按照官方的自定义加密算法流程,要新建一个插件模块,实现IKeyGenerator
和IStringFog
接口。前者可以用一用,但后者我并不满意,因为它设计的方法,对我有些繁琐了。
java
/**
* Interface of how to encrypt and decrypt a string.
*
* @author Megatron King
* @since 2018/9/20 16:15
*/
public interface IStringFog {
/**
* Encrypt the data by the special key.
*
* @param data The original data.
* @param key Encrypt key.
* @return The encrypted data.
*/
byte[] encrypt(String data, byte[] key);
/**
* Decrypt the data to origin by the special key.
*
* @param data The encrypted data.
* @param key Encrypt key.
* @return The original data.
*/
String decrypt(byte[] data, byte[] key);
/**
* Whether the string should be encrypted.
*
* @param data The original data.
* @return If you want to skip this String, return false.
*/
boolean shouldFog(String data);
}
这里我们也直接新建buildSrc
模块。这里我直接给出我IKeyGenerator
的实现:NativeKeyGenerator
,它把项目中的字符串生成对应的int值。也要同步修改主模块build.gradle的stringfog配置中的kg值。
kotlin
package safe.string
import com.github.megatronking.stringfog.IKeyGenerator
class NativeKeyGenerator : IKeyGenerator {
companion object{
val map = HashMap<String, Int>()
}
@Synchronized
override fun generate(text: String): ByteArray {
var i = map[text]
if(i == null){
i = map.size
map[text] = i
}
return i.toString().toByteArray()
}
}
既然不想用IStringFog
类,那么需要修改StringFog插件的核心源码了。StringFog分为多个模块,可能会把我们项目结构变得复杂,但实际上可以利用类加载机制相关原理动态替换需要修改的类(只需要相同包名类名直接放入buildSrc模块即可,它会比StringFog插件先被类加载器加载,从而实现替换的效果),而不需要拉取完整代码放入项目中。
我们的解密类如下,也就是StringFog通过ASM插入的要调用的函数:
kotlin
package safe.string
import androidx.annotation.Keep
@Keep
object NativeStringFog {
const val NATIVE_NAME = "string_safe"
@Keep
@JvmStatic
external fun decrypt(key: Int): String
}
对应的native代码:这里直接让GPT造一份把UTF-8数组转Java字符串的代码,然后稍微改改。为了测试简单,直接使用静态注册了:
c
#include "string_safe.h"
#include <jni.h>
#include <string.h>
#include <stdlib.h>
JNIEXPORT jstring JNICALL
Java_safe_string_NativeStringFog_decrypt(JNIEnv *env, jobject thiz, jint key) {
// UTF-8 byte array
char* utf8Str = (char*)__string_safe_list[key];
// Calculate length of the byte array
jsize length = strlen(utf8Str);
// Create a Java byte array
jbyteArray javaBytes = (*env)->NewByteArray(env, length);
// Set the Java byte array region with utf8Str
(*env)->SetByteArrayRegion(env, javaBytes, 0, length, (jbyte *) utf8Str);
// Find the Java String class and its constructor (byte[] -> String)
jclass stringClass = (*env)->FindClass(env, "java/lang/String");
jmethodID ctor = (*env)->GetMethodID(env, stringClass, "<init>", "([B)V");
// Create the Java String object by calling the constructor
jstring javaString = (*env)->NewObject(env, stringClass, ctor, javaBytes);
// Clean up local references
(*env)->DeleteLocalRef(env, stringClass);
(*env)->DeleteLocalRef(env, javaBytes);
return javaString;
}
然后在所有字符串使用前,尽早加载so。参考StringFog源码,复制一份StringFogClassVisitor
(以及它依赖的类)到buildSrc模块,先修改canEncrypted
函数不加密"string_safe"(以免后面忘了..),因为我们调用System.loadLibrary("string_safe")
之前无法解密字符串:
java
private boolean canEncrypted(String value) {
return (value != null && value.trim().length() != 0)
&& value.length() < 65536 >> 2 && mStringFogImpl.shouldFog(value)
&& !value.equals("string_safe");
}
在主模块build.gradle
编写在StringFog执行完ASM操作之后的任务:生成刚刚的c代码对应的头文件,只需要读NativeKeyGenerator
的map成员:
groovy
tasks.register('makeSafeStringNativeHeader') {
println("file = " + projectDir.parentFile)
def headerFile = new File(projectDir.parentFile, "app/src/main/cpp/string_safe.h")
if(!headerFile.exists()){
headerFile.setText("const char* __string_safe_list[];")
}
doLast {
def fileWriter = new FileWriter(headerFile)
NativeKeyGenerator.map.forEach{k,v->
// println("---> v: " + v + ", k: " + k)
fileWriter.write("const char __string_safe_$v[] = {${k.getBytes().join(",")}, 0};\n")
}
fileWriter.write("const char* __string_safe_list[] = {\n")
for(int i = 0; i < NativeKeyGenerator.map.size() ; i++){
fileWriter.write(" __string_safe_$i,\n")
}
fileWriter.write("};\n")
fileWriter.flush()
}
}
复制StringFogClassGenerator
到buildSrc模块,直接注释掉它generate
方法所有内容,这里是用来在项目中插入StringFogWrapper
类的,我们已经用不到它了。
刚刚修改过的StringFogClassVisitor
类,注意要把它改为public修饰的,因为默认的包可见性只有同一类加载器加载的类才有效,否则过不去JVM的校验。然后简单起见,直接删掉对mode
的判断,构造方法注释掉的代码如下,代码中其他位置顺着这个改动去修改:
java
/* package */ public StringFogClassVisitor(IStringFog stringFogImpl, StringFogMappingPrinter mappingPrinter,
String fogClassName, ClassWriter cw, IKeyGenerator kg, StringFogMode mode) {
super(Opcodes.ASM7, cw);
this.mStringFogImpl = stringFogImpl;
this.mMappingPrinter = mappingPrinter;
this.mKeyGenerator = kg;
/*fogClassName = fogClassName.replace('.', '/');
mInstructionWriter = new NativeInstructionWriter(fogClassName);
if (mode == StringFogMode.base64) {
this.mInstructionWriter = new Base64InstructionWriter(fogClassName);
} else if (mode == StringFogMode.bytes) {
this.mInstructionWriter = new ByteArrayInstructionWriter(fogClassName);
} else {
throw new IllegalArgumentException("Unknown stringfog mode: " + mode);
}*/
}
然后修改encryptAndWrite
方法如下:
java
private void encryptAndWrite(String value, MethodVisitor mv) {
/*byte[] key = mKeyGenerator.generate(value);
byte[] encryptValue = mStringFogImpl.encrypt(value, key);
String result = mInstructionWriter.write(key, encryptValue, mv);
mMappingPrinter.output(getJavaClassName(), value, result);*/
String nativeKey = new String(mKeyGenerator.generate(value));
pushNumber(mv, Integer.parseInt(nativeKey));
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "safe/string/NativeStringFog",
"decrypt", "(I)Ljava/lang/String;", false);
mMappingPrinter.output(getJavaClassName(), value, nativeKey);
}
这里pushNumber
方法哪里来的?它是ByteArrayInstructionWriter
的内部方法,加上static
直接放到外面就好了,需要用它给NativeStringFog
的decrypt
方法传入int类型参数。
定义好任务执行顺序,确保在编译native代码之前生成头文件:
groovy
afterEvaluate{
transformClassesWithStringFogForDebug.finalizedBy makeSafeStringNativeHeader
transformClassesWithStringFogForRelease.finalizedBy makeSafeStringNativeHeader
tasks.matching { it.name.startsWith('configureCMake') }.configureEach { cmakeTask ->
// configureCMakeDebug / configureCMakeRelease
cmakeTask.dependsOn makeSafeStringNativeHeader
}
}
完成这些步骤,重新打包运行,效果如图:
其他
上文介绍了将Kotlin/Java字符串加密到native层的关键步骤,以及涉及到的一些技巧。
还有一些坑要注意,比如如果在attachBaseContext
加载so,参数要加问号(如下图),否则kotlin会插入用于检查null抛出异常的指令(含字符串,风险如下下图)
如果想投入到正式项目中,还需要自行做一些事情:
- 便于后续维护的整理(例如目前为了简单,破坏了StringFog的"mode"参数)
- 字符串池优化(防止创建过多的String对象)
- Native层防护(函数改为动态注册+strip符号、防动态调试、SO运行时解密等..)等。