Android应用层组件的保护策略(下)

上篇介绍的是攻击者的思路,本文讲介绍如何从开发者的角度来防止自己的公开组件被攻击,分享一下自己平时见到的比较值得学习架构和自己开脑洞想出来的一些架构。

声明:未经允许本文禁止转载。

一、合理定义和使用权限

exported是第一道防线,把该属性设置为false以后,只有相同uid的应用可以访问到,而相同uid的权限比较难获取,除非是从其他app里打过去的,已经超出了预设的游戏规则。

Permission是第二道防线,四个组件的标签定义中均可以设置,定义方式在android的文档里也有,声明一个dangerous或者signature的权限,如果不是签名文件泄露的话,一般也不会出问题 但是由于业务需求,可能要exported=true而且permission留空,所以才需要在代码里进行check,光manifest是不够用的,也是本文的重点——如何在exported=true的情况下进行防御。

二、Activity保护

Activity触发的地方是onCreate,除非是某程序员不想干了才会在里面写Runtime.getRuntime().exec(),不然大部分逻辑都是靠用户去点击才能触发的。而onCreate的参数是Bundle,不包含调用者的信息,所以无法做直接的check。 如果被untrusted app开起来的话,activity栈是很不一样的,非要检测也是可以测出来的,从ActivityManager里拿信息(但是Android L开始已经禁止了第三方APP使用该功能)虽然是第三方APP开起来的,在log里已经看不到关于第三方APP的任何信息。

1
2
3
4
5
6
04-09 03:14:55.580 12329-12329/? D/XIXI: .TargetActivity
04-09 03:14:55.580 12329-12329/? D/XIXI: com.example.apackage.TargetActivity
04-09 03:14:55.580 12329-12329/? D/XIXI: com.example.apackage
04-09 03:14:55.580 12329-12329/? D/XIXI: .Launcher
04-09 03:14:55.580 12329-12329/? D/XIXI: com.android.launcher3.Launcher
04-09 03:14:55.580 12329-12329/? D/XIXI: com.android.launcher3

暂时还没有想到确认调用者的身份的方法,所以只要不要在onCreate里写一些后台运行的东西,让所有敏感操作都由用户交互的方式来触发,就不会出现大问题。

另外一个注意的地方是嵌入Activity的webview,历史上出现过大大小小的事件,例如大约五年前的JavascirptInterface导致任意代码执行,历年的pwn2own,几天前的玄武所谓的克隆攻击,很多厂商都可能忽视webview引入的问题。

第一个问题是js引擎被打,在android O之前,webview和主进程是相同uid的,拥有与APP完全相同的权限,近年来针对浏览器的攻击也在增多,所以风险是有的,但出现概率不是很高,防御起来也只能是升级系统。值得一提的是Android O以后,默认的webview已经是isolated权限,就算js引擎被打了,也无法直接利用,需要过沙箱。

第二个问题是addJavascriptInterface时引入的问题,开发者为了方便用户会开放一些接口,如果加载第三方的页面的话,也是有权限去调用Java层开放出来的接口的。解决方式也有,思路也比较多,例如仅允许访问特定的域名(除非主站被XSS了,这已经不是客户端的问题了),或者牺牲服务器的性能,使用非对称加密技术来进行一次鉴权。在实际的应用中,大多数是没有太多保护的,只要有机会控制url,就可以使用代码里的“后门”了。

第三个是允许访问本地文件引入的问题,也是一个老生常谈的问题,一般需要加过滤或者直接删掉这个功能。 举个webview保护恰当的例子:某APP里最常见的webview里有配置选项,其中有多个它的wrapper,exported=true的那个wrapper是不允许注册javascriptInterface和访问文件的,而另外一个不公开的wrapper,会允许javascriptInterface。这样做既可以浏览第三方提供的URL,也能保证不被恶意js攻击。

三、Service保护

Service保护起来应该是最简单、思路最多的地方,代码部分对Service的check方式比较多,Service是经过Binder进行通信的,而Binder是自带callerUid、callerPid的,可以从这方面出发;另一方面IBinder的数据是不可靠的,需要谨慎去操作。

