Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

openJK-dev/AppJiagu-java

Open more actions menu

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ApkJiagu

介绍

该项目讲诉如何将 apk 反编译、编译、修改 AndroidManifest文件、加密 dex 文件、重新打包、签名、加固

Android APK 加固技术探究(一)

Android APK 加固技术探究(二)

Android APK 加固技术探究(三)

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

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 的加固流程全部讲完

参与贡献

  1. Fork 本仓库
  2. 新建 Feat_xxx 分支
  3. 提交代码
  4. 新建 Pull Request

特技

  1. 使用 Readme_XXX.md 来支持不同的语言,例如 Readme_en.md, Readme_zh.md
  2. Gitee 官方博客 blog.gitee.com
  3. 你可以 https://gitee.com/explore 这个地址来了解 Gitee 上的优秀开源项目
  4. GVP 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
  5. Gitee 官方提供的使用手册 https://gitee.com/help
  6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 https://gitee.com/gitee-stars/

About

该项目讲诉如何将 apk 反编译、编译、修改 AndroidManifest文件、加密 dex 文件、重新打包、签名、加固

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

Morty Proxy This is a proxified and sanitized view of the page, visit original site.