应用安全

防Hook

第一、Xposed框架将Hook信息存储在字段fieldCache,methodCache,constructorCache 中, 利用java 反射机制获取这些信息,检测Hook信息中是否含有App中敏感的方法,字段,构造方法

第二、检测进程中使用so名中包含关键”hack|inject|hook|call” 的信息,这个主要是防止有的人不用这个Xposed框架,而是直接自己写一个注入功能

第三、root检测原理是:是否含有su程序和ro.secure是否为1

第四、防止被Hook的方式就是可以查看XposedBridge这个类,有一个全局的hook开关,所有有的应用在启动的时候就用反射把这个值设置成true,这样Xposed的hook功能就是失效了

第五、如果应用被Xposed进行hook操作之后,抛出的异常堆栈信息中就会包含Xposed字样,所以可以通过应用自身内部抛出异常来检测是否包含Xposed字段来进行防护

Log

泄露app中的信息

线上仅仅写入文件,不打印log

完整性校验(防篡改),签名

<应用安全防护和逆向分析>–第12章

  • MANIFEST.MF文件

包含了apk文件中所有文件的数据摘要内容SHA1+Base64,属性名为"SHA1-Digest"(安全散列算法) 另外还有一个"Name"属性,值为该文件在apk包中的路径, 例子:

Manifest-Version: 1.0


Name: AUTHORS

SHA1-Digest: tNG7y16mMckMxD5PMWbMbm15YuM=


Name: AndroidManifest.xml

SHA1-Digest: DpoXWRs62IFQkzew0PZZ1IhBr4E=


Name: META-INF/android.arch.lifecycle_livedata.version

SHA1-Digest: OxxKFJcpzAROGjnfMbNijNv1+JU=

  • CERT.SF文件 计算MANIFEST.MF文件整体的SHA1值,再经过BASE64编码后记录在主属性(文件头)上的"SHA1-Digest-Mainifest"属性下 MANIFEST.MF文件的每个条目做一个SHA1+Base64之后记录在同名块中,属性名为"SHA1-Digest", 例子:
Signature-Version: 1.0

Created-By: 1.0 (Android)

SHA1-Digest-Manifest: lCe0n/umuXx2pJRbNdAPxSJQGyU=

X-Android-APK-Signed: 2


Name: AUTHORS

SHA1-Digest: NJvhMmfYrxh85l+E46t/lOXoRmk=


Name: AndroidManifest.xml

SHA1-Digest: N/26hYDkRfJLe6NEN7232YcVe1M=


Name: META-INF/android.arch.lifecycle_livedata.version

SHA1-Digest: MlMwWIwNpuS2rz/AzXm5Hqaliu4=
  • CERT.RSA文件 对CERT.SF文件用私钥加密出签名,再将签名以及包含公钥信息的数字证书一同写入CERT.RSA

二进制文件,因为RSA文件加密了,所以需要用openssl命令查看内容

openssl pkcs7 -inform DER -in CERT.RSA -noout -print_certs -text

证书信息和X.509证书格式对应

一个完整的签名过程如下所示:

Dex 完整性校验

crc或者hash

可以将 CRC 值存储在 string 资源文件中,当然也可以放在自己的服务器上, 通过运行时从服务器获取校验值。基本步骤如下:

[*]首先在代码中完成校验值比对的逻辑,此部分代码后续不能再改变,否则 CRC 值会发生变化;
[*]从生成的 APK 文件中提取出 classes.dex 文件,计算其 CRC 值,其他 hash 值类似;
[*]将计算出的值放入 strings.xml 文件中。

APK 完整性校验

由于在开发 Android 应用程序时,无法知道完整 APK 文 件的 Hash 值,所以这个 Hash 值的存储无法像 Dex 完整性校验那样放在 strings.xml 文件中, 所以可以考虑将值放在服务器端,因为对 APK 的任何改动都会影响到最后的 Hash 值。

签名安全(防止二次打包)

本地验证

在应用入口处添加签名(或签名的md5)验证,如果发现签名不正确就立刻退出程序 可以考虑在native进行签名验证

