Android APK 加固技术探究(三)

Android APK 加固技术探究(一)

Android APK 加固技术探究(二)

Android APK 加固技术探究(三)

为了保证 Android 应用的源码安全性,我们一般会对上线的应用进行代码混淆,然而仅仅做代码混淆还不够,我们还要对我们的应用加固,防止别人通过反编译获取到我们的源码。目前 apk 加固技术比较成熟完善,市面上比较流行的有“360加固”。本文就 apk 加固技术做一个技术探究,希望读者看过后能明白加固的其中原理,并也能自己实现加固方案。

源码地址:https://gitee.com/openjk/apk-jiagu

Android apk 加固技术探究(二)中,我们已经通过创建 Steady 模块生成了一个 shell.arr 文件,用来对加密后的 dex 文件进行解密和类加载操作。这篇文章主要讲解如何对原 apk 的 dex 加密和把 shell.arr 打入到原 apk 中并最终生成一个新的 apk

一、反编译 APK 文件

Android APK 加固技术探究(一)中讲解了如何反编译 apk 文件,这里使用 apktool 这个工具来反编译 apk。通过执行命令 java -jar outlibs/apktool_2.5.0.jar d '待解压apk路径' -o '解压后存放的路径'

/**
 * 反编译 APK 文件
 */
fun apkDecode(){
    println("开始反编译")
    val process = Runtime.getRuntime()
        .exec("java -jar outlibs/apktool_2.5.0.jar d "+ orginApk.absolutePath+" -o "+apkDecode.absolutePath)
    process.waitFor()
    if(process.exitValue() != 0) {
        FileUtils.printStream(process.errorStream)
    }else{
        FileUtils.printStream(process.inputStream)
    }
    process.destroy()
}

二、修改 AndroidManifest.xml 文件

步骤一中获得了解压后的文件目录,找到目录中的 AndroidManifest 文件。这里修改 AndroidManifest.xml 文件尝试过有2种方式,一种是通过 “AXMLEditor.jar” 和 “AXMLPrinter2.jar” 工具修改 AndroidManifest.xml 文件,另一种是通过 SAX 的方式解析 xml 文件,然后在相应的节点位置插入需要的数据,最后发现方法一虽然修改了xml 文件但是最终打包的新 APK 中 AndroidManifest.xml 文件没有生效,后来使用方法二生效了。下面把2种方式的代码都贴出来,如果哪个大佬发现了方法一中的问题,还请不吝赐教。

方法一:关于 “AXMLEditor.jar” 和 “AXMLPrinter2.jar” 两个工具如何使用可以自行百度,这里不做展开

/**
 * 修改 AndroidManifest
 */
fun changeAndroidManifest(apkUnzipDir:File){
    val aManifest = apkUnzipDir.listFiles { _, name ->
        name?.equals("AndroidManifest.xml") == true
    }
    val file = if (aManifest != null && aManifest.isNotEmpty()) {
        aManifest[0]
    }else{null}
    file?.let {
        //将模版插入 AndroidManifest 中
        val process2 = Runtime.getRuntime()
            .exec("java -jar outlibs/AXMLEditor.jar -tag -i tool/src/main/assets/ApplicationName.xml " +
                    file.absolutePath+" "+file.absolutePath)
        process2.waitFor()
        if(process2.exitValue() != 0) {
            println("2")
            FileUtils.printStream(process2.errorStream)
        }
        process2.destroy()

        //解析出原来的 Application 类名
        var process0 = Runtime.getRuntime()
            .exec("java -jar tool/libs/AXMLPrinter2.jar "+file.absolutePath)
        process0.waitFor()
        val applicationPath = XmlParseUtils.sax2xml(process0.inputStream)
        if(process0.exitValue() != 0){
            println("0")
            FileUtils.printStream(process0.errorStream)
        }
        process0.destroy()

        //参考 https://github.com/fourbrother/AXMLEditor
        //修改 Application 下 插入标签的值
        val process1 = Runtime.getRuntime()
            .exec("java -jar tool/libs/AXMLEditor.jar -attr -i meta-data package value "+applicationPath
                    + " " + file.absolutePath+" "+file.absolutePath)
        process1.waitFor()
        if(process1.exitValue() != 0){
            println("1")
            FileUtils.printStream(process1.errorStream)
        }
        process1.destroy()

        //参考 https://github.com/fourbrother/AXMLEditor
        //修改 Application 下 name 标签
        val process3 = Runtime.getRuntime()
            .exec("java -jar tool/libs/AXMLEditor.jar -attr -m application package name com.sakuqi.shell.NewApplication"
                    + " " + file.absolutePath+" "+file.absolutePath)
        process3.waitFor()
        if(process3.exitValue() != 0){
            println("3")
            FileUtils.printStream(process3.errorStream)
        }
        process3.destroy()

        //解析出原来的 Application 类名
        var process4 = Runtime.getRuntime()
            .exec("java -jar tool/libs/AXMLPrinter2.jar "+file.absolutePath)
        process4.waitFor()
        FileUtils.printStream(process4.inputStream)
        process4.destroy()

    }
}

