Android字符串安全(二):Kotlin/Java字符串加密到Native层,扩展StringFog插件

前言

上一篇文章: Android字符串安全(一):如何逆向破解纯Kotlin/Java的字符串加密/混淆 - 掘金 (juejin.cn)

版本号:StringFog 4.0.1、Gradle 7.4.2

知名安全大厂的字符串加密效果

通过前文可以看到,StringFog默认的字符串加密效果,只能稍微阻挡不了解smali的普通码农,如果了解过一些JVM基本概念,就可以完美还原。它还把字符串变长了,仿佛白白增大了包体积。

给读者看张图,参考下某安全大厂旗下的某分身产品,他们的Kotlin/Java层字符串加密是如何做的,这也是我们最终要实现的效果。

扩展StringFog实现Kotlin/Java层字符串加密到Native

按照官方的自定义加密算法流程,要新建一个插件模块,实现IKeyGeneratorIStringFog接口。前者可以用一用,但后者我并不满意,因为它设计的方法,对我有些繁琐了。

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直接放到外面就好了,需要用它给NativeStringFogdecrypt方法传入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运行时解密等..)等。
相关推荐
bbqz0074 天前
逆向通达信 x 逆向微信 x 逆向Qt
c++·qt·微信·逆向·通达信·wechat·tdx
暴走的海鸽4 天前
小白也能懂:逆向分析某网站加速乐Cookie参数流程详解
爬虫·python·逆向
lll...lll19 天前
linux 部署瑞数6实战(维普,药监局)sign第二部分
java·linux·运维·服务器·python·逆向·药监局
lll...lll22 天前
弹幕逆向signature、a_bogus
java·python·逆向·抖音·弹幕·sign·abogus
lll...lll24 天前
京东小程序h5st
python·小程序·逆向·h5st·京东购物
怜渠客1 个月前
记一次源码部分丢失后补救过程
ida·逆向·freebasic
花园宝宝小点点1 个月前
车联网安全入门——CAN总线逆向(ICSim)
安全·汽车·can·逆向·车联网安全
Jay 171 个月前
PolarCTF 2024夏季个人挑战赛 个人WP
web安全·密码学·逆向·二进制·ctf·misc·polar
暴走的海鸽1 个月前
“JS逆向 | Python爬虫 | 动态cookie如何破~”
爬虫·python·逆向