服务端验证

将签名信息携带请求参数中参与加密,服务端进行签名校验,失败就返回错误数据即可

二次打包者很难伪造我们的签名,所以我们可以在程序中插入签名检查的代码,提高攻击门槛。我们可以在so中判断当前应用的签名,前面分析过,只要别人拿不到你的keystore就没办法伪造你的签名

context--pacakgeManager--packageInfo--signatures获取当前应用签名和正确的比对

防止二次打包,判断签名是否一致。 获取apk的签名hash值与apk的签名文件的hash值进行判断,不一致说明apk被二次打包。 获取签名hash值的代码如下:

public int getSignature(String packageName) {        
        PackageManager pm = this.getPackageManager();  
        PackageInfo pi = null;  
        int sig = 0;  
        try {  
            pi = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);  
            Signature[] s = pi.signatures;  
            sig = s[0].hashCode();    
        } catch (Exception e1) {  
            sig = 0;  
            e1.printStackTrace();  
        }  
        return sig;  
}

Manifest Cheating

利用后缀为png的非png文件

APK 伪加密

APK 实际上是 Zip 压缩文件,但是 Android 系统在解析 APK 文件时,和传统的解压缩软 件在解析 Zip 文件时还是有所差异的,利用这种差异可以实现给 APK 文件加密的功能。Zip 文件格式可以参考 MasterKey 漏洞分析的一篇文章。在 Central Directory 部分的 File Header 头 文件中,有一个 2 字节长的名为 General purpose bit flags 的字段,这个字段中每一位的作用 可以参考 Zip 文件格式规范的 4.4.4 部分,其中如果第 0 位置 1,则表示 Zip 文件的该 Central Directory 是加密的,如果使用传统的解压缩软件打开这个 Zip 文件,在解压该部分 Central Directory 文件时,是需要输入密码的,如图 14 所示。但是 Android 系统在解析 Zip 文件时并 没有使用这一位,也就是说这一位是否置位对 APK 文件在 Android 系统的运行没有任何影响。 一般在逆向 APK 文件时,会首先使用 apktool 来完成资源文件的解析,dex 文件的反汇编工 作,但如果将 Zip 文件中 Central Directory 的 General purpose bit flags 第 0 位置 1 的话, apktool(version:1.5.2)将无法完成正常的解析工作,如图 15 所示,但是又不会影响到 APK 在 Android 系统上的正常运行

image 图 14 传统解压缩软件需要输入密码进行解压缩

image 图 15 apktool 解析伪加密的 APK 文件失败

模拟器检测

在分析 APK 的过程中会借助于 Android 模拟器,比如分析网络行为,动态调试等。 因此从 APK 自我保护的角度出发,可以增加对 APK 当前运行环境的检测,判断是否运行在 模拟器中,如果运行在模拟器中可以选择退出整个应用程序的执行或者跳到其他分支。

1.属性检测

例如Build.BRAND 和 Build.DEVICE。 另外还可以通过检测 IMEI,IMSI 等值来判断是否是模拟器,在模拟器中,这两个值默认 分别是 000000000000000 和 310260000000000,通过以下代码可以获取 IMSI 值:

1. TelephonyManager manager = (TelephonyManager)getSystemService(TELEPHONY_SERVICE);
2. String imsi = manager.getSubscriberId();

2.虚拟机文件检测

相 对 于 真 实 设 备 , Android 模 拟 器 中 存 在 一 些 特 殊 的 文 件 或 者 目 录 , 如 /system/bin/qemu-props,该可执行文件可以用来在模拟器中设置系统属性。另外还有 /system/lib/libc_malloc_debug_qemu.so 文件以及/sys/qemu_trace 目录。我们可以通过检测这些 特殊文件或者目录是否存在来判断 Android 应用程序是否运行在模拟器中

3.基于 Cache 行为的模拟器检测方法

BlueBox 关于 Android 模拟器检测的方法 http://bluebox.com/corporate-blog/android-emulator-detection/

4.基于代码指令执行的模拟器检测方法

DexLabs 关于 Android 模拟器检测的方法 http://dexlabs.org/blog/btdetect

