目录
1.前言
对《使用Compose Desktop开发一款桌面端多功能Apk工具》这篇文章还有印象吗,文章地址:https://juejin.cn/post/7122645579439538183,这是笔者目前阅读量最高的文章了,现在这款工具已经进行了多次升级并且移植到我们的CMS系统了,然而遗憾的是其跟公司APP端的业务绑定比较紧密导致最终也不适合开源出来。
不过这次呢我又使用ComposeDesktop开发了一款桌面端APK逆向工具,里面用到的很多都是从之前的桌面端工具拷贝过来的,原理一致,代码呢已经开源到了GitHub:https://github.com/vsLoong/ApkNurse (目前基础的部分已搭建完成,剩下的功能慢慢完善,大家有想法尽管给笔者提)
2.小感慨
距离上次写文章过去小半年了,开年后一直在应用下架与上架之间反复横跳,隐私合规与违规之间来回游走,真的是焦头烂额。不过好在基本摸清了国内各大应用商店上架的相关规则,以及马甲包判定的相关原理。
马甲包这个东西,高情商的说法应该叫“APP矩阵”,像我这种情商比较低的就直呼为马甲包了。为什么会做马甲包,不瞒大家,社交直播类型的这种应用一不小心可能就踩坑被封了,虽然各种鉴黄、风控等基本设施都有了,但道高一尺魔高一丈,总会有一些漏网之鱼。再加上政策的收紧,所以多应用布局就成了必然了。
那么这种情况下有两个选择:
- 1、像个乖孩子一样从头重新开发新的应用出来
- 2、像个耍小聪明的孩子一样用Product Flavor的方式克隆应用出来
第一种方式费时费力,费钱费人,除非是新项目,否则一般公司绝对不会选择这么做。第二种方式简单粗暴,快速高效,是绝大部分人的首选,但是其缺点也很明显,代码、资源等文件重复度高,容易被判定为相似应用,从而被限流(OPPO)或者拒绝上架(HUAWEI),所以我们就需要一个与之对抗的策略。
上文说了这么多想必大家也能猜到我后续新的文章应该往哪个方向去了,但是呢,别着急一口气吃成胖子,我们简单点,先从APK文件的逆向讲起来,了解下逆向所需的工具,然后我们将其集成到我们自己的桌面端应用中。这个桌面应用的目标就是要做到能解码APK文件,能修改解码后的文件,例如修改应用名,应用图标等等简单的东西,最后再重新打包为新的APK文件,对齐、重签名即可。
3.逆向工具简介
3.1.ApkTool
GitHub地址:https://github.com/iBotPeaches/Apktool
网站地址:https://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地址:https://github.com/skylot/jadx
对比ApkTool,Jadx能将APK、Dex文件等,直接反编译出来Java源代码。它同时提供了图形化的界面,命令行功能,以及相关依赖库。
在这次示例中我们直接集成了Jadx在Maven仓库中提供的依赖,具体方式GitHub中也有说明,有需要请参考https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library:
// 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地址:https://github.com/pxb1988/dex2jar (已经有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地址:https://github.com/java-decompiler (已经有4年未更新了)
网站主页:http://java-decompiler.github.io/
将Jar文件反编译为Java文件的工具,有命令行工具JD-Core,有图形化页面JD-GUI,也提供了Maven依赖库。
Procyon
GitHub地址:https://github.com/mstrobel/procyon
根据大部分逆向工程师的对比,这个工具比较好用,常用命令如下(在更换为jadx前我使用的也是这款工具):
# 反编译input.jar文件到outputDir文件夹中
java -jar procyon-decompiler.jar -o outputDir input.jar
Fernflower
GitHub地址:https://github.com/fesh0r/fernflower
IDEA自带的反编译工具,也比较不错。但是需要自己使用gradle编译出来可执行的jar文件,常用命令如下:
# 反编译input.jar文件到outputDir文件夹中
java -jar fernflower.jar input.jar outputDir
CFR
GitHub地址:https://github.com/leibnitz27/cfr
网站主页:https://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()来遍历然后存储相应的信息。
为了更专注于查看我们需要的文件,我们也添加了工程目录类型切换功能,类似IDEA一样,当切换为Packages模式的时候,就会将java源文件显示在java目录中,资源文件显示在res目录中,smali文件目录也会显示出来,方便大家对比java和smali文件:
当然了,当项目结构树显示出来后,或者选中源码文件后,右侧代码编辑区域长度或者宽度会超出应用大小,如下图所示,所以需要支持横向滚动和纵向滚动功能:
这对Compose来说完全不是问题,我们可以直接封装一个简单的Composable函数出来,如下所示:
/**
* 带有滚动条的面板
* 支持横向滚动条,竖向滚动条
*/
@Composable
fun ScrollPanel(
modifier: Modifier,
verticalScrollStateAdapter: ScrollbarAdapter,
horizontalScrollStateAdapter: ScrollbarAdapter,
content: @Composable BoxScope.()