我正在参加「启航计划」

1.前语

对《运用Compose Desktop开发一款桌面端多功用Apk东西》这篇文章还有形象吗,文章地址:/post/712264…,这是笔者现在阅读量最高的文章了,现在这款东西现已进行了多次晋级而且移植到咱们的CMS体系了,可是惋惜的是其跟公司APP端的业务绑定比较紧密导致终究也不适合开源出来。

不过这次呢我又运用ComposeDesktop开发了一款桌面端APK逆向东西,里边用到的很多都是从之前的桌面端东西复制过来的,原理一致,代码呢现已开源到了GitHub:github.com/vsLoong/Apk… (现在根底的部分已搭建完成,剩余的功用慢慢完善,咱们有想法尽管给笔者提)

2.小感慨

间隔上次写文章曩昔小半年了,开年后一直在运用下架与上架之间反复横跳,隐私合规与违规之间来回游走,真的是焦头烂额。不过好在根本摸清了国内各大运用商店上架的相关规则,以及马甲包断定的相关原理。

马甲包这个东西,高情商的说法应该叫“APP矩阵”,像我这种情商比较低的就直呼为马甲包了。为什么会做马甲包,不瞒咱们,交际直播类型的这种运用一不小心或许就踩坑被封了,尽管各种鉴黄、风控等根本设备都有了,但道高一尺魔高一丈,总会有一些漏网之鱼。再加上方针的收紧,所以多运用布局就成了必然了。

那么这种状况下有两个选择:

  • 1、像个乖孩子相同从头从头开发新的运用出来
  • 2、像个耍小聪明的孩子相同用Product Flavor的方法克隆运用出来

第一种方法费时费力,费钱费人,除非是新项目,不然一般公司肯定不会选择这么做。第二种方法简略粗犷,快速高效,是绝大部分人的首选,可是其缺点也很明显,代码、资源等文件重复度高,容易被断定为相似运用,然后被限流(OPPO)或许拒绝上架(HUAWEI),所以咱们就需求一个与之对抗的策略。

上文说了这么多想必咱们也能猜到我后续新的文章应该往哪个方向去了,可是呢,别着急一口气吃成胖子,咱们简略点,先从APK文件的逆向讲起来,了解下逆向所需的东西,然后咱们将其集成到咱们自己的桌面端运用中。这个桌面运用的方针便是要做到能解码APK文件,能修正解码后的文件,例如修正运用名,运用图标等等简略的东西,最终再从头打包为新的APK文件,对齐、重签名即可。

3.逆向东西简介

3.1.ApkTool

GitHub地址:github.com/iBotPeaches…
网站地址:ibotpeaches.github.io/Apktool/

这个是现在我最常用的东西,它能够将APK文件解码为资源文件和smali代码等,咱们能够修正解码后的布局文件、图片、字符串等等资源,甚至能够修正smali代码。修正完成后,ApkTool还能够将这些文件从头打包成一个新的APK文件。

常用指令如下:

# 解码APK文件,能够运用decode指令
apktool decode old.apk -o outputDir

# 将解码后的文件构建为一个新的APK文件,能够运用build指令
apktool build outputDir -o new.apk

**留意:**从头构建好的APK文件现已丢失了签名信息,运用的话还需求进行对齐、签名操作。现在咱们运用后期一些定制的功用的根本都会运用他。

3.2.Jadx

GitHub地址:github.com/skylot/jadx
比照ApkTool,Jadx能将APK、Dex文件等,直接反编译出来Java源代码。它同时供给了图形化的界面,指令行功用,以及相关依靠库。

在这次示例中咱们直接集成了Jadx在Maven库房中供给的依靠,详细方法GitHub中也有阐明,有需求请参阅github.com/skylot/jadx…:

// jadx依靠相关
commonMainImplementation("io.github.skylot:jadx-core:1.4.7")
commonMainImplementation("io.github.skylot:jadx-dex-input:1.4.7")

反编译Dex文件相关代码如下:

/**
* 运用Jadx将dex文件反编译为java源文件
*/
private fun dexFile2java(
    dexFile: File,
    outDirPath: String
) {
    val jadxArgs = JadxArgs()
    jadxArgs.setInputFile(dexFile)
    jadxArgs.outDir = File(outDirPath)
    try {
        JadxDecompiler(jadxArgs).use { jadx ->
            jadx.load()
            jadx.save()
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

3.3.其他东西

在实践运用呢我也是运用上面两者居多,其他还有一些小东西也都不错,给咱们简略推荐一下。

3.3.1.Dex文件反编译为Jar文件

dex2jar

GitHub地址:github.com/pxb1988/dex… (现已有2年未更新了)
望文生义,将Dex文件转化为Jar文件的东西。有一个缺点,当dex文件比较大的时分,反编译会经常性卡死。

下载下来后是一个紧缩包,解压后有win上的.bat可履行文件,以及unix体系上的.sh可履行文件,以mac上运用为例:

# 将classes.dex文件反编译为output.jar
./d2j-dex2jar.sh -o output.jar classes.dex

3.3.2.Jar文件反编译为Java文件

JavaDecompiler

GitHub地址:github.com/java-decomp… (现已有4年未更新了)
网站主页:java-decompiler.github.io/

将Jar文件反编译为Java文件的东西,有指令行东西JD-Core,有图形化页面JD-GUI,也供给了Maven依靠库。

Procyon

GitHub地址:github.com/mstrobel/pr…

依据大部分逆向工程师的比照,这个东西比较好用,常用指令如下(在更换为jadx前我运用的也是这款东西):

# 反编译input.jar文件到outputDir文件夹中
java -jar procyon-decompiler.jar -o outputDir input.jar

Fernflower

GitHub地址:github.com/fesh0r/fern…

IDEA自带的反编译东西,也比较不错。可是需求自己运用gradle编译出来可履行的jar文件,常用指令如下:

# 反编译input.jar文件到outputDir文件夹中
java -jar fernflower.jar input.jar outputDir

CFR

GitHub地址:github.com/leibnitz27/…
网站主页:www.benf.org/other/cfr/

这个东西我自身并没有运用过,这儿便不再过多介绍了,有兴趣的朋友能够自行查看下。

4.桌面端逆向APK运用的开发

接下来咱们就需求运用Compose来开发桌面端APK逆向东西了,需求集成上文提及的ApkTool供给的可履行jar文件以及jadx的Maven库房依靠。

4.1.文件拖拽

首要呢咱们需求支撑将APK文件拖动到咱们的东西面板上,然后自动履行后续的流程。之前的文章也写过,不过文章宣布后就发现了一个bug,增加了这个功用后导致调整运用窗口巨细的功用失效了。不过后来 @virogu 读者反应并给出了处理的计划:

@Composable
fun DropHerePanel(
    modifier: Modifier,
    composeWindow: ComposeWindow,
    onFileDrop: (List<File>) -> Unit
) {
    val component = remember {
        ComposePanel().apply {
            val target = object : DropTarget() {
                override fun drop(event: DropTargetDropEvent) {
                    event.acceptDrop(DnDConstants.ACTION_REFERENCE)
                    val dataFlavors = event.transferable.transferDataFlavors
                    dataFlavors.forEach {
                        if (it == DataFlavor.javaFileListFlavor) {
                            val list = event.transferable.getTransferData(it) as List<*>
                            list.map { filePath ->
                                File(filePath.toString())
                            }.also(onFileDrop)
                        }
                    }
                    event.dropComplete(true)
                }
            }
            dropTarget = target
            isOpaque = false
        }
    }
    val pane = remember {
        composeWindow.rootPane
    }
    Box(
        modifier = modifier
            .onPlaced {
                val x = it.positionInWindow().x.roundToInt()
                val y = it.positionInWindow().y.roundToInt()
                val width = it.size.width
                val height = it.size.height
                component.setBounds(x, y, width, height)
            },
        contentAlignment = Alignment.Center
    ) {
        Text(text = "请拖拽文件到这儿哦", fontSize = 36.sp, color = textColor)
        DisposableEffect(true) {
            pane.add(component)
            onDispose {
                pane.remove(component)
            }
        }
    }
}

4.2.结构工程目录

在上一节中,咱们完成了文件拖拽功用,当APK文件被拖拽到当前工程面板后,咱们首要运用aapt2东西解析APK文件,获取到根本信息后结构工程根目录,例如“包名_版别号”。然后运用ApkTool解码APK文件到decode文件夹中,解码结束后咱们运用jadx将解码后的smali文件反编译为java源文件,并别的存储到decompiled_java文件夹中。假如需求查看dex文件的话,咱们能够解压apk文件到decompress文件夹中,这样咱们就造好了一个根本的工程Project目录,然后结构目录树交给Compose的LazyColumn来显现,结构目录树的过程咱们能够运用file.walk()来遍历然后存储相应的信息。

使用Compose开发一款桌面端APK逆向工具

为了更专注于查看咱们需求的文件,咱们也增加了工程目录类型切换功用,类似IDEA相同,当切换为Packages形式的时分,就会将java源文件显现在java目录中,资源文件显现在res目录中,smali文件目录也会显现出来,方便咱们比照java和smali文件:

使用Compose开发一款桌面端APK逆向工具

当然了,当项目结构树显现出来后,或许选中源码文件后,右侧代码修正区域长度或许宽度会超出运用巨细,如下图所示,所以需求支撑横向翻滚和纵向翻滚功用:

使用Compose开发一款桌面端APK逆向工具

这对Compose来说彻底不是问题,咱们能够直接封装一个简略的Composable函数出来,如下所示:

/**
 * 带有翻滚条的面板
 * 支撑横向翻滚条,竖向翻滚条
 */
@Composable
fun ScrollPanel(
    modifier: Modifier,
    verticalScrollStateAdapter: ScrollbarAdapter,
    horizontalScrollStateAdapter: ScrollbarAdapter,
    content: @Composable BoxScope.() -> Unit
) {
    Row(modifier = modifier) {
        Column(modifier = Modifier.fillMaxHeight().weight(1f)) {
            Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
                content()
            }
            HorizontalScrollbar(
                adapter = horizontalScrollStateAdapter,
                style = ScrollbarStyle(
                    minimalHeight = 16.dp,
                    thickness = 8.dp,
                    shape = RoundedCornerShape(4.dp),
                    hoverDurationMillis = 300,
                    unhoverColor = Color.White.copy(alpha = 0.20f),
                    hoverColor = Color.White.copy(alpha = 0.50f)
                ),
                modifier = Modifier.fillMaxWidth().background(color = Color.Transparent)
            )
        }
        VerticalScrollbar(
            adapter = verticalScrollStateAdapter,
            style = ScrollbarStyle(
                minimalHeight = 16.dp,
                thickness = 8.dp,
                shape = RoundedCornerShape(4.dp),
                hoverDurationMillis = 300,
                unhoverColor = Color.White.copy(alpha = 0.20f),
                hoverColor = Color.White.copy(alpha = 0.50f)
            ),
            modifier = Modifier.fillMaxHeight().background(color = Color.Transparent)
        )
    }
}

4.3.文件标签页

做这个标签页功用的时分触及到了一个常识点【固有特性丈量】,内容介绍请参阅developer.android.google.cn/jetpack/com…。 如下所示,当标题栏的长度长短不一的时分,咱们需求现依据标题的详细长度来决议下方指示器的长度。

使用Compose开发一款桌面端APK逆向工具
使用Compose开发一款桌面端APK逆向工具

此时咱们就能够利用Compose的固有特性丈量来完成该作用,在最外层的Column中运用Modifier.width(intrinsicSize = IntrinsicSize.Max)修饰符来指定依照子项的最大宽度进行丈量,子项TextField宽度为自适应,子项指示器(Box)的宽度为最大固有宽度。这样当文本的内容长度改变的时分,指示器的宽度也会随之进行改变,示例代码如下:

val textValue = remember {
    mutableStateOf("hello, this is intrinsic measurements sample.")
}
Column(
    modifier = Modifier.width(intrinsicSize = IntrinsicSize.Max)
        .padding(16.dp)
) {
    TextField(
        value = textValue.value,
        onValueChange = {
            textValue.value = it
        },
    )
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(3.dp)
            .clip(shape = RoundedCornerShape(50))
            .background(color = Color(0xFF3674f0))
    )
}

作用如下:

使用Compose开发一款桌面端APK逆向工具

4.4.关键字高亮

原本做完java和smali文件的字符串显现后就打当作下一个功用了,可是代码显现出来后总觉的缺了点什么,和真正的IDE一比照才发现少了关键字高亮的功用,现在只针对Java关键字完成了高亮作用,如下所示:

使用Compose开发一款桌面端APK逆向工具

以java来讲,关键字就有50多个,不或许分别匹配50多次然后找出来关键字的索引位置,这样功率就太低了。那么这儿就需求一个高效的多形式匹配算法-【Aho-Corasick】,仅需一次遍历,就能匹配出一切的关键字(字符串)。至于算法原理,还请咱们自行学习,这儿咱们直接依靠java中的Aho-Corasick算法库来完成咱们的需求,现在选用的依靠库及版别如下:

commonMainImplementation("com.hankcs:aho-corasick-double-array-trie:1.2.2")

当匹配出来一切的关键字后,为了完成富文本样式,字体、颜色、下划线等作用,在Text中就能够经过AnnotatedString来处理,在TextField中,能够经过设置VisualTransformation�参数来完成,该参数用于操控输入文本的可视化样式转化,能够用于完成各种文本改换作用,例如躲藏暗码、格式化文本等。只会影响输入文本的可视化样式作用,并不影响实践的数据模型。代码如下:

TextField(
    value = textContent,
    // ...省掉了其他参数设置
    onValueChange = {},
    visualTransformation = if (textType == "java") {
        JavaKeywordVisualTransformation
    } else {
        VisualTransformation.None
    }
)

完成Java关键字高亮的VisualTransformation类:

/**
 * java关键字高亮显现
 */
object JavaKeywordVisualTransformation : VisualTransformation {
    override fun filter(text: AnnotatedString): TransformedText {
        val textString = text.text
        return TransformedText(toAnnotatedString(textString), OffsetMapping.Identity)
    }
}
/**
 * String转化为AnnotatedString
 */
private fun toAnnotatedString(string: String): AnnotatedString {
    val keywordColor = Color(0xFFCF8E6D)
    val treeMap = TreeMap<String, String>()
    keywordList.forEach {
        treeMap[it] = it
    }
    // 构建 Aho-Corasick 自动机
    val acdat = AhoCorasickDoubleArrayTrie<String>()
    acdat.build(treeMap)
    val stringLength = string.length
    return buildAnnotatedString {
        append(string)
        val hitList = acdat.parseText(string)
        hitList.forEach { hit: AhoCorasickDoubleArrayTrie.Hit<String> ->
            val start = hit.begin
            val end = hit.end
            // 假如后一个字符不是空格,证明不是一个单独的单词,则跳过
            if (end < stringLength) {
                val char = string[end]
                if (!char.isWhitespace()) {
                    return@forEach
                }
            }
            addStyle(
                style = SpanStyle(color = keywordColor),
                start = start,
                end = end
            )
        }
    }
}

4.5.本地图片显现

要显现APK解码后的图片资源,咱们需求从本机上加载图片资源,所以下面这种painterResource的方法就不适用了:

Image(
    painter = painterResource(项目resources文件夹中的图片文件,支撑png、svg等格式),
    contentDescription = "",
)

能够换用loadImageBitmap(InputStream)的方法,示例如下:

// 本地图片文件
val imageFile = File(filePath)
// 加载本地图片
Image(
    bitmap = loadImageBitmap(imageFile.inputStream()),
    contentDescription = ""
)

4.6.修正APK解码后的资源

以这样一个场景为例,咱们需求给某个APP抓包,可是Android7.0及以上抓包是会呈现问题的,无法容易抓取到Https恳求的明文数据。这是因为在7.0有一个名为“Network Security Configuration”的安全功用,这儿不再赘述。咱们的意图便是在解码后的res/xml目录中增加一个network_security_config_debug.xml文件,内容大致如下:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <base-config cleartextTrafficPermitted="true">
    <trust-anchors>
      <certificates src="system" overridePins="true" />
      <certificates src="user" overridePins="true" />
    </trust-anchors>
  </base-config>
</network-security-config>

然后需求在清单文件中增加或许修正application节点下的的android:networkSecurityConfig属性为”@xml/network_security_config_debug”,以此来允许咱们运用user目录下的证书来到达https抓包的意图。

XML文件的创立和修正功用咱们运用dom4j来处理,首要增加依靠:

// xml文件解析
commonMainImplementation("org.dom4j:dom4j:2.1.3")

运用dom4j生成network_security_config_debug.xml文件的代码如下:

fun createNetWorkSecurityConfigXml(
    outputFilePath: String
) {
    val document = DocumentHelper.createDocument()
    val networkSecurityConfigElement = document.addElement("network-security-config")
    val baseConfigElement = networkSecurityConfigElement.addElement("base-config")
    baseConfigElement.addAttribute("cleartextTrafficPermitted", "true")
    val trustAnchorsElement = baseConfigElement.addElement("trust-anchors")
    trustAnchorsElement.addElement("certificates")
        .addAttribute("src", "system")
        .addAttribute("overridePins", "true")
    trustAnchorsElement.addElement("certificates")
        .addAttribute("src", "user")
        .addAttribute("overridePins", "true")
	var xmlWriter: XMLWriter? = null
    var outputStream: FileOutputStream? = null
    try {
        val litterFile = File(outputFilePath)
        outputStream = FileOutputStream(litterFile)
        val outputFormat = OutputFormat.createPrettyPrint()
        outputFormat.encoding = "UTF-8"
        xmlWriter = XMLWriter(outputStream, outputFormat)
        xmlWriter.write(document)
    } catch (e: Throwable) {
        e.printStackTrace()
    } finally {
        xmlWriter?.close()
        outputStream?.close()
    }
}

修正清单文件的功用就交给咱们脑补完成了,无非便是解析xml文件,查找到application节点,判断是否有android:networkSecurityConfig属性,有则改之,无则加之。

4.7.从头打包APK并对齐、签名

再次阐明一下:首要大部分的功用都是依据jar文件或exe文件(Windowns上)或许其他可履行文件(Mac上),那么在Java/Kotlin中咱们能够经过如下方法来调用这些外部程序,exec其实终究也是调用了ProcessBuilder,全体的原理便是如此:

//方法1
Runtime.getRuntime().exec(cmd)
//方法2
ProcessBuilder(cmd)

这儿触及到的便是安卓打包的相关常识了,需求运用到android sdk/build-tools下的相关东西了,例如zipalign、apksigner.jar等东西。有一点需求留意,官方解释如下:

Caution: You must use zipalign at a specific point in the build process. That point depends on which app-signing tool you use:

If you use apksigner, zipalign must be used before the APK file has been signed. If you sign your APK using apksigner and make further changes to the APK, its signature is invalidated. If you use jarsigner (not recommended), zipalign must be used after the APK file has been signed.

因为咱们运用apksigner.jar对APK文件进行签名,所以需求在签名前先运用zipalign进行对齐处理,根本指令如下(这儿以Mac为例):

zipalign -p -f -v 4 对齐前的APK途径 对齐后的APK途径

运用ProcessBuilder()对APK文件对齐的方法便是如下这样,runCMD()方法咱们下一末节再详细介绍:

fun alignApk(
    srcApkPath: String,
    outputApkPath: String
): Boolean {
    var isAlignSuccess = false
    val result = runCMD(
        localZipAlignPath(), // 复制到本地的zipalign文件,区别体系、架构
        "-p",
        "-f",
        "-v",
        "4",
        srcApkPath,
        outputApkPath,
        onLine = {
            logger("Align APK : $it")
            if (it.contains("Verification succesful")) {
                isAlignSuccess = true
            }
        }
    )
    return result == 0 && isAlignSuccess
}

对齐后,运用apksigner.jar进行签名的过程也同理,签名的指令如下:

apksigner sign --verbose --ks 签名文件途径 --ks-pass pass:${签名文件暗码} --ks-key-alias 签名别名 --key-pass pass:${签名暗码} --out 签名后的APK途径 签名前的APK途径

运用runCMD()方法履行代码如下:

    fun signApk(
        alignedApkPath: String,
        outputApkPath: String,
        keyStorePath:String,
        keyStorePassword:String,
        keyAlias:String,
        keyPassword:String
    ): Boolean {
        var isSignSuccess = false
        val result = runCMD(
            "java",
            "-jar",
            localApkSignerJarPath(), 	// 复制到本地的apksigner.jar文件
            "sign",
            "--verbose",
            "--ks",
            keyStorePath,
            "--ks-pass",
            "pass:${keyStorePassword}",
            "--ks-key-alias",
            keyAlias,
            "--key-pass",
            "pass:${keyPassword}",
            "--out",
            outputApkPath,
            alignedApkPath,
            onLine = {
                logger("Sign APK : $it")
                if (it.contains("Signed")) {
                    isSignSuccess = true
                }
            }
        )
        return result == 0 && isSignSuccess
    }

留意:为什么需求将resources中的相关文件复制到本地,在之前的文章中有提到,ComposeDesktop打包好后,这些资源会被统一打包到一个jar文件中去,无法直接履行,所以需求先将其复制出来到本地。

4.8.体系及架构的适配

正常状况下咱们简略的对ProcessBuilder进行如下封装就能够履行大部分的指令或脚本了:

/**
 * 运转CMD指令(不区别体系环境)
 */
fun runCMD(
    vararg elements: String,
    directory: File? = null,
    onLine: (String) -> Unit
): Int {
    val cmdStringBuilder = StringBuilder()
    elements.forEach {
        cmdStringBuilder.append(it).append(" ")
    }
    val process = ProcessBuilder(*elements)
        .directory(directory)
        .redirectErrorStream(true)
        .start()
    val reader = BufferedReader(
        InputStreamReader(
            process.inputStream,
            Charset.forName("UTF-8")
        )
    )
    var line: String?
    while (reader.readLine().also { line = it } != null) {
        line?.let {
            onLine(it)
        }
    }
    return process.waitFor()
}

可是针对上一末节介绍的东西,咱们会发现有的东西是区别体系和架构的,以对齐东西zipalign为例:Windows上应该用zipalign.exe,Mac上应该用zipalign可履行文件,可是Mac近年来推出的Apple芯片是ARM架构,所以针对ARM和AMD64等不同架构也要选择不同的zipalign文件。

这就要求咱们先准备好这些不同架构、不同体系的资源,做到依据不同体系、架构进行加载适配(以下方法并不彻底适配一切体系和架构,还需完善):

/**
 * 获取体系名,依据称号判断体系类型
 */
fun getSystem(): String {
    return System.getProperties().getProperty("os.name")
}
/**
 * 判断是否是mac
 */
fun isMac(): Boolean {
    return getSystem().lowercase().contains("mac")
}
/**
 * 判断是否是windows
 */
fun isWindows(): Boolean {
    return getSystem().lowercase().contains("windows")
}
/**
 * 获取cpu架构
 */
fun geChip(): String {
    return System.getProperties().getProperty("os.arch")
}
/**
 * 判断是否是arm架构
 */
fun isArm(): Boolean {
    return geChip().lowercase().contains("aarch64")
}
/**
 * 判断是否是amd64架构
 */
fun isAmd64(): Boolean {
    return geChip().lowercase().contains("amd64")
}

组织这些文件的时分,咱们将文件依照如下方法放到了resources目录,如下所示:

使用Compose开发一款桌面端APK逆向工具
当需求履行这些文件的时分,咱们依据体系类型及架构复制对应的东西到本地进行存储(只是针对常用的Mac和Win进行了适配,其他暂无)。

当履行这些可履行文件的时分,Mac和Linux上能够直接履行(前提是需求可履行权限),可是在Win上履行相关指令前,或许需求一个cmd /c的前缀才行,所以针对不同的体系,履行指令也需求做一层区别,如下所示:

/**
 * 运转CMD指令
 * 区别win和unix(Mac OS和Linux)环境,一般履行exe或许unix上的可履行文件
 *
 * @param winCMD win指令前缀
 * @param unixCMD unix指令前缀
 * @param cmdSuffix 统一的后缀,会拼接到前面的数组上
 */
fun runCMD(
    winCMD: Array<String>,
    unixCMD: Array<String>,
    cmdSuffix: Array<String> = emptyArray(),
    directory: File? = null,
    onLine: (String) -> Unit
): Int {
    val cmd = if (isWindows()) {
        winCMD
    } else {
        unixCMD
    } + cmdSuffix
    return runCMD(elements = cmd, directory = directory, onLine = onLine)
}

那么针对不同的体系,以gradlew指令打包的状况示例(很突兀的就切换到这儿了,因为在咱们的打包服务中有这样的状况),运用方法则如下所示:

runCMD(
    winCMD = arrayOf("cmd", "/c", "gradlew"),
    unixCMD = arrayOf("./gradlew"),
    cmdSuffix = arrayOf("assembleXxxRelease"),
    directory = targetDir,
    onLine = {
    }

5.其他问题一览

5.1.APK文件的解紧缩

下面是一个常用的解压文件的代码示例,当APK文件较小的时分,解压正常,可是当一个APK文件比较大的时分,这时分解压就会呈现异常状况,在注释中能够看到,zipEntry获取到的name成果一直是空的,导致直接退出了解压:

fun decompressByZip(
    zipFilePath: String,
    outputDirPath: String
) {
    val buffer = ByteArray(1024)
    try {
        val outputDir = File(outputDirPath)
        if (!outputDir.exists()) {
            outputDir.mkdirs()
        }
        val zipInputStream = ZipInputStream(File(zipFilePath).inputStream())
        var zipEntry: ZipEntry? = zipInputStream.nextEntry
        while (zipEntry != null) {
            // 有获取到成果为空字符串的状况
            if (zipEntry.name.isNullOrBlank()) {
                continue
            }
            val newFile = File(outputDirPath, zipEntry.name)
            if (zipEntry.isDirectory) {
                newFile.mkdirs()
            } else {
                val parentDir = newFile.parentFile
                if (parentDir != null && !parentDir.exists()) {
                    parentDir.mkdirs()
                }
                val bufferedOutputStream = BufferedOutputStream(FileOutputStream(newFile))
                var bytesRead: Int
                while (zipInputStream.read(buffer).also { bytesRead = it } != -1) {
                    bufferedOutputStream.write(buffer, 0, bytesRead)
                }
                bufferedOutputStream.close()
            }
            zipEntry = zipInputStream.nextEntry
        }
        zipInputStream.closeEntry()
        zipInputStream.close()
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

最终或许是因为APK文件运用了非标准的紧缩方法,所以换用了Apache Commons Compress,依靠库及版别如下:

commonMainImplementation("org.apache.commons:commons-compress:1.22")

解压代码示例如下:

fun decompressByZip(
    zipFilePath: String,
    outputDirPath: String
) {
    try {
        val outputDir = File(outputDirPath)
        if (!outputDir.exists()) {
            outputDir.mkdirs()
        }
        ZipFile(zipFilePath).use { zip ->
            zip.entries.asSequence().forEach { entry ->
                val entryFile = File(outputDir, entry.name)
                logger("decompress zip: ${entryFile.name}")
                // 保证父目录存在
                entryFile.parentFile?.mkdirs()
                if (entry.isDirectory) {
                    // 假如是目录,创立对应的目录
                    entryFile.mkdirs()
                } else {
                    // 假如是文件,将文件解压到方针目录
                    zip.getInputStream(entry).use { input ->
                        FileOutputStream(entryFile).use { output ->
                            input.copyTo(output)
                        }
                    }
                }
            }
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

6.总结

文章到此就进入了结尾,全体下来自我感觉文章比较散乱,尽管文章全体没有多少深度,可是所触及的常识点仍是比较多的。因为自己也是刚触摸逆向的菜鸟一只,文章中所用的东西都是最根本的,无法获取到加固后的APK源码,不过处理下根本的资源文件仍是比较容易的。后续或许也是需求触摸脱壳等相关内容,有进展会及时再跟咱们文章共享,就到这儿吧。

赶上端午,祝咱们端午节健康,也高兴!!!