方法二:SAXReader 的使用方式自行查看相关 API 文档

/**
 * 修改 xml 文件
 */
fun changeAndroidManifest(){
    println("开始修改 AndroidManifest")
    var manifestFile = File("output/apktool/decode/AndroidManifest.xml")
   changeXmlBySax(manifestFile,"com.sakuqi.steady.SteadyApplication")
   //com.sakuqi.steady.SteadyApplication名称为 Shell.arr 中的Application 类
}

/**
 * 修改xml文件
 */
fun changeXmlBySax(fileXml:File,newApplicationName:String){
    var sax = SAXReader()
    var document = sax.read(fileXml)
    var root = document.rootElement
    var application = root.element("application")
    //原有的 application 名称
    var applicationName = application.attributeValue("name")
    var applicationAttr = application.attribute("name")
    //将壳中的 application  替换原来的 application
    applicationAttr.text = newApplicationName

    var element = application.addElement("meta-data")
    element.addAttribute("android:name","app_name")
    element.addAttribute("android:value",applicationName)
    saveDocument(document,fileXml)

}
fun saveDocument(document:Document,file:File){
    var osWrite = OutputStreamWriter(FileOutputStream(file))
    var format = OutputFormat.createPrettyPrint()// 获取输出的指定格式
    format.encoding = "UTF-8"
    var writer = XMLWriter(osWrite,format)
    writer.write(document)
    writer.flush()
    writer.close()
}

三、编译修改 AndroidManifest.xml 后的反编译目录

/**
 * 编译 APK 文件
 */
fun apkBuild(){
    println("开始重新编译")
    val process = Runtime.getRuntime()
        .exec("java -jar outlibs/apktool_2.5.0.jar b "+ "反编译后的目录"+" -o "+ “编译后的目录”)
    process.waitFor()
    if(process.exitValue() != 0) {
        FileUtils.printStream(process.errorStream)
    }else{
        FileUtils.printStream(process.inputStream)
    }
    process.destroy()
}

四、解压 APK 文件并加密所以 Dex 文件

解压使用的是 java.util.zip.ZipFile 类,这里封装了工具类最后会放到源码里,这里就不展开了。解压后需要将原 apk 中的签名文件删除,以便后续重新签名。过滤出解压目录下的所有 dex 后缀文件,然后对其进行加密,需要注意的是加密方式需要和 shell.arr 中的解密方式保持一致,这里使用的是 AES 的加密方式,源代码会在后续的开源项目中展示。加密后需要将原来的 dex 文件删除。大致代码如下:

/**
 * 解压 APK 文件并加密所有的dex文件
 */
fun unZipApkAndEncrypt(){
    println("解压 APK")
    val apkUnzipDir = File("output/unzip/apk")
    if(!apkUnzipDir.exists()){
        apkUnzipDir.mkdirs()
    }
    FileUtils.delete(apkUnzipDir)
    ZipUtils.unZip(apkBuild,apkUnzipDir)
    //删除 META-INF/CERT.RSA,META-INF/CERT.SF,META-INF/MANIFEST.MF
    val certRSA = File(apkUnzipDir,"META-INF/CERT.RSA")
    certRSA.delete()
    val certSF = File(apkUnzipDir,"META-INF/CERT.SF")
    certSF.delete()
    val manifestMF = File(apkUnzipDir,"META-INF/MANIFEST.MF")
    manifestMF.delete()
    //changeAndroidManifest(apkUnzipDir)
    //获取dex 文件
    val apkFiles = apkUnzipDir.listFiles(object :FilenameFilter{
        override fun accept(dir: File?, name: String?): Boolean {
            return name?.endsWith(".dex") == true
        }
    })
    for (dexFile in apkFiles){
        val name = dexFile.name
        println("dex:$name")
        val bytes = DexUtils.getBytes(dexFile)
        val encrypt: ByteArray? = EncryptUtils.encrypt(bytes, EncryptUtils.ivBytes)
        val fos: FileOutputStream = FileOutputStream(
            File(
                dexFile.parent,
                "secret-" + dexFile.getName()
            )
        )
        fos.write(encrypt)
        fos.flush()
        fos.close()
        dexFile.delete()
    }

}

五、解压壳 aar 得到 class.jar ,然后把 class.jar 在转换成 class.dex,再将class.dex 移到原 apk 的解压目录,最后压缩成新的 apk 文件

这里解压依然使用的是 unzip 的工具类,转换 class.dex 使用的是 Android SDK 中自带的命令 dx

/**
 * 解压壳aar 并转化jar 为dex
 */
