目录
4:合并AndroidManifest,so库,assets,res目录
3、versionCode和targetSdkVersion
前言
反编译打包:母包的反编译,渠道SDK的装载,合并,渠道包的回编,签名……
先看一张流程图,了解反编译打包模式的一些阶段性问题
1:apktool的反编译
1、使用工具
apktool.jar
下载地址:iBotPeaches / Apktool / Downloads — Bitbucket
2、命令行
java -jar -Xms512m -Xmx512m apktool_2.6.0.jar -q d -b -f xxx.apk -o targetdir
3、效果展示
简单包的目录( 正常去合并)
包含koltin,多个smali包的目录(母包包含多个smail,需要复制到第一个内,便于分包时计算,进行合并)
4、问题考虑
1.需要考虑母包大小,反编译成本,这个阶段比较吃cpu
2.需要考虑母包目录结构,方便后续分包的计算
3.如仅对AndroidManifest有改动,如版本号,权限,包名等,可以手动改文件,再进行回编,建议还是脚本处理。
2:SDK的jar转dex
1、使用工具
#--------dx工具---------或
dx.bat所在目录:android-sdk\build-tools\28.0.2\dx.bat
dx.jar所在目录:android-sdk\build-tools\28.0.2\lib\dx.jar
#--------d8工具---------
d8.bat所在目录:android-sdk\build-tools\28.0.2\d8.bat
d8.jar所在目录:android-sdk\build-tools\28.0.2\lib\d8.jar
2、命令行
dx
dx.bat --dex --output=dstDir + "/classes.dex" sdk1.jar sdk2 ...
dx.bat --dex --min-sdk-version=26 --output=dstDir + "/classes.dex" sdk1.jar ...
d8
d8.bat --lib android.jar --output dstDir sdk1.jar ...
3、效果展示
4、问题考虑
1.dx工具不支持lambda,脱糖需要d8或者dx的--min-sdk-version=26
2.如果使用d8,屏蔽一下d8.jar里的warning日志,可以减少命令行输出
3.如果渠道提供的是aar版本,需要分解aar,然后再转dex文件
3:dex转smail的过程
1、使用工具
baksmali.jar
下载地址:JesusFreke / smali / Downloads — Bitbucket
2、命令行
java -jar baksmali.jar -o targetdir dexFile
3、效果展示
这样子就可以将SDK的smali合并到母包目录下了,然后再进行资源文件的处理。
4、问题考虑
1、需要考虑SDK目录合并到母包里的冲突问题(脚本是直接会覆盖掉的,现在打包倒是没有遇到问题,不确定后续会不会有。是个潜在风险,游戏方集成的sdk版本是1.0.0,渠道用到sdk是1.1.0,因为第三方sdk不同,最好是高版本兼容低版本。目前脚本是渠道覆盖母包,如果渠道里的SDK是高版本,可能就会兼容了,那如果渠道的低,母包的高,覆盖后,新特性新功能肯定就丢失了。可以选择不覆盖,或者剥离渠道SDK冲突的SDK,需要对smail目录结构比较熟悉)
2、认识smail语法
3、针对不同游戏,集成微信支付,收不到回调,需在WXPayEntryActivity.smail文件里,替换包名,这样子回编译后,apk包名路径下才会有微信的回调类。
4:合并AndroidManifest,so库,assets,res目录
1、合并AndroidManifest
这个过程倒不难,毕竟可以拿到反编译后的AndroidManifest。需要考虑的是packagename,meta标签,launchMode等的修改,以及合并过程中已存在的标签,需要用到xml.etree.cElementTree库。可以参考用python修改AndroidManifest.xml_sunbofiy23的博客-CSDN博客
2、so库
因为每个游戏支持的cpu架构不同,而大部分渠道都要手游支持64位了,因此,so库的合并需要适配 armeabi-v7a|arm64-v8a
3、assets文件
这个没什么说的,主要是一些特殊参数的写入,比如游戏在渠道生成的appid,appkey等
4、res目录
直接将sdk res copy至母包res,为后续重新生成R.java做准备。
5:添加icon角标,启动页等
1、添加icon
处理思路是:拿到渠道方提供的icon角标,利用PIL库的Image,将角标png和游戏png贴合,因为drawable和mipmap目录经编译后会加上-v4,因此贴后的png放到-v4目录下。至于是放到mipmap-v4还是drawable-v4,需要结合AndroidManifest确认,不同开发者提供的母包不同,使用规范也不同,理论上mipmap
2、启动页
这个过程的处理思路,大致是:如有需要添加启动页的渠道,
1、准备xml布局和要加载的xx.png合并到母包的layout和drawable目录下
2、removeGameLaunchActivity
将母包的启动Activity
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
intent-filter去掉
3、将自己准备的SplashActivity写入母包AndroidManifest,参考代码如下:
def appendSplashActivity(decompileDir, splashType):
manifestFile = decompileDir + "/AndroidManifest.xml"
manifestFile = file_utils.getFullPath(manifestFile)
ET.register_namespace('android', androidNS)
key = '{' + androidNS + '}name'
screenkey = '{' + androidNS + '}screenOrientation'
theme = '{' + androidNS + '}theme'
lanchLoad = '{' + androidNS + '}launchMode'
configChanges = '{' + androidNS + '}configChanges'
tree = ET.parse(manifestFile)
root = tree.getroot()
applicationNode = root.find('application')
if applicationNode is None:
return
splashNode = SubElement(applicationNode, 'activity')
splashNode.set(key, 'com.lhcit.game.api.activity.GameSplashActivity')
splashNode.set(theme, '@android:style/Theme.Black.NoTitleBar.Fullscreen')
if splashType[:1] == '1':
splashNode.set(screenkey, 'landscape')
else:
splashNode.set(screenkey, 'portrait')
splashNode.set(lanchLoad,"singleTop")
splashNode.set(configChanges,"orientation|screenSize|keyboardHidden")
intentNode = SubElement(splashNode, 'intent-filter')
actionNode = SubElement(intentNode, 'action')
actionNode.set(key, 'android.intent.action.MAIN')
categoryNode = SubElement(intentNode, 'category')
categoryNode.set(key, 'android.intent.category.LAUNCHER')
tree.write(manifestFile, 'UTF-8')
4、找到smail文件,替换内部的跳转action。因为我们在准备GameSplashActivity这个Activity时,除了会去加载xml,还有延时跳转到游戏主Activity,intent跳转的路径先用占位符声明,毕竟每个游戏不一致,因此在打包的时候,还需要按包名完整路径+游戏的Activity替换掉占位符。
3、versionCode和targetSdkVersion
在渠道包送审的过程,一些渠道会对targetSdkVersion有要求,比如UC要求低于26,xx渠道低于28,而母包可能是30+,如果我们手动处理的话,需要准备多个不同版本的母包,这样子效率会很低。因此,在反编译后可以拿到apktool.yml文件,我们便可以通过脚本进行文本修改替换。versionCode的场景和需求类似。
6:处理R资源
这里的R资源处理逻辑是:合并完Sdk的res到母包res目录后,用aapt工具重新生成R.java文件。然后将R.java通过javac编译为class文件,class文件转为dex,dex转为smail后,合并到母包里。
1、使用工具
aapt.exe
aapt所在目录:android-sdk\build-tools\28.0.2\aapt.exe
javac
javac所在目录:jdk\bin\javac.exe
拿到R.class后,进行dx,baksmail的转化过程,最后合并到母包里。
2、命令行
aapt p -f -m -J targetdir -S sourcedir -I android.jar -M AndroidManifest.xml
需要依赖android.jar ,以及合并后的AndroidManifest文件,注意大小写 。生成的R文件是依据应用包名生成,如果SDK里也有R路径,就需要将应用R.java copy至临时文件,修改应用路径为SDK的R路径,这样子再编译,转换后,SDK包名下的smali里也有一份R文件,避免运行时找不到SDK的资源。这里的解决方案是适配多包名,参考问题考虑4-1
3、效果展示
4、问题考虑
1、三方SDK直接通过R...这种方式引用资源,导致运行时找不到,需要适配多包名
U8SDK——多包名下生成R.java(海外SDK接入普遍遇到的坑)
2、aapt 去合并 aapt2编译的包,resource.arsc文件,不能以attr开头
基于U8SDK安卓打包反编译,回编错误记录_u8sdk github_sunbofiy23的博客-CSDN博客
3、母包全局自定义标签经过反编译,变为局部标签后,找不到资源。需处理渠道和母包R$styleable.smali,可参考博客
U8SDK——declare-styleable自定义资源的合并
7:分包
如果R资源没问题,下面就是谷歌限制的安卓65536问题了(Android Dalvik可执行文件.dex中的Java方法数引用超过65536)
处理思路是先计算smail方法数,那么如何较准确的计算呢?我们先遍历整个smail目录,然后读文本,这里就要用到smail语法了,在smail文件里,方法是以.method开始,invoke-结尾,因此,我们可以通过这个规则计数。
当超过65536后,我们将一个smail目录分成多个smail_classes,这样在回编的时候,单个smail就不会超过6553了。
问题:当母包里已经包含有多个smail目录时,怎么准确计算,完成分包呢?这里的处理思路:可以在反编译后,将多的smail合并到第一个里,只要不回编译,是没有65536问题的,这样子,第一个smail目录一定很大,再到分包阶段,利用方法计数,重新分包。
8:回编译
1、使用工具
apktool.jar
下载地址:iBotPeaches / Apktool / Downloads — Bitbucket
2、命令行
java -jar -Xms512m -Xms512m apktool_2.6.0.jar -q b -f decompile -o output/r.apk
3、效果展示
4、问题考虑
1.比较吃cpu性能
2.Could not smali file:有一些lamdba表达式,smail语法无法识别,在dex转smail的过程里,避免使用dx工具,安卓已支持d8语法脱糖
3.有时候因为分包不准确,导致的回编还会超过65536,解决方案是分包阈值调小。尽量每一个smail目录不要太接近65536
9:签名
1、使用工具
jarsigner.exe
所在目录:jdk\bin\jarsigner.exe
apksigner.jar
所在目录:build-tools\27.0.3\lib\apksigner
2、命令行
1.查看apk的签名信息
jarsigner.exe -verify -verbose -certs xx.apk
2.查看jsk或keystore文件的签名信息
keytool -v -list -keystore xx.keystore
3.对apk进行v1签名
jarsigner -keystore xx.keystore -storepass xx -keypass xaliaspwd xx.apk alias -sigalg SHA256withRSA -digestalg SHA-256
4.对apk进行v2签名
java -jar apksigner.jar sign -verbose --ks xx.keystore --ks-pass pass:xx --ks-key-alias xx.keystore --key-pass pass:xx --out output.apk source.apk
3、效果展示
1.对apk进行签名信息查看
2.对apk进行v1签名
2.对apk进行v2签名
明显能感觉签名时间缩短了。
4、问题考虑
1.jarsigner签名工具SHA1算法警告
使用jarsigner工具apk签名算法问题_采用sha1withrsa算法风险_sunbofiy23的博客-CSDN博客
2.jarsigner替换为apksigner后,签名效率的提升
因jarsigner是对每一个java文件签名,因此时间较长,apksigner是对整个文件签名,用时较短
3.Android7.0 tool里使用的是的apksigner.jar支持到v2签名。如果使用7.0以上的 ,默认包含v1,v2,v3签名,根据渠道要求,选择合适的签名工具。
10:优化
开发者在上传应用到Google play的时候相信都会遇到过“您上传的APK有没有经过Zipalign处理”的失败提示,显而易见Google对Zipalign工具的重视。
Zipalign是一个对Apk包里的所有文件进行存档对齐的优化工具,它的目的是确保所有文件里未压缩的数据都从它所属文件的开始位置(如顶格写数据)并以指定的对齐方式排列。尤其是.apk压缩包中的图片资源和未加工处理的相关文件,对齐的方式是以4个字节对齐。
其好处是能够减少应用程序的RAM(Random Access Memory 随机存储器)内存资源消耗,提高用户使用的顺畅度。Google的Adnroid开发文档中特别之处在于发布应用到最终客户之前务必使用Zipalign工具对你的.apk文件进行优化。
1、使用工具
zipalign.exe
所在目录:android-sdk\build-tools\27.0.3\zipalign.exe
2、命令行
4表示指定的对应字节数,是一个整数则必须指定为4
zipalign -f 4 xx.apk target.apk
11:加固与脱壳
加固本质上就是对 dex 文件进行加壳处理,让一些反编译工具反编译到的是 dex 壳,而不是 dex 文件本身。具体的实现方式是,将原 dex 文件进行加密,再合成到 dex 壳中,而系统运行应用的时候,会加载 dex 壳文件,而 dex 壳里面有一个自定义的 ClassLoader 类,它会将原有 dex 文件进行解密,然后再加载到 dex 数组中。因为项目目前的加固还是使用360免费的加固服务。自己也尝试看了360的官方文档,也尝试了使用命令行去加固。免费版是不支持的。
这里贴了一下360命令行加固文档:
以及收费标准和支持服务:
后续,自己也是尝试了下载360加固服务,效果如图,免费版支持客户端,不支持命令行的。
而脱壳本质上就是对加固进行破解,常用的手段是内存dump,市面上用一些hook框架,比如frida等。关于frida的介绍,可以从我的另一篇博客了解 python+camille+frida,实现对apk隐私信息的堆栈分析_sunbofiy23的博客-CSDN博客
而使用frida进行脱壳,可以参考
https://github.com/GuoQiang1993/Frida-Apk-Unpack
12:番外(多线程尝试):
自己也尝试了开启多线程打包,但是经过试验,发现单线程一个渠道包反编译假设耗时100s,到开启多线程,如3个线程后,变成了300s。而后分析原因,可能是打包工具,打包流程开启了多线程,因为反编译,回编,分包这些阶段,设计大量的copy及jre,而本机jdk只有一个,而导致了耗时加倍。也考虑过因为同一个母包,只需反编译一次,但是回编分包这些阶段是没办法一次解决的,毕竟是多渠道包的工作,那集群开发是不是可以解决,但是集群相关知识,自己又不太了解,或者集群这个成本太大了,小公司根本负担不起。所以多渠道的线程优化任重而道远。
3线程3渠道包:
反编译耗时380s左右
回编耗时203s左右
分包耗时477s左右
主线程单渠道:
反编译耗时93s
分包耗时130s
回编耗时180s左右
总结
一些渠道或者工信部,会要求apk进行加固,理论在签名结束后,还会有apk加固的阶段,我们当时采用的是360,腾讯乐固的第三方方案, 其实也有一些自研的加固方式
最开始接触这套打包方式时,坑真的太多了,后续一点点的解决,然后整理总结,包括工具的升级,慢慢的支持通用化配置到多渠道打包,日志管理,各阶段效率的改善等等。总之,一点点的分析,然后尝试,再大的难题也会解决掉。