Android有什么好的反逆向apk反编译大师破解版apk的方法或应用

一种apk文件防逆向的加固方法
一种apk文件防逆向的加固方法
【专利摘要】本发明公开一种APK文件防逆向的加固方法,其包括步骤:将原Classes.dex删除源代码后得到第一Classes.dex文件;将原Classes.dex文件加密处理得到第二Classes.dex文件;生成用于解密第二Classes.dex文件的Encrypt.so文件,二次加密得到Encrypt’.so文件;生成可将Encrypt’.so文件;修改源程序入口指向;以第一Classes.dex文件、AndroidManifest’.xml文件分别替换待加固的APK文件中原Classes.dex文件和原AndroidManifest.xml文件,然后将第二Classes.dex文件及Encrypt’.so文件存放在待加固的APK文件的指定文件目录下,打包封装处理后,完成了APK文件的加固处理。本发明可以规避现有Android逆向工具的反编译,增加反编译的难度,从而可以极大程度地提高APK文件的安全性。
【专利说明】-种APK文件防逆向的加固方法
【技术领域】
[0001] 本发明涉及移动终端应用安全领域,特别涉及一种APK文件防逆向的加固的方 法。
【背景技术】
[0002] APK (Android PacKage的缩写)即Android安装包。将APK文件(即APK应用程 序)直接传到Android模拟器或Android操作系统的终端设备(比如安卓智能手机)中执 行即可安装。APK文件其实是zip格式,但后缀名被修改为apk,在windows系统上可以通 过解压缩工具(比如winrar软件)直接解压查看。解压APK文件后,一般的可看到的目录 结构如下表1所示:
【权利要求】
1. 一种APK文件防逆向的加固方法,其特征在于,包括步骤:
将待加固的APK文件中原Classes, dex文件复制一份,在复制所得的Classes, dex文 件中删除APK文件的功能源代码后,保存得到第一 Classes, dex文件;
将原Classes, dex文件用预设的加密解密算法进行加密处理,得到第二Classes, dex 文件;
生成用于解密第二Classes, dex文件的Encrypt, so文件,对该Encrypt, so文件进行 二次加密,得到Encrypt' . so文件;
生成可将Encrypt' ? so文件解密为Encrypt, so文件的Entry, so文件;
将待加固的APK文件中原AndroidManifest. xml文件的源程序入口指向,由指 向原Classes, dex文件更改为指向第一 Classes, dex文件,修改处理后的文件记为 AndroidManifest' ? xml 文件;
以第一 Classes, dex文件、AndroidManifest' . xml文件分别替换待加固的APK文件 中原Classes, dex文件和原AndroidManifest. xml文件,然后将第二Classes, dex文件及 Encrypt' . so文件存放在待加固的APK文件的指定文件目录下,打包封装处理后,完成了 APK文件的加固处理。
2. 根据权利要求1所述一种APK文件防逆向的加固方法,其特征在于,将第二 Classes, dex文件及Encrypt' ? so文件存放在待加固的APK文件的文件目录res之下。
3. 根据权利要求1所述一种APK文件防逆向的加固方法,其特征在于,打包封装处理的 步骤至少包括使用Android SDK中提供的签名工具对封装文件进行签名的步骤。
4. 根据权利要求1所述一种APK文件防逆向的加固方法,其特征在于,预设的加密解密 算法为标准或已知任意一种文件加密解密算法。
【文档编号】G06F21/14GKSQ
【公开日】日
申请日期:日
优先权日:日
【发明者】刘鹏
申请人:刘鹏当前位置: &
> Android软件逆向破译教程下载
Android软件逆向破译教程完美版
程序员的前途一片大好,Android软件逆向破译教程专为你们而准备,apk逆向破解教程是一份手把手教你逆向分析Android程序的教程,专业破解软件,Android软件逆向破译教程由浅入深、循序渐进地讲解了Android系统的软件安全、逆向分析与加密解密技术,破解如此简单!
Android软件逆向破译教程说明:
包括Android软件逆向分析和系统安全方面的必备知识及概念、如何静态分析Android 软件、如何动态调试Android 软件、Android 软件的破解与反破解技术的探讨,以及对典型Android 病毒的全面剖析。
Android软件逆向破译教程下载
高速下载器通道
其他下载地址
下载不了?
41 K | 简体中文 | 0分
最近一直在看网友们在寻找InstallShield 12注册机,小编今天特意找...
32 K | 简体中文 | 0分
免费RAD Studio XE5修改补丁是体积非常小仅有30KB大小的应用程序...
290 K | 简体中文 | 0分
之前已经介绍了PhpStorm8官方中文版,现在我们需要的是相关的phpsto...
33.65 MB | 英文 | 0分
CodeRush Xpress是由DevExpress和微软共同创建的代码编辑器,具有...
下载排行榜Android apk如何加固防止被破解(防止逆向编译)
时间: 01:20:10
&&&& 阅读:238
&&&& 评论:
&&&& 收藏:0
标签:&&&&&&&&&&&&&&&&&&&&&&&&&&&现在主要工具是接触SDK,为了防止游戏包被破解编译,以及发现加密串,我来分享下以下几点:
防破解技术主要有四种实现方式:
1.代码混淆(ProGuard)技术
2.签名比对技术
.so 动态库技术
4.动态加载技术
5.第三方平台加密以及检测漏洞
这篇文章中也提及到了相关的知识点。
第一种: 代码混淆技术(ProGuard)
该技术主要是进行代码混淆,降低代码逆向编译后的可读性,但该技术无法防止加壳技术进行加壳(加入吸费、广告、病毒等代码),而且只要是细心的人,依然可以对代码依然可以对代码进行逆向分析,所以该技术并没有从根本解决破解问题,只是增加了破解难度。
第二种: 签名比对技术
该技术主要防止加壳技术进行加壳,但代码逆向分析风险依然存在。而且该技术并不能根本解决被加壳问题,如果破解者将签名比对代码注释掉,再编译回来,该技术就被破解了。
第三种: NDK .so动态库技术,该技术实现是将重要核心代码全部放在C文件中,利用NDK技术,将核心代码编译成.so动态库,再用JNI进行调用。该技术虽然能将核心代码保护起来,但被加壳风险依然存在。
第四种: 动态加载技术,该技术在Java中是一个比较成熟的技术,而Android中该技术还没有被大家充分利用起来。
第五种: 第三方平台使用
主要讲解第四种方法,该技术可以有效的防止逆向分析、被破解、被加壳等问题,动态加载技术分为以下几步:
将核心代码编译成dex文件的Jar包
对jar包进行加密处理
在程序主入口利用NDK进行解密
再利用ClassLoader将jar包进行动态加载
利用反射技术将ClassLoader 设置成系统的ClassLoader。
主要优点有:
1.核心代码在被加密的jar中,所以破解者无法解压出class文件,如果加密秘钥被破解者拿到,那将是另外一层面的安全问题了。
2.该技术也可以有效防止加壳技术,代码是动态加载上来的,破解者的壳程序无法加入到已加密的jar包中,及时破解者注入壳程序入口,壳程序因为不在ClassLoader 的jar包中,所以也无法被执行起来,除非破解者替换ClassLoader的jar包,关掉NDK解密代码.但这种安装到手机上,已经不在是我们的应用,用户一定会将其卸载掉。
所以综合起来比较,第四种动态加载技术是最安全的,但效率问题,本人并没做严格测试,粗略实验了一下,效率并没有明显降低。
public static boolean enOrDecryptFile(byte[] paramArrayOfByte,
String sourceFilePath, String destFilePath,int mode){
File sourceFile = new File(sourceFilePath);
File destFile = new File(destFilePath);
CipherOutputStream cout = null;
FileInputStream in
FileOutputStream out = null;
if (sourceFile.exists() && sourceFile.isFile()) {
if (!destFile.getParentFile().exists()) {
destFile.getParentFile().mkdirs();
destFile.createNewFile();
in = new FileInputStream(sourceFile);
out = new FileOutputStream(destFile);
SecretKeySpec secretKeySpec = new SecretKeySpec(defPassword, "AES");
cipher = Cipher.getInstance("AES");
cipher.init(mode, secretKeySpec);
cout = new CipherOutputStream(out, cipher);
byte[] cache = new byte[CACHE_SIZE];
int nRead = 0;
while ((nRead = in.read(cache)) != -1) {
cout.write(cache, 0, nRead);
cout.flush();
}catch (IOException e) {
e.printStackTrace();
return false;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return false ;
} catch (NoSuchPaddingException e) {
e.printStackTrace();
return false ;
}catch (InvalidKeyException e) {
e.printStackTrace();
return false;
if(cout != null){
cout.close();
} catch (IOException e) {
e.printStackTrace();
if(out != null){
out.close();
} catch (IOException e) {
e.printStackTrace();
if(in != null){
in.close();
} catch (IOException e) {
e.printStackTrace();
return true;
return false;
jar用SDK\platform-tools\下的dx命令进行dex格式转化
然后再用加密工具将生成jar文件进行加密处理
最后通过代码动态加载:
File file = new File("/data/data/" + base.getPackageName() + "/.cache/")
if (!file.exists()) {
file.mkdirs()
Runtime.getRuntime().exec("chmod 755 " + file.getAbsolutePath()).waitFor()
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace()
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace()
Util.copyJarFile(this)
Object currentActivityThread = RefInvoke.invokeStaticMethod(
"android.app.ActivityThread", "currentActivityThread",
new Class[] {}, new Object[] {})
String packageName = getPackageName()
HashMap mPackages = (HashMap) RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mPackages")
WeakReference wr = (WeakReference) mPackages.get(packageName)
MyClassLoader dLoader = new MyClassLoader("/data/data/"
+ base.getPackageName() + "/.cache/classdex.jar", "/data/data/"
+ base.getPackageName() + "/.cache", "/data/data/"
+ base.getPackageName() + "/.cache/", base.getClassLoader())
class1 = dLoader.loadClass("com.example.test.TestActivity")
Log.i("b364","-----------&class1: "+class1)
} catch (ClassNotFoundException e){
Log.i("b364","-----------&class not found Exception!")
e.printStackTrace()
Log.i("b364","------&PackageInfo: "+wr.get())
// DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,
// libPath, (ClassLoader) RefInvoke.getFieldOjbect(
// "android.app.LoadedApk", wr.get(), "mClassLoader"))
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
wr.get(), dLoader)
处理完成,如果在Application中做特别处理也是可行的。之前就有人分析了爱加密的加密方式,不过这里不做阐述,有兴趣可以一起讨论。
下篇文章中我们来讲讲如何 逆向apk的动态库
标签:&&&&&&&&&&&&&&&&&&&&&&&&&&&
&&国之画&&&& &&&&chrome插件
版权所有 京ICP备号-2
迷上了代码!Android逆向之旅---带你爆破一款应用的签名验证问题_Android开发_动态网站制作指南
Android逆向之旅---带你爆破一款应用的签名验证问题
来源:人气:48
在之前的文章中说过Android中的安全和破解是相辅相成的,为了防止被破解,很多应用做了一些防护策略,但是防护策略也是分等级,一般简单的策略就是混淆代码和签名校验,而对于签名校验很多应用都是会做的,一般现在就两种方式:
第一种:签名校验不通过直接退出程序,也就是你会发现回编译二次打包运行失败的现象
第二种:签名校验不通过不触发程序指定逻辑,导致没有错误信息,也进不了应用的现象
关于Android中应用防护策略的文章可以看这里:Android中应用的攻防之战&今天我们就来看一下签名校验的应用案例,在回编译二次签名之后运行进不去的问题,其实在之前已经分析过了一个签名验证的问题,那里的问题是回编译二次打包运行失败的问题。不了解的同学可以去这里看一下:Android中使用静态方式破解apk应用;这种运行失败的应用比较好解决,因为这种签名校验的一般都在应用的入口处,所以直接在入口处加一些日志,通过打印日志就可以看到,入口一般是的onCreate方法和attachBaseContext方法,而通过这个案例之后我们也学习到一个快速定位签名校验的方法的技巧:全局搜索signatures字符串,因为我们知道如果要做签名验证,必须调用系统的一个api方法:getPackageManager().getPackageInfo(getPackageName(),
64).signatures,所以在用Jadx工具反编译apk之后,全局搜signatures字符串可以立马定位到签名校验的方法。
二、案例分析
上面分析完了之前一个解决签名校验的案例,下面就用这个小游戏作为简单分析,这个游戏在市场都可以下载到:
本文研究的是V3.3版本的。我们下载游戏之后,可以利用apktool进行反编译,然后回编译重签名,安装运行:
游戏可以运行成功,但是卡在这里,进不了游戏了,这时候我们就猜想他可能有签名验证逻辑,可能放在本地,也有可能放在服务端,我们利用上面说到的那个技巧:在Jadx中全局搜&signatures&字符串内容:
这时候会发现有很多地方都在使用,其实可以推断出,这个游戏为了防止二次打包在很多地方都加了签名验证的逻辑,这样对于破解也是增加一定的难度,因为这里签名校验的地方太多了,所以没法一个一个修改,所以咋们用另外一种方式高效率的解决问题:在运行游戏的时候发现游戏卡在进度条了,所以猜想和网络请求有关,所以咋们就通过Fiddler抓包来分析他的请求信息:
游戏打开就这些请求,但是作为一个游戏不可能就这点请求的,这个也是卡住的原因,我们可以这么做,用一个正常的游戏在抓包看看情况:
这个是没有二次打包签名的请求信息,和上面二次打包签名之后的请求相比较,多了很多请求,可以发现有一个get_user_info这个接口比较特殊,所以我们可以去Jadx全局搜这个请求接口:
这里看到定义了url的地方,然后在全局Find Usage的地方:
点进去看方法调用:
继续查看这个方法的调用地方:
这里有四个地方调用这个方法,但是可以依次排除,最终定位到是com.wepie.snake.module.home.b.d.e()方法,那么下面来看一下如何进行排除的,可以从下往上排除,看最下面一个方法:
然后进入l.a方法中查看逻辑:
这里其实是个请求,请求接口是d.r的值:
看到是这个接口信息,但是我们在用Fiddler抓包的时候并没有看到这个请求:
所以这个地方可以过滤了,其他三个地方的分析结果类似。所以通过这种方式定位到了方法调用的地方:
我们在查找这个e方法调用的地方:
在这里被调用了,通过调用结果发现,有第一个判断:
这里有一个字符串比较相等的逻辑,很有可能是比较签名信息的,可以查看h.a这个方法:
这里貌似有一个字符串的构造算法,我们为了看到最终的字符串内容,可以新建一个简单的项目,然后把这个方法打印一下看看结果:
看到这个最终生成的字符串内容是:678a930bf92ad1,然后再看一下equals的o.a的方法:
这个方法是获取应用签名信息的,然后用计算结果。所以到这里我们就知道校验签名逻辑是,获取应用的签名信息,然后MD5一下,和&678a930bf92ad1&字符串进行比较。那么我们二次签名之后,这个判断肯定就是false了,所以后续的逻辑就不走了,没有后面的请求,发现卡在开始界面了。
那么问题找到了,改起来就比较简单了,直接反编译游戏,然后找到这个&com.wepie.snake.helper.f.o.a()方法对应的smali代码:
把这个方法直接返回&678a930bf92ad1&字符串即可。有的同学说这个怎么改?是手动编写samli代码吗?肯定不是的,咋们可以先写一个简单的返回这个字符串的方法,然后反编译得到这段smali代码就可以了,可千万别自己手动的去编写,除非你有这个耐心和毅力,反正我没有。替换完成之后,咋们就回编译,二次签名运行安装游戏,可惜的是还是卡住了,所以还得回去看代码:
咋们修改了一次签名校验方法,进入了第一层签名判断,继续往下看代码:
这里又有一个判断,点进去查看逻辑:
果然,这里又有了一次签名校验方法,所以还得手动的修改,修改方法和上面类似,把这个p方法改一下即可:
修改成功之后,再次回编译重签名安装运行,可惜的是还是卡住了,进不了游戏,这时候我们在次抓包看看:
这次比之前多了一个config_v3_android请求,但是还是没有get_user_info的请求!所以还得去看代码逻辑,不过从请求结果来看,我们之前的两次签名校验修改已经有效果了,继续看下面的代码:
这里可以看到,有一个设置进度条的逻辑,而且有一个tag=999的日志,貌似是取配置信息的进度条,那么我们可以查看这个日志信息:
看到了这个日志信息之后,发现有开始取config到获取成功config了,但是后面就没有日志了,所以这里猜想应该是在本地解析这个配置信息的时候还有判断,咋们全局搜一个字符串信息&getConfigAndroid&:
第一条信息就是我们想要的,点击进入:
果然这里还有一个判断,进入查看:
又是一次签名校验的逻辑,好吧,还得再一次改这个com.wepie.snake.helper.b.a.i()方法了:
修改成功之后,再次回编译二次签名安装运行即可,这次终于运行成功,进入游戏界面了。
三、签名校验破解方案
好了,到这里我们来总结一下关于如何解决应用二次签名校验的问题:
第一步:先在Jadx中打开应用之后,全局搜字符串内容:&signatures&,这个就可以定位到获取应用签名信息的地方了,然后可以依次跟踪校验的地方了。
第二步:在第一步搜索结果比较多的情况下,我们可以通过应用运行效果采用这两种方式:
第一种方式:如果应用运行失败,那么可能是在应用入口处作了签名校验逻辑,如果校验失败就退出程序。一般入口处是在Application的onCreate方法和attachBaseContext方法。第二种方式:如果应用可以运行,但是卡在某一个地方,我们可以采用抓包技术来分析应用运行到哪一步卡住了,因为现在的应用都会有请求的,如果是卡住了,那么请求就会和原始包的请求不同,可以根据不同的请求结果来跟踪代码逻辑,就比如本文的案例。
上面就是本人总结的签名校验的大致解决步骤和方法,但是肯定还有其他场景,比如有的应用会把签名放到native层,但是这些校验逻辑都是可以做处理的,也有的应用把签名信息放到服务端进行校验,这个也是可以处理的。放在native层的话,最终也是通过java层做连接访问的,只要到了java层,那么就可以找到校验方法的地方,放在服务端校验,还是可以通过抓包查看请求来解决的。
四、签名校验破解后续
从上面看其实在给应用做防护的时候,签名校验这种方式不是百分百的安全,只能防止一些破解小白。其实本文的案例中可以发现,这个应用其实想通过多个地方的签名校验来做到全局校验,本文中可以看到我们做了三次的签名校验方法代码修改才成功的,而且后续版本也不排除他们还会继续增加,以及在其他地方逻辑处也做了签名校验判断。
从本文中也可以看到如果一个应用在很多地方都做了签名校验,那么我们手动修改会显得很麻烦,其实这里有这个思路:获取应用的签名方法是固定的:getPackageManager().getPackageInfo(getPackageName(), 64).signatures,我们可以直接修改应用的Application信息,通过反射机制,把校验对象的字符串内容设置到signatures中,说白了就是用反射来修改应用的签名信息,这样在应用中所有获取签名的地方都是我们设置的指定签名值,也就是比对的那个常量字符串内容,所有的签名校验方法都是返回true了。这个思路本文不再尝试了,感兴趣的同学可以尝试。就是一个反射调用即可。有的同学可能在想怎么不用Xposed进行hook应用的签名获取方法那样不就更简单了。这里一定要注意呀,我们为什么要解决签名校验,就是因为我们想二次签名打包,而Xposed进行hook的时候是不需要二次打包的,主要hook点找到,不用进行二次打包安装就可以实现破解了。这两种方式思路是不同的,要注意哈!
五、给应用安全防护的意见
通过本文案例,其实对于现在应用防护有这些建议:首先关于应用签名校验这块逻辑,可以做的更安全点,就是在native层用反射调用系统获取签名的方法,然后直接在native层进行比较,如果发现签名不正确,就退出程序,全部放在native层做,这样安全系数会高点!
但是这种也不是最安全的,安全点也就是考虑加固方案了,现在加固平台很多,选择企业版加固,对于破解难度那是肯定会加大的。但是加固有一个弊端,就是崩溃率的问题,因为了解加固原理的同学都知道,现在加固平台都会涉及到so逻辑,而Android因为机型和系统版本复杂,导致兼容性不好,会有一定的加固崩溃率的,而对于一些用户量比较多的应用和游戏,他们无法忍受这种崩溃率,那么就放弃加固了。但是加固还是一个相对安全的策略!
严重声明:本文的目的只有一个,通过一个案例来分析现在应用的签名校验逻辑,如何破解这种校验方式,如果有人利用本文内容进行任何商业目的和非法牟利,带来的任何法律责任将由操作者本人承担,和本文作者没有任何关系,所以还是由衷的希望大家秉着技术学习的目的阅读此文,非常感谢!
本文通过一个案例分析了应用签名信息校验的防护策略破解,最后也总结了现在这种签名校验防护策略的弊端和改进的地方,如何做到安全系数比较高的防护。但是也不是最安全的,因为没有绝对的安全!安全和逆向是相互进步!共同促进社会进步和技术革新!最后要是阅读完本文内容觉得有收获就点个赞多多分享,要是有打赏那就最好了啦!
优质网站模板标签:至少1个,最多5个
近些年来移动 APP 数量呈现爆炸式的增长,黑产也从原来的PC端移到了移动端,伴随而来的逆向攻击手段也越来越高明。本篇章主要介绍应用加固的最基础的四种方式:1.proguard 混淆 2.签名比对验证 3.ndk 编译 .so 动态库 4.代码动态加载
原文地址:
应该大多数开发者都不会关注应用会不逆向破解,而且现在有第三方厂商提供免费的加固方案,所以 apk 应用的安全性就全部依赖于第三方。但是如果第三方加固方案被破解那么 apk 就陷于被动,所以我们也可以通过一些手段来加固应用本身逻辑,或者数据的加密。
最常见的加固就是 proguard 混淆,通过混淆可以加大逻辑分析的难度。对于数据的加密通过 ndk 开发动态库基本上可以防止大部分的破解者,逆向 native 跟逆向 java 不是一个级别的。
下面介绍的四种加固方式成本比较低,开发者可以很快开发完成,并且也不会影响应用的兼容性。
0x01 proguard混淆
ProGuard 是一个 SourceForge 上知名的开源项目。官网网址是:。
java 编译后的字节码很容易被反编译,基本上 java 编译器都可以比如 eclipse、idea,还有用的比较多的 JD-GUI。都可以将 java 字节码转化为 java 源码。ProGuard的主要作用就是混淆。当然它还能对字节码进行缩减体积、优化等,但那些对于我们来说都算是次要的功能。
ProGuard is a free Java class file shrinker, optimizer, obfuscator, and preverifier. It detects and removes unused classes, fields, methods, and attributes. It optimizes bytecode and removes unused instructions. It renames the remaining classes, fields, and methods using short meaningless names. The resulting applications and libraries are smaller, faster, and a bit better hardened against reverse engineering.
Android 配置
Android 2.3 以后 SDK 中已经默认支持 ProGuard 混淆,具体配置文件在 sdk-root/tools/proguard/proguard-android.txt。
SDK 内置 ProGuard 混淆文件:
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
# Optimization is turned off by default. Dex does not like code run
# through the ProGuard optimize and preverify steps (and performs some
# of these optimizations on its own).
-dontoptimize
-dontpreverify
# Note that if you want to enable optimization, you cannot just
# include optimization flags in your own projec
# instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your
# project.properties file.
-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService
# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
native &methods&;
# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
void set*(***);
*** get*();
# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
-keepclassmembers class **.R$* {
public static &fields&;
# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version.
We know about them, and they are safe.
-dontwarn android.support.**
# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep
-keep @android.support.annotation.Keep class * {*;}
-keepclasseswithmembers class * {
@android.support.annotation.Keep &methods&;
-keepclasseswithmembers class * {
@android.support.annotation.Keep &fields&;
-keepclasseswithmembers class * {
@android.support.annotation.Keep &init&(...);
在 Android Studio 中构建新项目时会在每个 module 下生成 proguard-rules.pro 这里可以配置一些自定义的 ProGuard 规则。在 build.gradle 中配置是否启动混淆配置, 一般情况下编译 release 版本会启用混淆,debug 版本关闭:
buildTypes {
signingConfig signingConfigs.tdSignConf
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.tdSignConf
minifyEnabled false
在Android Studio 中启用 ProGuard 混淆会在/app/build/outputs/mapping/release下生成下面四个文件:
mapping.txt —& 表示混淆前后代码的对照表
—& 描述apk内所有class文件的内部结构
seeds.txt —& 列出了没有被混淆的类和成员
usage.txt —& 列出了源代码中被删除在apk中不存在的代码
mapping.txt 文件非常重要。如果你的代码混淆后会产生bug的话,log提示中是混淆后的代码,希望定位到源代码的话就可以根据mapping.txt反推。每次发布都要保留它方便该版本出现问题时调出日志进行排查,它可以根据版本号或是发布时间命名来保存或是放进代码版本控制中。
如果是手动 ProGuard 混淆可以在配置文件中添加 -printmapping选项。
异常堆栈还原
混淆后代码变量、函数名、类名、都已经变成了随机的字母,crash信息也就看不错在什么地方。所以就需要上面提到 mapping.txt 保存了混淆前后的代码对照表。
例如有个错误日志:
Caused by: java.lang.NullPointerException
at cc.gnaixx.be.u(Unknown Source)
at cc.gnaixx.at.v(Unknown Source)
at cc.gnaixx.at.d(Unknown Source)
at cc.gnaixx.av.onReceive(Unknown Source)
这个日志中我们根本无法排查错误在哪一行,但是 ProGuard 提供了一个还原工具,工具在proguard/bin/retrace.sh。执行:
#crash.txt 为错误栈文件
retrace.sh -verbose mapping.txt crash.txt
执行后的效果:
Caused by: java.lang.NullPointerException
at cc.gnaixx.UtilTelephony.boolean is800MhzNetwork()(Unknown Source)
at cc.gnaixx.ServiceDetectLte.void checkAndAlertUserIf800MhzConnected()(Unknown Source)
at cc.gnaixx.ServiceDetectLte.void startLocalBroadcastReceiver()(Unknown Source)
at cc.gnaixx.ServiceDetectLte$2.void onReceive(android.content.Context,android.content.Intent)(Unknown Source)
这里需要注意一点如果你想查单个错误信息,必须在错误信息前加 at, 不然无法找出错误位置。
-include {filename}
从给定的文件中读取配置参数
-basedirectory {directoryname}
指定基础目录为以后相对的档案名称
-injars {class_path}
指定要处理的应用程序jar,war,ear和目录
-outjars {class_path}
指定处理完后要输出的jar,war,ear和目录的名称
-libraryjars {classpath}
指定要处理的应用程序jar,war,ear和目录所需要的程序库文件
-dontskipnonpubliclibraryclasses
指定不去忽略非公共的库类。
-dontskipnonpubliclibraryclassmembers 指定不去忽略包可见的库类的成员。
-keep {Modifier} {class_specification}
保护指定的类文件和类的成员
-keepclassmembers {modifier} {class_specification}
保护指定类的成员,如果此类受到保护他们会保护的更好
-keepclasseswithmembers {class_specification}
保护指定的类和类的成员,但条件是所有指定的类和类成员是要存在。
-keepnames {class_specification}
保护指定的类和类的成员的名称(如果他们不会压缩步骤中删除)
-keepclassmembernames {class_specification}
保护指定的类的成员的名称(如果他们不会压缩步骤中删除)
-keepclasseswithmembernames {class_specification}
保护指定的类和类的成员的名称,如果所有指定的类成员出席(在压缩步骤之后)
-printseeds {filename}
列出类和类的成员-keep选项的清单,标准输出到给定的文件
-dontshrink
不压缩输入的类文件
-printusage {filename}
-whyareyoukeeping {class_specification}
-dontoptimize
不优化输入的类文件
-assumenosideeffects {class_specification}
优化时假设指定的方法,没有任何副作用
-allowaccessmodification
优化时允许访问并修改有修饰符的类和类的成员
-dontobfuscate
不混淆输入的类文件
-printmapping {filename}
混淆前后代码的对照表
-applymapping {filename}
重用映射增加混淆
-obfuscationdictionary {filename}
使用给定文件中的关键字作为要混淆方法的名称
-overloadaggressively
混淆时应用侵入式重载
-useuniqueclassmembernames
确定统一的混淆类的成员名称来增加混淆
-flattenpackagehierarchy {package_name}
重新包装所有重命名的包并放在给定的单一包中
-repackageclass {package_name}
重新包装所有重命名的类文件中放在给定的单一包中
-dontusemixedcaseclassnames
混淆时不会产生形形色色的类名
-keepattributes {attribute_name,...}
保护给定的可选属性,例如LineNumberTable, LocalVariableTable, SourceFile, Deprecated, Synthetic, Signature, InnerClasses.
-renamesourcefileattribute {string}
设置源文件中给定的字符串常量
下面这些规则主要是自己在使用过程中遇到过的问题
# 忽略指点错误,减少打印日志,因为我是针对jar做混淆,打印一堆没用日志有点烦
-dontnote java.**, javax.**, org.**
# 日志打印类名重新定义为"ProGuard"字符串
-renamesourcefileattribute ProGuard
# 保留源文件名,保留行号,保留annotation
-keepattributes SourceFile, LineNumberTable, *Annotation*
# 保持枚举 enum 类不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
# 保存内部类不被混淆
-keep class cc.gnaixx.TestClass$* {*;}
0x02 签名比对验证
Android APK的发布是需要签名的。签名机制在Android应用和框架中有着十分重要的作用。
例如,Android系统禁止更新安装签名不一致的APK;如果应用需要使用system权限,必须保证APK签名与Framework签名一致,等等。要破解一个APK,必然需要重新对APK进行签名。而这个签名,一般情况无法再与APK原先的签名保持一致。(除非APK原作者的私钥泄漏)简单地说,签名机制标明了APK的发行机构。
Android签名机制
为了说明APK签名比对对软件安全的有效性,我们有必要了解一下Android APK的签名机制。为了更易于大家理解,我们从Auto-Sign工具的一条批处理命令说起。
要签名一个没有签名过的APK,可以使用一个叫作auto-sign的工具。auto-sign工具实现签名功能的命令主要是这一行命令:
java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk
这条命令的意义是:通过signapk.jar这个可执行jar包,以“testkey.x509.pem”这个公钥文件和“testkey.pk8”这个私钥文件对“update.apk”进行签名,签名后的文件保存为“update_signed.apk”。
对于此处所使用的私钥和公钥的生成方式,这里就不做进一步介绍了。这方面的资料大家可以找到很多。我们这里要讲的是signapk.jar到底做了什么。
signapk.jar是Android源码包中的一个签名工具。由于Android是个开源项目,所以,很高兴地,我们可以直接找到signapk.jar的源码!路径为/build/tools/signapk/SignApk.java。
对比一个没有签名的APK和一个签名好的APK,我们会发现,签名好的APK包中多了一个叫做META-INF的文件夹。里面有三个文件,分别名为MANIFEST.MF、CERT.SF和CERT.RSA。signapk.jar就是生成了这几个文件(其他文件没有任何改变。因此我们可以很容易去掉原有签名信息)。
通过阅读signapk源码,我们可以理清签名APK包的整个过程。
1、 生成MANIFEST.MF文件:
程序遍历update.apk包中的所有文件(entry),对非文件夹非签名文件的文件,逐个生成SHA1的数字签名信息,再用Base64进行编码。具体代码见这个方法:
private static Manifest addDigestsToManifest(JarFile jar)
//关键代码如下:
for (JarEntry entry: byName.values()) {
String name = entry.getName();
if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
!name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
(stripPattern == null ||!stripPattern.matcher(name).matches())) {
InputStream data = jar.getInputStream(entry);
while ((num = data.read(buffer)) & 0) {
md.update(buffer, 0, num);
Attributes attr =
if (input != null) attr = input.getAttributes(name);
attr = attr != null ? new Attributes(attr) : new Attributes();
attr.putValue("SHA1-Digest", base64.encode(md.digest()));
output.getEntries().put(name, attr);
之后将生成的签名写入MANIFEST.MF文件。关键代码如下:
Manifest manifest = addDigestsToManifest(inputJar);
je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
这里简单介绍下SHA1数字签名。简单地说,它就是一种安全哈希算法,类似于MD5算法。它把任意长度的输入,通过散列算法变成固定长度的输出(这里我们称作“摘要信息”)。你不能仅通过这个摘要信息复原原来的信息。另外,它保证不同信息的摘要信息彼此不同。因此,如果你改变了apk包中的文件,那么在apk安装校验时,改变后的文件摘要信息与MANIFEST.MF的检验信息不同,于是程序就不能成功安装。
2、生成CERT.SF文件:
对前一步生成的Manifest,使用SHA1-RSA算法,用私钥进行签名。关键代码如下:
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(privateKey);
je = new JarEntry(CERT_SF_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureFile(manifest,
new SignatureOutputStream(outputJar, signature));
RSA是一种非对称加密算法。用私钥通过RSA算法对摘要信息进行加密。在安装时只能使用公钥才能解密它。解密之后,将它与未加密的摘要信息进行对比,如果相符,则表明内容没有被异常修改。
3、生成CERT.RSA文件:
生成MANIFEST.MF没有使用密钥信息,生成CERT.SF文件使用了私钥文件。那么我们可以很容易猜测到,CERT.RSA文件的生成肯定和公钥相关。
CERT.RSA文件中保存了公钥、所采用的加密算法等信息。核心代码如下:
je = new JarEntry(CERT_RSA_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureBlock(signature, publicKey, outputJar);
//其中writeSignatureBlock的代码如下:
private static void writeSignatureBlock(
Signature signature, X509Certificate publicKey, OutputStream out)
throws IOException, GeneralSecurityException {
SignerInfo signerInfo = new SignerInfo(
new X500Name(publicKey.getIssuerX500Principal().getName()),
publicKey.getSerialNumber(),
AlgorithmId.get("SHA1"),
AlgorithmId.get("RSA"),
signature.sign());
PKCS7 pkcs7 = new PKCS7(
new AlgorithmId[] { AlgorithmId.get("SHA1") },
new ContentInfo(ContentInfo.DATA_OID, null),
new X509Certificate[] { publicKey },
new SignerInfo[] { signerInfo });
pkcs7.encodeSignedData(out);
分析完APK包的签名流程,我们可以清楚地意识到:
Android签名机制其实是对APK包完整性和发布机构唯一性的一种校验机制。
Android签名机制不能阻止APK包被修改,但修改后的再签名无法与原先的签名保持一致。(拥有私钥的情况除外)。
APK包加密的公钥就打包在APK包内,且不同的私钥对应不同的公钥。换句话言之,不同的私钥签名的APK公钥也必不相同。所以我们可以根据公钥的对比,来判断私钥是否一致。
APK签名比对的实现方式
具体的实现方式在之前的一篇博客中有进行了介绍,实现方式其实很简单:
0x03 ndk 编译.so 动态库
逆向 ndk 跟逆向 java 代码难度真不是在一个等级上的,ndk 开发除了安全性相对 java 开发高很多,而且在运行效率也高很多。
所以 NDK 开发相对于 JAVA 开发的优势:
1.代码的保护。由于apk的java层代码很容易被反编译,而C/C++库反汇难度较大。
2.可以方便地使用现存的开源库。大部分现存的开源库都是用C/C++代码编写的。
3.提高程序的执行效率。将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率。
4.便于移植。用C/C++写得库可以方便在其他的嵌入式平台上再次使用。
但是 NDK 的兼容性相对 JAVA 差很多,所以在进行 NDK 开发的时候兼容性是一个很大的问题。
之前写了几篇关于 NDK 开发的文章基本覆盖了 NDK 开发的所有内容了。
这些教程都是基于 experimental 版本的 gradle 进行开发,但是这种版本的配置相对于普通版本的 gradle 有很多不同。并且创建一个新的 module 都需要更新 build.gradle 配置非常麻烦。
最近 Android Studio 更新到了2.2 版本,可以支持两种方式的 NDK开发:
1. ndk-build 开发
这种方式非常简单只需要在 build.gradle 添加 ndk 编译配置:
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
2. cmake 开发
先贴出官方文档,以后再介绍。
官网文档:
0x04 代码动态加载
这篇文章是在看雪上看到的对动态加载的原理介绍的比较清楚,大多数应用也是采用这种方式对 dex 文件进行加密处理。
熟悉Java技术的朋友,可能意识到,我们需要使用类加载器灵活的加载执行的类。这在Java里已经算是一项比较成熟的技术了,但是在Android中,我们大多数人都还非常陌生。
类加载机制
Dalvik虚拟机如同其他Java虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。而在Java标准的虚拟机中,类加载可以从class文件中读取,也可以是其他形式的二进制流。因此,我们常常利用这一点,在程序运行时手动加载Class,从而达到代码动态加载执行的目的。
然而Dalvik虚拟机毕竟不算是标准的Java虚拟机,因此在类加载机制上,它们有相同的地方,也有不同之处。我们必须区别对待。
例如,在使用标准Java虚拟机时,我们经常自定义继承自ClassLoader的类加载器。然后通过defineClass方法来从一个二进制流中加载Class。然而,这在Android里是行不通的,大家就没必要走弯路了。参看源码我们知道,Android中ClassLoader的defineClass方法具体是调用VMClassLoader的defineClass本地静态方法。而这个本地方法除了抛出一个“UnsupportedOperationException”之外,什么都没做,甚至连返回值都为空。
static void Dalvik_java_lang_VMClassLoader_defineClass(const u4* args, JValue* pResult)
Object* loader = (Object*) args[0];
StringObject* nameObj = (StringObject*) args[1];
const u1* data = (const u1*) args[2];
int offset = args[3];
int len = args[4];
Object* pd = (Object*) args[5];
char* name = NULL;
name = dvmCreateCstrFromString(nameObj);
LOGE("ERROR: defineClass(%p, %s, %p, %d, %d, %p)\n", loader, name, data, offset, len, pd);
dvmThrowException("Ljava/lang/UnsupportedOperationE", "can't load this type of class file");
free(name);
RETURN_VOID();
Dalvik虚拟机类加载机制
那如果在Dalvik虚拟机里,ClassLoader不好使,我们如何实现动态加载类呢?Android为我们从ClassLoader派生出了两个类:DexClassLoader和PathClassLoader。其中需要特别说明的是PathClassLoader中一段被注释掉的代码:
/**//* --this doesn't work in current version of Dalvik--
if (data != null) {
System.out.println("--- Found class " + name + " in zip[" + i + "] '" + mZips[i].getName() + "'");
int dotIndex = name.lastIndexOf('.');
if (dotIndex != -1) {
String packageName = name.substring(0, dotIndex);
synchronized (this) {
Package packageObj = getPackage(packageName);
if (packageObj == null) {
definePackage(packageName, null, null, null, null, null, null, null);
return defineClass(name, data, 0, data.length);
这从另一方面佐证了defineClass函数在Dalvik虚拟机里确实是被阉割了。而在这两个继承自ClassLoader的类加载器,本质上是重载了ClassLoader的findClass方法。在执行loadClass时,我们可以参看ClassLoader部分源码:
protected Class&?& loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class&?& clazz = findLoadedClass(className);
if (clazz == null) {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
// Don't want to see this.
if (clazz == null) {
clazz = findClass(className);
因此DexClassLoader和PathClassLoader都属于符合双亲委派模型的类加载器(因为它们没有重载loadClass方法)。也就是说,它们在加载一个类之前,回去检查自己以及自己以上的类加载器是否已经加载了这个类。如果已经加载过了,就会直接将之返回,而不会重复加载。
DexClassLoader和PathClassLoader其实都是通过DexFile这个类来实现类加载的。这里需要顺便提一下的是,Dalvik虚拟机识别的是dex文件,而不是class文件。因此,我们供类加载的文件也只能是dex文件,或者包含有dex文件的.apk或.jar文件。
也许有人想到,既然DexFile可以直接加载类,那么我们为什么还要使用ClassLoader的子类呢?DexFile在加载类时,具体是调用成员方法loadClass或者loadClassBinaryName。其中loadClassBinaryName需要将包含包名的类名中的”.”转换为”/”。我们看一下loadClass代码就清楚了:
public Class loadClass(String name, ClassLoader loader) {
String slashName = name.replace('.', '/');
return loadClassBinaryName(slashName, loader);
在这段代码前有一段注释,截取关键一部分就是说:If you are not calling this from a class loader, this is most likely not going to do what you want. Use { Class#forName(String)} instead. 这就是我们需要使用ClassLoader子类的原因。至于它是如何验证是否是在ClassLoader中调用此方法的,我没有研究,大家如果有兴趣可以继续深入下去。
有一个细节,可能大家不容易注意到。PathClassLoader是通过构造函数new DexFile(path)来产生DexFile对象的;而DexClassLoader则是通过其静态方法loadDex(path, outpath, 0)得到DexFile对象。这两者的区别在于DexClassLoader需要提供一个可写的outpath路径,用来释放.apk包或者.jar包中的dex文件。换个说法来说,就是PathClassLoader不能主动从zip包中释放出dex,因此只支持直接操作dex格式文件,或者已经安装的apk(因为已经安装的apk在cache中存在缓存的dex文件)。而DexClassLoader可以支持.apk、.jar和.dex文件,并且会在指定的outpath路径释放出dex文件。
另外,PathClassLoader在加载类时调用的是DexFile的loadClassBinaryName,而DexClassLoader调用的是loadClass。因此,在使用PathClassLoader时类全名需要用”/”替换”.”。
这一部分比较简单,因此我就不赘言了。只是简单的说下。
可能使用到的工具都比较常规:javac、dx、eclipse等。其中dx工具最好是指明--no-strict,因为class文件的路径可能不匹配。
加载好类后,通常我们可以通过Java反射机制来使用这个类。但是这样效率相对不高,而且老用反射代码也比较复杂凌乱。更好的做法是定义一个interface,并将这个interface写进容器端。待加载的类,继承自这个interface,并且有一个参数为空的构造函数,以使我们能够通过Class的newInstance方法产生对象。然后将对象强制转换为interface对象,于是就可以直接调用成员方法了。
关于代码加密的一些设想
最初设想将dex文件加密,然后通过JNI将解密代码写在Native层。解密之后直接传上二进制流,再通过defineClass将类加载到内存中。
现在也可以这样做,但是由于不能直接使用defineClass,而必须传文件路径给dalvik虚拟机内核,因此解密后的文件需要写到磁盘上,增加了被破解的风险。
Dalvik虚拟机内核仅支持从dex文件加载类的方式是不灵活的,由于没有非常深入的研究内核,我不能确定是Dalvik虚拟机本身不支持还是Android在移植时将其阉割了。不过相信Dalvik或者是Android开源项目都正在向能够支持raw数据定义类方向努力。
我们可以在文档中看到Google说:Jar or APK file with "classes.dex". (May expand this to include "raw DEX" in the future.);在Android的Dalvik源码中我们也能看到RawDexFile的身影(不过没有具体实现)。
在RawDexFile出来之前,我们都只能使用这种存在一定风险的加密方式。需要注意释放的dex文件路径及权限管理,另外,在加载完毕类之后,除非出于其他目的否则应该马上删除临时的解密文件
目前的方式如果被识别后破解还是相对比较简单的,即使加密后的 dex 文件也需要在 load 前进行解密操作,并且需要把文件缓存在本地。所以只需要在合适的地方下个断点就可以把加密的 dex 拷贝下来。
但是我们可以通过其他方式对 dex 进行加密,比如隐藏函数。这样即使拿到了 dex 也无法通过逆向工具查看源码。
参考文章:
3 收藏&&|&&7
你可能感兴趣的文章
12 收藏,4.6k
10 收藏,9.6k
2 收藏,622
分享到微博?
技术专栏,帮你记录编程中的点滴,提升你对技术的理解收藏感兴趣的文章,丰富自己的知识库
明天提醒我
我要该,理由是:

我要回帖

更多关于 apk逆向工具 的文章

 

随机推荐