编辑按

crownless:目前,通过容器技术和虚拟化技术,可以实现安卓APP多开、定制、代码注入、进行各种控制,这给DIY甚至是灰产、黑产开辟了新的可能性,而且可以躲避一些杀毒软件对恶意代码的查杀。为了实现容器,需要对底层的技术有着深刻的理解,而且要考虑到易用程度。容器技术不仅是一门技术,也是一门艺术!

嘉宾介绍

邓维佳(ID:virjar),毕业于四川大学软件工程专业。目前致力于Android安全相关技术研究,包括App加固脱壳、Android群控技术、Android多开容器等。

讲师围绕Android容器,介绍了目前开源的容器方案和实现,展现了多种思路,通过他的演讲,观众深入理解了多开机制,以及如何通过容器的方式,绕过安全软件的检测等等。

演讲具体内容

以下为速记全文:

最近几年有挺多多开的实现,很多厂商都实现了,比如一些APP的多开或者多环境的功能,它们能实现我们对安卓手机里同一个APP能够存在登陆两个帐号,在营销等有很多应用。这里比较流行的一个东西就是VA

这张图是2016年开源的框架,这个框架里能够对同样一个APP实现无限的分身,我们如果用来做灰产,或者通过这个模式对APP里面的逻辑进行挟持、控制,是非常方便的。

我觉得VA是非常优秀的框架,在这里它实现对安卓系统很多功能的类似于系统层面的包装,作者安卓的了解已经出神入化了。

最近在安卓的hook等各种方面的发展,可能有其他思路可以把VA功能做更多扩展。做安全的都了解VA里有两个功能,就像多开跟简单的控制。

我们做什么?先看看VA架构图,有同学把VA流程图、架构图做了一个梳理,但是因为我这里想对VA有其他功能的变种,所以我这个图跟大家认可的图不一样,主要是三个技术点,是VA本身的机理理念有三个最重要的模块:

一个是IO重定向。IO重定向能实现多种空间隔离的最基本原理,就是能通过对应文件系统的访问,实现Relocation。

第二个模块是插桩子系统。对于安卓原生来说,所有组件必须在Android清单文件里配置,所以如果是在安卓基本组件不去注册的话,那么它是没办法去执行的。

但是在VA里面我们实现免安装去打开一个APP,它是怎么实现的?主要是通过插桩,在里面配置各种没有实际意义的、但就是去系统里占位的一些配置项。

然后在VA里面,它是以这种插装的形式去系统申请资源,当资源申请到之后,它需要去对插装里的各种配置进行还原,比如把一个StubActivity还原成一个真实的Activity。

另外一块是系统服务模拟,基本把SystemServer这一层做一次模拟,包括安卓系统的各种Manager,所以它有一个进程,这个进程代替了安卓的SystemServer的角色,这个进程非常复杂。所以,在VA里面我们能够通过这个工具、这个APP、这样一个用户操作的APP界面,实现分身、多开。

毕竟VA是在一个虚拟的环境里,我们能不能把这个VA模拟成单层的像一个APP一样?比如我某个APP,然后通过VA,把它的图标等等看起来跟跟原始APP一样,就能够实现我对这个APP进行定制、进行代码注入、进行各种控制化,比如把广告去掉,或者我们加自己的广告,或者加自己的密码拦截,坏事好事等等。

这个APP如果变成一个普通APP、普通APP,对于C端用户来说,做转化之后他看不出跟原始APP有区别。

所以VA本身看起来是有一个用户界面需要去点击、安装、做各种配置,如果我把VA去进行一个包装之后,让VA的容器环境跟VA的内嵌的APK打包成一个的话,就变成现在这个样子。

来看这张图,这个图是拿着line做实验,能看到上一张图是在VA本身环境里,在APP里面进行的各种APP的打开、安装等等。看这张图,它其实是在安卓的VA的外面,我们看到这个图里有多个line的图标。

所以对VA的分身,可以看这个VA本身一个APK里面进行分身,除此之外,还有一种方式实现在APP层面的分身。做成这个样子之后,它跟APP本身的痕迹可能就有些不一样了,当然了,有其它的好处,我是哪一个APP,把APP处理、把APP分身,那么就是它的漏洞。

