安全相关
- 免混淆规则是全局生效(但推荐在各模块自己配置免混淆规则,这样移除该模块的时候会直接把该模块的免混淆规则一起移除)
- 混淆开关是模块内生效
学习加密,防止核心代码泄露
- 反编译出来的目录MATE-INF 是放签名文件的。如果dex等代码有变动那么需要重新签名
.apk
文件生成过程:
问题:
- 如何达到加密效果?
- 为什么是两个系列的dex?
用后台代码运行。壳dex存放加解密的细节。核心代码在另一个dex中,对这个dex进行加密,这样classLoader
就无法识别这个dex。这样反编译就打不开这个dex,达到保护核心代码的目的。 - 壳dex怎么来的?
- 壳dex如何保护源dex?
- 如何签名?
- 如何运行新dex(如何脱壳)?
后台代码:
加密流程:对apk
进行unzip
,遍历dex
,对dex
进行AES
加密,用流写回文件
获取壳dex
(这是个library
工程中的aar
得到的)流程:对apk
进行unzip
,遍历获取classes.jar
,调用命令行dx.bat
获取壳dex
解密流程:(安装apk
过程中只做校验签名)解密发生在运行时,一边运行一边解密。
思路来源于tinker热修复(github上可以找到源码)(热修复:对Framework层的hook技术)
dex加载顺序(有个数组dexpath[]
保存dex
,通过dexpath[]
的脚标来顺序加载。我们热修复就是新建一个数组,把修复过bug的dex放到数组的最前面)
ClassLoader
双亲委托机制(谁先加载就用谁作为执行的class)–》热修复原理
Application最早执行的方法是attachBaseContext
完成apk加固,需要具备的知识体系有哪些?
知识点 | 面试体系 |
---|---|
熟练掌握Java IO相关代码 | Java语言进阶RandomAccessFile |
深入研究Android apk的启动流程 | 精通Android FrameWork 层 |
精通Multidex文件加载机制,精通类加载机制 | 精通Android FrameWork 层 JVM ,DVM ClassLoader |
明确dex文件的基本构造,了解dex文件相关源码 | 精通Android FrameWork 层 dex |
APK打包的基本流程需要理解 | Gradle工具数量 |
掌握C/C++语言及NDK开发 | 精通C/C++及NDK开发 |
掌握Java反射和动态代理 | Java语言进阶基础 |
扩展:
微信中加密后的有些类部分可见有些类都不可见:实现是针对某些核心代码进行做标记,针对有标记的部分(都是二进制数据)进行加密,就可以部分不可见。
加固
apk 上架之前需要加固(借助加固工具)
加固都会把签名去掉,需要重新签名
乐固只有 exe,只有加固的功能【不能用了】- 360 加固宝,下载【只勾选“签名校验”】,有加固
和重新签名的功能【只有每日一次免费加固,oppo、三星不能上架】 - oppo在线加固 【用基础版加固即可,分oppo渠道包】
- 梆梆加固(我用PC版加固)【加固比较慢,加固后的包也比较大,分三星渠道包】
- AndroidToolKit安卓工具包,功能有:
- 签名信息 - 分析(APK/签名)的签名信息(modulus、md5、sha-1、sha-256等)
- APK信息 - 解析
AndroidManifest.xml
,提取部分信息 - APK签名 - 对APK进行签名
- 签名生成 - 生成签名证书
- 图标生成 - 一键生成多尺寸图标
- 缓存清理 - 清理Android项目缓存目录
安全-Android应用加固原理
so加固
加壳步骤:
- 增加init函数:
cpp
文件添加void my_init(){}
Android.mk
文件添加:LOCAL_LDFLAGS += -WI,-init=my_init
- 将编译好的
so
拷贝到upx-3.96-win64
文件夹中 - 用命令行进入
upx-3.96-win64
目录中 - 输入命令:
upx --android-shlib 文件名称.so
- 加壳后的
so
会替换原有的so
加固了就安全了?几个措施让你的 Android 应用更安全
背景
最近新开发了一款工具类型的软件 移动工具箱,然而某天下午忽然群里来了一个不速之客说我的软件被破解了。虽然,该软件无需付费并且没有广告(很良心 :) ),也进行了安全加固,但是还是很轻易得被别人破解了。现象是,启动页换成了别人的页面,需要用户点击页面上的按钮分享几次破解者的信息才能进入应用。并且,每次打开应用都是如此。
看到这我的心情还是非常复杂的。一开始觉得自己做个应用都赚不到钱,还要被这些人利用。本是同根生,相煎何太急呀。然而,转念一想,这里有些东西还是可以学习一下,于是我去他们的链接下载了软件,本着学习的精神进行了一波分析并制定了几个方案来加强应用安全。
分析
检查破解应用签名
决定要进行分析之后,我首先想到的就是去检查下应用的签名,这里使用 keytool 即可,指令如下:
1 | keytool -printcert -jarfile 你的apk地址 |
获取的结果如下,
很显然,应用的签名已经发生了变化,应用被别人二次打包了。实际上,我的应用是在 360 上面进行了软件加固(免费版),但是还是如此轻松地被别人完成了二次打包。
签名校验
如果仅仅是签名发生了变化,那么解决方式倒也简单。在应用内部增加一个签名校验就可以了。不过签名校验也有需要注意的地方。其一,签名校验可以放在 Java 层来完成,也可以放在 native 层通过 C++ 来完成。其二,在应用内部进行整个数字签名的校验还是部分校验。这是因为如果写入完整的数字签名很容易被别人发现,即便写入到 so 中,写入完整字符串比部分更容易被别人找到。
在 Java 中,你可以按照下面这种方式获取应用的数字签名。我已经把相关的方法写成了工具类,你可以在 Github 上面获取 AppUtils:
1 | public static String getAppSignatureMD5(final String packageName) { |
如果是使用 C++ 进行签名校验可以使用下面的方法,
1 | jbyteArray getSignatureByteArray(JNIEnv *env, jobject context, jstring algorithm) { |
这里使用的是 CMake 进行编译,使用 Android Studio 进行开发。上面的逻辑很简单,就是把之前的 java 层获取签名的方法照搬到了 native 方法里。而且,借助于 Android Studio 进行开发,可以帮助我们减轻很多工作量,比如方法的映射关系就不需要你一个一个的进行对比,可以通过提示直接完成转换。当然,上面只是使用 java 的类完成了获取签名的字节数组的逻辑,进一步转换成为 hash 字符串还要进行其他操作。这里就不把代码贴出来了。
因为除了进行这些签名校验我还加了其他的安全措施,所以这部分代码不便开源。不过,我已经把一些基础功能的代码和环境配置打包成了压缩文件,你可以到我公众号里回复【安卓签名校验】获取完整的代码。
拿到了签名之后当然是进行签名校验了,基本的字符串比较即可。当然,你可以进行部分匹配,这样增加了破解的难度。
签名校验就安全了?
增加了上述的签名校验只不过是第一步,也是比较常规的安全操作。但是这样操作未必就能防止其他用户破解。进一步进行反编译,我又发现一些新的东西。破解包的目录结构挺有意思的,
相比于没有破解的包,在 lib 下面多了两个文件,这里的 libarm.so 的内容尚不清楚,这里的 libhook.apk 是一个 apk 文件,并且签名跟我的原始的包的签名一致,但是 apk 内容并不是我的完整包。所以,我猜测这里的 Apk 文件有其他用途,并且仅仅使用签名校验并不安全。因为别人可以读取你的应用的签名,然后通过某种方式进行伪造,让获取签名的方法返回的结果不是安装包真实的签名而是你的真实包的签名。这里的 apk 很有可能就是用来获取签名的。
于是,我进一步对应用进行资源反编译,这里使用 apktool 来完成,这里如下:
1 | apktool d 破解应用.apk |
于是我发现启动应用是被篡改掉了的,这里的启动应用已经被修改为 arm.SignerPro
,并且这里的启动类已经被修改为 SplashActivity
:
然后,我们将 Apk 解压之后对 dex 文件进行反编译。这里使用的是 d2j-dex2jar 来完成。命令如下,
1 | d2j-dex2jar classes.dex |
然后,我们根据反编译的结果,查看启动 Application,
这里的 Application 继承了 InvocationHandler。InvocationHandler 想必大家都不陌生,Retrofit 就是通过在 InvocationHandler 实现的代理来解析请求参数,并根据注解和入参来完成 OkHttp 请求的。我们找到该接口的实现方法 invoke()
. 从上图中很明显得可以看出,我们的 getPackageInfo()
方法有很大可能被别人 hook 了。这个方法就是上面说的用来获取应用签名的方法,这里会对 getPackageInfo()
方法进行处理,并返回 signs 作为签名的获取结果而不是应用真实的签名结果。
当然,上面都是猜着,因为他们也只破解了第一个版本的包,第二个版本的包我加了新的安全措施,并且悄悄地增加了几个隐藏的页面用来在应用内获取签名和其他的信息来验证我的想法。
不管怎么说,仅仅加上签名校验很可能是不安全的!
其他安全措施
其实即便他们 hook 了获取签名的 getPackageInfo()
方法我们仍然有许多措施来应对。他们这里的 hook 有几个地方可以入手,
首先,我们并没看到 signs 的赋值操作,我猜测是通过 native 方法来完成的赋值,他们如果每次都尝试返回 signs 的话并没有屡次对 signs 进行重新赋值。而从 PackageManager 中多次读取到的 Signature 对象应该是不同的。所以,如果我们多次读取签名的时候返回的同一对象,是不是可以认为该方法被别人 hook 了呢?
另外,这里 hook 了
getPackageInfo()
方法。只是我们无法判断他们 hook 的范围,我们可以尝试自定义一个getPackageInfo()
方法,然后自定义返回的对象类型,如果返回的不是我们指定的类型,就可以得出结论,我们的getPackageInfo()
方法被别人 hook 了。比如,1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Fake {
public void getPackageInfo() { }
public FakePackageInfo getPackageInfo(int flags) String packageName, throws PackageManager.NameNotFoundException {
return new FakePackageInfo();
}
public FakePackageInfo getPackageInfo(int flags) VersionedPackage versionedPackage, throws PackageManager.NameNotFoundException {
return new FakePackageInfo();
}
public static class FakePackageInfo { }
}当然,如果他们只 hook 了 PackageManager 的方法,这个就不适用了。
除了签名校验,我们还可以检查其他项。比如检查 Application 是否是我们的启动 Application,可以使用如下代码获取应用的 Application 并进行比较,
1
2
3
4
5
6
7
8
9
10
11public static String getApplicationName(final String pkgName) {
if (StringUtils.isSpace(pkgName)) return null;
try {
PackageManager pm = UtilsApp.getApp().getPackageManager();
ApplicationInfo ai = pm.getApplicationInfo(pkgName, 0);
return ai == null ? null : ai.className;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return null;
}
}此外,我们还可以对应用的启动 Activity 进行校验。因为他们对我们的应用的启动 Activity 进行了修改,于是我们启动 Activity 进行了修改,所以这也可以作为一个应对措施:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public static String getAppLauncher(final String pkgName) {
if (StringUtils.isSpace(pkgName)) return null;
try {
PackageManager pm = UtilsApp.getApp().getPackageManager();
PackageInfo pi = pm.getPackageInfo(pkgName, 0);
if (pi == null) return null;
Intent resolveIntent = new Intent(Intent.ACTION_MAIN, null);
resolveIntent.addCategory(Intent.CATEGORY_LAUNCHER);
resolveIntent.setPackage(pi.packageName);
List<ResolveInfo> resolveInfoList = pm.queryIntentActivities(resolveIntent, 0);
ResolveInfo resolveInfo = resolveInfoList.iterator().next();
if (resolveInfo != null) {
return resolveInfo.activityInfo.name;
}
return null;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return null;
}
}上面提到的也只是一部分方法,当然还有其他的可以操作的方法。我们也可以在对类似的破解事件进行多分析来总结,尝试从多个维度做好安全防护。
总结
上面是我在应对破解方面的一些简单的总结,除了进行常规的加固之外,还进行了其他的维度的校验。当然,我其实对逆向和破解的了解并不是那么深入,这里权当抛砖引玉了。
上文中提到的反编译工具,如果需要的话可以到我的工号里回复【安卓反编译工具】领取。这里提到的移动工具箱呢是我最近开发的一款工具软件,除了一些常用的工具之外,内部还包含了 18 种基于 OpenCV 的图像处理,部分代码我也丢到了 Github 上面。此外,还包含了两个 Github 爬虫工具用来帮助开发者更好地使用 Github,以及常见的字符串加密、文件加密和时间戳工具等,希望能够对你有帮助。
脱壳
反编译
反编译需要用到的工具
apktool
:资源文件获取,可以提取出图片文件和布局文件进行使用查看
dex2jar
:将apk反编译成java源码(classes.dex
转化成jar文件)
jd-gui
:查看apk中classes.dex
转化出额jar,即源码文件
单dex的反编译步骤
把
classes.dex
转成jar
1
dex2jar.bat classes.dex
通过
jd-gui
查看 jar 内容把
classes-dex2jar.jar
拖到jd-gui
界面即可查看
多dex的反编译步骤
一个个反编译dex2jar很麻烦,直接反编译全部apk
1 | d2j-dex2jar.bat xxx.apk |
dex2jar在2.0版本是不支持multidex的。
新版本dex-tools-2.1-SNAPSHOT是支持multidex的。
问题:出现错误Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
解决:d2j_invoke.bat
内更改jvm大小为2048 java -Xms512m -Xmx2048m -cp "%CP%" %*
后期-反编译-Null大佬的
安全相关问题
H5文件加固检测
应用内若存在明文存储的H5资源文件,则会泄露页面基本布局和一些重要的信息,如登录界面、支付界面等。攻击者可篡改H5资源文件,可能植入钓鱼页面或者恶意代码,导致用户账号、密码、支付密码等敏感信息泄露。更有甚者,通过H5代码暴露相关活动的业务逻辑,可能被黑产团队用来刷红包、薅羊毛等,造成经济损失。
解决方案
可以使用第三方的专业安全加固方案,对应用中的H5文件进行加固保护。
RSA加密算法不安全使用带来的安全风险
RSA加密算法是一种非对称加密算法。当其密钥长度过短,通常认为长度小于512位时,就会存在较高的被破解风险;没有使用正确的工作模式和填充方式,将会存在重放攻击的风险。因RSA加密算法不安全使用造成的加密方法失效,可能造成客户端隐私数据泄露、加密文件破解、传输数据被获取、中间人攻击等后果,导致用户敏感信息被窃取。
解决方案
- 使用RSA算法进行数字签名时,建议密钥长不要低于512位,推荐1024位
- 使用RSA加密时,如果设置工作模式为ECB,建议填充方式为OAEPWithSHA256AndMGF1Padding
截屏攻击风险
截屏攻击是指在APP运行过程中,界面被监控并且截屏或者录屏。截屏攻击主要发生在APP登录、身份认证、资金操作等界面。Android5.0中增加了一个MediaProjection接口,它提供截屏或者录屏的服务,准许APP拥有截取屏幕或者记录系统音频的能力。同时系统允许其他消息窗口覆盖在系统的录制提示上,从而在用户无感知的情况下启动录屏或者截屏工具。通过此方式,攻击者可以获取APP关键界面的截图或者录像,从而获取用户的敏感信息。
解决方案
在Activity的oncreate()方法中调用:getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE);或者getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);来达到防止截屏攻击的目的。
ZipperDown漏洞
如果大量应用会读取zip压缩包进行相关逻辑业务,常见的场景就是从服务器下载压缩包,进行资源、代码热更新。在解压zip包时,如果对文件名没有进行限制,通过在文件名加上 ../../ 前缀的方式,可以将文件解压到任意路径。如果攻击者用远程劫持或者本地替换等方式将APP将要加载的正常zip包替换为带有路径前缀的恶意zip包,而APP又未对解压文件的文件名称进行处理,则可能会出现攻击者可以对应用资源、代码进行任意篡改、替换,从而实现远程代码劫持等安全问题。
解决方案
1. 对zip包进行解压操作时,在获取文件名后,添加过滤代码对文件名中可能包含的 ../ 进行过滤判断。
2. 使用通信协议加密技术,对通信过程中的数据进行加密保护,保证数据不被篡改。
3. 建议客户端与服务端使用加密通道进行数据交互,并对传输数据进行完整性校验,防止zip包被拦截替换。
用户敏感信息明文传输
用户在登录过程中,与服务器端交互时明文传输用户名、密码或者验证码等,可能会导致用户敏感信息暴露。
解决方案
应用与服务器交互过程中,应该对用户名、密码、手机号和验证码等敏感信息进行加密传输。
Java层代码动态调试风险问题
应用级配置文件build.gradlel中的调试标记默认是开启,即可以被Java调试工具如JDB进行调试,但是这样可能会出现被获取和篡改用户敏感信息,甚至可以分析并且修改代码实现的业务逻辑,例如窃取用户密码、绕过验证码防护等。
解决方案
在应用正式发布前,显示设置android:debuggable属性为false,关闭Java动态调试功能、不允许被调试。
数据越权备份风险
Android 2.1以上的系统可以为APP提供应用程序数据的备份和恢复功能,该功能由AndroidMainfest.xml文件中的allowBackup 属性值控制,其默认值为true。当该属性没有显式设置为false时,攻击者可通过adb backup和adb restore对APP的应用数据进行备份和恢复,从而可能获取明文存储的用户敏感信息,如用户的密码、证件号、手机号、交易密码、身份令牌、服务器通信记录等。利用此类信息攻击者可伪造用户身份,盗取用户账户资产,或者直接对服务器发起攻击。
解决方案
将AndroidMainfest.xml文件中的allowBackup属性值设置为false来关闭应用程序的备份和恢复功能;也可以使用专业安全加固方案的本地数据保护功能,避免本地数据泄露。
日志数据泄露风险
调试信息函数可能输出重要的调试信息,常见的就是Log日志类其中包含的信息可能会导致用户信息泄露,泄露核心代码逻辑等,为发起攻击提供便利,例如:Activity的组件名;通信交互的日志;跟踪的变量值等。
解决方案
应用内使用统一的Log控制基类,可以灵活的控制Log的输出打印。(测试环境允许打印日志、正式环境不打印);或者使用第三方的日志框架。
代码混淆相关问题
at com.netease.nis.sdkwrapper.Utils.rL(Native method)
原因:少添加了对oaid
(oaid_sdk_1.0.25.aar
)的免混淆。
注意:此处
com.netease.
跟云信的类库名称neteaseim
很像很容易搞混
解决:
添加混淆的方式有两种:
- 一种是在主module的
proguard-rules.pro
中统一添加。 - 一种是在各自library的
proguard-rules.pro
中添加。这种方式更灵活些,可以被多个主module使用
此处的是aar,没有自己的proguard-rules.pro
,所以添加到主module中
1 |
|
“-keepclassmembers class * extends android.app.Activity {public void *(android.view.View);}” 混淆报错
aapt_rules.txt 出现
1 | AGPBI: {"kind":"error","text":"Expected field or method name at D:\\code\\Company\\xm597_dev\\app\\build\\intermediates\\aapt_proguard_file\\a0032Release\\aapt_rules.txt:1315:33\n-keepclassmembers class * { *** @{view::onUserProfileClick}(android.view.View); }\n ^","sources":[{"file":"D:\\code\\Company\\xm597_dev\\app\\build\\intermediates\\aapt_proguard_file\\a0032Release\\aapt_rules.txt","position":{"startLine":1314,"startColumn":32,"startOffset":95272}}],"tool":"R8"} |
~~解决:所有layout中 android:onClick="@{view::clickMethod}"
更改成 android:onClick="@{() -> view.clickMethod()}"
~~
最后发现改错layout了,所以上面的都不起效。正确的layout中有这个方法,但是相关联的View中没有这个方法,所以在release开启混淆的情况下会报错(在不开混淆的时候居然不报错)