使用开发一款桌面端多功能Apk工具,你值得拥有

2023-08-11
来源:网络整理

1 简介

《使用和开发桌面多功能Apk工具》这篇文章你还有印象吗,文章地址:这是作者阅读次数最多的文章。 现在这个工具已经升级了很多次,移植给我们了,但可惜的是它和公司APP端的业务绑定紧密,所以最终不适合开源。

但这一次,我使用并开发了一个桌面APK逆向工具。 里面使用的很多工具都是从以前的桌面工具复制过来的。 原理是一样的,代码已经开源:(目前基础部分已经搭建完成,其余功能会逐步完善,如果有什么想法请告诉我)

2、小情绪

距离上次写文章已经过去不到半年的时间了。 年初之后,我一直在应用程序删除和重新启动之间来回切换,在隐私合规和侵犯之间来回切换。 实在是太累了。 好在我已经基本搞清楚了国内各大应用商店的相关规则,以及马甲袋判断的相关原则。

马甲包,情商高的应该叫“APP矩阵”,像我这样情商低的就叫马甲包。 为什么要做背心袋? 说实话,这类社交直播应用一不小心就有可能被屏蔽。 虽然各种色情、风控等基础设施都有,但路高一尺,魔高一丈。 一只脚,总会有漏网之鱼。 加之政策收紧,多应用布局已成必然。

那么这种情况下有两种选择:

第一种方法费时、费力、成本高。 除非是新项目,大多数公司绝不会选择这么做。 第二种方法简单粗暴,快速高效,是大多数人的首选,但其缺点也很明显。 代码、资源等文件重复度高,很容易被判断为类似应用,从而被限制(OPPO)或者拒绝put(),所以我们需要一个策略来应对。

上面说了这么多,你一定能猜到我后续的新文章应该往哪个方向发展,不过也不用担心一口气吃胖。 让我们保持简单。 我们先从APK文件的反面开始理解。 逆向工程所需的工具,然后我们将其集成到我们自己的桌面应用程序中。 这个桌面应用程序的目标是能够“解码APK”文件,修改解码后的文件,比如修改应用程序名称、应用程序图标等简单的东西,最后“重新打包”成一个新的APK文件,“对齐” ,重新签名”。

3. 逆向工程工具介绍 3.1.

地址:

网站地址:

这是我目前最常用的工具。 它可以将APK文件解码为资源文件和代码等。我们可以编辑解码后的布局文件、图片、字符串等资源小程序开发横向布局,甚至可以修改代码。 修改后,这些文件也可以重新打包成新的APK文件。

常用命令如下:

# 解码APK文件,可以使用decode命令
apktool decode old.apk -o outputDir

#
 将解码后的文件构建为一个新的APK文件,可以使用build命令
apktool build outputDir -o new.apk

**注意:** 重建的APK文件已经丢失了签名信息,如果使用需要对齐签名。 目前我们应用后期的一些定制功能基本都会用到他。

3.2. 贾德克斯

地址:

相比之下,Jadx可以直接将APK、Dex文件等反编译成Java源代码。 它还提供了图形界面、命令行功能以及相关的依赖库。

本例中我们直接集成了仓库中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文件