5.其他方法

其他一些检测方法,可以参考如下文献:

[*]DISSECTING THE ANDROID BOUNCER
[*]逃离安卓动态检测
[*]Guns and Smoke to Defeat Mobile Malware
[*]DEX EDUCATION 201 ANTI-EMULATION
[*]INSECURE MAGZINE 34 – Introduction to Android malware analysis

反调试检测

在对 APK 逆向分析时,往往会采取动态调试技术,可以使用 netbeans+apktool 对反汇编 生成的 smali 代码进行动态调试。为了防止 APK 被动态调试,可以检测是否有调试器连接。

isDebuggerConnected或debuggable

Android 系统在 android.os.Debug 类中提供了 isDebuggerConnected()方法,用于检测是否有调 试器连接。可以在 Application 类中调用 isDebuggerConnected()方法,判断是否有调试器连接, 如果有,直接退出程序。

除了 isDebuggerConnected 方法,还可以通过在 AndroidManifest 文件的 application 节点中 加入 android:debuggable=”false”使得程序不可被调试,这样如果希望调试代码,则需要修改 该值为 true,因此可以在代码中检查这个属性的值,判断程序是否被修改过,代码如下:

1. if(getApplicationInfo().flags &= ApplicationInfo.FLAG_DEBUGGABLE != 0){
2. System.out.println("Debug");
3. android.os.Process.killProcess(android.os.Process.myPid());
4. }

linux ptrace机制

破解者使用IDA进行动态方式调试so文件,从而获得重要信息,利用IDA进行so动态调试是基于进行的注入技术,然后使用linux中的ptrace机制,进行调试目标进程的附加操作。ptrace机制有一个特点,如果一个进程被调试了,在他进程的status文件中有一个字段TracerPid会记录调试者的进程id

//查找进程号
ps |grep com.example.xxx
//打开该进程的status文件
cat /proc/${pid}/status

反调试:轮询遍历自己进程的status文件,读取TracerPid字段,如果大于0,代表自己的应用在被人调试,所以立马退出程序

破解者仍然可以通过IDA给JNI_OnLoad方法下断点,然后调试,找到检测轮询代码,使用nop指令替换检测指令,就相当于把检测代码给注释了。

IDA调试,循环检查端口

我们在之前破解逆向的时候,需要借助一个强大利器,那就是IDA,在使用IDA的时候,我们知道需要在设备中启动android_server作为通信,那么这个启动就会默认占用端口23946

我们可以查看设备的tcp端口使用情况 cat /proc/net/tcp

其中5D8A转化成十进制就是23946,而看到uid是0,因为我们运行android_server是root身份的,uid肯定是0了。所以我们可以利用端口检查方式来进行反调试策略,当然这种方式不是万能的,后面会详细介绍如何解决这样的反调试方法。同时还要检查有没有android_server这个进程,有的话也是表示有可能被调试了。

当然还有最近很常用的Frida框架,他的端口号是27042和27043,以及进程名是frida-server。另可错杀一千不能放过一个。

混淆

可以增加破解难度并减少apk包大小

代码混淆

现在破解查看java层代码有以下两种方式:

  1. 直接解压classes.dex文件,使用dex2jar工具转换为jar文件之后再用jd-gui工具查看类结构
  2. 使用apktool工具直接反编译apk,得到并阅读smali源码

ProGuard

ProGuard 是一款免费的 Java 代码混淆工具,提供了文件压缩、优化、混淆和 审核功能

由于在某些情况下,ProGuard 会错误地认为某些代码没有被使用,如在 只在 AndroidManifest 文件中引用的类,从 JNI 中调用的方法等。对于这些情况,需要在 proguard-project.txt 文件中添加-keep 命令,用来保留类或方法。关于 ProGuard 更加详细的配 置项可以参考 ProGuard Manual。

DexGuard

DexGuard 是特别 针对 Android 的一款代码优化混淆的收费软件,提供代码优化混淆、字符串加密、类加密、 Assets 资源加密、隐藏对敏感 API 的调用、篡改检测以及移除 Log 代码。DexGuard 的进一步 分析可以参考 JEB 上的相关 blog,可以在这里总结一下:

1.字符串加密 2.assets 加密

资源混淆

微信已经开源:https://github.com/shwenzhang/AndResGuard

微信资源混淆AndResGuard原理

资源混淆核心处理过程如下: 1、生成新的资源文件目录,里面对资源文件路径进行混淆(其中涉及如何复用旧的mapping文件),例如将res/drawable/hello.png混淆为r/s/a.png,并将映射关系输出到mapping文件中。

2、对资源id进行混淆(其中涉及如何复用旧的mapping文件),并将映射关系输出到mapping文件中。

3、生成新的resources.arsc文件,里面对资源项值字符串池、资源项key字符串池、进行混淆替换,对资源项entry中引用的资源项字符串池位置进行修正、并更改相应大小,并打包生成新的apk。

不能被混淆的代码

枚举

枚举类内部存在 values 方法,混淆后该方法会被重新命名,并抛出 NoSuchMethodException。Android 系统默认的混淆规则中已经添加了对于枚举类的处理,我们无需再去做额外工作。

被反射的元素

原因在于:代码混淆过程中,被反射使用的元素会被重命名,然而反射依旧是按照先前的名称去寻找元素,所以会经常发生 NoSuchMethodException 和 NoSuchFiledException 问题。

实体类

实体类即我们常说的"数据类",当然经常伴随着序列化与反序列化操作。很多人也应该都想到了,混淆是将原本有特定含义的"元素"转变为无意义的名称,所以,经过混淆的"洗礼"之后,序列化之后的 value 对应的 key 已然变为没有意义的字段,这肯定是我们不希望的。 反序列化的过程创建对象从根本上来说还是借助于反射,混淆之后 key 会被改变,所以也会违背我们预期的效果。

四大组件

Android 中的四大组件同样不应该被混淆。原因在于:

  1. 四大组件使用前都需要在 AndroidManifest.xml文件中进行注册声明,然而混淆处理之后,四大组件的类名就会被篡改,实际使用的类与 manifest 中注册的类并不匹配,故而出错。
  2. 其他应用程序访问组件时可能会用到类的包名加类名,如果经过混淆,可能会无法找到对应组件或者产生异常。

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()),详情可以自行查阅

exported配置为true风险

Activity、Service、Provider、Receiver四大组件若配置为android:exported =”true”,将可以被外部应用调用,这样存在安全隐患的风险。

解决方案:

在应用的AndroidManifest.xml文件中,设置组件的android:exported属性为false或者通过设置自定义权限来限制对这些组件的访问。

NDK

一般来说,如果代码中对处理速度有较高要求或者为了更好地控制硬件,抑或者为了 复用既有的 C/C++代码,都可以考虑通过 JNI 来实现对 Native 代码的调用。 由于逆向 Native 程序的汇编代码要比逆向 Java 汇编代码困难,因此可以考虑在关键代 码部位使用 Native 代码,如注册验证,加解密操作等。

万一别人将你的so库直接copy出来拿去用了呢?因此,我们还需要在native层对应用的包名、签名进行鉴权校验,如果不是自己的应用,不返回相关信息,或者直接退出应用!

https://www.jianshu.com/p/fe0206f8be5b

安全数据自动加密到so

A simple way to encrypt your secure data like passwords into a native .so library.

https://github.com/MEiDIK/Cipher.so

优化方案(支持library module):

library module: 在plugin中代码写死所有library的安全数据,之后copy cpp并修改之后生成so,以get("${key1}")方式暴露方法给调用者提供key1获取安全数据

application module: 根据plugin中的library安全数据,配合app中配置的安全数据,copy cpp并修改之后生成so(同名so),删除所有library module下的so

ELF和反汇编

image