所以如果我对现在流行那些的APP进行这样的操作,然后实现一些代码,最后实现各种控制,其实对于普通用户来说,他是分辨不了的。

比如我们公司写了一个APP,有些功能限制,我做一些插件、做一些什么东西之后,我能够把这个发放给一些普通人,这个时候它是不是做灰产、做黑产,或者做些DIY,这种大家都了解的东西。

另外可以进行有害的、有病毒的APP的隐藏,因为这个在VA里面我们的APP可以实现免安装的运行。在免安装运行之后,所有APK的一些,比如有害的痕迹在VA里是一个文件,这个文件可以进行转储、进行加密、进行压缩等等。

这种情况下通过杀毒软件、通过代码痕迹的扫描,因为它只是一个资源,这时候常规的黄、赌、毒那些大家没想到的有害的APP都可以经过这种方式包装,包装之后基本上有害杀毒软件平台对它进行痕迹检测、特征检测也是检测不出来的。

在VA层面上,对刚才的那张图做了两个改造,它要实现同样一个APP进行分身,所以IO层面这个模块依然是需要存在的。这不是这个图里面的,第一个APP应该是在进程外实现的单个APP单纯的转发。

除此之外,是我们把Stub系统的插装模块给去掉了,为什么?因为在VA里面它是需要免安装一些运行APK,但是我如果拿已知的APK跟VA融合的,那么这个APK本身的配置文件我们是知晓的,所以这时根本不需要做插装。

为什么这么做?因为这个时候我们是已知的,本身对VA来说,它对插装模块有一些对原生系统的兼容性问题不是很好处理,我们把这个模块去掉以后,那一块兼容性基本不需要考虑了。

还一个是对于VA里的SystemServer的模拟的服务层,服务层在我们这也不需要了,为什么?我们现在对一个APP包装就是VA的引擎加上我们自己的,VA引擎包括单个APP,我们说分身是在那个,APP是在外面分身的,这时我不需要再托管系统的各种功能、activity栈等,全都不需要了,这都可以抹掉了,抹掉之后我也不需要去考虑。

对于VA原生来说,它的开源版本从8.0之后基本就不怎么支持了,主要是它需要一直去跟随安卓本身框架源码的变化,然后做些自己的patch、做自己的功能实现。如果我把这个模块也拿掉的话,我也不需要考虑这一层兼容性的处理了。

但是这一层也还需要一点点,需要什么?VA宿主apk的package和运行内部的apk所见的Package是不一样的,package是安卓里面唯一定位的一个APP的标志。也就是说VA对于系统来说,它是VA的package,但是对于它内部运行的APP来说,它是自己的package。

所以如果它想调用系统功能,比如想查看系统某个包的信息、调用其他APP的API,要经过SystemServer的调用,这时对于应用来说,他自己的包名发送给SystemServer时,SystemServer肯定认为这是一个不合法的包名。

所以我们还需要有一个简单的模块,这个模块是package的Transefer。比如我这是io.m.app,然后我内部一个APP是com.a.b,com.a.b去调用API时,它肯定传递到自己的package,就是com.a.b,然后对于安卓系统来说,它能见到的是io.m.app。

所以我们需要这样一个包的转换的模块功能。基于这个改造以后,上面那张图就能实现了,相对来说它的痕迹,至少未来插装系统是不需要了,SystemServer兼容性也不需要了。相对应的这两层,我们做反VA、反容器的痕迹也会发生变化,在这种情况下,除了开始的io重定向的痕迹现在还没抹掉。

还有一点是签过签名,因为对于安卓系统来说,它看到的签名是宿主的签名,但是对于内部APP来说,它的签名是它自己的。所以我我们在服务这一层还是要保留签名模拟功能

当然,这个服务已经不是个进程了,而单纯是个模块了,它需要把原生的、把内部的签名读到,给它解析出来。当APP去获取自己签名时,要返回apk自己真实的签名。

