严正声明:
- 1、相关破解技术仅限于技术研究使用,不得用于非法目的,否则后果自负。
上一篇文章《因一纸设计稿,我把竞品APP扒得裤衩不剩(上)》是一篇比较简单的:
技术文,但是这评论区的风气,貌似有点不对???
冤枉啊,小弟真没去过这种地方,也没体验过这种“服务”,只是道听途说,可能:
我描述得「绘声绘色」,加之各位看官「浮想联翩」,才会觉得「煞有介事」。
em…那个,可以扶下我起来么,那个…跪久了…腿有点麻…
顺带恭喜下:FPX 3-0 G2,喜提S9总冠军,FPX牛逼!!!破音!!!
亚索的快乐你不懂~
哈哈,回到本文:
顺带分享几个笔者常逛的逆向论坛:
- 看雪论坛:https://bbs.pediy.com/
- 吾爱破解:https://www.52pojie.cn/
- 蚁安网:https://bbs.mayidui.net/
- 逆向大佬姜维:http://www.520monkey.com/
贴心提醒:
此文内容较多,可能会有些枯燥,建议先点赞收藏,茶余饭后再慢慢品尝~
在开始折腾Android APP逆向前,你需要:
1、一台「具有完整Root权限」的Android手机,注意是「完整Root」权限!!!
比如「魅族手机」在设置->安全->Root权限,中可以开启Root权限,但是却是「阉割的Root权限」,安装SuperSu重启后就一直卡气球。
2、怎么Root?根据自己的手机机型百度和逛各种搞机论坛吧(不要问我!)一般的常见的流程:
解BL锁(BootLoader) -> 刷第三方Recovery(如TWRP) -> 卡刷Maglisk 或 SuperSU(Android 8.0以前)
3、不要轻易尝试使用哪种「一键Root」的软件(大部分是毒瘤,如KingRoot),当然不是就不能用,可以过河拆桥,比如我的魅蓝E2的Root流程:
4、推荐些能Root的手机?
Google Pixel亲儿子(真原生,香,就是性价比不高),小米,一加 等。
5、没钱买Android机或者已经有不能Root的手机了,可以试试「Android模拟器」
AS自带的AVD模拟器Root可以参见《搞机:AS自带模拟器AVD Root 和 Xposed安装》
也可以使用其他第三方的安卓模拟器,比如「夜游安卓模拟器、BlueStacks蓝叠」等。
在开始折腾APP逆向前,先来了解一些概念与名词~
获取APK的渠道:酷安、应用宝,豌豆荚等应用市场下载,有些还提供「应用历史版本」下载。
APK本质上是一个「压缩包」,把「.apk后缀」改为「.zip后缀」后解压,可以看到如下目录结构 (可能还有其他文件):
简单介绍下:
所谓的「编译」,就是把「源码、资源文件等」按照一定的「规则」打包成APK,官网 提供了详细的编译构建过程图:
简述下大概流程:
Step 1:资源文件处理「AAPT」
- assets会原封不动地打包在APK中;
- res中每一个资源会赋予资源ID,以常量形式定义在R.java中,生成一个resource.arsc文件(资源索引表)。
Step 2:aidl文件「aidl」
- 将aidl后缀的文件转换为可用于进程通信的C/S端Java代码。
Step 3:Source Code「Java Compiler」
- 编译生成.class文件。
Step 4:代码混淆「ProGuard」(可选)
- 增加反编译难度,命名缩短为1-2个字母的名字,压缩(移除无效类、属性、方法等),优化bytecode移除没用的结构。
Step 5:转换为dex「dx.bat」
- 把所有claas文件转换为classes.dex文件,class -> Dalvik字节码,生成常量池,消除冗余数据等。(方法数超65535会生成多个dex文件)
Step 6:打包「ApkBuilder」
- 把resources.arsc、classes.dex、其他的资源一块打包生成未签名apk。
Step 7:签名「Jarsigner」
- 对未签名apk进行debug或release签名。
Step 8:对齐优化「zipalign」
- 使apk中所有资源文件距离文件起始偏移为4字节的整数倍,从而在通过内存映射访问apk文件时会更快。
如果想了解更多编译构建流程可移步至:《10分钟了解Android项目构建流程》
而「反编译」则是反过来了,通过一些反编译工具,提取出源码,转换过程如下:
「APK ====> Dex ====> Jar(class文件)/Smali ==> Java源码」。
APK可以说是每个Android开发仔的「心血结晶」,把各种自己觉得「牛逼哄哄的奇淫巧技」封装其中。但总有些「心怀叵测」想去搞你的APP,通过一些「反编译工具」获取你的源码,然后为所欲为:
- 加广告:你应用免费,给你加点广告,亦或者改成付费,然后下载量比你的多,气不气?
- 破解付费:你应用收费,Hook掉你的检测方法,发个破解包,还到处传播,气不气?
- 恶意攻击:逆向得出请求接口规律,批量短信验证注册,耗光你的短信池等,气不气?
这种「恶劣」的行径令人「气愤」像极了某类经典「动作电影」里的桥段:
- 男子上进,努力工作,妹子贤惠,料理家务;
- 妹子每天做好饭菜,等男子回来,一起吃饭,满怀憧憬,畅谈以后的二人世界;
- 酒饱饭后,温饱思XX,不可描述一番,却被「居心叵测」的邻居给盯上了;
- 和往常一样,男子出门上班,妹子在家做家务,晾衣服;
- 邻居 上线,用「谎言」诱骗妹子开门,接而挤门而入;
- 用「暴力和胁迫」,无视妹子的やめて和反抗,违背个人意愿;
- 粗暴地把衣服一件件褪去,仅剩下那「万恶的马赛克」;
- 守护着最后的一处「绝对领域」;
- 在几番不可描述后,把妹子占为己有,然后像玩物般戏耍。
看着妹子「因情绪过激而身体抽搐」,哭得「梨花带雨」,不禁让人「心生怜惜」,像我这种感性的蓝孩子:
总会忍不住抽上几张抽纸,“静静抹泪”,擦拭完,顿觉索然无味,一片空明,然后开始反思:
为什么那个邻居不是我?呸呸呸…
除了「同情女主」和「斥责坏人」外,应该如何避免这种事情的发生呢?
- 1、花点钱,请个「保镖」看门,坏人想进来要先过保镖这一关;
- 2、给妹子「加个锁」,让坏人无法不可描述,只能望而兴叹。
可以把例子中的「妹子」看做是我们编写的「APK」,而「请保镖」和「加锁的操作」则可以看做是「APK加固」,另外加固又称「加壳」,壳的定义:
一段专门负责「保护软件不被非法修改或反编译的程序」,一般先于程序运行,拿到控制权,然后完成它们保护软件的任务。
有「加壳」,自然也有「脱壳」,即去掉这层壳,拿到源码,也称为「砸壳」。
关于加固技术的发展,看雪上有篇:《一张表格看懂:市面上最为常见的 Android 安装包(APK)五代加固技术发展历程及优缺点比较》,不过图不怎么清晰,笔者重新排版了一下,有兴趣的读者可以看看:
「混淆」可以类比为上面「万恶的马赛克」,阻碍人类进步的绊脚石。而混淆则是增加了反编译的难度,同理,「反混淆」则对应「去除马赛克」,试图还原它原来的样子。
加固虽然能在一定程度上「防止反编译和二次打包」,但加固后的APP可能会带来一些问题:
体积增大,启动速度变慢,兼容问题等
网上「免费加固」方案有很多,脱壳教程也是烂大街,而且有些恶心的第三方加固还会给你加点料(360加固锁屏广告),而使用「企业级的加固」,则需要支付不菲的费用,所以很多APP直接选择了「裸奔」。先来讲解一下未加固的怎么获取源码吧~
使用Jadx的注意事项:
使用jadx-gui可直接打开apk查看源码,但如果APK比较大(classes.dex有好几个),会直接卡死(比如微信),笔者的做法是命令行一个个dex文件去反编译,最后再把反编译的文件夹整合到同一个目录下。
这样的操作繁琐且重复,最适合批处理了,遂写了个反编译的批处理脚本(取需):
"""
自动解压apk,批量使用jadx进行反编译,结果代码汇总
"""
import os
import shutil
import zipfile
from datetime import datetime
apk_file_dict = {} # APK路径字典
# 遍历构造APK路径字典(构造文件路径列表,过滤apk,拼接)
def init_apk_dict(file_dir):
apk_path_list = list(filter(lambda fp: fp.endswith(".apk"),
list(map(lambda x: os.path.join(file_dir, x), os.listdir(file_dir)))))
index_list = [str(x) for x in range(1, len(apk_path_list) + 1)]
return dict(zip(index_list, apk_path_list))
# 移动文件夹
def move_dir(origin_dir, finally_dir):
shutil.move(origin_dir, finally_dir)
# 如果文件夹存在删除重建
def deal_dir_existed(path):
if os.path.exists(path):
print("检测到文件夹【%s】已存在,执行删除..." % path)
shutil.rmtree(path)
os.makedirs(path)
# 判断目录是否存在,不存在则创建
def is_dir_existed(path, mkdir=True):
if mkdir:
if not os.path.exists(path):
os.makedirs(path)
else:
return os.path.exists(path)
# 获取目录下的所有文件路径
def fetch_all_file(file_dir):
return list(map(lambda x: os.path.join(file_dir, x), os.listdir(file_dir)))
# 解压文件到特定路径中
def unzip_file(file_name, output_dir):
print("开始解压文件...")
f = zipfile.ZipFile(file_name, 'r')
for file in f.namelist():
f.extract(file, os.path.join(os.getcwd(), output_dir))
print("文件解压完毕...")
if __name__ == '__main__':
print("遍历当前目录下所有APK...")
apk_file_dict = init_apk_dict(os.getcwd())
print("遍历完毕...\n\n============ 当前目录下所有的APK ============\n")
for (k, v) in apk_file_dict.items():
print("%s.%s" % (k, v.split(os.sep)[-1]))
print("\n%s" % ("=" * 45))
choice_pos = input("%s" % "请输入需要反编译APK的数字编号:")
print("=" * 45, )
choice_apk = apk_file_dict.get(choice_pos)
apk_name = choice_apk.split(os.sep)[-1][:-4] # APK名字
# 创建相关文件夹
crack_dir = os.path.join(os.getcwd(), apk_name) # 工程根目录
deal_dir_existed(crack_dir)
crack_apktool_dir = os.path.join(crack_dir, "apktool" + os.sep) # APKTool反编译目录
deal_dir_existed(crack_apktool_dir)
crack_jadx_dir = os.path.join(crack_dir, "jadx" + os.sep) # JADX反编译目录
deal_dir_existed(crack_jadx_dir)
crack_temp_dir = os.path.join(crack_dir, "temp" + os.sep) # 解压后文件的临时存储路径
deal_dir_existed(crack_temp_dir)
# 利用APKTool提取资源文件
begin = datetime.now() # 计时
print("APKTool提取资源文件...")
os.system("./apktool d %s -f -o %s" % (choice_apk, crack_apktool_dir))
# 复制一份AndroidManifest.xml、res、assets文件到外部
shutil.copy(os.path.join(crack_apktool_dir, "AndroidManifest.xml"), os.path.join(crack_dir, "AndroidManifest.xml"))
shutil.copytree(os.path.join(crack_apktool_dir, "res" + os.sep), os.path.join(crack_dir, "res" + os.sep))
shutil.copytree(os.path.join(crack_apktool_dir, "assets" + os.sep), os.path.join(crack_dir, "assets" + os.sep))
print("资源文件提取完毕")
# 利用jadx反编译源码
print("JADX反编译提取源码...")
choice_apk_zip = shutil.copy(choice_apk, choice_apk.replace(".apk", ".zip"))
unzip_file(choice_apk_zip, crack_temp_dir)
print("开始批量反编译dex文件")
for dex in list(filter(lambda fp: fp.endswith(".dex"), fetch_all_file(crack_temp_dir))):
os.system(
"./jadx -d {0} {1}".format(os.path.join(crack_jadx_dir, dex.split(os.sep)[-1][:-4]), dex))
print("所有dex文件反编译完毕")
# 将资源文件移入
shutil.move(os.path.join(crack_dir, "AndroidManifest.xml"), os.path.join(crack_jadx_dir, "AndroidManifest.xml"))
shutil.move(os.path.join(crack_dir, "res" + os.sep), os.path.join(crack_jadx_dir, "res" + os.sep))
shutil.move(os.path.join(crack_dir, "assets" + os.sep), os.path.join(crack_jadx_dir, "assets" + os.sep))
# 删除临时文件夹,压缩文件
shutil.rmtree(crack_temp_dir)
os.unlink(choice_apk_zip)
end = datetime.now()
print("收尾操作~~~\n反编译完成,总耗时:%s秒" % (end - begin).seconds)
执行前,你需要把apktool相关的东西,丢到jadx/build/jadx/bin目录下,如图所示:
接着终端键入:python3 auto_extract_apk.py,回车后输入对应编号,回车开始编译:
静待片刻后:
Tips:这里没有把多个classes文件夹整合到一起,是因为有些APP会出现合并冲突。
打开反编译后的目录,有如下两个文件夹:
按照自己的需要用Android Studio打开其中一个就好了:
- apktool目录:apktool反编译后的内容,主要用于smail动态调试。
- jadx目录:反编译成Java的内容。
代码是拿到了,但是打开代码,「一堆的abcd」,跟到眼花,可以试下「反混淆」,方案有两类,一种是通过「代码逆推」出名字,另一种是通过「统计逆推」出名字。
第一种方案的工具有很多(Jeb2,simplify等),前者付费需破解,Java版本有限制,Mac配置有点麻烦,故笔者用的是后者:「Simplefy」,Github仓库:https://github.com/CalebFenton/simplify,使用方法也很简单:
打开终端依次键入:
# 拉取仓库代码
git clone --recursive https://github.com/CalebFenton/simplify.git
# 来到目录下
cd simplify
# 编译
./gradlew fatjar
编译后完,执行下述指令即可反混淆APK:
# 反混淆APK(需要反混淆的APK,反混淆后的APK名)
./gradlew build && cp xxx.apk yyy.apk
静待反混淆完毕,接着反编译批处理脚本走一波,打开MapFragment比对下:
相比混淆前,多了一些变量名,当然也不是完全的,偶尔还是有abcd,但是可读性稍微提高了些,比如查找的时候不用在一个个adcd排除,但是,编译挺耗时的,而且我的电脑风扇呼呼呼地响。
第二种是通过统计的方法,利用统计推断出名字:DEGUARD:http://apk-deguard.com/,打开官网:
选择需要反混淆的APK后,Upload上传,接着等待处理完成,!!!别关页面!!!
一般需等待1-10分钟,处理完成后,点击output.apk,把APK下载到本地,同样执行批处理脚本反编译一波,和simplefy反混淆后的代码对比下:
大同小异,另外,反混淆并不能100%还原,而且还可能有些小错误,比如下面的代码:
虽说反编译后的可读性有所提高,但建议还是搭配着混淆的源码看。
终于来到很多同学期待的脱壳环节,先说明下,笔者只是「工具党」水平,不会Native层的,so文件调试!如果本节的工具,你脱不出来,或者脱出来有问题,笔者也是爱莫能助。看雪有很多帮人脱壳的大佬,可以在上面发个帖子求助下~
1、判断是哪种加固
解压apk后在assets目录下看到so文件,比如360加固宝:libjagu.so和libjiagu_x86.so,百度搜下名字就知道是哪家的加固了,也可以直接用后面讲的「MT文件管理器2.0」直接查看。
2、FDex2脱壳(只适用于Android 7或以下版本,可以脱市面上大多数免费加固,成功率较高,推荐)
- 有ROOT:安装「XposedInstaller」和「FDex2」
- 没ROOT:安装「VirtualXposed」「FDex2」
比如:这里有个「360免费版加固的APK」,直接用jadx反编译后导入AS,但是反编译后的classes:
只有这么一丢丢点东西,把「待脱壳应用」安装到手机上,接着用FDex2来脱壳
已Root玩家:XposedInstall启用FDex2插件重启后,按如下步骤脱:
adb root
adb pull /data/user/0/包名 电脑文件夹
# 按照文件从大到小排序!!!
jadx aaa.dex -d classes
jadx bbb.dex -d classes1
jadx ccc.dex -d classes2
行吧,脱壳成功,这里其实还可以还原APK的(二次打包),等下再讲~
未root玩家,安装打开VirtualXposed,添加应用:Fdex2和待脱壳应用
如果炮制,只是dex的路径有些不一样。
3、反射大师(和FDex类似,下载地址:https://www.lanzous.com/b04xxlujg)
注意,同样只支持Android 7.0及以下,adb安装后,xposed启用插件,重启手机,接着打开反射大师:
Step 1:选中待脱壳APP,弹出对话框选择打开
Step 2:点击中间的六芒星,弹出如下对话框,长按「写出DEX」
Step 3:等待写出完毕,可以在/storage/emulated/0中找到导出的dex:
Step 4:pull到电脑上用jadx-gui打开看看:
行吧,脱壳成功,就是我们想要的dex了,另一个classes2.dex则是相关的~:
4、dumpDex脱壳(Github:https://github.com/WrBug/dumpDex/releases)
官方仓库的README.md中有一句:
可以的话建议自己编译,流程也很简单:
# 1、拉取项目代码到本地
git clone https://github.com/WrBug/dumpDex.git
# 2、AS中Open项目,等待编译完成
# 3、删掉build.gradle里签名相关的代码
# 4、点击顶部菜单栏Build -> Build APK,或者直接在终端./gradlew clean build
# 5、adb命令直接把编译生成的apk安装到手机上
# 6、接着来到如下左图路径,把对应的so,通过adb push到目录下:
adb push lib/armeabi-v7a/libnativeDump.so /data/local/tmp
adb push lib/arm64-v8a/libnativeDump.so /data/local/tmp/libnativeDump64.so
# 修改权限
adb shell
su
chmod 777 /data/local/tmp/libnativeDump.so
chmod 777 /data/local/tmp/libnativeDump64.so
# 临时关闭SELinux(重启后会失效,可调用getenforce查询)
setenfore 0
# 7、打开XposedInstaller看已经启用DumpDex插件,是的话重启手机
# 8、开机后,打开想脱壳的应用,不用理闪退,接着打开data/data/包名查看是否有Dump目录
# 9、进入如果出现下图所示的多个dex,说明脱壳成功,否则可能是脱壳失败
# (看是否有报错信息),或者不支持(比如360加固免费版只支持新版,不支持旧版)。
另外,脱出来的dex不一定就可用,比如某个用了「腾讯御安全」的应用:
用jadx-gui打开这的dex,一堆这样的错误:
出现这个的原因是「指令集被抽取」,打开smail文件你就知道了:
方法指令都被nop(零)替换了,工具党到这里就可以放弃了,要调试so文件。
Tips:本节用到的东西,都有给出比较官方的下载链接!!!你也可以到公号
「抠腚男孩」输入000,回复对应序号下载,谢谢~
参考文献:
- Android打包流程