Android APK Reinforcement Technology Exploration (1)

Android APK Reinforcement Technology Exploration (II)

Android APK Reinforcement Technology exploration (3)

In order to ensure the source security of Android applications, we usually obfuscate the online applications. However, it is not enough just to obfuscate the code. We also need to harden our applications to prevent others from obtaining our source code through decompilation. At present, APK reinforcement technology is mature and perfect, and “360 reinforcement” is popular on the market. This paper makes a technical exploration on APK reinforcement technology, hoping that readers can understand the principle of reinforcement after reading, and can realize the reinforcement scheme by themselves.

Source code address: gitee.com/openjk/apk-…

In the exploration of Android APK reinforcement technology (II), we have generated a shell.arr file by creating the Steady module, which is used to decrypt the encrypted dex file and perform class loading operations. This article mainly explains how to encrypt the dex of the original APK and how to drive shell. Arr into the original APK and finally generate a new APK

Decompile APK file

Android APK hardening technology explore (1) explains how to decompile APK file, here uses the apktool to decompile APK. Run the Java -jar outlibs/ apkTool_2.5.0. jar d ‘ApK directory to be decompressed’ -o ‘Directory to be decompressed’

/** * decompile APK file */
fun apkDecode(a){
    println("Start decompilating")
    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()
}
Copy the code

Modify androidmanifest.xml file

Step 1 to get the decompressed file directory, find the directory in the AndroidManifest file. There are two ways to modify the androidmanifest.xml file. One is to modify the Androidmanifest.xml file with the tools “AXMLEditor. Jar” and “axmlPrinter2.jar”. The other is to parse XML file by SAX, and then insert the data needed at the corresponding node position. Finally, it is found that although the XML file is modified in method 1, the androidmanifest.xml file in the new APK package is not effective, and method 2 is effective later. The code for both methods is posted below, and if any of them find problems in method 1, please feel free to comment.

Method 1: how to use “AXMLEditor. Jar” and “axmlPrinter2. jar” two tools can be baidu, here do not expand

/** * Modify AndroidManifest */
fun changeAndroidManifest(apkUnzipDir:File){
    valaManifest = apkUnzipDir.listFiles { _, name -> name? .equals("AndroidManifest.xml") = =true
    }
    val file = if(aManifest ! =null && aManifest.isNotEmpty()) {
        aManifest[0]}else{null} file? .let {// Insert the template into 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()

        // Parse the original Application class name
        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()

        / / reference https://github.com/fourbrother/AXMLEditor
        // Change the value of the insert label under 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()

        / / reference https://github.com/fourbrother/AXMLEditor
        // Change the name tag under Application
        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()

        // Parse the original Application class name
        var process4 = Runtime.getRuntime()
            .exec("java -jar tool/libs/AXMLPrinter2.jar "+file.absolutePath)
        process4.waitFor()
        FileUtils.printStream(process4.inputStream)
        process4.destroy()

    }
}
Copy the code

Method 2: See the API documentation for how to use SAXReader

/** * Modify the XML file */
fun changeAndroidManifest(a){
    println("Start modifying AndroidManifest")
    var manifestFile = File("output/apktool/decode/AndroidManifest.xml")
   changeXmlBySax(manifestFile,"com.sakuqi.steady.SteadyApplication")
   . / / com. Sakuqi. Steady SteadyApplication name for the Shell. The arr of Application class
}

/** * Modify the XML file */
fun changeXmlBySax(fileXml:File,newApplicationName:String){
    var sax = SAXReader()
    var document = sax.read(fileXml)
    var root = document.rootElement
    var application = root.element("application")
    // The original application name
    var applicationName = application.attributeValue("name")
    var applicationAttr = application.attribute("name")
    // Replace the application in the shell with the original 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()// Gets the specified format of the output
    format.encoding = "UTF-8"
    var writer = XMLWriter(osWrite,format)
    writer.write(document)
    writer.flush()
    writer.close()
}
Copy the code

Androidmanifest.xml decompiler directory