这是我现在研究的对VA的一种变形,但是这种改造方式是有问题的,就是它一定会存在两个package的问题,为什么两个package?比如在外围、在控制层面它是.io.virtual.app,但是对于它是com.a.b。com.a.b它到APP上自己去读的时候,它所见到的文件路径应该是” /data/data/自己的包名”,而不是”/data/data/.io.virtual.app”,但实际上这个文件夹在安卓系统是不存在的,也没有权限读写的,它能读写的只是” /data/data/.io.virtual.app”。

那怎么办?所以文件的这个模块是不存在的。我们想,能不能让它存在?就是宿主的包名跟内部运行APP的包名完全一致的话,是不是可以?我是不是能把这个做得更像一点?

这是我后来发现的第二种思路我们用加壳的思路去做这个容器。什么叫加壳?加壳其实就是把原来别人的APP变成一些资源,然后做些加密,把APP的一些入口的信息替换成自己的,先运行壳代码,壳代码运行了之后把资源解密出来,解密出来之后把相关的数据还原、相关的流程还原,然后开始正常APK的流程。

如果我让这个壳APK在我的这个容器环境内运行的话,我是不是也能在有壳APP运行入口之前先运行我的代码,然后我在把我的坏事做完之后,然后再把运行的控制权交给壳程序,让它脱壳,壳脱完之后再进行业务逻辑的运行。所以我在壳的外面如果再套一层壳,是不是也能实现这些注入控制?

所以在存在加壳的情况下,如果对他进行重打包、功能改造、代码修改是基本不现实的,但是如果我让这个APP运行在我的环境里,然后我用动态注入的方式,当这个APP运行在内存里,我在进行一部分劫持,其实也是另一种重打包的思路,这就是第二种思路。

这个时候看这张图,黄色的地方是原生APK的资源,我会把我们的框架,就是我们自己壳外部的资源,跟原生我想处理的合并的这个APK资源进行融合,它们融合的方式就是把清单文件里的入口修改成我们自己的入口,classes.dex是安卓的代码资源文件,我把这个classes.dex所有的代码文件全部替换成我自己的,再把这个Manifest的文件入口修正到我自己这里。

这样APP运行时肯定会先运行我外面这个入口代码,外层这个APP运行之后,我去把资源文件里面原生APK数据都进来,加载之后进行对象替换,以及各种模拟加壳加固的思路,把所有切换完之后,再把这个控制权交给APK。那么它是有壳,它自己脱壳,它自己运行正常业务逻辑。

但是这个地方有一个问题。为什么我们能够去修改这个AndroidManifest.xml文件?现在很多APK是修改不了AndroidManifest.xml文件的,因为大部分有反资源重打包,有资源混淆对抗等等。

而且这个AndroidManifest.xml跟这个resource.arsc文件,它们是同时出现的,它们都是通过安卓aapt进行打包输出的。通过apktool解存在资源混淆对抗的包的资源文件的话,一般都会失败的。

但我这为什么能改?其实原理很简单,就是我并不需要把AndroidManifest.xml解成文本格式,以及把这个resource.arsc资源解成对应的资源文件,都不需要解。在安卓Manifest文件里,它其实就是一个安卓ARSC的格式,在二进制层面我们可以往它的字符串常量池里加一些常量,在二进制层面去修改它的数据内容。

我们现在不考虑签名问题的话,其实在二进制层面修改,能够把我们自己入口的classes配置修正到这个AndroidManifest里面去。AndroidManifest.xml文件一定是需要被安卓系统解析的,所以它一定是不会存在这个层面的对抗,再怎么混淆也不可能混淆到安卓系统无法识别,所以安卓系统能读,那么我这也就能读、能改。

但是这种方式也是有一些弊端的,我们看看这张图的最终效果,这是我们用套个壳的方式去运行APK,它有几秒等待时间。这跟VA本身的运行机理一样的,VA如果去打开一个没有安装的APP,它也是会有几分钟时间的等待。为什么?在安卓5之后,所有的APK在安装的时候需要有一个过程,需要把这个DEX转成OAT的格式,这个过程在安装过程中需要等待,小APP可能就很快,稍微大点的APP可能需要等5分钟以上。

在这种套壳容器方案里面,如果我们同步去等待这个资源的加载过程,大APP会有5、6分钟的卡屏,这个卡屏对我们用户来说不是很友好的,所以我们需要做一个跳转的页面。像刚才咱们需要做一个跳转,这其实跟VA本身其实是一样的,VA本身也是有一个安装过程,只是大家在VA里去安装看apk,可以方便的现实进度条,看起来足够友好。