uid无法伪造,可以判断是否为1000(system),也可以通过uid来查询包名,看看包名是否为白名单,看看包的签名是否与预期一致,思路多种多样。但需要注意的是一定不能让白名单里的包名本身是未安装的,这样攻击者可以写一个包名与之相同的APP,就直接bypass掉了check。

在onTransact时候,多加一些对数据合法性的校验,来防止内存破坏的攻击或者本地DOS,著名的案例有16年的Bitmap Unmap的CVE,在传递Bitmap时导致的权限提升和代码执行。Binder传数据是不可信的,最好在IO的过程中多加思考,安全编程。

举一个防御写的好的APP,思路比较新奇:hsf(HuaweiServiceFramework)说白了就是个自带的后门,什么功能都有,但是无法使用。其主要逻辑,是根据Binder传来的uid查packageName,之后读取packageName的Manifest里的meta-data,拿到值以后进行RSA解密,确认解密后得到的是packageName和apk签名摘要,再判断是否与预期一致,是一种万无一失的策略。笔者也推荐使用这种方式,让指定的包名和签名的APP才能通过校验。

再举一个自己设计的例子,目前还没有看到这种操作:仅公开一个Service,去保护多个敏感的Service。在onTransact里进行鉴权,某些低危的transactCode下,可以直接放行;某些高危的transactCode下,进行鉴权后返回敏感Service的binder,从而减少鉴权的次数,管理起来也比较方便。

四、Receiver保护

Receiver又是一个很头疼的问题,同样因为缺乏caller的信息,只能通过加密等方式来保证数据安全,这里拿一些PUSH_SDK为例,它们经常作为Receiver存在,直接接受信息也必须开放,所以保护起来也是很有意思的。

通过逆向各大推送服务提供者的SDK,大致是可以理清里面的逻辑,防御思路可以从里面抄抄,由于时间过于久远,本文只能挑一些能够回忆得起来的防御手段。将来有时间分析时,再更新一下主流的防御手段,以及自己的PUSH_SDK设计。

1、在私有目录下,存放近几个随机的值,猜对其中某个值的消息才算合法的消息,收到新的消息时候就进行一次更新。其实对抗方法也是有的,恶意APP可以常见也接收同类消息,偷到这个值就可以绕过检测了。

2、在Manifest里写meta-data,使用meta-data作为非对称密钥去解密数据,做好防重放的准备,保证不被恶意利用。 3、(实在想不起来了,罪过罪过。。。

另外要注意动态注册的广播默认是开放的,在注册时候可以主动标明权限,但很多开发者都会忘掉这个事情。

整体思路是开发的时候要时刻认为receiver得到的数据是不可靠的,通过转移风险的方式来做保护,例如收到后进行一次在线的查询将风险转移到服务器,或者去访问可信的Provider获取数据将风险转到对Provider的保护。

五、Provider保护

Provider保护比较单调,并且不会直接引起代码执行,毕竟目的是数据库操作。本文也没有合适的案例,只能提供一些安全上的建议,例如根据对数据库本身的功能,适当地加上writePermission和readPermission,在实现Provider的各个方法时尽可能减少参数的可控范围,过滤掉恶意的增删改,多进行代码审计等。

有一个已经被修复的案例,某厂商的某APP,缺乏对Provider保护导致的插入恶意数据,再下一次应用启动时候一定程度上影响程序逻辑。修复的方式也很简单,在组件上添加了writePermission。

六、总结

组件的防御比攻击简单很多,需要注意的点也就那么几个,一部分保护在Manifest里可以使用Permission做到,另一部分保护在Java代码通过各种校验里可以做到,谨记对公开的组件时刻要认为外部的数据全部统统一切都是不可信的,在发布前最好由经验丰富的人来审计一下可能出问题的地方,可以极大减少被攻击的风险。因为大部分是逻辑的洞,利用成功率比较高,漏洞的组合也很有趣,设计方式很多,本文无法面面俱到,仍然有很多值得探索的地方,欢迎各位前辈来交流攻防的经验。