我们发现了之前辛苦藏到so中的字符串"Hello from C++"。如果想看代码实现,可以使用objdump命令,能看到反汇编后的代码。 现在我们可以理解ELF文件已经是机器指令了,我们如果想看一些代码逻辑,要么像机器一样读机器指令,要么把这些机器指令反编译成人类方便阅读的汇编代码(汇编语言人类也很难读的好吧),这个过程我们就是反汇编。 当然真正去破解一些so文件时,使用上面的办法效率太低,这是可以去使用一些专业的破解工具,里面集成了很多很强大的功能,可以大大提高工作效率,例如使用IDA直接把so打开,我们可以很方便的定位到jni函数:

image

从这个例子的分析,我们为了增加破解难度,可以动态注册jni函数并且自定义函数名,避免破解者一眼就找到Java_com_xxx这样的native函数。另外一点就是要隐藏在字符串,不可以直接明文写在代码中,至少做个拼接吧,或者添加一定的逻辑动态生成。大家可以想一下,还有没有其他的办法增加破解难度?

应用加固

DEX加固

  • 源项目(需要加固加密的apk)
  • 加壳项目(使用加壳项目对源apk进行加密,并将加密后的源apk和脱壳apk进行合并得到新的apk)
  • 脱壳项目-android项目(此时得到的已经不是一个完整意义上的apk程序了,它的主要工作是解密源apk,然后加载源apk,让其可以正常运行起来)

具体是修改dex head中的checksum,signature和file_size并将加密后的源apk和其大小追加到dex文件末尾就行

https://www.jianshu.com/p/4ff48b761ff6 image

运行脱壳程序加载原dex文件

Dalvik虚拟机会加载我们经过修改的新的classes.dex文件,并最先运行ProxyApplication类

在attachBaseContext方法里,主要做两个工作:

读取classes.dex文件末尾记录加密dex文件大小的数值,则加密dex文件在新classes.dex文件中的位置为:len(新classes.dex文件) – len(加密dex文件大小)。然后将加密的dex文件读取出来,加密并保存到资源目录下

然后使用自定义的DexClassLoader加载解密后的原dex文件

在onCreate方法中,主要做两个工作:

通过反射修改ActivityThread类,并将Application指向原dex文件中的Application

创建原Application对象,并调用原Application的onCreate方法启动原程序

image

使用像xposed框架这样的hook技术,类似于降维打击,可以绕过加固技术轻松获取到dex文件。目前的乐固、360等大厂加固都可以绕过,从原理上看,加固技术对于这种hook技术获取dex也没有什么好办法,作为APP的作者,需要加强hook方面的防御来提高加固技术的安全性

so加固

<应用安全防护和逆向分析>–第十四章

数据安全

加密和JNI写入Native层

秘钥及敏感信息不要在类中硬编码敏感信息,可以使用JNI将敏感信息写到Native层。

SharePreferences

首先不应当使用SharePreferences来存放敏感信息,sharedpreferces存储的xml文件数据可能被反编译拿到。存储一些配置信息时也要配置好访问权限,如私有的访问权限 MODE_PRIVATE(Activity.MODE_PRIVATE,//默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容),避免配置信息被篡改。

SQLite数据库文件的安全性

  • 描述:敏感信息是否明文存储
  • 检测:检测数据库里面的重要信息,比如账号密码之类的是否明文存储
  • 建议:重要信息进行加密存储

allowBackUp属性

<应用安全防护和逆向分析>–第十一章

逆向工具对抗

在逆向分析 Android 应用程序时,一般会使用 apktool,baksmali/smali,dex2jar,androguard, jdGUI 以及 IDA Pro 等。因此可以考虑使得这些工具在反编译 APK 时出错来保护 APK,这些工 具大部分都是开源的,可以通过阅读其源代码,分析其在解析 APK、dex 等文件存在的缺陷, 在开发 Android 应用程序时加以利用。

总结

APK 自我保护的技术并不能做到完全的保护作用,只是提高了逆向分析的难度, 在实际运用中应该根据情况多种技术结合使用。这些技术其实很多来源于 Android 恶意代码, 所以可以关注 Android 恶意代码中使用的一些技术来应用到自己开发的 Android 应用程序中。

参考

APK 的自我保护

http://www.droidsec.cn/

https://developer.android.com/topic/security/data

Android安全防护