/** * build (){println(" start compiling ") val process = Runtime.getruntime ().exec(" java-jar" Jar b "+" decompiled directory "+" -o "+" compiled directory ") process.waitfor () if(process.exitValue()! = 0) { FileUtils.printStream(process.errorStream) }else{ FileUtils.printStream(process.inputStream) } process.destroy() }Copy the code

Decompress the APK file and encrypt the Dex file

Zip file class, which encapsulates the utility classes and will eventually be put into the source code, so I won’t expand it here. After decompressing the file, delete the signature file from the original APK for subsequent signature. Filter out all dex suffix files in the decompressed directory and encrypt them. It should be noted that the encryption method must be consistent with the decryption method in shell.arr, AES encryption method is used here, and the source code will be displayed in the subsequent open source project. After encryption, delete the original dex file. The general code is as follows:

/** * Unzip APK files and encrypt all dex files */
fun unZipApkAndEncrypt(a){
    println("Decompression APK")
    val apkUnzipDir = File("output/unzip/apk")
    if(! apkUnzipDir.exists()){ apkUnzipDir.mkdirs() } FileUtils.delete(apkUnzipDir) ZipUtils.unZip(apkBuild,apkUnzipDir)/ / remove 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)
    // Get the dex file
    val apkFiles = apkUnzipDir.listFiles(object :FilenameFilter{
        override fun accept(dir: File? , name:String?).: Boolean {
            returnname? .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()
    }

}
Copy the code

5, decompress aar to obtain class.jar, and then convert class.jar to class.dex, and then move class.dex to the original APK decompression directory, and finally compress into a new APK file

Here unzip is still used to decompress the utility class, and convert class.dex is the Android SDK’s own command dx

/** * Decompress aar and convert jar to dex */
fun makeDecodeDex(a){
    println("Decompression shell AAR")
    var shellUnzipDir = File("output/unzip/shell")
    if(! shellUnzipDir.exists()){ shellUnzipDir.mkdirs() } FileUtils.delete(shellUnzipDir)/ / the AAR
    ZipUtils.unZip(shellAAR,shellUnzipDir)
    // Convert jar to dex
    println("Convert jar to dex")
    var shellJar = File(shellUnzipDir,"classes.jar")
    var shellDex = File("output/unzip/apk"."classes.dex")
    DexUtils.dxCommand(shellJar,shellDex)
    moveLibSoToApk()
    / / packaging
    println("Packaging APK")
    var unsignedApk = File("output/unsigned_$orginApkName")
    ZipUtils.zip(File("output/unzip/apk"),unsignedApk)
}

/** * move the lib file from the shell to apk */
fun moveLibSoToApk(a){
    var shellUnzipLibDir = File("output/unzip/shell/jni")
    var apkUnzipLibDir = File("output/unzip/apk/lib")
    if(! apkUnzipLibDir.exists()){ apkUnzipLibDir.mkdirs() } FileUtils.copy(shellUnzipLibDir,apkUnzipLibDir) }Copy the code
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("Execution successful :"+process.exitValue())
        }
        process.destroy()
    }

    /** * Read file *@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
    }
}
Copy the code

Five, the compressed new APK file zip alignment operation

/** * align */
fun zipalign(a){
    println("Align packaged 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("The zipalign error")
            FileUtils.printStream(process.errorStream)
        } else {
            FileUtils.printStream(process.inputStream)
        }
        println("Complete alignment of APK")
        process.destroy()
    }catch (e:Exception){
        println("Alignment timeout...")}}Copy the code

6. Sign the aligned APK file

/** * Sign to APK */
fun jksToApk(a){
    println("Signature APK")
    var signedApk = File("output/signed_$orginApkName")
    val alignedApk = File("output/unsigned-aligned_$orginApkName")
    SignUtils.signature(alignedApk,signedApk,signFile.absolutePath)
}
Copy the code
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"."Password"."-keypass"."Password"."-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("Signature execution failed")
        }
        println("finish signed")
        process.destroy()
    }
}
Copy the code

So far, the reinforcement process of APK is finished