反编译
反编译代码
- dex2jar 这个工具用于将dex文件转换成jar文件
- jd-gui 这个工具用于将jar文件转换成java代码 ,使用jd-gui工具打开classes-dex2jar.jar这个文件就能查看Java代码
反编译资源
- apktool 这个工具用于最大幅度地还原APK文件中的9-patch图片、布局、字符串等等一系列的资源。
没有反编译资源之前,AndroidManifest.xml和activity_main.xml这样的资源文件都是非明文的,无法阅读。
重新打包
smali文件夹的目录结构和我们源码中src的目录结构是几乎一样的,主要的区别就是所有的java文件都变成了smali文件。smali文件其实也是真正的源代码,只不过它的语法和java完全不同,它有点类似于汇编的语法,是Android虚拟机所使用的寄存器语言。
使用smali语法,修改代码,就能重新编译自己的apk。但是apk还不能安全,因为还没签名。
因为无法获得原来正版的签名,可以使用Android Studio生成自己的签名,进行打包,生成自己的apk。
参考:
混淆
混淆的好处:
- 令 APK 难以被逆向工程,增加反编译的成本。
- 打包时移除无用资源,减少 APK 体积。
启动混淆
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
- minifyEnabled 设置为
true
来开启混淆 - 设置 shrinkResources 为
true
来开启资源的压缩。 - 一般在打 release 包时才启用混淆,因为混淆会增加额外的编译时间,所以不建议在 debug 模式下启用。
- 需要注意的是:只有在启用混淆的前提下开启资源压缩才会有效!
- 以上代码中的 proguard-android.txt 表示 Android 系统为我们提供的默认混淆规则文件,而 proguard-rules.pro 则是我们想要自定义的混淆规则,
不能被混淆的元素
枚举
枚举类内部存在 values
方法,混淆后该方法会被重新命名,并抛出 NoSuchMethodException
。Android 系统默认的混淆规则中已经添加了对于枚举类的处理,我们无需再去做额外工作。
被反射的元素
原因在于:代码混淆过程中,被反射使用的元素会被重命名,然而反射依旧是按照先前的名称去寻找元素,所以会经常发生 NoSuchMethodException
和 NoSuchFiledException
问题。
实体类
实体类即我们常说的"数据类",当然经常伴随着序列化与反序列化操作。很多人也应该都想到了,混淆是将原本有特定含义的"元素"转变为无意义的名称,所以,经过混淆的"洗礼"之后,序列化之后的 value
对应的 key
已然变为没有意义的字段,这肯定是我们不希望的。
反序列化的过程创建对象从根本上来说还是借助于反射,混淆之后 key
会被改变,所以也会违背我们预期的效果。
四大组件
Android 中的四大组件同样不应该被混淆。原因在于:
- 四大组件使用前都需要在 AndroidManifest.xml 文件中进行注册声明,然而混淆处理之后,四大组件的类名就会被篡改,实际使用的类与
manifest
中注册的类并不匹配,故而出错。 - 其他应用程序访问组件时可能会用到类的包名加类名,如果经过混淆,可能会无法找到对应组件或者产生异常。
JNI 调用的Java 方法
当 JNI 调用的 Java 方法被混淆后,方法名会变成无意义的名称,这就与 C++ 中原本的 Java 方法名不匹配,因而会无法找到所调用的方法。
其他不应该被混淆的
- 自定义控件不需要被混淆
- JavaScript 调用 Java 的方法不应混淆
- Java 的 native 方法不应该被混淆
- 项目中引用的第三方库也不建议混淆
参考
WebView
问题:WebView明文存储密码带来的安全漏洞
- WebView组件默认开启了密码保存功能,会提示用户是否保存密码,当用户选择保存在WebView中输入的用户名和密码,则会被明文保存到应用数据目录的databases/webview.db中。攻击者可能通过root的方式访问该应用的WebView数据库,从而窃取本地明文存储的用户名和密码。
解决方案:
- 开发者调用 WebView.getSettings().setSavePassword(false),显示调用API设置为false,让WebView不存储密码
四大组件
问题:动态注册Receiver风险
- 使用BroadcastReceiver组件需要动态注册或者静态注册,如果动态注册广播,即在代码中使用registerReceiver()方法注册BroadcastReceiver,只有当registerReceiver()的代码执行到了才进行注册,取消时则调用unregisterReceiver()方法。但registerReceiver()方法注册的BroadcastReceiver是全局的并且默认可导出的,如果没有限制访问权限,可以被任意外部APP访问,向其传递Intent来执行特定的功能。因此,动态注册的BroadcastReceive可能会导致拒绝服务攻击、APP数据泄漏或是越权调用等安全风险
解决方案:
- 1:在 AndroidManifest.xml 文件中使用静态注册 BroadcastReceiver,同时设置 exported="false",不被外部应用调用。 2:必须动态注册 BroadcastReceiver时,使用registerReceiver(BroadcastReceiver,IntentFilter,broadcastPermission,android.os.Handle)函数注册。 3:Android8.0新特性想要支持静态广播、需要添加intent.setComponent(new ComponentName()),详情可以自行查阅
问题:公共组件配置风险
- Activity、Service、Provider、Receiver四大组件若配置为android:exported =”true”,将可以被外部应用调用,这样存在安全隐患的风险。
解决方案:
- 在应用的AndroidManifest.xml文件中,设置组件的android:exported 属性为false或者通过设置自定义权限来限制对这些组件的访问。值得一提的是,若部分功能使用前提是配置必须使用exported为true,这种情况开发者应该根据实际情况来进行集成
问题:数据越权备份风险
- Android 2.1以上的系统可以为APP提供应用程序数据的备份和恢复功能,该功能由AndroidMainfest.xml文件中的allowBackup 属性值控制,其默认值为true。当该属性没有显式设置为false时,攻击者可通过adb backup和adb restore对APP的应用数据进行备份和恢复,从而可能获取明文存储的用户敏感信息,如用户的密码、证件号、手机号、交易密码、身份令牌、服务器通信记录等。利用此类信息攻击者可伪造用户身份,盗取用户账户资产,或者直接对服务器发起攻击。
解决方案:
- 将AndroidMainfest.xml文件中的allowBackup属性值设置为false来关闭应用程序的备份和恢复功能;也可以使用专业安全加固方案的本地数据保护功能,避免本地数据泄露。
数据存储安全:加密和JNI写入Native层
1、秘钥及敏感信息
此类配置应当妥善存放,不要在类中硬编码敏感信息,可以使用JNI将敏感信息写到Native层。
2、SharePreferences
首先不应当使用SharePreferences来存放敏感信息,sharedpreferces存储的xml文件数据可能被反编译拿到。存储一些配置信息时也要配置好访问权限,如私有的访问权限 MODE_PRIVATE(Activity.MODE_PRIVATE,//默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容),避免配置信息被篡改。
3、SQLite数据库文件的安全性 – 描述:敏感信息是否明文存储 – 检测:检测数据库里面的重要信息,比如账号密码之类的是否明文存储 – 建议:重要信息进行加密存储
日志数据泄露:Log控制,正式环境不打印
问题:日志数据泄露风险
- 调试信息函数可能输出重要的调试信息,常见的就是Log日志类其中包含的信息可能会导致用户信息泄露,泄露核心代码逻辑等,为发起攻击提供便利,例如:Activity的组件名;通信交互的日志;跟踪的变量值等
解决方案:
- 应用内使用统一的Log控制基类,可以灵活的控制Log的输出打印。(测试环境允许打印日志、正式环境不打印);或者使用第三方的日志框架
参考:
网络安全:HTTPS、使用签名和数据加密加密
1.1 无处不在的安全隐患
- 因为http协议是明文传输的,可以采取MD5值传输和存储,近几年MD5破解能力提高,所以现在通常生成MD5值时都需要加盐,例如MD5(name+pwd)作为密码存储,同样的密码生成的值是不一样的,在一定程度上提高了安全性。
- 攻击不一定需要用户名密码,或许可以直接打token和uid的主意。,一旦用户登录后,服务器就通过token来标识身份。如果这个token被黑客劫持了,他就可以冒充你的身份进行攻击,如果token机制设计不合理,攻击者甚至可以直接暴力去撞token。大部分网站的机制也类似,服务器通过一个sessionId标识用户,攻击者一旦拿到这个sessionId,就可以去冒充一个合法用户。
1.2 使用https是否就万事大吉
- https分单向认证和双向认证,大部分的应用场景是c/s模式,这时通常都是采用的单向认证的方式,也就是说可以保证客户端拿到的数据是后台发送的,攻击难度很大,除非你能忽悠别人安装并信任你的证书
- 但是,很多人蹭wifi时或者在网上下载一些资源,系统提示要信任什么东西,看都不看就点确定。
- 使用抓包工具时,给目标设备安装并信任装包工具的自签名证书,这时候就可以分析https请求
- 杜绝 HTTPS 抓包的原理很简单,其实就是拦截非法的证书,只通过我们信任的 HTTPS 证书的请求。
- 还有一种比较简单,直接设置okhttp禁用代理模式,也可以避免一些抓包。但是用户可能需要代理上网,这么禁止会引来其他问题
- 参考:www.jianshu.com/p/11577eb0c…
1.3 使用签名和加密数据
上面可以看到,https并不能阻挡攻击者分析请求接口并发起恶意请求攻击,为了增加攻击者分析请求的难度,通常可以采用两种方式:
-
使用签名。
- 即给你的请求参数添加一个签名,后台服务接收到请求时,先验证签名,签名不正确的话,则不予处理。签名规则五花八门,大致策略就是根据请求参数做一些运算最后生成一个唯一的字符串当做sign
- 微信支付的签名的规则可以参考:pay.weixin.qq.com/wiki/doc/ap…
-
数据加密。
- post到服务器和从服务器返回的数据都做加密,这样的话即使攻击者拿到你的数据,他不知道你的加密算法就无能为力了。
-
秘钥使用JNI将敏感信息写到Native层
参考:
JNI:储存敏感信息,核心算法和秘钥
- 一些常量是不会/不能被混淆的,这种敏感信息就需要额外保护
- 核心算法和秘钥,一般选择放到native层
native代码的安全性保证
- 相对于java代码容易被反编译,使用NDK开发出来的原生C++代码编译后生成的so库是一个二进制文件,这无疑增加了破解的难度。利用这个特性,可以将客户端敏感信息写在C++代码中,增强应用的安全性。
- 万一别人将你的so库直接copy出来拿去用了呢?因此,我们还需要在native层对应用的包名、签名进行鉴权校验,如果不是自己的应用,不返回相关信息,或者直接退出应用!
安全风险
- 用ndk开发,将密钥放在so文件,加密解密操作都在so文件里,这从一定程度上提高了的安全性,挡住了一些逆向者,但是有经验的逆向者还是会使用IDA破解的。
- 在so文件中不存储密钥,so文件中对密钥进行加解密操作,将密钥加密后的密钥命名为其他普通文件,存放在assets目录下或者其他目录下,接着在so文件里面添加无关代码(花指令),虽然可以增加静态分析难度,但是可以使用动态调式的方法,追踪加密解密函数,也可以查找到密钥内容。
参考:
即时通讯安全篇(四):实例分析Android中密钥硬编码的风险
加壳与脱壳
无论是编译java代码生成的dex文件,还是编译C/C++代码生成的so文件,反编译成本都不是特别的高。
加壳直观理解就是给程序加一层壳,可以用来对原程序进行资源压缩、防调试、防注入、防反编译,也就是说通过一个壳把原来的程序保护了起来。
我们知道一个常规Android程序它的所有代码都在dex文件中,程序启动时要先把这个dex文件载入到内存中,所以如果要加壳的话,主要工作就是把原dex文件加密或者隐藏起来,放一个新的壳dex到apk中,程序启动时运行这个壳dex,然后这个壳dex在运行时再加载原dex,用一张图表示如下:
-
xposed框架这样的hook技术,类似于降维打击,可以绕过加固技术轻松获取到dex文件。目前的乐固、360等大厂加固都可以绕过。
- 加固也避免dex文件直接暴露在apk压缩文件中,但是加固也有明确的缺点,可能会影响启动的速度,apk体积增大,无法使用补丁,依然可以脱壳逆向。所以加固已经不再被推荐使用,基本上大公司的apk都不会使用加固技术。
补充:hook和xposed
什么是 Hook
Hook 又叫“钩子”,它可以在事件传送的过程中截获并监控事件的传输,将自身的代码与系统方法进行融入。这样当这些方法被调用时,也就可以执行我们自己的代码。
Xposed
- 可以在不修改APK的情况下影响程序运行(修改系统)的框架服务。
- 替换自己的代码,使得程序加载Xposed的jar包,完成劫持
- 在
install
的时候需要root
权限,但是运行时不需要root
权限
通过替换 /system/bin/app_process 程序控制 Zygote 进程,使得 app_process 在启动过程中会加载 XposedBridge.jar 这个 Jar 包,从而完成对 Zygote 进程及其创建的 Dalvik 虚拟机的劫持。 Xposed 在开机的时候完成对所有的 Hook Function 的劫持,在原 Function 执行的前后加上自定义代码
Xposed
框架介绍以及原理
Xposed
是Github
上rovo89
大佬设计的一个针对Android平台
的动态劫持项目,通过替换/system/bin/app_process
程序控制Zygote
进程,使得app_process
在启动过程中会加载XposedBridge.jar
这个jar
包,从而完成对Zygote
进程及其创建的Dalvik虚拟机
的劫持。
因为Xposed
工作原理是在/system/bin
目录下替换文件,在install
的时候需要root
权限,但是运行时不需要root
权限。
看到这里很多人会很懵,什么是Zygote
?简单来说在Android
系统中,应用程序进程都是由Zygote
进程孵化出来的,而Zygote
进程是由Init
进程启动的。Zygote
进程在启动时会创建一个Dalvik
虚拟机实例,每当它孵化一个新的应用程序进程时,都会将这个Dalvik
虚拟机实例复制到新的应用程序进程里面去,而一个应用程序进程被Zygote
进程孵化出来的时候,不仅会获得Zygote
进程中的Dalvik
虚拟机实例拷贝,还会与Zygote
一起共享Java运行时
库。这也就是可以将XposedBridge
这个jar
包加载到每一个Android
应用程序中的原因。XposedBridge
有一个私有的Native(JNI)
方法hookMethodNative
,这个方法也在app_process
中使用。这个函数提供一个方法对象利用Java
的Reflection
机制来对内置方法覆写。。。。等等这些都会借鉴各路大神的思路和分析,总而言之,就是从底层替换方法,可以让我们在不修改APK
源码的情况下,通过自己编写的模块来影响程序运行的框架服务,实现类似于自动抢红包、微信消息自动回复等功能。 其实,从本质上来讲,Xposed
模块也是一个Android
程序。但与普通程序不同的是,想要让写出的Android
程序成为一个``Xposed 模块,要额外多完成以下四个硬性任务:
1、让手机上的xposed框架知道我们安装的这个程序是个xposed模块。
2、模块里要包含有xposed的API的jar包,以实现下一步的hook操作。
3、这个模块里面要有对目标程序进行hook操作的方法。
4、要让手机上的xposed框架知道,我们编写的xposed模块中,哪一个方法是实现hook操作的。
参考:
加密算法
加密主要有对称加密、非对称加密,不可逆加密。
对称加密AES
AES主要是用在数据本身的加密,即使传输过程中被截取了,也是加密过后的数据。但AES的弊端的是,客户端加密的话,密钥肯定是储存在app中,如果app被成功破解了,数据也就被暴露了。所以只有app本身程序的安全也解决了,app才能相对安全。
非对称加密RSA
因为RSA加密有个长度限制,这就导致了RSA加密不能用于所有的数据交互。但是可以用到一些短数据,比如用户个人信息之类的,在交易中,一次订单的数据也不是很大等。
不可逆加密
比如MD5加密、SHA加密等。所谓的不可逆加密就是,只能单向加密,不能反向解密。MD5把数据加密,最后得到固定长度的16进制编码。这个加密的作用一般是匹配验证,验证某个数据是否改变。比如密码,在向服务器存储密码,一般不会存储明文密码。安卓本地存储个标志也一般不会明文存储。
Android官方
推出了JetPack Security 。