相关文章推荐
爱听歌的抽屉  ·  纯前端利用 js-xlsx ...·  1 年前    · 
睿智的红豆  ·  解决Python memory ...·  1 年前    · 
Android APP漏洞之战—插件化漏洞和解压缩漏洞详解

Android APP漏洞之战—插件化漏洞和解压缩漏洞详解

本文为看雪论坛优秀文章
看雪论坛作者ID:随风而行aa



一、 前言


最近一直处于忙碌的状态,花了很长一段时间,抽出碎片时间才将这篇帖子写完,本文结合上文的动态加载文章一起学习,本文主要讲述Android中存在的插件化漏洞、签名机制漏洞、解压缩漏洞等,并对一些经典的漏洞进行了复现,本文的相关实验文件由于太多,后面都会上传到知识星球。本文第二节主要讲述Dex文件结构、Zip文件结构、Android签名机制。本文第三节主要讲述插件化漏洞和解压缩漏洞的安全场景。本文第四节主要对插件化漏洞进行讲述。本文第五节主要对解压缩漏洞进行讲述。本文第六节主要对Janus漏洞原理进行讲述。



二、 基础知识


1.Dex文件基本结构


dex文件是anroid虚拟机Dalik运行的一种文件,包含应用程序的全部操作指令以及运行时数据,下面我们看下.class文件和.dex文件的区别:


我们可以发现dex文件将原来每个文件都有的共有信息合成一体,从而减少了class的冗余。下面我们进一步详细看dex文件结构。


我们可以发现dex文件主要由3大部分组成,分别是:文件头、索引区、数据区。其中索引区主要包括字符串、类型、方法、域、方法的索引。数据区主要包括类的定义、数据区、链路数据区。

上面我们可以看出Dex文件由许多部分组成,其中Dex Header最为重要,因为Dex的其他组成部分,都需要通过Dex Header中的索引才能找到。


(1)Dex Header


dex文件头一般固定为0x70个字节大小,包括标志、版本号、校验码、sha-1签名以及其他一些方法、类的数量和偏移地址等信息。如图所示:



结合上面的两张图进行对照,下面我们进一步详细的描述dex文件的结构。



(2)索引区


dex文件索引区主要是对一些字符串、类型、方法、域、方法的索引,方法可以查找到对应的数据位置。



(3)数据区


数据区一般包括类的定义区、数据区、链接数据区。类的定义区一般存放dex文件中一些类对象的声明,数据区则存放代码原数据,链接数据区一般提供从索引区到数据区的链接映射关系。


2. Zip文件结构


zip文件是比较常见的压缩文件,我们先来看一下zip文件的基本结构图:


通过图中我们可以看出,zip文件一般分为三个部分:源文件数据存储区、中心目录区、中心目录结束标识。


(1)源文件数据存储区


记录着压缩的所有文件的内容信息,其数据组织结构是每个文件都由local file header、file data、data descriptor三部分组成。


<1> file header


用于标识文件的开始,文件结构如下:


<2>file data


主要存放相应的压缩文件的源数据。


<3>data descriptor


一般用于标识该文件压缩结束,该结构只有在相应的header中通用标记字段的第3位设为1时才会出现,紧接在压缩文件源数据后。这个数据描述符只用在不能对输出的 ZIP 文件进行检索时使用。例如:在一个不能检索的驱动器(如:磁带机上)上的 ZIP 文件中。如果是磁盘上的ZIP文件一般没有这个数据描述符。


(2)中心目录区


对于待压缩的目录而言,每一个子目录对应一个压缩目录源数据,记录该目录的描述信息。压缩包中所有目录源数据连续存储在整个归档包的最后,这样便于向包中追加新的文件。头部的结构如下:


(3)中心目录结束标识


目录结束标识存在于整个归档包的结尾,用于标记压缩的目录数据的结束,结构如下:


3.Android APK签名机制


应用签名主要是避免外部恶意解压、破解或者反编译修改内容,签名的本质是:

认证:Android 平台上运行的每个应用都必须有开发者的签名。在安装应用时,软件包管理器会验证 APK 是否已经过适当签名,安装程序会拒绝没有获得签名就尝试安装应用。