fun makeDecodeDex(){
    println("解压壳 AAR")
    var shellUnzipDir = File("output/unzip/shell")
    if(!shellUnzipDir.exists()){
        shellUnzipDir.mkdirs()
    }
    FileUtils.delete(shellUnzipDir)
    //解压 AAR
    ZipUtils.unZip(shellAAR,shellUnzipDir)
    //将 jar 转成 dex
    println("将 jar 转成 dex")
    var shellJar = File(shellUnzipDir,"classes.jar")
    var shellDex = File("output/unzip/apk","classes.dex")
    DexUtils.dxCommand(shellJar,shellDex)
    moveLibSoToApk()
    //打包
    println("打包 APK")
    var unsignedApk = File("output/unsigned_$orginApkName")
    ZipUtils.zip(File("output/unzip/apk"),unsignedApk)
}

/**
 * 将壳中的lib文件移到apk 中
 */
fun moveLibSoToApk(){
    var shellUnzipLibDir = File("output/unzip/shell/jni")
    var apkUnzipLibDir = File("output/unzip/apk/lib")
    if(!apkUnzipLibDir.exists()){
        apkUnzipLibDir.mkdirs()
    }

    FileUtils.copy(shellUnzipLibDir,apkUnzipLibDir)
}
object DexUtils {
    @Throws(IOException::class,InterruptedException::class)
    fun dxCommand(jar:File,dex:File){
        var runtime = Runtime.getRuntime()
        var process = runtime.exec("dx --dex --output "+dex.absolutePath+" "+jar.absolutePath)
        try {
            process.waitFor()
        }catch (e:InterruptedException){
            e.printStackTrace()
            throw e
        }
        if(process.exitValue() != 0){
            val inputStream = process.errorStream
            var buffer = ByteArray(1024)
            val bos = ByteArrayOutputStream()
            var len = inputStream.read(buffer)
            while (len != -1){
                bos.write(buffer,0,len)
                len = inputStream.read(buffer)
            }
            System.out.println(String(bos.toByteArray(), Charset.forName("GBK")))
            throw RuntimeException("dx run failed")
        }else{
            System.out.println("执行成功:"+process.exitValue())
        }
        process.destroy()
    }

    /**
     * 读取文件
     * @param file
     * @return
     * @throws Exception
     */
    @Throws(Exception::class)
    fun getBytes(file: File?): ByteArray {
        val r = RandomAccessFile(file, "r")
        val buffer = ByteArray(r.length().toInt())
        r.readFully(buffer)
        r.close()
        return buffer
    }
}

五、将压缩后的新的 apk 文件进行 zip 对齐操作

/**
 * 对齐
 */
fun zipalign(){
    println("将打包的 apk 对齐")
    var unsignedApk = File("output/unsigned_$orginApkName")
    val alignedApk = File("output/unsigned-aligned_$orginApkName")
    val process = Runtime.getRuntime().exec(
        "zipalign -p -f -v 4 " + unsignedApk.absolutePath + " " + alignedApk.absolutePath)
    process.waitFor(5,TimeUnit.SECONDS)
    try {
        if (process.exitValue() != 0) {
            println("zipalign 出错")
            FileUtils.printStream(process.errorStream)
        } else {
            FileUtils.printStream(process.inputStream)
        }
        println("完成 apk 的对齐")
        process.destroy()
    }catch (e:Exception){
        println("对齐超时...")
    }
}

六、将对齐后的 apk 文件进行签名

/**
 * 对 APK 签名
 */
fun jksToApk(){
    println("签名 APK")
    var signedApk = File("output/signed_$orginApkName")
    val alignedApk = File("output/unsigned-aligned_$orginApkName")
    SignUtils.signature(alignedApk,signedApk,signFile.absolutePath)
}
object SignUtils {
    @Throws(InterruptedException::class, IOException::class)
    fun signature(unsignedApk: File, signedApk: File, keyStore: String) {
        val cmd = arrayOf(
            "jarsigner",
            "-sigalg",
            "SHA1withRSA",
            "-digestalg",
            "SHA1",
            "-keystore",
            keyStore,
            "-storepass",
            "密码",
            "-keypass",
            "密码",
            "-signedjar",
            signedApk.absolutePath,
            unsignedApk.absolutePath,
            "alinas"
        )
        val process = Runtime.getRuntime().exec(cmd)
        println("start sign")
        try {
            val waitResult = process.waitFor()
            println("waitResult: $waitResult")
        } catch (e: InterruptedException) {
            e.printStackTrace()
            throw e
        }

        println("process.exitValue() " + process.exitValue())
        if (process.exitValue() != 0) {
            val inputStream = process.errorStream
            var len: Int
            val buffer = ByteArray(2048)
            val bos = ByteArrayOutputStream()
            len = inputStream.read(buffer)
            while (len != -1) {
                bos.write(buffer, 0, len)
                len = inputStream.read(buffer)
            }
            println(String(bos.toByteArray(), Charset.forName("gbk")))
            throw RuntimeException("签名执行失败")
        }
        println("finish signed")
        process.destroy()
    }
}

至此 apk 的加固流程全部讲完

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容