能明显的看到,用这个方案实现容器的话,一定需要做首页的跳转。所以在刚才那个AndroidManifest.xml里面,我们第一个是把入口代码修正到我们的入口去,第二是我们需要插入一个页面,当页面运行时我们需要一步的去进行dex2oat,当它执行完成之后回调,我们才把原生APP的入口打开。

这样有一个好处,第一个,APK体积会小很多,相对代码入口重编译来说,它只需要存在外面壳的自己的几个dex,很多资源是不需要的,所以说不会存在APK膨胀的问题,原来的这种方式打包的APP基本上也还是那么大,但刚才说的是资源的对抗是不需要的,因为我们是在二进制层面直接修改Manifest的入口,我根本没有对它进行资源的解包。

但是它有一个contentprovider的问题,我们都知道当appication初始化之后、启动之后,它会安装contentprovider,但是因为我们的contentprovider配置到Manifest里面面,那么它一定会进行contentprovider的安装过程。

因为当我们的代码没有run起来之前,没有把内部资源APK运行起来之前,我们是没办法拿到的classLoader。这是如果强行去安装contentprovider的话,它是会报ClassNotFoundException的。

当然,很多时候contentprovider可能会需要同步调用,我们这个只能做成异步,因为它有延时,做成异步的话,对于调用方可能看到在报错。但是如果我只是单纯对这一个APP放到我们容器里进行分析、进行插件的定制的话,其实对我们来说也是无所谓的。

基本的流程就是这个样子,首先,APK启用的时候会跑到我们这里来,我们第一步需要把contentprovider安装给hook掉,就是我们不能让它在这时进行contentprovider安装。contentprovider安装时,先把contentprovider信息收集起来,但是我们现在先拒绝掉,第二步是判断dex是不是需要同步加载,如果打开一个页面时就同步加载内部容器内资源的话,它会卡屏几分钟,这时用户肯定无法等待的。

这时如果不能同步加载,我们需要先打开activity,在activity里面启动异步线程,然后在进行APK的load,load之后如果是同步的话,比如后台线程以及一些service,这些其他的在后台的组件的话,那我们同步进行安装处理。以及如果我之前曾经打开过这个,它其实已经进行了安装,这时我不需要再进行异步的。完成之后,我们把classLoader替换掉。

classLoader替换掉之后,把资源也重置一下,指向我们自己媒资里面另一个原生APK的地址,它会把资源也替换成原生APK的东西。然后我们再正常流程,模拟安卓启动的过程,把appication创造起来,去模拟它的生命周期,再把contentprovider启动。这时候ClassLoader已经出现了,那么contentprovider可以安装了,流程到这时,对原生APK来说,它看到的可能就以为是正常的流程。

这是第二种思路,这个类似于VA的环境,然后在内部它是一个没有安装的APK的资源。所以本身跟VA很将近,但是跟VA相比来说,大家可以想象一下,如果你在VA里在安装一个VA是会有问题的,因为底层安卓的各种service底层有大量对象以package作为ID。然后我们这种方式实现了只有一个pacakge,就是容器外面的package跟我所运行apk资源的package是完全一样的。

第三种思路就是multiDex。在4.4以前一个APK文件一般只有一个Dex,但是我们业务会比较大,比如我们代码越来越多,就会超过65535以上的限制,APK足够大时需要进行dex分包,就需要存在multidex,但这个功能在4.4以前是需要单独写逻辑去兼容的,但是在5.0以后这个multidex被安卓原生控制了。

如果我能够修改Manifest入口的话,那我往这个APK里去附加一个我自己的dex文件,然后再把Manifest里那个入口指向我自己、指向我的代码。先运行我们的代码,代码运行之后,再把控制权交回原生的一些逻辑,通过这种思路也是能实现注入的。

但这种注入相对于我们重打包来说,我是没有去修改dex的内容,所以对原生APK来说,它如果存在对抗逻辑的话,如果它去检查dex的签名或者dex文件指纹,也是检查不出来什么东西的,因为我根本没对它进行任何修改。当然基于这个思路做的,是不会有启动加载延时。