地址:(已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文件

地址:(4年未更新)

网站首页:

将Jar文件反编译为Java文件的工具包括命令行工具JD-Core、图形化页面JD-GUI以及依赖库。

地址:

根据大多数逆向工程师的比较,这个工具比较好用,常用命令如下(换成jadx之前我也用过这个工具):

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

地址:

IDEA自带的反编译工具也相当不错。 但需要自己使用编译好的可执行jar文件。 常用命令如下:

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

病死率

地址:

网站首页:

我自己没有使用过这个工具,所以这里就不过多介绍了。 有兴趣的朋友可以自己去看看。

4.桌面逆向APK应用开发

接下来我们需要用它来开发桌面APK逆向工具,需要集成上面提供的可执行jar文件以及jadx的仓库依赖。

4.1. 文件拖放

首先我们需要支持将APK文件拖拽到我们的工具面板中,然后自动执行后续流程。 之前的文章也写过,但是文章发表后发现了一个bug。 添加该功能后,调整应用程序窗口大小的功能失效。 但随后@读者反馈并给出了解决方案:

@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文件拖到当前项目面板时,我们首先使用工具解析APK文件,获取“包名_版本号”等基本信息后构造项目根目录。 然后将解码后的APK文件复制到该文件夹​​中。 解码后,我们使用jadx将解码后的文件反编译为java源文件并存放在文件夹中。 如果我们需要查看dex文件,我们可以将apk文件解压到一个文件夹中,这样我们就创建了一个基本的项目目录,然后构建目录树来显示。 我们可以使用 file.walk 来构建目录树。 ()来遍历然后存储对应的信息。

为了更专注的查看我们需要的文件,我们还添加了项目目录类型切换功能,与IDEA类似,切换到模式时,java目录下会显示java源文件,资源文件会在res目录下显示出来,文件目录也会显示出来,方便大家比较java和文件:

当然,当显示项目结构树,或者选择源代码文件时,右侧代码编辑区域的长度或宽度会超出应用程序大小,如下图所示,所以需要支持水平滚动和垂直滚动功能:

这根本不是问题,我们可以直接封装一个简单的函数,如下:

/**
 * 带有滚动条的面板
 * 支持横向滚动条,竖向滚动条
 */


@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. 文件选项卡页

在做这个标签页功能的时候,涉及到一个知识点【固有特性测量】。 请参阅内容介绍。 如下所示,当标题栏的长度不同时,我们需要根据标题的具体长度来确定下面的指示器的长度。

此时,我们可以利用固有的特征测量来实现这种效果,在最外层使用.( = .Max)修饰符来指定根据子项的最大宽度进行测量,子项的宽度为自适应,并且子项指示器(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))
    )
}

效果如下:

横向布局和纵向布局_什么是横向布局_小程序开发横向布局

4.4. 关键词高亮

本来,在显示java和文件字符串之后,我打算做下一个功能。 然而代码显示出来后,总感觉少了点什么。 和真实的IDE对比后发现缺少关键字高亮功能。 目前仅针对Java关键字实现高亮效果,如下:

java方面,有50多个关键字。 不可能匹配超过50次再找出关键字的索引位置,这样效率太低了。 那么这里介绍一种高效的多模式匹配算法——[Aho-],只需一次遍历即可匹配所有关键字(字符串)。 至于算法原理请自行学习。 这里我们直接依赖java中的Aho-算法库来满足我们的需求。 目前使用的依赖库及版本如下:

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

当所有关键字都匹配时,为了实现丰富的文本样式、字体、颜色、下划线等效果,可以通过“文本”中的“”进行处理,可以通过设置“文本”中的参数“”来实现”。 用于控制输入文本的视觉风格变换,可用于实现各种文本变换效果,如隐藏密码、格式化文本等。它只影响输入文本的视觉风格效果,并不影响输入文本的视觉风格效果。影响实际的数据模型。 代码如下所示:

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

实现Java关键字高亮的类:

/**
 * 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()
    keywordList.forEach {
        treeMap[it] = it
    }

    // 构建 Aho-Corasick 自动机
    val acdat = AhoCorasickDoubleArrayTrie()
    acdat.build(treeMap)

    val stringLength = string.length

    return buildAnnotatedString {
        append(string)

        val hitList = acdat.parseText(string)
        hitList.forEach { hit: AhoCorasickDoubleArrayTrie.Hit ->
            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解码后的图片资源,我们需要从本机加载图片资源,所以下面的方法不适用:

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

可以使用()代替,示例如下:

// 本地图片文件
val imageFile = File(filePath)

// 加载本地图片
Image(
    bitmap = loadImageBitmap(imageFile.inputStream()),
    contentDescription = ""
)

4.6. APK解码后修改资源

以这样的场景为例,我们需要对一个APP进行抓包,但是抓包.0及以上的数据包就会出现问题,并且无法轻易抓到请求的明文数据。 这是因为7.0中有一个名为“ ”的安全功能,这里不再赘述。 我们的目的是在解码后的res/xml目录下添加一个.xml文件,内容大致如下:


<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>

那么就需要在文件中将“”节点下的“:g”属性添加或者修改为“@xml/”,这样才能让我们使用用户目录下的证书来达到抓包的目的。

我们使用创建和修改XML文件的功能来处理,首先添加依赖:

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

生成.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文件,找到节点,判断是否有:g属性。

4.7. 重新打包 APK 并对齐和签名

我再解释一下:首先,大部分功能都是基于jar文件或者exe文件(上图)或者其他可执行文件(Mac上),那么我们可以在Java/中通过如下方式调用这些外部程序,exec就是其实最终的调用,总体原理是这样的:

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

这里涉及到的是打包的相关知识,需要用到sdk/-下的相关工具,比如.jar等工具。 有一点需要注意的是,官方的解释如下:

:您必须在 中使用 at a 。 在您使用的应用程序工具上:

如果使用,必须是已经使用过的APK文件。 如果您签署您的 APK 并制作该 APK,则其为 。如果您使用(不),则必须使用已使用的 APK 文件。

因为我们使用.jar来对APK文件进行签名,所以在签名之前我们需要用它来进行对齐。 基本命令如下(这里以Mac为例):

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

使用()对齐APK文件的方法如下。 ()方法将在下一节中介绍:

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
}

对齐后,用.jar签名的过程是一样的。 签名命令如下:

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

使用()方法执行代码如下:

    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
    }

注意:为什么需要复制本地的相关文件,上一篇文章提到,打包后这些资源会被打包成jar文件,不能直接执行,所以需要先复制出来当地的。

4.8. 系统和架构适配

一般情况下,我们只需将大多数命令或脚本封装起来即可执行,如下所示:

/**
 * 运行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()
}

但对于上一节介绍的工具,我们会发现有些工具是区分系统和架构的。 以对齐工具为例:网上应该使用.exe,Mac上应该使用可执行文件,但Mac近几年推出的芯片都是ARM架构,所以不同架构要选择不同的文件例如 ARM 和 .

这就需要我们先准备好这些不同架构和系统的资源,以便根据不同的系统和架构进行加载和适配(以下方法并不完全适合所有系统和架构,需要改进):

/**
 * 获取系统名,根据名称判断系统类型
 */

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")
}

在整理这些文件时,我们将文件放入如下目录中,如下:

当需要执行这些文件时,我们根据系统类型和架构将相应的工具复制到本地存储(仅适配常用的Mac和Win,没有其他)。

在执行这些可执行文件时,Mac和都可以直接执行(前提是需要可执行权限),但是在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)
}

然后针对不同的系统,用命令打包的例子(这里切换起来很唐突,因为我们的打包服务就有这种情况),用法如下:

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

5. 其他问题清单 5.1. APK文件解压

下面是一个常用的解压文件的代码示例。 当APK文件较小时,解压正常,但当APK文件比较大时,解压时会出现异常情况。 你可以在评论中看到它。 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文件使用了非标准的压缩方式,所以被替换了。 依赖库及版本如下:

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()
    }
}

六、总结

文章到这里就结束了。 总体来说,我感觉这篇文章比较散乱。 文章整体虽然没有太多深度,但还是涉及到很多知识点。 由于我也是刚接触逆向工程的菜鸟,所以文中使用的工具都是最基本的,无法获取加固后的APK源代码,但是处理基础资源还是比较容易的文件。 后续可能还需要接触拆包等相关内容。 如果有进展的话我会及时把文章分享给大家,就到这里吧。

正值端午节,祝大家端午节健康快乐! ! !

分享