验证完整性:软件包管理器在安装应用前会验证应用摘要,如果破解者修改了 apk 里的内容,那么摘要就不再匹配,验证失败。


(1)应用签名方案类型


截止到Android12,Android支持三种应用签名方案:

v1:基于jar签名
v2:提高验证性能&覆盖范围(Android 7.0 Nougat引入)
v3:支持密钥轮换(Android 9.0 Pie引入)


为了提高兼容性,必须按照v1,v2,v3的先后顺序采用签名方案,低版本平台会忽略高版本的签名方案在APK中添加额外数据,具体流程图如下:


<1>签名方案v1


最基本的签名方案,是基于Jar的签名。v1签名后会增加META-INF文件夹,其中会有如下三个文件:


v1签名流程:


① 计算每个文件的 SHA-1 摘要,进行 BASE64 编码后写入摘要文件,即 MANIFEST.MF 文件;

② 计算整个 MANIFEST.MF 文件的 SHA-1 摘要,进行 BASE64 编码后写入签名文件,即*.SF 文件;

③ 计算 MANIFEST.MF 文件中每一块摘要的 SHA-1 摘要,进行 BASE64 编码后写入 签名文件,即*.SF 文件;

④ 计算整个 *.SF 文件的数字签名(先摘要再私钥加密);

⑤ 将数字签名和 X.509 开发者数字证书(公钥)写入 *.RSA 文件;


验证流程:


主要包括验证签名、校验完整性两个步骤:步骤1:验证签名步骤

① 取出*.RSA 中包含的开发者证书,并校验其合法性

② 用证书中的公钥解密*.RSA中包含的签名

③ 用证书中的公钥计算*.SF的签名

④ 对比(2)和(3)的签名是否一致


步骤2:验证完整性

① 检查 APK 中包含的所有文件,对应的摘要值与 MANIFEST.MF 文件中记录的值一致

② 使用证书文件(RSA 文件)检验签名文件(SF 文件)没有被修改过

③ 使用签名文件(SF 文件)检验 MF 文件没有被修改过


上面任何一个步骤验证失败,则整个APK验证失败。问题:

覆盖范围不足:Zip 文件中部分内容不在验证范围,例如 META-INF 文件夹;

验证性能差:验证程序必须解压所有压缩的条目,这需要花费更多时间和内存存在Janus漏洞:恶意开发人员可以通过Janus漏洞去绕过Android 的v1签名验证机制。


<2>签名方案v2


Android7.0 中开始引入了APK签名方案v2,一种全文件签名方案,该方案能够发现对APK的受保护部分进行所有更改,相比v1来说校验速度更快,覆盖的范围也更广。但是考虑到版本兼容的问题,所以一般常见了v1+v2的混合签名模式。我们由上文知道Zip文件主体分为:源文件数据存储区、中心目录区、中心目录结束标识。EoCD中记录了中央目录的起始位置,在源文件数据存储区和中心目录区插入其他数据不会影响Zip的解压。因此v2签名后会在源文件数据存储区和中心目录区插入APK 签名分块(APK Signing Block)。如下图所示。从左到右边,我们定义为区块 1~4。

v2签名块(APK Signing Block)本身又主要分成三部分:

SignerData(签名者数据):主要包括签名者的证书,整个APK完整性校验hash,以及一些必要信息。

Signature(签名):开发者对SignerData部分数据的签名数据。

PublicKey(公钥):用于验签的公钥数据。


签名流程:相比v1签名方案,v2签名方案不再以文件为单位计算摘要,而是以1MB为单位将文件拆分为多个连续的快(chunk),每个分区的最后一个快可能会小于1MB。v2签名流程如下:


① 对区块 1、3、4,按照 1MB 大小分割为多个块(chunk)

② 计算每个块的摘要

③ 计算(2)中所有摘要的签名

④ 添加X.509开发者数字证书(公钥)


验证流程:


因为v2签名机制是在Android 7.0上版本才支持,因此对于Android 7.0以及以上版本,在安装过程中,如果v2 签名块,则必须走 v2 签名机制,不能绕过。否则降级走 v1 签名机制。v1 和 v2 签名机制是可以同时存在的,其中对于 v1 和 v2 版本同时存在的时候,v1 版本的 META_INF 的 .SF 文件属性当中有一个 X-Android-APK-Signed 属性:

X-Android-APK-Signed: 2


v2签名本身的验证过程:


① 利用PublicKey解密Signature,得到SignerData的hash明文。

② 计算SignerData的hash值。

③ 两个值进行比较,如果相同则认为APK没有被修改过,解析出SignerData中的证书。否则安装失败。

④ 如果是第一次安装,直接将证书保存在应用信息中。

⑤ 如果是更新安装,即设备中原来存在这个应用,验证之前的证书是否与本次解析的证书相同。若相同,则安装成功,否则失败。


<3>签名方案v3


Android 9.0中引入了新的签名方式v3,v3签名在v2的基础上,仍然采用检查整个压缩包的校验方式。不同的是在签名部分增可以添加新的证书(Attr块)。在这个新块中,会记录我们之前的签名信息以及新的签名信息, 支持密钥轮换,即以密钥转轮的方案,来做签名的替换和升级。这意味着,只要旧签名证书在手,应用能够在 APK 更新过程中更改其签名密钥。v3 签名新增的新块(attr)存储了所有的签名信息,由更小的 Level 块,以链表的形式存储。签名流程:v3版本签名块也分成同样的三部分,与v2不同的是在SignerData部分,v3新增了attr块,其中是由更小的level块组成。每个level块中可以存储一个证书信息。前一个level块证书验证下一个level证书,以此类推。最后一个level块的证书,要符合SignerData中本身的证书,即用来签名整个APK的公钥所属于的证书。从v2到v3的过渡:


签名校验:Android 的签名方案的升级都需要确保向下兼容。因此,在引入 v3 方案后会根据 APK 签名方案,v3 -> v2 -> v1 依次尝试验证 APK。而较旧的平台会忽略 v3 签名并尝试 v2 签名,最后才去验证 v1 签名。如下图所示:


注意:对于覆盖安装的情况,签名校验只支持升级而不支持降级。即一个使用 V1 签名的 Apk,可以使用 V2 签名的 Apk 进行覆盖安装,反之则不允许。v3签名自身的校验:


① 利用PublicKey解密Signature,得到SignerData的hash明文。

② 计算SignerData的hash值。

③ 两个值进行比较,如果相同则认为APK没有被修改过,解析出SignerData中的证书。否则安装失败。

④ 逐个解析出level块证书并验证,并保存为这个应用的历史证书。

⑤ 如果是第一次安装,直接将证书与历史证书一并保存在应用信息中。

⑥ 如果是更新安装,验证之前的证书与历史证书,是否与本次解析的证书或者历史证书中存在相同的证书,其中任意一个证书符合即可安装。


<4>三种签名的比较和校验时机


v2、v3的比较如下图所示:


v1签名方案:基于 Jar 的签名方案,但存在的问题:完整性覆盖范围不足 & 验证性能差。

v2签名方案:通过条目内容区、中央目录区之间插入APK 签名分块(APK Signing Block)对v1签名进行了优化。

v3签名方案:支持密钥轮换,新增的新块(attr)存储了所有的签名信息,对v2签名进行了优化。

验证签名的时机主要要了解Android安装应用的方式:

系统应用安装:开机时完成,没有安装界面。

网络下载的应用安装:通过市场应用完成,没有安装界面。

ADB工具安装:没有安装界面。

第三方应用安装:通过packageinstall.apk应用安装,有安装界面。


但是其实无论通过哪种方式安装都要通过PackageManagerService来完成安装的主要工作,最终在PMS中会去验证签名信息,如v3验证方式一样。


4.Android动态加载


Android动态加载总会涉及到插件化、热部署、热修复等,这里我在网上查阅资料后,给大家总结了下动态加载的场景使用和分类:


动态加载,就是程序运行时,可以加载外部的可执行文件并运行,这样使得我们可以不用安装apk就可以更新应用,针对一些SDK项目,可以加快app新版本的覆盖率、快速修复线上bug。这里运行时是指应用冷启动并开始工作后,外部可以是SD卡,可以是data目录,也可以是jniLib目录,这些可执行文件是没有随着应用一起编译的。动态加载的特点:

① app在运行的时候,可以通过加载一些本身不存在的文件,来实现一定功能,这种经常应用在app更新的过程中。

② 可执行文件是可以替换的,更换静态资源不属于动态加载。

③ 动态加载的核心思想就是动态调用外部的dex文件,Android Apk自带的dex是程序入口,所有功能可以直接从服务器中下载dex来完成。


Android动态加载按照工作机制不同,可以分为虚拟机层动态加载和Native层动态加载两大类。这里由于本文主要讲解动态加载方面漏洞,所以对热更新、热修复等原理就不深究了,大家感兴趣可以下去查阅相关资料,动态加载原理详细可以参考我上一篇帖子:Android加壳脱壳学习(1)——动态加载和类加载机制详解( bbs.pediy.com/thread-27


三、 插件化和解压缩安全场景和分类


1.插件化漏洞的安全场景


前文我们知道了Android的动态加载机制和签名机制,Android插件化机制具有模块解耦性,可以动态升级按序加载,而且当下很多APP都使用了热部署、热修复、插件化等技术都采用了动态加载技术,这样可以实现APP的快速更新,但是也带来一定的安全隐患,使得很多恶意软件能熬过安全检测,来动态加载代码。而执行加载绕过执行漏洞一般与Android 的签名机制密不可分,所以上文我们也很详细的讲解了Android的签名机制。


2.插件化漏洞的分类


很多APP通过动态加载一些dex或so文件,但是考虑到存在动态加载的安全性问题,往往会对加载的文件进行签名校验机制,因此我们可以将插件化漏洞分为两类:动态加载漏洞和签名校验绕过漏洞。


3.解压缩漏洞的安全场景


Android中经常会涉及到解压缩问题,比如动态加载机制,可能下载了apk/zip文件,然后在本地做解压工作,还有就是一些资源在本地占用apk包的太大,就也打包成zip放到服务端,使用的时候再下发。Android在解压zip文件,使用的是ZipInputStream和ZipEntry类,代码比较简单,但是ZipEntry.getName的方法存在的漏洞就是返回的是文件名,并没有对特殊字符处理,linux中../可以命令文件但是这个可以进行穿越上层目录,就会带来一定的安全隐患。


4.签名机制和解压缩漏洞分类


我们这里列举两个典型的漏洞:如下所示:



四、 插件化漏洞原理分析和复现


1.动态加载漏洞


(1)原理分析


Android系统提供类加载器DexClassLoader,可以在运行时动态加载执行包含的JAR或APK文件内的DEX文件,这样可能导致所加载的Dex文件被恶意应用替换或代码注入,如果不对Dex文件进行签名校验,就可能导致加载的是恶意代码,这样就会进一步造成严重危害。


(2)案例1——动态加载


案例准备:

原apk
加载dex


我们先编写一个测试类文件,然后生成dex文件,这里我们在dex文件中只加入字符串信息,我们源apk并未加入签名校验机制。


我们先将dex文件放到模拟器的sdcard/下。


我们新建一个程序,然后编写主程序的代码,并授权sd读取权限。

Context appContext = this.getApplication();
testDexClassLoader(appContext,"/sdcard/classes.dex");


Context appContext = this.getApplication();
testDexClassLoader(appContext,"/sdcard/classes.dex");


然后我们编写类加载器代码。

private void testDexClassLoader(Context context, String dexfilepath) {
        //构建文件路径:/data/data/com.emaxple.test02/app_opt_dex,存放优化后的dex,lib库
        File optfile = context.getDir("opt_dex",0);
        File libfile = context.getDir("lib_dex",0);
        ClassLoader parentclassloader = MainActivity.class.getClassLoader();
        ClassLoader tmpclassloader = context.getClassLoader();
    //可以为DexClassLoader指定父类加载器
        DexClassLoader dexClassLoader = new DexClassLoader(dexfilepath,optfile.getAbsolutePath(),libfile.getAbsolutePath(),parentclassloader);
        Class clazz = null;
        try {
            clazz = dexClassLoader.loadClass("com.example.test.TestClass");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        if(clazz!=null){
            try {
                Method testFuncMethod = clazz.getDeclaredMethod("test02");
                Object obj = clazz.newInstance();
                testFuncMethod.invoke(obj);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
    }


效果显示:


这里说明加载成功了,如果我们这里写的是一段恶意代码,这样就会进行攻击,造成破坏。


(3)安全防护


我们上文的动态加载漏洞,是因为源APK并未对加载的dex文件进行签名校验,从而导致容易导入恶意代码,当然从Android 4.4后加入了对JAR/DEX存放目录文件的user_id 和动态加载JAR/DEX的进程的user_id是否一致的判断,如果不一致将抛出异常导致加载失败,这样就很好的可以防范替换加载的dex文件,进行恶意注入。解决方案:

① 将动态加载的DEX/APK文件放置在APK内部或应用私有目录中。

② 使用加密网络协议https进行下载加载的并将其放置在应用私有目录中。

③ 对加载的Dex文件进行完整性校验和签名校验。


2.签名检验绕过漏洞


(1)原理分析


我们知道一般对APK的验证,主要使用的是签名校验或者MD5校验,使用校验的方式较多。而签名校验一般是处理APK中动态加载或防止二次重打包的问题。我们可以将APK中的签名检验机制进一步进行分类:

java层的签名校验:

原理:这种是开发者在APK java层中加入了签名校验代码,然后通过校验加入文件的MD5值或者SHA1值来对文件进行校验。

解决方案:一般这种情况,我们通过定位到APK中的签名代码段,然后进行hook 篡改或者进行修改后重打包就可以进行绕过。


so层的签名校验:

原理:由于java可解释语言的原因,所以后来开发者又将签名代码放入so层,从而增加逆向工作的难度。

解决方案:这种情况,同样可以使用IDA或GDB进行动态调试确定到签名代码段,然后使用hook 注入技术或静态修改来进行绕过。


在线签名校验:

原理:由于前两种方式都是静态校验的方式,这样的安全性仍然较低,后来更多厂商通过服务器在线进行验证,将签名密钥发送然后在so层或java层中进行校验。

解决方案:这种情况,我们要使用抓包软件对服务器发送的数据包进行抓取,在成功获取正确密钥后,再去hook对应的签名代码段,从而就可以实现绕过。


这一部分完整性保护大家可以详细的参考看雪陌殇大佬的帖子:Android应用完整性保护总结( bbs.pediy.com/thread-25


(2)案例2——java层签名绕过


案例:书旗小说.apk我们发现书旗小说在进行重新签名后,再次安装会报错,首先我们AndroidKiller解析APP:


然后我们开始进行定位,这里我们使用常见的定位点:signature、killProcess、PageManager。

signature、killProcess、PageManager 一般是签名代码的关键函数,所以当我们发现这三个函数同时出现,很大程度代表了签名点。

我们这里搜索signature或killProcess,我们找到了签名三兄弟:


分析签名的逻辑,修改后回编译,再安装显示成功。


(3)案例3——so层签名绕过


因为so层和java层签名绕过原理相近,只是so层是分析汇编代码,java层分析Smali源码,这里我们参考一个博主的案例,我列举一下。首先我们根据NDK注册定位到so层的入口点,去查找JNI_Onload函数,然后开始去查找上面的三兄弟。


这里我们就很好的定位到了代码段,后续就是分析逻辑,修改对应的校验点即可。

最后就会发现签名成功的绕过。


(4)案例4——在线签名绕过


在线签名校验主要是抓取校验部分的数据包,然后去查找cookie中的public_key,或者签名Signature值,通过分析数据包后再定位到相应的代码段将值回传到相应的代码段即可。这里有一个案例大家可以参考Android APP漏洞之战(6)——HTTP/HTTPs通信漏洞详解( bbs.pediy.com/thread-27 )中酷狗直播的漏洞实现,这里就是通过在线修改了MD5值,然后使得程序在升级过程中绕过了升级校验,从而成功的注入了恶意病毒。


(5)安全防护


① java层和so层都可以进一步混淆,来防止字符定位的方法。

② 可以使用反调试技术,来防止动态调试进行定位的方法。

③ 可以采用对frida和xposed的检测,来进行防止hook注入。

④ 可以尽量采用在线签名,加密传输报文:

客户端将本地程序信息上传到服务端,服务端返回一段校验代码。客户端动态执行代码,返回校验结果。

在登陆接口将登录信息在NDK层进行加密,用签名信息进行加密,在登陆接口实现中,进行解密,如果失败不允许登陆。



五、Zip解压缩漏洞分析和复现


1.原理分析


因为Linux系统中../代表向上级目录跳转,攻击者可以通过构造相应的Zip文件,利用多个'../'从而改变zip包中某个文件的存放位置,费用该替换掉应用原有的文件,完成目录穿越。这样严重可能会导致任意代码执行漏洞,危害应用用户的设备安全和信息安全。Java 代码在解压 zip 文件时,会使用到 ZipEntry 类的 getName() 方法,如果 zip 文件中包含 ../ 的字符串,该方法返回值会原样返回。如果没有过滤掉 getName() 返回值中的 ../ 字符串,继续解压缩操作,就会在其他目录中创建解压的文件。


2.漏洞复现


样本:

海豚浏览器 V11.4.18
攻击的so文件:libdolphin.so
Poc攻击代码


我们打开海豚浏览器,并用Fiddler去监控海豚浏览器,Fiddler的配置大家可以参考我之前博客。


这里我们可以通过抓包去发现主题下载的申请链接,然后我们将主题下载下来,然后解包查看结构,这里我们重命名为zip文件。


我们可以发现下载下来的三个资源文件,这也说明海豚浏览器的主题本质是一个zip包。那么我们如何实现zip目录穿越了,我们是不是可以尝试去构建一个这样的zip包,去替换浏览器的下载包,并重命令去文件名,使得替换浏览器中的关键文件,这里我们就尝试去替换浏览器中的libdolphin.so文件。我们先查看该文件的位置:


此时我们知道了libdolphin.so文件的存放位置,目录为:/data/data/com.dolphin.browser.express.web/files,这样我们只需要将我们制作的libdolphin.so去替换原文件即可。我们编写一个libdolphin.so文件。


然后我们将生成的so文件重新命名libdolphin.so文件,接下来我们再使用我们的Poc代码更改名称:

import zipfile
if __name__ == '__main__':
    ZipPath = '../../../../../data/data/com.dolphin.browser.express.web/files/libdolphin.so'
    zp = zipfile.ZipFile('/root/Desktop/zipAttack/attack.zip','w')
    zp.write('/root/Desktop/zipAttack/libdolphin.so',ZipPath)


此时我们就成功的构造了我们的攻击文件attack.zip。


然后我们只需要对海豚浏览器下载主题的包进行劫持替换即可。


然后我们再次点击手机下载相应主题,发现主题是成功的下载,但是并没有替换成功。经过验证,我们发现首先正常命名的so文件是可以正常的和主题一起下载成功的。


然后我们验证,Android中直接重命令文件../../libdolphin.so是可以直接回到上级目录的。所以综上是因为我测试的Android6.0 已经打了补丁,在进行解压的时候对../这种情况进行了过滤,这样就导致不能进行成功的穿越。当然这里我们主要是理解zip穿越的原理,这样就可以在很多地方利用这个原理存在的漏洞了。


3.安全防护


对重要的 zip 压缩包文件进行数字签名校验,校验通过才进行解压。

检查 zip 压缩包中使用 ZipEntry.getName() 获取的文件名中是否包含 ../ 或者 .. 字符。

更换 zip 解压方式,不使用 ZipEntry.getName() 的方式,使用 ZipInputStream 替代。

Google的修复意见:

InputStream is = new InputStream(untrustedFileName);
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(is));
while((ZipEntry ze = zis.getNextEntry()) != null) {
  File f = new File(DIR, ze.getName());
  String canonicalPath = f.getCanonicalPath();
  if (!canonicalPath.startsWith(DIR)) {
    // SecurityException