大家看到这个地方我用了XposedAPI,大家做安卓安全的都了解这个框架,我通过这个实现java代码的hook。能看到我这个是能跳出来的,就是右边代码,左边能看出来它这个。

不管通过哪个手段,能够把Xposed的一些功能实现引进来,这时至少在java层能够hook所有java逻辑。所以我基于重打包的方式,通过写java代码,进行一些钩子函数的代码注入。其实动态hook实现重打包和源码级别重打包相比,第一种更加方便。

然后我们把插件代码跟原生想要攻击apk合成一个APK,它就是单独的另一个产品,比如广告去掉之后做DIY。

大概的思路就是下面这张图,这张图里所有资源都是我想要攻击的APP的资源,除开在classes里面新增了一个dex文件,这个dex文件是没自己入口的逻辑。我修改改的只有AndroidManifest文件里那个<applicationname=”entry”>,只能从二进制层面去修改它的入口。和刚刚一样,二进制层面修改AndroidManifest文件一定能够成功,不存在所谓的资源对抗。

最后一种思路是入口修改,为什么我把这个放在最后讲?因为现在已经有比较成熟的方案,我把dex入口层面的代码进行重编译,比如在壳代码之前插入bootstrap指令,然后把这个代码流程转到我们定制的逻辑里面去,进行我们的逻辑代码执行之后再返回控制流。这个其实有几个已经实现了。比如太极,xpatch。

通过对AndroidManifest的解析,我们找到入口是那个dex,找到dex以后找到它的代码文件,如果它是appication的话,在attachBaseContext的时候,加入我们的smali指令,或者如果它是MainActivity,在MainActivity的attachBaseContext加入插入smali指令。

但这有一个问题,有一个6535的问题,就是如果我进行dex文件的重新编译的话,我们是增加了新的代码到入口dex里面,我们说的是dex文件本身里面各种编码都是两个字节,各种字符串的数量全是在两个字节,如果这个dex的某个数量已经是快接近于6万多,如果我再新增一坨东西放到那个dex里,它一旦超过了6535就会失败了。

所以这个地方需要插入的东西,只能是一个浅层的,加入一个启动代码,这个启动代码仅仅是为了加载媒资里我们自己的dex或者我们自己的APK资源。这样我可能只需要加1个类,然后2、3个方法,实现媒资里apk自动解码,classesLoader的创建。这个APK加的东西很少,这种情况下不太有可能出现6535的限制问题。

如果说我把dex文件进行修改的话,它dex文件的特征一定会有些变化,所以如果我进行dex文件检测的话就能检测出来,但实际也是不对的,为什么?因为在安全的世界里,你hook我也能hook,如果我把这个io重定向做了,让她读到了真实的apk资源就可以。

所以是这个流程,我们在入口进行重编译,因为我们重打包,签名是在所有容器里、所有沙箱里都需要去做的,所以其中一个重要功能就是对packageManager进行hook,实现签名拦截,以及所有他们可能检测的点、资源需要进行重定向,甚至把我们增加特征抹除掉,然后再加载我们自己的插件,然后再到正常的启动流程。这个针对于大部分APK来说也是能做到成功的。

整理一下,左边是怎么攻,右边是怎么防,因为我们进行重打包,PackageManager是需要我们hook的,它有两种思路,一种是用代理的方式实现,一种是通过hook的方式,我其实是能够去hook ART的一些东西。

如果它存在一些文件签名,我通过重定向的方式让它读到真的,然后各种拦截,maps等各种都能伪造。最主要的就是两个,就是第6条,我通过二进制层面去修改入口,能够对抗资源混淆,但是代理痕迹是classLoader,classLoader不是指向/data/app的,而是指向/data/宿主里面的。

还有就是针对于dex重编译的话,dex特征发生变化,可以在oat文件中找到特征,这个最主要是在maps里OAT文件,通过maps的内存描述能够读取到。然后可以检查oat特征的。

声明:本文来自看雪学院,版权归作者所有。文章内容仅代表作者独立观点,不代表安全内参立场,转载目的在于传递更多信息。如有侵权,请联系 anquanneican@163.com。